{"record":{"id":"daa1f94aac8cf4ab","repo":"Stirling-Tools/Stirling-PDF","slug":"cancel-payg-bundle-quote-failed","errorCode":null,"errorMessage":"cancel-payg-bundle-quote failed","messagePattern":"cancel-payg-bundle-quote failed","errorType":"exception","errorClass":"StripeFunctionError","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/portal/billing/stripe.ts","lineNumber":454,"sourceCode":"  };\n}\n\n/**\n * Cancel an unpaid prepaid-bundle purchase via {@code cancel-payg-bundle-quote}: the edge fn voids the\n * invoice (delete if draft, void if finalized), best-effort cancels the Stripe quote, and voids the quote\n * row so the buyer can start over. Nothing was charged (capacity is credited on invoice.paid), so there's\n * no refund. Throws a StripeFunctionError on failure (e.g. {@code invoice_already_paid}).\n */\nexport async function cancelBundleQuote(req: {\n  teamId: number;\n  quoteId: number;\n}): Promise<void> {\n  const res = await invoke<{ success?: boolean; error?: string }>(\n    \"cancel-payg-bundle-quote\",\n    { team_id: req.teamId, quote_id: req.quoteId },\n  );\n  if (!res.success) {\n    throw new StripeFunctionError(\n      res.error ?? \"cancel-payg-bundle-quote failed\",\n    );\n  }\n}\n\n/**\n * Fetch the Stripe-rendered quote PDF for a persisted quote, via the {@code create-payg-bundle-quote}\n * GET route (streams application/pdf). Returns a Blob the caller can object-URL for download.\n */\nexport async function fetchBundleQuotePdf(quoteId: number): Promise<Blob> {\n  ensureSaasSupabase();\n  const supabase = getSupabaseClient();\n  if (!supabase) {\n    throw new StripeFunctionError(\n      \"SaaS Supabase not configured — set VITE_SUPABASE_URL.\",\n      \"unconfigured\",\n    );\n  }","sourceCodeStart":436,"sourceCodeEnd":472,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/portal/billing/stripe.ts#L436-L472","documentation":"Fallback thrown by cancelBundleQuote when cancel-payg-bundle-quote returns success === false. Cancellation reverts a quote to an open/editable row and charges nothing (capacity is only credited on invoice.paid), so a failed cancel is surfaced but is often a recoverable race rather than data loss. The comment explicitly names invoice_already_paid as a typical cause.","triggerScenarios":"cancel-payg-bundle-quote responds success:false — most often because the quote's invoice was already paid (you cannot cancel a paid/finalized invoice), or the quote was already cancelled, or the function hit a Stripe error.","commonSituations":"User clicks Cancel after the invoice was paid in another tab; cancel on a quote whose accept+finalize already completed; concurrent cancel/accept; Stripe-side rejection inside the function.","solutions":["Treat cancel failure as expected: catch StripeFunctionError and inspect the code (e.g. invoice_already_paid) before toasting.","Refresh the quote state after a failed cancel — it may already be paid/cancelled.","Disable the Cancel control once the quote is in a non-cancellable status.","Ensure the function returns a stable error code so the UI can branch gracefully."],"exampleFix":"// before\nawait cancelBundleQuote({ teamId, quoteId });\n\n// after\ntry {\n  await cancelBundleQuote({ teamId, quoteId });\n} catch (e) {\n  if (e instanceof StripeFunctionError && /already.*paid/i.test(e.message)) {\n    notifications.show({ message: \"Invoice already paid — cannot cancel.\", color: \"orange\" });\n    refreshQuote();\n    return;\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"function canCancelQuote(quote: LatestBundleQuote | null): boolean {\n  return quote != null && quote.status !== \"cancelled\" && quote.status !== \"paid\";\n}\n\nif (!canCancelQuote(latestQuote)) {\n  notifications.show({ message: \"Quote cannot be cancelled.\", color: \"orange\" });\n  return;\n}","typeGuard":"function isCancelOk(res: { success?: boolean; error?: string }): boolean {\n  return res.success === true;\n}","tryCatchPattern":"try {\n  await cancelBundleQuote({ teamId, quoteId });\n} catch (e) {\n  if (e instanceof StripeFunctionError && /already.*paid/i.test(e.message)) {\n    notifications.show({ message: \"Invoice already paid — cannot cancel.\", color: \"orange\" });\n    refreshQuote();\n    return;\n  }\n  throw e;\n}","preventionTips":["Treat cancel failure as an expected race, not a fatal error — catch and inspect the code.","Refresh quote state after a failed cancel; it may already be paid/cancelled.","Disable the Cancel control once the quote is non-cancellable."],"tags":["billing","stripe","supabase","edge-function","invoice","quote","race"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}