{"record":{"id":"69959c487026404b","repo":"Stirling-Tools/Stirling-PDF","slug":"accept-payg-bundle-quote-failed","errorCode":null,"errorMessage":"accept-payg-bundle-quote failed","messagePattern":"accept-payg-bundle-quote failed","errorType":"exception","errorClass":"StripeFunctionError","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/portal/billing/stripe.ts","lineNumber":390,"sourceCode":"  error?: string;\n}\n\n/**\n * Accept the Stripe quote for a persisted quote row, via {@code accept-payg-bundle-quote}. Acceptance\n * generates the net-terms invoice as a DRAFT (auto_advance off) and returns the hosted URL; the\n * payment step ({@link finalizeBundleInvoice}) stamps the recipient + PO and finalizes it. Payable by\n * card on the hosted page, or by bank transfer / PO. Capacity is credited only on invoice.paid.\n */\nexport async function acceptBundleStripeQuote(req: {\n  teamId: number;\n  quoteId: number;\n}): Promise<BundleInvoice> {\n  const res = await invoke<BundleInvoiceResponse>(\"accept-payg-bundle-quote\", {\n    team_id: req.teamId,\n    quote_id: req.quoteId,\n  });\n  if (!res.success || !res.invoice_id) {\n    throw new StripeFunctionError(\n      res.error ?? \"accept-payg-bundle-quote failed\",\n    );\n  }\n  return {\n    invoiceId: res.invoice_id,\n    hostedInvoiceUrl: res.hosted_invoice_url ?? null,\n    invoicePdf: res.invoice_pdf ?? null,\n    status: res.status ?? null,\n  };\n}\n\n/**\n * Finalize the accepted bundle invoice (stamping an optional PO), via {@code finalize-payg-bundle-invoice}.\n * Returns the hosted checkout URL + PDF. Called by both Download-invoice and Pay-online; idempotent\n * server-side (an already-finalized invoice comes back as-is, PO locked).\n */\nexport async function finalizeBundleInvoice(req: {\n  teamId: number;","sourceCodeStart":372,"sourceCodeEnd":408,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/portal/billing/stripe.ts#L372-L408","documentation":"Fallback thrown by acceptBundleStripeQuote when accept-payg-bundle-quote returns success === false OR omits invoice_id. Accepting a quote must raise a Stripe invoice, so a missing invoice_id is a hard failure regardless of the success flag.","triggerScenarios":"accept-payg-bundle-quote responds success:false (with res.error, or this fallback if error omitted), or success:true without invoice_id. Common when the quote was already accepted/cancelled/expired, or Stripe invoice creation failed inside the function.","commonSituations":"Double-accept of the same quote (concurrent clicks / page reload); quote in a non-open status; Stripe error creating the invoice; function bug returning success without the invoice handle.","solutions":["Refresh the quote state (getLatestBundleQuote) and only show Accept when status is open/valid.","Make accept idempotent on the client: disable the button while the request is in flight and after success.","Inspect res.error / function logs for the real cause (e.g. quote_already_accepted).","Ensure the edge function sets invoice_id on every success path and a meaningful error otherwise."],"exampleFix":"// edge function — before\nif (alreadyAccepted) return json({ success: true });\n\n// after\nif (alreadyAccepted) {\n  return json({ success: false, error: 'quote_already_accepted' });\n}\nreturn json({ success: true, invoice_id: invoice.id });","handlingStrategy":"try-catch","validationCode":"function canAcceptQuote(quote: LatestBundleQuote | null): boolean {\n  return quote != null && quote.status === \"open\";\n}\n\nif (!canAcceptQuote(latestQuote)) {\n  notifications.show({ message: \"Quote cannot be accepted in its current state.\", color: \"orange\" });\n  return;\n}","typeGuard":"function isBundleInvoiceResponse(res: BundleInvoiceResponse | null): res is BundleInvoiceResponse & { success: true; invoice_id: string } {\n  return res != null && res.success === true && typeof res.invoice_id === \"string\";\n}","tryCatchPattern":"try {\n  await acceptBundleStripeQuote({ teamId, quoteId });\n} catch (e) {\n  if (e instanceof StripeFunctionError && /already.*accept/i.test(e.message)) {\n    notifications.show({ message: \"This quote was already accepted.\", color: \"orange\" });\n    refreshQuote();\n  } else throw e;\n}","preventionTips":["Only show Accept when the quote status is open/valid (gate via getLatestBundleQuote).","Make accept idempotent in the UI: disable while in-flight and after success.","Ensure the edge function returns a stable error code for double-accept."],"tags":["billing","stripe","supabase","edge-function","invoice","quote","contract"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}