From ac550151e62eb15837a1b5b3c6ae4c633d28f0d0 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 13 Sep 2026 03:01:56 +0000 Subject: [PATCH 1/2] affiliate: store a merchant with update-or-insert, since upsert cannot target lower(origin) Registering a merchant in the directory answered "there is no unique or exclusion constraint matching the ON CONFLICT specification" in production: the unique index is on lower(origin) and PostgREST upsert only names columns. The fallback handled the update but not the first insert. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CDEiDss9RWYibtmxSk5Gr2 --- lib/affiliate/directory.ts | 41 ++++++++++---------------------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/lib/affiliate/directory.ts b/lib/affiliate/directory.ts index 8b9a912..5f016a0 100644 --- a/lib/affiliate/directory.ts +++ b/lib/affiliate/directory.ts @@ -90,36 +90,17 @@ export async function addOrRefreshProgram(input: string, addedBy: string | null) } return { ok: false, error: found.error }; } - const { data, error } = await svc - .from("affiliate_programs") - .upsert( - { - origin: found.origin, - descriptor: found.raw, - verified: found.verified, - fetched_at: now, - error: null, - added_by: addedBy, - updated_at: now, - }, - { onConflict: "origin" }, - ) - .select("id, origin, descriptor, verified, fetched_at, error") - .single(); - if (error || !data) { - // The unique index is on lower(origin), which upsert cannot target; fall back to update-or-insert. - const existing = await directoryRow(found.origin); - if (existing) { - await svc - .from("affiliate_programs") - .update({ descriptor: found.raw, verified: found.verified, fetched_at: now, error: null, updated_at: now }) - .eq("id", existing.id); - const row = await directoryRow(found.origin); - return row ? { ok: true, row, warnings: found.warnings } : { ok: false, error: "Could not store the program." }; - } - return { ok: false, error: error?.message ?? "Could not store the program." }; - } - return { ok: true, row: toRow(data), warnings: found.warnings }; + // The unique index is on lower(origin), which a PostgREST upsert cannot + // name, so: update the row that exists, else insert. + const existing = await directoryRow(found.origin); + const patch = { descriptor: found.raw, verified: found.verified, fetched_at: now, error: null, updated_at: now }; + const { error } = existing + ? await svc.from("affiliate_programs").update(patch).eq("id", existing.id) + : await svc.from("affiliate_programs").insert({ origin: found.origin, added_by: addedBy, ...patch }); + if (error) return { ok: false, error: error.message }; + const row = await directoryRow(found.origin); + if (!row) return { ok: false, error: "Could not store the program." }; + return { ok: true, row, warnings: found.warnings }; } /** Re-read every merchant not read in the last day (spec, "Directories" rule 1). */ From 7230564ba7a630610c15e7eb2c249ad28604b9da Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 13 Sep 2026 03:02:38 +0000 Subject: [PATCH 2/2] affiliate: the same update-or-insert for joins, whose index is also on lower(origin) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CDEiDss9RWYibtmxSk5Gr2 --- lib/affiliate/directory.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/affiliate/directory.ts b/lib/affiliate/directory.ts index 5f016a0..a5546a1 100644 --- a/lib/affiliate/directory.ts +++ b/lib/affiliate/directory.ts @@ -214,14 +214,12 @@ export async function joinExternal( const mine = await ensureMembershipForUser(user); if (!mine) return { ok: false, error: "Could not create your CrawlProof affiliate profile." }; - const { data: inserted, error: insErr } = await svc - .from("affiliate_joins") - .upsert( - { owner_id: user.id, origin: row.origin, program_id: program.id, status: "pending", terms: program.pays, updated_at: new Date().toISOString() }, - { onConflict: "owner_id,origin,program_id", ignoreDuplicates: false }, - ) - .select(JOIN_COLUMNS) - .single(); + // Same lower(origin) index as the directory: a refused or ended twin is + // reopened in place, otherwise a fresh row is inserted. + const fresh = { owner_id: user.id, origin: row.origin, program_id: program.id, status: "pending", terms: program.pays, error: null, updated_at: new Date().toISOString() }; + const { data: inserted, error: insErr } = twin + ? await svc.from("affiliate_joins").update(fresh).eq("id", twin.id).select(JOIN_COLUMNS).single() + : await svc.from("affiliate_joins").insert(fresh).select(JOIN_COLUMNS).single(); if (insErr || !inserted) return { ok: false, error: insErr?.message ?? "Could not start the join." }; const site = env.siteUrl.replace(/\/$/, "");