diff --git a/app/api/affiliate/route.ts b/app/api/affiliate/route.ts index cc1e3e7..bb34973 100644 --- a/app/api/affiliate/route.ts +++ b/app/api/affiliate/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { readSession, authConfigured, SESSION_COOKIE } from "@/lib/session"; import { getAffiliate, enrollAffiliate, setAffiliateCommission, listReferrals, AFFILIATE_FLOOR, findOrCreateAccountByEmail } from "@/lib/db"; +import { parseCommissionPercent } from "@/lib/affiliate"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -54,7 +55,9 @@ export async function POST(req: NextRequest) { if (aff.plan !== "paid") { return NextResponse.json({ error: `Free plan is floored at ${AFFILIATE_FLOOR}%. Upgrade to $1/mo to lower it.` }, { status: 403 }); } - await setAffiliateCommission(id, Number(body.commission_pct)); + const pct = parseCommissionPercent(body.commission_pct); + if (pct == null) return NextResponse.json({ error: "Enter a valid commission percentage." }, { status: 400 }); + await setAffiliateCommission(id, pct); } else { // Default action: enroll (idempotent). await enrollAffiliate(id); diff --git a/lib/affiliate.ts b/lib/affiliate.ts new file mode 100644 index 0000000..2292a65 --- /dev/null +++ b/lib/affiliate.ts @@ -0,0 +1,10 @@ +const COMMISSION_RE = /^(?:\d+)(?:\.\d{1,2})?$/; + +export function parseCommissionPercent(value: unknown): number | null { + if (typeof value === "number") return Number.isFinite(value) ? value : null; + if (typeof value !== "string") return null; + const text = value.trim(); + if (!COMMISSION_RE.test(text)) return null; + const pct = Number(text); + return Number.isFinite(pct) ? pct : null; +} diff --git a/tests/affiliate-commission-input.test.mjs b/tests/affiliate-commission-input.test.mjs new file mode 100644 index 0000000..06ad3fb --- /dev/null +++ b/tests/affiliate-commission-input.test.mjs @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { parseCommissionPercent } from "../lib/affiliate.ts"; + +test("parseCommissionPercent accepts finite numbers and decimal strings", () => { + assert.equal(parseCommissionPercent(25), 25); + assert.equal(parseCommissionPercent("80"), 80); + assert.equal(parseCommissionPercent(" 12.5 "), 12.5); +}); + +test("parseCommissionPercent rejects coerced or malformed values", () => { + assert.equal(parseCommissionPercent("1e2"), null); + assert.equal(parseCommissionPercent("0x10"), null); + assert.equal(parseCommissionPercent("80abc"), null); + assert.equal(parseCommissionPercent(["80"]), null); + assert.equal(parseCommissionPercent({ valueOf: () => 80 }), null); + assert.equal(parseCommissionPercent(Number.NaN), null); + assert.equal(parseCommissionPercent(Number.POSITIVE_INFINITY), null); +});