feat(graphql): serve the 11 backlog subscriptions; facts commit with their writes (marketplace, pick lists, integration syncs)

Merged
EV
11d ago
+6431-314

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;
  • @Permissions uses the resource's VIEW grant;
  • resolve: deliverPayloadAsIs.

None has a REST twin, which matches every existing stream (api-parity passes).

Stream(s)WhereGrant / notes
fulfillmentCreated, fulfillmentChangedfulfillment-subscription.resolver.tsFULFILLMENTS_VIEW (the events already existed)
entitlementKeyChangedentitlement-key-subscription.resolver.tsfollows entitlement_key.revoked only. issued stays withheld because its payload carries the holder's e-mail
customerSubscriptionChangedcustomer-subscription-stream.resolver.tsSUBSCRIPTIONS_VIEW
sellerChanged, sellerOfferingChanged, sellerPayoutChanged, sellerSettlementChanged, sellerTransactionChangedmarketplace-subscription.resolver.tsSellerAccessGuard, 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
pickListChangedpick-list-subscription.resolver.tsnew events pick_list.created/assigned/started/picked/canceled
integrationSyncCompletedintegration-tenant-sync.resolver.tsnew 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, submit and startOffboarding gain events they never had. seller.updated names 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.
  • create appends as the last step of its existing all-or-nothing write.

Integration syncs.

  • IntegrationTenantSyncService.recordSyncCompleted stamps lastSyncedAt and appends the fact in one transaction. The lookup is id + tenantId; the organization on the event is the connection's own.
  • GitHub: step 9 replaces develop's lastSyncedAt update 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 created are still appended after their own transaction. That service is under heavy recent churn.
  • Fulfillment create keeps 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 appended gets Expected: "ACTIVE" Received: "SUSPENDED"; seller.submitted expects [["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 move expects [["pick_list.started",…,true]] and receives [].
  • GitHub: records a finished run once… gets Expected 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 results fails with NotFoundException.
    • a run with no tenant in its context still answers its results fails with INTEGRATION_SYNC_UNSCOPED.
    • a failed recording does not fail an Upwork run fails with the outbox is unavailable.

Verification (on develop d184a31b71f)

ProjectResult
fulfillment19 suites / 388 tests passed
entitlement14 / 362
subscription11 / 318
marketplace22 / 654
warehouse15 / 454
integration-github6 / 88
integration-hubstaff8 tests
integration-upwork48 tests
core (integration-tenant, integration, graphql/subscriptions, event-outbox)18 suites / 378, integration-tenant 87 after the review round
tsc --noEmit0 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 gates30 run, failed: 0
schema snapshotregenerated, diff = the 11 streams and their types

Merge notes

  • The generated schema.graphql also changes in #10650 (Q10) and #10644 (other lane). Whichever merges later regenerates it with schema-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, and startOffboarding gain events they never had, and seller.updated names changed fields, never their values. sellerOfferingChanged delivers only created and withdrawn.
  • 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 409 PICK_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.
  • recordSyncCompleted stamps lastSyncedAt and appends integration_tenant.synced in one transaction; GitHub fails loudly if recording fails, while Hubstaff and Upwork record as a best-effort epilogue.
  • Subscription-service events other than created and fulfillment create keep their existing append-after-write shape.
  • Warehouse tests restore their spies in a finally and 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.