Skip to content

Commit db7efe3

Browse files
alexpmicheletclaude
andcommitted
fix(delivery): confirm delivery order reads pricingSnapshot from payment row
confirmDeliveryOrderOnCourseCreated gated on order.pricingSnapshot, which is undefined for a delivery order until confirmation freezes it -> the gate self-blocked and paid delivery orders never reached "nouvelle" even with a created Uber course. Source the snapshot from the payments row (where recordPaymentIntent persisted it), like the pickup path does. Test made realistic: a delivery order carries no order.pricingSnapshot pre-confirm (only the payment row does), so the suite now catches this gate bug. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b4e079d commit db7efe3

2 files changed

Lines changed: 29 additions & 7 deletions

File tree

packages/backend/convex/lib/delivery/course.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,15 @@ async function seedOrderWithDelivery(
110110
...(customerContact?.phone !== undefined
111111
? { customerPhone: customerContact.phone }
112112
: {}),
113-
pricingSnapshot: PRICING,
113+
// Realistic: a DELIVERY order carries NO frozen snapshot before it is
114+
// confirmed (the snapshot lives on the `payments` row until
115+
// `confirmDeliveryOrderOnCourseCreated` freezes it onto the order). Only
116+
// pickup is already confirmed here. Setting it on the delivery order would
117+
// mask the gate bug this suite must catch (#108 confirm reads the payment).
114118
createdAt: now,
115-
...(mode === "click_collect" ? { paidAt: now } : {}),
119+
...(mode === "click_collect"
120+
? { pricingSnapshot: PRICING, paidAt: now }
121+
: {}),
116122
});
117123
await ctx.db.insert("payments", {
118124
tenantId,

packages/backend/convex/lib/delivery/course.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getTenantById,
1313
getTenantDeliveryByOrder,
1414
getTenantOrder,
15+
getTenantPaymentByOrder,
1516
patchTenantDelivery,
1617
readCustomerFicheById,
1718
requireTenantOrder,
@@ -76,7 +77,9 @@ export const readCourseInputs = internalQuery({
7677
const order = await getTenantOrder(ctx, args.tenantId, args.orderId);
7778
const tenant = await getTenantById(ctx, args.tenantId);
7879
const customer =
79-
order !== null ? await readCustomerFicheById(ctx, order.customerId) : null;
80+
order !== null
81+
? await readCustomerFicheById(ctx, order.customerId)
82+
: null;
8083
return {
8184
delivery,
8285
order,
@@ -131,8 +134,11 @@ export const applyCourseResult = internalMutation({
131134
* seam (which guards the state machine + records the customer aggregates). IDEMPOTENT:
132135
* a re-trigger after the order is already `nouvelle` (or any non-pending state) is a
133136
* clean no-op — the `confirmTenantOrderPayment` guard requires `en attente de
134-
* paiement`, so we check first and skip rather than throw. Reads the frozen pricing
135-
* from the order itself (set at checkout/payment). System-side (no actor); the
137+
* paiement`, so we check first and skip rather than throw. The frozen pricing is
138+
* read from the `payments` row (where 2.5-B `recordPaymentIntent` persisted it) —
139+
* NOT from `order.pricingSnapshot`, which is undefined until THIS confirmation
140+
* writes it (a delivery order carries no snapshot before it is confirmed, so
141+
* reading it from the order self-blocked the gate). System-side (no actor); the
136142
* tenant is structural. Returns whether it confirmed on this call.
137143
*/
138144
export const confirmDeliveryOrderOnCourseCreated = internalMutation({
@@ -143,12 +149,22 @@ export const confirmDeliveryOrderOnCourseCreated = internalMutation({
143149
// Only the gated pending state transitions; anything else (already confirmed on
144150
// a prior run, or aborted) is left untouched — idempotent, never a double count.
145151
if (order.status !== "en attente de paiement") return { confirmed: false };
146-
if (order.pricingSnapshot === undefined) return { confirmed: false };
152+
// The immutable charged amount lives on the `payments` row (set at
153+
// `recordPaymentIntent`), not on the order (which only gets its frozen
154+
// snapshot HERE, via `confirmTenantOrderPayment`). Source it from there.
155+
const payment = await getTenantPaymentByOrder(
156+
ctx,
157+
args.tenantId,
158+
args.orderId,
159+
);
160+
if (payment === null || payment.pricingSnapshot === undefined) {
161+
return { confirmed: false };
162+
}
147163
await confirmTenantOrderPayment(
148164
ctx,
149165
args.tenantId,
150166
args.orderId,
151-
order.pricingSnapshot,
167+
payment.pricingSnapshot,
152168
);
153169
return { confirmed: true };
154170
},

0 commit comments

Comments
 (0)