{"record":{"id":"b1407fe4c81dd2b0","repo":"Stirling-Tools/Stirling-PDF","slug":"no-saas-session","errorCode":null,"errorMessage":"No SaaS session","messagePattern":"No SaaS session","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"frontend/editor/src/portal/api/procurement.ts","lineNumber":371,"sourceCode":" * shared project — pressing \"Generate quote\" in dev billed nothing but left real objects behind.\n */\nasync function demoEdgeResponse(\n  fn: string,\n  quoteId: number,\n): Promise<Response | undefined> {\n  const base = saasApiBase();\n  if (!base) return undefined;\n  return resolveDemoResponse(new URL(`${base}/functions/v1/${fn}`), {\n    method: \"POST\",\n    body: { quote_id: quoteId },\n  });\n}\n\nasync function invokeEdge<T>(fn: string, quoteId: number): Promise<T> {\n  const demo = await demoEdgeResponse(fn, quoteId);\n  if (demo) return (await demo.json()) as T;\n  const supabase = getSupabaseClient();\n  if (!supabase) throw new Error(\"No SaaS session\");\n  const { data, error } = await supabase.functions.invoke<T>(fn, {\n    body: { quote_id: quoteId },\n  });\n  if (error) throw error;\n  if (data == null) throw new Error(`${fn} returned no data`);\n  return data;\n}\n\n/** Turn a draft into an issued Stripe Quote (finalized → gets a number + PDF, shareable). */\nexport function issueQuote(quoteId: number): Promise<QuoteResult> {\n  return invokeEdge<QuoteResult>(\"issue-procurement-quote\", quoteId);\n}\n\n/** Accept an issued quote → Stripe creates the committed subscription + first invoice. */\nexport function acceptQuote(quoteId: number): Promise<AcceptResult> {\n  return invokeEdge<AcceptResult>(\"accept-procurement-quote\", quoteId);\n}\n","sourceCodeStart":353,"sourceCodeEnd":389,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/portal/api/procurement.ts#L353-L389","documentation":"Plain Error('No SaaS session') thrown by invokeEdge() in procurement.ts when getSupabaseClient() returns a falsy value. invokeEdge() drives Supabase Edge Functions (issue/accept-procurement-quote) via supabase.functions.invoke. A demo/MSW response is consulted first; only when no demo matches AND there is no Supabase client does this throw.","triggerScenarios":"issueQuote() or acceptQuote() called when the Supabase client is not initialised (SaaS auth not bootstrapped, or running in a flavour/context that never created the client), and no demo handler intercepts the edge-function URL.","commonSituations":"Calling procurement quote actions in a self-hosted/non-SaaS context where the Supabase client was never created; the auth bootstrap that initialises the client has not run yet; a test forgot to mock getSupabaseClient.","solutions":["Ensure the Supabase client is initialised (SaaS auth bootstrap) before procurement actions are reachable — gate the quote UI on session presence.","Catch the error and route the user to SaaS login/link.","In tests, mock getSupabaseClient to return a stub client (the existing test pattern).","Replace the plain Error with SaasNotLinkedError for consistency with http.ts so callers handle one error type."],"exampleFix":"// before — plain Error, inconsistent with http.ts SaasNotLinkedError\nconst supabase = getSupabaseClient();\nif (!supabase) throw new Error(\"No SaaS session\");\n\n// after — reuse the named error type so UI handling is uniform\nimport { SaasNotLinkedError } from \"@portal/api/http\";\nconst supabase = getSupabaseClient();\nif (!supabase) throw new SaasNotLinkedError();","handlingStrategy":"validation","validationCode":"import { getSupabaseClient } from \"@app/auth/supabase/supabaseClient\";\nfunction hasSaasSession(): boolean { return getSupabaseClient() != null; }\n// before quote actions: if (!hasSaasSession()) promptSaasLogin();","typeGuard":"function isNoSaasSession(e: unknown): boolean {\n  return e instanceof Error && e.message === \"No SaaS session\";\n}","tryCatchPattern":"try { return await issueQuote(quoteId); }\ncatch (e) { if (isNoSaasSession(e)) { navigate('/portal/link'); return; } throw e; }","preventionTips":["Gate procurement quote actions on getSupabaseClient() presence.","Ensure SaaS auth bootstrap runs before procurement views mount.","Unify on SaasNotLinkedError for consistent caller handling across the portal."],"tags":["portal","procurement","saas","supabase","auth","edge-functions"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}