Credit-order offerings are not scoped to the credit account's org-market, and Delivery has no authoritative market to scope by
Why
In the Delivery workbench "Create credit order" drawer, the Package dropdown shows offerings from more than one market at once. Operators see the same package label repeated (for example, "4 Lesson Package (30 minutes each)" and "8 Lesson Package (30 minutes each)" each appear twice, and "Single Lesson (30 minutes)" and "Trial Lesson (30 minutes)" each appear twice) because the list mixes catalogs from multiple markets. The correct behavior is that the dropdown should only show offerings belonging to the org-market of the credit account being charged.
The drawer reads offerings from Revenue's GET /api/v1/orders/credit-purchases and creates orders through POST /api/v1/orders/credit-purchases, both of which key on platform_market_id. The problem is that Delivery has no reliable, authoritative way to know which market the credit account belongs to, so it cannot pass the right platform_market_id, and the fallback scoping leaks the full cross-market catalog.
What is happening in Delivery today
Delivery fetches offerings server-side with the page-level market filter (the ?market= URL param). When the operator has not selected a specific market (the common "all" case), Delivery sends no platform_market_id and Revenue returns the union of every market's offerings. Delivery then tries to narrow that list client-side using the customer card's market, which is derived from the customer's upcoming-lesson service area (or a single-market heuristic), not from the credit account. That client-side filter has three leaks we will close on our side regardless of this memo's outcome:
- Offerings with a null
platform_market_idalways pass the filter. - When no offering matches the card's market, the code falls back to showing the entire catalog rather than an empty list.
- The scoping key is the lesson-context market, which is not guaranteed to equal the credit account's market.
We can tighten leaks 1 and 2 unilaterally. Leak 3 is the substantive one and it is not something Delivery can fix alone: the authoritative market lives in Revenue, which owns both the offering catalog and the credit account. Delivery's readRevenueCreditAccountCoverage call (GET /api/v1/credit-accounts/coverage) is already part of the drawer's data load, and it returns credit_account_id, balances, and reservation state, but no market. So even after we harden the client filter, Delivery still has no trustworthy market to scope by.
Asks
We would like Revenue to decide which of these it prefers, since Revenue owns the surface and the offering-to-market policy:
Option A (smaller, Delivery does the scoping): add the credit account's platform_market_id to the GET /api/v1/credit-accounts/coverage response. Delivery already calls coverage in this flow, so one extra field gives us the authoritative market; we then pass it into the offerings fetch and scope cleanly. Revenue's contract surface grows by one read-only field.
Option B (Revenue does the scoping): let GET /api/v1/orders/credit-purchases accept person_id or credit_account_id and resolve the market server-side, returning only that account's offerings. This centralizes the offering-to-market policy in Revenue (which is arguably where it belongs) and removes the need for Delivery to pass platform_market_id at all, but it is a larger change to the offerings endpoint contract.
Delivery's recommendation is Option A as the primary path: it is the minimal change, it reuses a call we already make, and it keeps Delivery responsible for its own UI scoping. We are comfortable with Option B if Revenue would rather own the scoping policy end to end; in that case we would drop our platform_market_id plumbing.
Either option likely warrants formalizing these two endpoints (coverage and credit-purchases), which are consumed by Delivery but not yet captured as a sub-spec, under the credit-reservation-lock or order-flow contract. We are happy to co-author that sub-spec once the shape is settled.
Once Revenue exposes an authoritative market by either option, Delivery will wire the drawer to scope offerings by it and remove the null-market passthrough and the fall-back-to-all behavior described above. We are not putting that on the ledger yet because it is gated on Revenue's choice of shape; we will declare it against the agreed contract version when this thread converges.
References
- Delivery consumer:
lib/revenue/reservation-client.ts(listRevenueCreditPurchaseOfferings,readRevenueCreditAccountCoverage). - Delivery drawer scoping:
app/workbench/page.tsx(server fetch withselectedPlatformMarketId) andapp/workbench/workbench-page.client.tsx(creditPurchaseOfferingsForCard). - Contract home:
contracts/credit-reservation-lock/andcontracts/order-flow/.