{"record":{"id":"da379da52a16b07e","repo":"Stirling-Tools/Stirling-PDF","slug":"payg-upsert-bundle-quote-returned-no-row","errorCode":null,"errorMessage":"payg_upsert_bundle_quote returned no row","messagePattern":"payg_upsert_bundle_quote returned no row","errorType":"exception","errorClass":"StripeFunctionError","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/portal/billing/stripe.ts","lineNumber":173,"sourceCode":"  input: BundleQuoteInput,\n): Promise<BundleQuote> {\n  const rows = await rpc<BundleQuoteRow[]>(\"payg_upsert_bundle_quote\", {\n    p_team_id: input.teamId,\n    p_posture_policies: input.posturePolicies,\n    p_size_mult: input.sizeMult,\n    p_pipeline_mult: input.pipelineMult,\n    p_pool_credits: input.poolCredits,\n    p_users: input.users,\n    p_provisioned_monthly_volume: input.provisionedMonthlyVolume,\n    p_price_minor: input.priceMinor,\n    p_currency: input.currency,\n    p_consented: input.consented,\n    p_eula_version: input.eulaVersion,\n    ...(input.quoteId != null ? { p_quote_id: input.quoteId } : {}),\n  });\n  const row = rows?.[0];\n  if (!row) {\n    throw new StripeFunctionError(\"payg_upsert_bundle_quote returned no row\");\n  }\n  return {\n    quoteId: row.quote_id,\n    status: row.status,\n    validUntil: row.valid_until,\n  };\n}\n\n/** A team's latest open bundle quote — {@code payg_get_latest_bundle_quote} result, for resume. */\nexport interface LatestBundleQuote {\n  quoteId: number;\n  users: number | null;\n  posturePolicies: number;\n  sizeMult: number;\n  pipelineMult: number;\n  poolCredits: number;\n  priceMinor: number | null;\n  currency: string | null;","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/portal/billing/stripe.ts#L155-L191","documentation":"Thrown by upsertBundleQuote when the payg_upsert_bundle_quote RPC resolves without error but returns zero rows. The helper indexes rows?.[0] and treats an empty result set as a contract failure — the function is expected to return exactly one quote row.","triggerScenarios":"The SQL function payg_upsert_bundle_quote returns no row — e.g. an input validation inside the function rejected the payload (returning nothing instead of raising), a conflict path that returns empty, or a RETURNING clause that matched nothing.","commonSituations":"Passing an invalid team_id, a currency/price the function rejects, or a consented/EULA-version combination the function gates on; a DB-level constraint that silently short-circuits; the function was redeployed with a bug in its RETURNING.","solutions":["Inspect the SQL function payg_upsert_bundle_quote — find branches that return no row and make them raise a meaningful exception instead.","Validate inputs before calling (teamId > 0, non-empty currency, priceMinor >= 0, consented flag).","Check the team exists and the caller's JWT has access to it (RLS/SECURITY DEFINER grant).","Re-run the RPC in the Supabase SQL editor with the same args to see the empty result and adjust."],"exampleFix":"// SQL function — before\nINSERT INTO payg_bundle_quote (...) VALUES (...) RETURNING *;\n-- on conflict or guard, returns nothing\n\n// after — raise instead of silently returning empty\nIF p_users <= 0 THEN\n  RAISE EXCEPTION 'invalid user count: %', p_users;\nEND IF;\nINSERT ... RETURNING *;","handlingStrategy":"validation","validationCode":"function isValidQuoteInput(i: BundleQuoteInput): boolean {\n  return (\n    i.teamId > 0 &&\n    i.users > 0 &&\n    typeof i.currency === \"string\" && i.currency.length === 3 &&\n    Number.isFinite(i.priceMinor) && i.priceMinor >= 0\n  );\n}\n\nif (!isValidQuoteInput(input)) {\n  throw new Error(\"Invalid quote input — refusing to call payg_upsert_bundle_quote.\");\n}\nawait upsertBundleQuote(input);","typeGuard":"function isQuoteRow(row: unknown): row is { quote_id: number; status: string; valid_until: string } {\n  return typeof row === \"object\" && row !== null &&\n    typeof (row as any).quote_id === \"number\";\n}","tryCatchPattern":"try {\n  await upsertBundleQuote(input);\n} catch (e) {\n  if (e instanceof StripeFunctionError && /returned no row/i.test(e.message)) {\n    notifications.show({ message: \"Quote could not be created. Check team and pricing.\", color: \"red\" });\n  } else throw e;\n}","preventionTips":["Validate BundleQuoteInput fully before calling (users>0, currency ISO, priceMinor>=0).","Ensure the SQL function raises a clear exception rather than returning empty on bad input.","Run the RPC in the Supabase SQL editor with sample args during dev to confirm a row is returned."],"tags":["billing","stripe","supabase","rpc","postgres","contract","quote"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}