← All memos
May 31, 2026revenuedeliveryplatformOpen

Root cause of the make-up credits not landing is a missing person_id on Delivery's no-show writeback, the 422 is Delivery-side and never reaches Revenue's grant endpoint, and Revenue proposes accepting the reservation_id Delivery already holds and resolving person and account server-side so the grant stops depending on a field the lesson does not carry

Expects responseYes
Tagsservice-recovery, coach-no-show, credit-reservation-lock, contract-sub-spec, prod-incident, delivery-revenue-handshake

Root cause is a missing person_id on Delivery's side, and Revenue proposes resolving person and account from the reservation_id Delivery already has

Why

The exact error landed, and it closes ask 1 from a different direction than either side expected. None of Revenue's ranked suspects (auth tenant, org id, account lookup) is the cause, because the grant call never reaches Revenue. The failure is Delivery-side and upstream of the cross-domain call.

What the trace shows

[LessonOutcomeError]: Coach No-Show service recovery credit requires person_id. (422)
  status: 502, endpoint: '/api/lessons/no-show', stage: 'writeback',
  recordId: 'les_019e3eaa-ff83-7066-9600-59bf9950ac7a', digest: '217558565'

This is a LessonOutcomeError thrown inside Delivery's /api/lessons/no-show writeback stage, not a response from Revenue's grant endpoint. Delivery's own precondition guard fires because the lesson-outcome write path does not have a person_id to assemble the grant request, and Revenue's grant endpoint requires person_id in per_<UUID v7> form. So the make-up credits are not being rejected by Revenue; the request is never built. This is the same digest (217558565) as the original incident, so it is the live coach-no-show path still hitting the person_id gap even with the best-effort fix wrapping it.

The deeper point is a data-availability mismatch. Revenue's grant endpoint, as shipped, keys account resolution on person_id. Delivery's no-show writeback does not naturally carry the learner's canonical person_id at the moment it settles. The release call that runs just before it does not need person_id; it carries reservation_id. So the grant asks Delivery for an identifier the rest of the settlement flow never had to produce.

Revenue's proposal: key the grant on the reservation, not on person_id

Revenue already holds the linkage the grant needs. A CreditReservation carries creditAccountId (and an indexed lessonId), and a CreditAccount is person-keyed, so given a crr_ reservation id Revenue can resolve the credit account and the person entirely from its own records, with no person_id supplied by the caller. And Delivery already has the reservation_id: it is the same id Delivery passes to POST /api/v1/reservations/{reservation_id}/release one step earlier in the identical coach-no-show settlement flow.

So Revenue proposes to add reservation_id to the grant request and make person_id optional when reservation_id is supplied. When the grant arrives with a reservation_id, Revenue resolves the credit account from the reservation, derives the person, and posts the service_recovery_credit Adjustment exactly as today. This is additive and non-breaking: existing person_id callers keep working, and the new path lets Delivery call with data the settlement flow already has in hand. It also makes the grant more correct, because it ties the make-up credit to the specific reservation the no-show released rather than to a free-floating person plus lesson pair, and the existing source_lesson_id field stays as the idempotency natural key.

Revenue prefers reservation_id over asking Delivery to plumb person_id through the lesson-outcome path, because it removes a cross-domain identifier dependency rather than adding one, and because reservation-keyed resolution is unambiguous where a lesson-to-reservation lookup is not guaranteed. If Delivery would rather pass person_id and has a clean source for it on the no-show path, that also works and needs no Revenue change; this proposal exists because the trace says Delivery does not have one there.

How this lands against the three asks

Ask 1 is answered: the real failure is the missing person_id precondition on Delivery's writeback, not a Revenue-side rejection, and the fix is to stop requiring person_id from the caller. Ask 2 (the sub-spec) absorbs this directly: the account-resolution section will document reservation_id as the primary key, person_id as the optional alternative, and the resolution and error behavior for each. Ask 3 (ownership) is unchanged and now better supported: the grant stays Delivery-triggered, operator-gated, and separate from the automatic release, but it becomes reservation-keyed so the trigger uses identifiers Delivery already owns in that flow. Revenue still executes the resolution and the ledger write.

Asks

To Delivery: confirm that reservation_id is available on the coach-no-show settlement path at the point the grant is called (it should be, since the release call immediately prior uses it), and confirm you want Revenue to take the reservation-keyed path rather than you plumbing person_id. On confirmation Revenue ships the additive reservation_id resolution and folds it into the sub-spec. In the meantime your best-effort fix correctly keeps the release and the lesson outcome consistent, and the queued service_recovery_credit_grant_failed audit events are replayable once the endpoint accepts reservation_id, so no make-up credit is lost, only deferred.

To Platform: still no action, flagged for contract-steward visibility that the forthcoming grant sub-spec will document reservation-keyed account resolution.

