fix(billing): restore JSON-safe license billing context - #2311
Conversation
|
Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews. |
| try { | ||
| orgData = await OrgService.getWithFeatures({ db, orgId, env }); | ||
| } catch { | ||
| orgData = null; | ||
| } |
There was a problem hiding this comment.
Bare catch swallows transient DB errors
The bare try/catch around OrgService.getWithFeatures treats every thrown exception — including database connection timeouts, network errors, or Drizzle query failures — identically to a deleted-org RecaseError. When a transient error fires, orgData is set to null, the job logs a warning, and returns silently. The queue never sees the error, so the job is not retried and the work is permanently dropped.
OrgService.getWithFeatures already has an allowNotFound: true option that returns null specifically for the not-found case while letting real errors propagate. Using that option keeps the intended graceful-skip behaviour for deleted orgs without silently discarding jobs on infrastructure failures.
| try { | |
| orgData = await OrgService.getWithFeatures({ db, orgId, env }); | |
| } catch { | |
| orgData = null; | |
| } | |
| // Fetch org with features once for all items. A missing org means it was | |
| // deleted after the job was queued (common in tests) — skip, don't fail. | |
| const orgData = await OrgService.getWithFeatures({ | |
| db, | |
| orgId, | |
| env, | |
| allowNotFound: true, | |
| }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/queue/createWorkerContext.ts
Line: 34-38
Comment:
**Bare catch swallows transient DB errors**
The bare `try/catch` around `OrgService.getWithFeatures` treats every thrown exception — including database connection timeouts, network errors, or Drizzle query failures — identically to a deleted-org `RecaseError`. When a transient error fires, `orgData` is set to `null`, the job logs a warning, and returns silently. The queue never sees the error, so the job is not retried and the work is permanently dropped.
`OrgService.getWithFeatures` already has an `allowNotFound: true` option that returns `null` specifically for the not-found case while letting real errors propagate. Using that option keeps the intended graceful-skip behaviour for deleted orgs without silently discarding jobs on infrastructure failures.
```suggestion
// Fetch org with features once for all items. A missing org means it was
// deleted after the job was queued (common in tests) — skip, don't fail.
const orgData = await OrgService.getWithFeatures({
db,
orgId,
env,
allowNotFound: true,
});
```
How can I resolve this? If you propose a fix, please make it concise.
Summary
Incident context
The production deployment contained the affected code path. The observed projectedPlanLicenseIds.has crash occurred once in a sandbox organization; no live-mode occurrences were found during the incident check.
Verification
Recovery provenance
Recovered dangling commit: b4331e347286393a4d3f5c62ab74036486fa8bf3
Applied hotfix commit: b140fed
Summary by cubic
Restore JSON‑safe license billing context to stop sandbox checkout crashes and preserve projected license IDs and seat counts across JSONB round‑trips. Also reinstate missing guards and fixes around Stripe setup, deleted org/feature races, and product copy reads.
includes/push; fix row resolution to useincludes. Prevents theprojectedPlanLicenseIds.hascrash after JSON persistence.isStripeConnected.inIdsto reliably read copied products immediately after copy.Written for commit cacdd9c. Summary will update on new commits.
Greptile Summary
This hotfix restores a dangling integration commit and patches a production crash (
projectedPlanLicenseIds.hasTypeError) caused byMapandSetinstances not surviving a JSONB round-trip in deferred checkout metadata.CustomerLicenseBillingContextfields changed fromMap/Setto plainRecord/string[]so the struct serialises cleanly to JSONB; all call-sites updated (setupCustomerLicenseBillingContext,applyCustomerLicenseTransitions,resolveLicenseBillingRowsThroughDefinition, and the two line-item helpers).FeatureService.updatenow returnsnullearly when the DB update matches zero rows, preventing a crash when a feature is deleted between read and update;createWorkerContextlogs a warning and skips gracefully when a queued job's org no longer exists;initStripeResourcesForProductsadds anisStripeConnectedguard for fresh sandbox sub-orgs.handleCopyProductspassesinIdsto the post-copylistFullcall to bypass a stale cache snapshot; test event reads are routed through a neweventsDb()helper that mirrors the server's split-DB resolution.Confidence Score: 4/5
The core billing fix is correct and well-scoped; the worker context change introduces a silent failure risk on transient DB errors.
The Map/Set → Record/array migration is consistent across all call-sites and the type definition, and the FeatureService race fix is straightforward. The one concern is in createWorkerContext.ts: the bare try/catch wrapping OrgService.getWithFeatures will absorb transient database errors as if the org were deleted, causing queued jobs to be silently skipped instead of retried. OrgService already exposes an allowNotFound option that would scope the graceful-skip to the not-found case only while letting real errors propagate.
server/src/queue/createWorkerContext.ts — the bare catch block should be replaced with the allowNotFound option on OrgService.getWithFeatures
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Deferred checkout triggered] --> B[setupCustomerLicenseBillingContext] B --> C["Build context with\nRecord<string,number> + string[]"] C --> D[Persist context to JSONB metadata] D --> E[JSONB round-trip preserves plain objects] E --> F[applyCustomerLicenseTransitions] F --> G["projectedPlanLicenseIds.push + includes dedup"] G --> H[resolveLicenseBillingRowsThroughDefinition] H --> I["includes check → select projected or persisted rows"] I --> J[customerLicenseToLineItems / ToStripeItemSpecs] J --> K[Correct seat counts billed] style D fill:#f9f,stroke:#333 style E fill:#f9f,stroke:#333%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[Deferred checkout triggered] --> B[setupCustomerLicenseBillingContext] B --> C["Build context with\nRecord<string,number> + string[]"] C --> D[Persist context to JSONB metadata] D --> E[JSONB round-trip preserves plain objects] E --> F[applyCustomerLicenseTransitions] F --> G["projectedPlanLicenseIds.push + includes dedup"] G --> H[resolveLicenseBillingRowsThroughDefinition] H --> I["includes check → select projected or persisted rows"] I --> J[customerLicenseToLineItems / ToStripeItemSpecs] J --> K[Correct seat counts billed] style D fill:#f9f,stroke:#333 style E fill:#f9f,stroke:#333Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "chore(billing): 🤖 clean recovered hotfi..." | Re-trigger Greptile