feat(graphql): serve the 11 backlog subscriptions; facts commit with their writes (marketplace, pick lists, integration syncs)
Follow-ups queue Q12b (owner, 2026-10-09: "build every promised-but-unserved name"). This serves the 11 GraphQL subscriptions that subscription-parity-check listed as backlog. With this PR the gate shows all 45 promised subscriptions served and BACKLOG_BASELINE goes from 11 to 0. The 6 streams with no producer are unchanged, and cartChanged stays refused by design.
Streams
Every stream follows the platform pattern:
tenantScopedTopicStream, which refuses to open without a tenant;- the row is re-read as the subscriber;
@Permissionsuses the resource's VIEW grant;resolve: deliverPayloadAsIs.
None has a REST twin, which matches every existing stream (api-parity passes).
| Stream(s) | Where | Grant / notes |
|---|---|---|
fulfillmentCreated, fulfillmentChanged | fulfillment-subscription.resolver.ts | FULFILLMENTS_VIEW (the events already existed) |
entitlementKeyChanged | entitlement-key-subscription.resolver.ts | follows entitlement_key.revoked only. issued stays withheld because its payload carries the holder's e-mail |
customerSubscriptionChanged | customer-subscription-stream.resolver.ts | SUBSCRIPTIONS_VIEW |
sellerChanged, sellerOfferingChanged, sellerPayoutChanged, sellerSettlementChanged, sellerTransactionChanged | marketplace-subscription.resolver.ts | SellerAccessGuard, so a seller's person hears only its own seller. Payout and settlement streams also need the payouts feature. sellerTransactionChanged answers a list, because one order's sale writes one row per seller |
pickListChanged | pick-list-subscription.resolver.ts | new events pick_list.created/assigned/started/picked/canceled |
integrationSyncCompleted | integration-tenant-sync.resolver.ts | new core event integration_tenant.synced; the payload carries ids and counts only |
Write-side changes reviewers should know about
Marketplace. Each fact now commits with its write: seller lifecycle (saveAndEmit), offering create and withdraw, payout failure, settlement record and close, ledger settle.
- These already appended an event on develop, but in a separate transaction after the save, so a failed append already failed the call. The failure surface is the same; the row now rolls back with its fact instead of staying committed.
updateSeller,submitandstartOffboardinggain events they never had.seller.updatednames the changed fields, never their values.
Pick lists.
- Assign, start, complete and cancel write the status and append the fact in one transaction, on either ORM.
- The move is now conditional on the status it read, so a list that another call moved meanwhile answers 409
PICK_LIST_EDIT_CONFLICT. Develop silently wrote it twice. createappends as the last step of its existing all-or-nothing write.
Integration syncs.
IntegrationTenantSyncService.recordSyncCompletedstampslastSyncedAtand appends the fact in one transaction. The lookup isid + tenantId; the organization on the event is the connection's own.- GitHub: step 9 replaces develop's
lastSyncedAtupdate and still fails loudly, as on develop. - Hubstaff and Upwork: these never stamped anything on develop, so recording is a best-effort epilogue. A failure is logged (integration id and message only) and the run returns its results as before. This means a Hubstaff run into an organization other than the connection's, or a run with no tenant in context, cannot be turned into a failure.
- Jira, Plane, WakaTime, Activepieces, Make.com, Zapier, Sim, Ever Async and Gauzy AI have no sync of their own and are not wired.
Not changed:
- Subscription-service events other than
createdare still appended after their own transaction. That service is under heavy recent churn. - Fulfillment
createkeeps its documented append-after-create shape.
Red first
Each spec below was run against develop's versions of the files.
- Marketplace atomicity: 11 of 18 fail. Examples:
seller.suspended › takes the write back when its fact cannot be appendedgetsExpected: "ACTIVE" Received: "SUSPENDED";seller.submittedexpects[["seller.submitted","seller-1",true]]and receives[]. - Warehouse (
warehouse-update-guards.db.spec.ts, real SQLite):› appends the fact through the transaction that writes the moveexpects[["pick_list.started",…,true]]and receives[]. - GitHub:
records a finished run once…getsExpected number of calls: 1 / Received: 0. Hubstaff: 2 of 5 fail. Upwork:ends a finished run by recording it…fails. - New streams and core service: the suites cannot load on develop (
Cannot find module './fulfillment-subscription.resolver', and the same for the others). - Review round (best-effort epilogue), red on the previous head
58b3770ba90:a Hubstaff run into an organization other than the connection's still answers its resultsfails withNotFoundException.a run with no tenant in its context still answers its resultsfails withINTEGRATION_SYNC_UNSCOPED.a failed recording does not fail an Upwork runfails withthe outbox is unavailable.
Verification (on develop d184a31b71f)
| Project | Result |
|---|---|
| fulfillment | 19 suites / 388 tests passed |
| entitlement | 14 / 362 |
| subscription | 11 / 318 |
| marketplace | 22 / 654 |
| warehouse | 15 / 454 |
| integration-github | 6 / 88 |
| integration-hubstaff | 8 tests |
| integration-upwork | 48 tests |
| core (integration-tenant, integration, graphql/subscriptions, event-outbox) | 18 suites / 378, integration-tenant 87 after the review round |
tsc --noEmit | 0 errors for core, fulfillment, entitlement, subscription, marketplace, warehouse, hubstaff, upwork. integration-github shows 6 errors, all in core files this PR doesn't touch; develop shows the same 6 |
| static gates | 30 run, failed: 0 |
| schema snapshot | regenerated, diff = the 11 streams and their types |
Merge notes
- The generated
schema.graphqlalso changes in #10650 (Q10) and #10644 (other lane). Whichever merges later regenerates it withschema-snapshot.ts --write. No other file overlaps. - The snapshot and gate baseline are one commit (
3803f777f3e). Taking only some item commits would need the snapshot regenerated.
🤖 Generated with Claude Code
Summary by cubic
Serves the 11 GraphQL subscriptions the subscription-parity-check gate listed as backlog, taking the backlog baseline from 11 to 0. The 6 streams with no producer are unchanged, cartChanged stays refused by design, and each new stream follows outbox facts, so none is silent.
Write-side changes
- Marketplace facts now commit in the same transaction as their writes;
updateSeller,submit, andstartOffboardinggain events they never had, andseller.updatednames changed fields, never their values.sellerOfferingChangeddelivers onlycreatedandwithdrawn. - Pick-list moves commit their write and fact in one transaction — including the first pick and a wave's cancel. Writes are predicated on the read (
id + status + version), so stale moves answer 409PICK_LIST_EDIT_CONFLICT; an outcome's transaction takes the list's row first, so overlapping outcomes serialize. A pick's stock movements commit inside the outcome's transaction (TypeORM) or are compensated with reversing movements (MikroORM), and a failed wave cancellation is retryable. recordSyncCompletedstampslastSyncedAtand appendsintegration_tenant.syncedin one transaction; GitHub fails loudly if recording fails, while Hubstaff and Upwork record as a best-effort epilogue.- Subscription-service events other than
createdand fulfillmentcreatekeep their existing append-after-write shape. - Warehouse tests restore their spies in a
finallyand reset all mocks per case. - The new stream fields state they deliver the row as it reads at delivery, not a snapshot of the move.