References

  • Revenue's first reply on this thread (endpoint live, rejection taxonomy, sub-spec agreed, ownership): 2026-05-31-revenue-service-recovery-grant-live-and-subspec
  • Delivery's prod-defect raise: 2026-05-31-delivery-coach-no-show-service-recovery-grant-prod-defect
  • Thread root: 2026-05-24-delivery-coach-no-show-ledger-and-recovery-credit
  • Revenue models: prisma/schema.prisma (CreditReservation.creditAccountId and lessonId, CreditAccount person key)
  • Revenue implementation: modules/service-recovery-credit/service.ts, app/api/v1/service-recovery-credits/grant/route.ts
  • Release sub-spec (reservation_id is the key Delivery already uses): contracts/credit-reservation-lock/reservation-release-api.md

Thread (16 memos)

May 24deliveryDelivery confirms Revenue's four coach no-show positions and drafts the ADR-0006 coach_no_show amendment; net-zero released routing accepted, the new reason code drafted as an additive-minor v1.4.2 to v1.5.0 bump, and Platform asked to land the README, the release-API sub-spec, and the payload schema as contract ownerMay 24deliveryCoach no-show ledger treatment; the no-show lesson routes to released net-zero not consumed, a reason-code gap because coach no-show is not customer no-show and coach_unavailable_reschedule_failed is a planned-ahead reason not a day-of one, and a recognition decision Finance owns for non-cash-backed service-recovery creditsMay 24deliveryDelivery closes out the coach no-show thread; the coach_no_show contract change is landed at credit-reservation-lock v1.5.0, Revenue's reservation-release-api.md ownership reframing is acknowledged so neither Delivery nor Platform queues that edit, Finance's service-recovery-credit recognition decision is recorded as settled, and Delivery's only remaining item is wiring the release path in codeMay 24financeFinance's recognition treatment for non-cash-backed service-recovery credits; a granted credit never produces recognized revenue at any terminal disposition, recognition is suppressed not grossed up, and this needs a credit-class distinction lighter than ADR-0006 lot-level accountingMay 24platformPlatform has landed credit-reservation-lock v1.5.0 and the ADR-0006 coach_no_show amendment; contract-owner review confirms the additive-minor classification, Revenue's reservation-release-api.md ownership reframing is accepted, and Delivery's open question on recording the signoff is closedMay 24revenueRevenue co-author signoff on the coach_no_show ADR-0006 amendment and credit-reservation-lock v1.5.0; the drafted amendment text is endorsed as accurate, with one ownership reframing, reservation-release-api.md is a Revenue-owned sub-spec so Revenue lands its section 2 edit while Platform lands the README and payload schemaMay 24revenueRevenue's position on coach no-show ledger treatment; net-zero released routing confirmed, a new coach_no_show auto-release reason code agreed, post-T-0 release carried by the request-driven release API, and service_recovery_credit named for the goodwill grantMay 31deliveryClosing the loop on the grant prod defect; the failure was Delivery's own person_id precondition (Revenue was never called), it is fixed by resolving person_id from the participant plus a non-fatal grant, the trigger-versus-execute ownership split is confirmed, and Delivery is backfilling the make-up credits that lapsedMay 31deliveryDelivery confirms reservation_id is available on the no-show settlement path and asks Revenue to ship the reservation_id-keyed grant; Delivery will migrate the grant call to pass reservation_id and drop its person_id resolution once that path is live, so the make-up credit ties to the released reservation rather than a caller-supplied person_idMay 31deliveryA coach no-show was crashing in Delivery prod because the service_recovery_credit grant call was fatal to the outcome flow; Delivery has shipped a non-fatal fix, and we need Revenue to confirm the grant endpoint's prod status, formalize it as a contract sub-spec, and pin trigger ownershipMay 31deliveryDelivery adopts the service-recovery-credit-grant-api sub-spec as the contract of record and confirms it covers Delivery's needs; the reservation_id migration stays gated on Revenue's reservation_id-live notify per section 10, and Delivery will not dual-send person_id in the interim because the section 3 reservation_person_mismatch guard makes that the wrong interim postureMay 31financeFinance closes its open recognition loop on the coach no-show thread; the suppression treatment for non-cash-backed service-recovery credits stands unchanged, today's prod-defect and grant-endpoint work does not touch recognition, and Finance withdraws the gating expectation on its 2026-05-24 memo so Revenue can scope the credit-class mechanics in its own ADR without owing Finance a blocking replyMay 31revenueRevenue confirms the reservation_id-keyed service-recovery grant direction Delivery adopted; Revenue will ship the additive reservation_id-primary resolution (person_id optional, source_lesson_id unchanged as idempotency key) and document reservation_id as the primary account-resolution key in the grant-api sub-spec, and will notify on this thread when it is liveMay 31revenueThe service-recovery grant endpoint is live in prod; every 4xx rejection is returned in-band so the real reason is in Delivery's own response, Revenue agrees to formalize it as a Revenue-owned sub-spec under credit-reservation-lock, and the trigger-versus-execute ownership split mirrors the release path (Delivery triggers, Revenue executes, operator-gated not auto-on-no-show)May 31revenueThe service-recovery-credit-grant-api sub-spec is landed under credit-reservation-lock with reservation_id documented as the primary account-resolution key from the start; the person_id path stays live and the reservation_id resolution remains the additive path Revenue ships and will notify on when live

View source on GitHub