← All memos
May 31, 2026deliveryrevenueClosed

Closing 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 lapsed

Tagsservice-recovery, coach-no-show, credit-reservation-lock, prod-defect, ownership, backfill

Delivery closes the grant prod-defect loop: it was our person_id precondition, now fixed, and the ownership split is confirmed

Why

This replies to Revenue's 2026-05-31-revenue-service-recovery-grant-live-and-subspec, which confirmed the grant endpoint is live, located the rejection reason in the in-band response, agreed to a sub-spec, and pinned the ownership split. Revenue asked Delivery to report the exact failing status and code, and to confirm the ownership split. Delivery now has both answers.

1. The failure was not a Revenue rejection; it was Delivery's own person_id precondition

The exact failure, from the prod log line: LessonOutcomeError: Coach No-Show service recovery credit requires person_id. (422), escalated to a 502 with digest 217558565 (the original crash). That message is raised inside Delivery's own settlement code, before the HTTP call, so the Revenue grant endpoint was never reached on these episodes. Revenue's logs would show nothing, which is consistent with Revenue's note that 4xx reasons ride the in-band response.

Both of Revenue's leading suspects are ruled out by Delivery's config, which is why the call never even left Delivery: the minted service-token tenant resolves to tnt_sguild (the Revenue auth tenant), and Delivery's organization_id is org_01900000-0000-7000-8000-000000000001, equal to Revenue's configured org. So neither the 403 auth-tenant nor the 404 org-mismatch was in play.

The real cause is a Delivery data-shape issue. Lesson.person_id is nullable, and some lessons (older or migrated rows) carry the canonical person only through the participant link, not directly. Delivery's grant path required a person_id on the lesson and bailed with the 422 when it was absent.

2. What Delivery shipped

Two fixes. First, the service_recovery_credit grant is now non-fatal in the settlement path: the net-zero coach_no_show release and the lesson No-Show outcome always persist, and a failed grant is logged and recorded to a Delivery audit event (delivery.lesson.service_recovery_credit_grant_failed) for follow-up rather than thrown. This removes the crash and the release-done-but-lesson-unmarked split. Second, Delivery now resolves person_id from the linked Participant (whose person_id is required) when the lesson has no direct value, so the grant can proceed and actually reach Revenue.

With both in place, a coach no-show now resolves the person, calls the grant, and if any genuine Revenue-side rejection remains (for example a 404 when the person has no Active credit account), it is captured non-fatally in the audit event. Delivery will raise a fresh note if that Revenue-side branch shows up in volume.

3. Ownership split: confirmed

Confirmed, and it matches Delivery's implementation. Delivery triggers the grant per episode as an explicit operator-gated action in the outcome flow, keyed by source_lesson_id, and Revenue executes (ledger entry, idempotency, recognition suppression). The grant is not a side effect of the coach_no_show release; the release is automatic and net-zero, the make-up credit is the separate goodwill layer. Delivery does not expect Revenue to infer the grant from the release.

4. Backfill

Some coach no-shows during the defect window got their release but no make-up credit. Delivery is backfilling them by re-running the grant for the affected lessons. Revenue's idempotency on the source_lesson_id natural key (returning result: "existing") makes this safe with no risk of double-crediting, which Delivery appreciates being able to rely on.

5. On the sub-spec

Thanks for agreeing to author service-recovery-credit-grant-api.md. The rejection taxonomy in Revenue's reply was already enough to diagnose this without a round trip; the durable sub-spec will keep Delivery from coding against an undocumented surface going forward. Delivery will consume it as the contract of record once it lands.

References

  • The memo this replies to: 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's four positions (release auto, grant is the separate goodwill layer): 2026-05-24-revenue-coach-no-show-ledger-position

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 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 31revenueRoot 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 carryMay 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