{"record":{"id":"da639127a62c1011","repo":"koala73/worldmonitor","slug":"customer-provenance-required","errorCode":"CUSTOMER_PROVENANCE_REQUIRED","errorMessage":"CUSTOMER_PROVENANCE_REQUIRED","messagePattern":"CUSTOMER_PROVENANCE_REQUIRED","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/billing.ts","lineNumber":4255,"sourceCode":"        q.eq(\"dodoSubscriptionId\", args.dodoSubscriptionId),\n      )\n      .unique();\n    if (!sub) {\n      throw new Error(\n        `[billing] deleteSubscriptionByDodoId: no subscription found with dodoSubscriptionId=\"${args.dodoSubscriptionId}\"`,\n      );\n    }\n\n    const userId = sub.userId;\n    const rawCustomerId = (sub.rawPayload as { customer?: { customer_id?: unknown } } | null)\n      ?.customer?.customer_id;\n    const customerId = sub.dodoCustomerId ||\n      (typeof rawCustomerId === \"string\" ? rawCustomerId : \"\");\n    // A user's current customer mapping does not prove this subscription's\n    // customer: shared customer rows are reassigned by later webhooks. Preserve\n    // the subscription until its customer can be repaired from provider/audit evidence.\n    if (!customerId.trim()) {\n      throw new ConvexError({ kind: \"CUSTOMER_PROVENANCE_REQUIRED\" });\n    }\n    if (customerId) {\n      const retainedOwner = await ctx.db.query(\"deletedSubscriptionCustomers\")\n        .withIndex(\"by_customer_user\", (q) => q.eq(\"dodoCustomerId\", customerId).eq(\"userId\", userId))\n        .first();\n      if (!retainedOwner) {\n        await ctx.db.insert(\"deletedSubscriptionCustomers\", { userId, dodoCustomerId: customerId });\n      }\n    }\n    // Index prefix — deliberately unfiltered by cohort so deleting a\n    // subscription reaps BOTH its day-0 and retro presentation rows.\n    const presentations = await ctx.db\n      .query(\"proActivationPresentations\")\n      .withIndex(\"by_subscription_cohort\", (q) => q.eq(\"subscriptionId\", sub._id))\n      .collect();\n    for (const presentation of presentations) {\n      await ctx.db.delete(presentation._id);\n    }","sourceCodeStart":4237,"sourceCodeEnd":4273,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/payments/billing.ts#L4237-L4273","documentation":"When deleting/retaining a subscription, the code resolves the Dodo customer ID from the subscription or raw webhook data and requires a non-empty value: the current user-customer mapping does not prove which customer a subscription belongs to (rows get reassigned by later webhooks). Without an attributable customerId, the deletion cannot preserve provenance, so it throws and the subscription is kept until the customer can be repaired from provider/audit evidence.","triggerScenarios":"Processing a deletion (webhook or mutation) where sub.dodoCustomerId is missing and rawCustomerId is absent, empty, or not a string, leaving customerId as \"\" after trim.","commonSituations":"Dodo webhook payload malformed or missing customer field; subscription created before customer-ID propagation was added; test fixtures omitting dodoCustomerId; webhook replay with a stripped payload.","solutions":["Repair the subscription's dodoCustomerId from provider (Dodo API) or audit records, then retry the deletion.","If processing a webhook, verify the payload includes the customer id before calling this path.","Check why sub.dodoCustomerId was never written — fix the subscription-creation/webhook handler that should persist it."],"exampleFix":"// before (webhook handler)\nawait handleSubscriptionDeleted({ id: body.data.id });  // customer id dropped\n// after\nawait handleSubscriptionDeleted({ id: body.data.id, dodoCustomerId: body.data.customer_id });","handlingStrategy":"validation","validationCode":"const customerId = sub.dodoCustomerId ?? body?.data?.customer_id;\nif (typeof customerId !== \"string\" || !customerId.trim()) {\n  throw new Error(\"webhook payload missing customer id; repair before deleting subscription\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  await deleteSubscription(args);\n} catch (e) {\n  if (isConvexError(e) && e.data?.kind === \"CUSTOMER_PROVENANCE_REQUIRED\") {\n    await repairCustomerFromProvider(e.data.subscriptionId); // Dodo API / audit, then retry\n  }\n}","preventionTips":["Validate Dodo webhook payloads include customer_id before enqueueing handlers.","Persist dodoCustomerId at subscription creation and on every webhook update.","Alert on subscriptions with missing dodoCustomerId before deletion is attempted."],"tags":["billing","webhooks","provenance","subscriptions"],"backgroundTag":"missing-required-argument","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}