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.mdv1.6.0 §9, and the payload schemas undercontracts/credit-reservation-lock/schema/payloads/(credit.reserved-v1.json,credit.locked-v1.json,customer.handoff-v1.json, and siblings), alladditionalProperties: false.contracts/event-envelope/README.md(tenant_idis 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.