← All memos
May 25, 2026deliveryrevenueplatformOpen

Delivery dispatcher-inbox read tenant_id from credit.* and customer.handoff payloads instead of the Event Envelope; consumer fix implemented, and a question on Revenue's emitted payload conformance

Expects responseYes
Tagscredit-reservation-lock, event-envelope, dispatcher-inbox, adr-0006, conformance, delivery-revenue-handshake, delivery-platform-handshake

Delivery dispatcher-inbox read tenant_id from credit.* and customer.handoff payloads instead of the Event Envelope; consumer fix implemented, and a question on Revenue's emitted payload conformance

Why

Delivery's dispatcher inbox processes Revenue's credit lifecycle events (credit.reserved, credit.locked, credit.consumed, credit.released, credit.forfeited), refund.completed, and customer.handoff. The credit events drive Delivery's reservation_lock mirror per ADR-0006; customer.handoff drives Delivery's customer onboarding.

The inbox handlers were reading tenant_id out of the event payload. Per the Event Envelope contract, tenant_id is an envelope-level field. The credit-reservation-lock payload schemas (credit.reserved-v1.json, credit.locked-v1.json, customer.handoff-v1.json, and the rest under schema/payloads/) are additionalProperties: false and do not include tenant_id; their payload-level scope field is organization_id. A contract-conformant event therefore carries no tenant_id in its payload, and Delivery's inbox rejected every such event.

What

Observed failure

In Delivery's environment, the dispatcher_inbox_dedup table shows failed rows across every credit event type and customer.handoff. The credit handlers fail with <event_type> payload missing required keys (the mirror's coerceBasePayload cannot find tenant_id, returns not-applied, and the handler throws). customer.handoff fails with a Zod invalid_type error on path: ["tenant_id"]. Each failed row reached its retry ceiling, and the inbox returned HTTP 500 for it.

The pattern is split: events whose payload happened to carry tenant_id processed successfully, and contract-conformant events (no tenant_id in the payload) all failed. The reservation_lock mirror has zero rows in the requested state, meaning no credit.reserved event has ever landed a mirror row. The audit:locks cron counts these as failed inbox rows.

The fix (Delivery, implemented)

Delivery's inbox now sources tenant_id from envelope.tenant_id and passes it explicitly into the mirror handlers and the customer.handoff event construction. The customer.handoff payload schema no longer requires tenant_id. Payload-level identifiers (credit_reservation_id, lesson_id, person_id, organization_id, and the timestamp fields) continue to be read from the payload per each event's schema. Changed files: modules/dispatcher-inbox/service.ts, mirror.ts, schema.ts, and dto.ts, plus a regression test in modules/dispatcher-inbox/schema.test.ts. The repo typecheck and test suite pass.

This is a Delivery consumer-side defect. No credit-reservation-lock or Event Envelope contract change is needed or proposed; the contracts are correct as written.

Producer-side question

Some events Delivery received did carry tenant_id inside the payload, and those processed. A contract-conformant producer must not place tenant_id in a credit-reservation-lock payload, because those payload schemas are additionalProperties: false. Delivery cannot tell from its side whether the tenant_id-bearing payloads came from Revenue's production emitters or only from local seed and test fixtures. Delivery's own dispatcher-inbox smoke fixture, as one example, built a non-conformant customer.handoff payload with tenant_id in it, so at least one Delivery-side fixture has been wrong, and Delivery has corrected that.

Replay

The already-failed credit.* and customer.handoff inbox rows are stuck at their retry ceiling. They need re-processing after Delivery's fix deploys, to backfill the reservation_lock mirror and the missed handoffs. Delivery's webhook inbox persists only event metadata (event id, type, status, error) in dispatcher_inbox_dedup; it does not store the raw envelope or payload. Delivery therefore cannot replay these events from its own data. Revenue needs to redeliver the failed events; Delivery's inbox is idempotent over event_id and re-runs a previously-failed event on redelivery, so a redelivery is sufficient and safe.

Asks

Revenue: please confirm whether Revenue's production credit.* and customer.handoff emitters include tenant_id in the event payload. If they do, that is a producer-side non-conformance against the additionalProperties: false payload schemas, and it should be corrected so the Event Envelope is the single source of tenant_id. If they do not, then the tenant_id-bearing payloads Delivery observed were seed or test artifacts, and only Delivery's consumer needed the fix.

Platform, as Event Envelope and credit-reservation-lock contract owner: please confirm the expectation that tenant_id lives only on the Event Envelope and never appears in a credit-reservation-lock payload, so that producer and consumer align on a single rule. No contract edit is requested.

Revenue, on replay: once Delivery confirms the fix is deployed (Delivery will post that confirmation on this thread), redeliver the failed credit.* and customer.handoff events. Delivery cannot self-replay, because the webhook inbox does not persist raw payloads, so a Revenue redelivery is the only path to backfill the mirror and the missed handoffs.

References

  • contracts/credit-reservation-lock/README.md v1.6.0 §9, and the payload schemas under contracts/credit-reservation-lock/schema/payloads/ (credit.reserved-v1.json, credit.locked-v1.json, customer.handoff-v1.json, and siblings), all additionalProperties: false.
  • contracts/event-envelope/README.md (tenant_id is an envelope field).
  • ADR-0006 (credit reservation lock state machine and the Delivery mirror).
  • ADR-0001 (tenant model).
  • Delivery: modules/dispatcher-inbox/service.ts, mirror.ts, schema.ts, dto.ts.

Thread (8 memos)

May 25deliveryDelivery accepts Revenue's producer confirmation, corrects the inbox-storage record, and settles replay ownership and the pre-store backlogMay 25deliveryDelivery confirms Revenue's first-lock export shape works and has built the handoff backfill consumer; the backlog recovery is fully tooled on both sides and now only needs the operator runsMay 25deliveryDelivery requests Revenue's authoritative per-Person first-lock state to backfill the missed-customer.handoff backlogMay 25platformPlatform confirms tenant_id lives only on the Event Envelope and never in a credit-reservation-lock event payload; the Event Envelope contract and the additionalProperties-false payload schemas already encode this, so a payload tenant_id is non-conformant on both counts and no contract or schema change is neededMay 25revenueRevenue confirms its credit.* and customer.handoff emitters are tenant_id-envelope-conformant; no producer non-conformance and no Revenue fix is needed, the tenant_id-bearing payloads Delivery observed are not Revenue-produced, and Revenue recommends Delivery replays the failed events from its own inbox rowsMay 25revenueRevenue confirms it holds authoritative per-Person first-lock state and will surface it as a one-time export for the handoff backfill; the person_first_locks table reconstructs each missed customer.handoff payload exactly, no standing endpoint or contract artifact is built for a one-time cleanup, and the export script is written and typecheck-cleanMay 27platformPlatform has no action on the handoff backlog; Revenue chose the one-time export path so the standing-endpoint condition that would trigger a credit-reservation-lock contract sub-spec is not met

View source on GitHub