Skip to content

Commit d90c3fe

Browse files
(SP:1) [SHOP] add DB guardrails for shipping/payment invariants
1 parent 7cc67b8 commit d90c3fe

10 files changed

Lines changed: 7277 additions & 102 deletions

frontend/db/schema/shop.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,19 @@ export const orders = pgTable(
325325
'orders_shipping_payer_present_when_required_chk',
326326
sql`${table.shippingRequired} IS DISTINCT FROM TRUE OR ${table.shippingPayer} IS NOT NULL`
327327
),
328+
check(
329+
'orders_terminal_shipping_status_chk',
330+
sql`
331+
(
332+
${table.paymentStatus} NOT IN ('failed', 'refunded')
333+
AND ${table.status} NOT IN ('CANCELED', 'INVENTORY_FAILED')
334+
)
335+
OR (
336+
${table.shippingStatus} IS NULL
337+
OR ${table.shippingStatus} IN ('cancelled', 'delivered')
338+
)
339+
`
340+
),
328341
check(
329342
'orders_intl_provider_restriction_chk',
330343
sql`${table.fulfillmentMode} <> 'intl' OR ${table.paymentProvider} in ('stripe', 'none')`
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
update shipping_shipments s
2+
set status = 'needs_attention',
3+
lease_owner = null,
4+
lease_expires_at = null,
5+
next_attempt_at = null,
6+
last_error_code = coalesce(s.last_error_code, 'ORDER_NOT_FULFILLABLE'),
7+
last_error_message = coalesce(
8+
s.last_error_message,
9+
'Shipping pipeline closed by DB backfill: order is not shippable.'
10+
),
11+
updated_at = now()
12+
from orders o
13+
where o.id = s.order_id
14+
and s.status in ('queued', 'processing')
15+
and (
16+
o.shipping_required is not true
17+
or o.payment_status <> 'paid'
18+
or o.status <> 'PAID'
19+
or o.inventory_status <> 'reserved'
20+
);
21+
--> statement-breakpoint
22+
23+
update orders o
24+
set shipping_status = 'cancelled'::shipping_status,
25+
updated_at = now()
26+
where (
27+
o.payment_status in ('failed', 'refunded')
28+
or o.status in ('CANCELED', 'INVENTORY_FAILED')
29+
)
30+
and o.shipping_status is not null
31+
and o.shipping_status not in ('cancelled'::shipping_status, 'delivered'::shipping_status);
32+
--> statement-breakpoint
33+
34+
alter table "orders"
35+
add constraint "orders_terminal_shipping_status_chk"
36+
check (
37+
(
38+
"orders"."payment_status" not in ('failed', 'refunded')
39+
and "orders"."status" not in ('CANCELED', 'INVENTORY_FAILED')
40+
)
41+
or (
42+
"orders"."shipping_status" is null
43+
or "orders"."shipping_status" in ('cancelled', 'delivered')
44+
)
45+
)
46+
not valid;
47+
--> statement-breakpoint
48+
49+
alter table "orders"
50+
validate constraint "orders_terminal_shipping_status_chk";
51+
--> statement-breakpoint
52+
53+
create or replace function shop_orders_close_shipping_pipeline_guardrail()
54+
returns trigger
55+
language plpgsql
56+
as $$
57+
declare
58+
was_shippable boolean := false;
59+
is_shippable boolean := false;
60+
is_terminal boolean := false;
61+
begin
62+
is_shippable := (
63+
new.shipping_required is true
64+
and new.payment_status = 'paid'
65+
and new.status = 'PAID'
66+
and new.inventory_status = 'reserved'
67+
);
68+
69+
if tg_op = 'UPDATE' then
70+
was_shippable := (
71+
old.shipping_required is true
72+
and old.payment_status = 'paid'
73+
and old.status = 'PAID'
74+
and old.inventory_status = 'reserved'
75+
);
76+
end if;
77+
78+
is_terminal := (
79+
new.payment_status in ('failed', 'refunded')
80+
or new.status in ('CANCELED', 'INVENTORY_FAILED')
81+
);
82+
83+
if is_terminal or (was_shippable and not is_shippable) then
84+
if new.shipping_status is not null
85+
and new.shipping_status not in ('cancelled', 'delivered')
86+
then
87+
new.shipping_status := 'cancelled'::shipping_status;
88+
end if;
89+
90+
update shipping_shipments s
91+
set status = 'needs_attention',
92+
lease_owner = null,
93+
lease_expires_at = null,
94+
next_attempt_at = null,
95+
last_error_code = coalesce(s.last_error_code, 'ORDER_NOT_FULFILLABLE'),
96+
last_error_message = coalesce(
97+
s.last_error_message,
98+
'Shipping pipeline closed by DB guardrail: order became non-shippable.'
99+
),
100+
updated_at = now()
101+
where s.order_id = new.id
102+
and s.status in ('queued', 'processing');
103+
end if;
104+
105+
return new;
106+
end;
107+
$$;
108+
--> statement-breakpoint
109+
110+
drop trigger if exists trg_orders_close_shipping_pipeline_guardrail on orders;
111+
--> statement-breakpoint
112+
113+
create trigger trg_orders_close_shipping_pipeline_guardrail
114+
before update of payment_status, status, inventory_status, shipping_required
115+
on orders
116+
for each row
117+
execute function shop_orders_close_shipping_pipeline_guardrail();
118+
--> statement-breakpoint
119+
120+
create or replace function shop_shipping_shipments_require_shippable_order_guardrail()
121+
returns trigger
122+
language plpgsql
123+
as $$
124+
begin
125+
if new.status in ('queued', 'processing') then
126+
if not exists (
127+
select 1
128+
from orders o
129+
where o.id = new.order_id
130+
and o.shipping_required is true
131+
and o.payment_status = 'paid'
132+
and o.status = 'PAID'
133+
and o.inventory_status = 'reserved'
134+
) then
135+
raise exception
136+
using errcode = '23514',
137+
constraint = 'shipping_shipments_shippable_order_chk',
138+
message = 'shipping_shipments queued/processing rows require a shippable paid order';
139+
end if;
140+
end if;
141+
142+
return new;
143+
end;
144+
$$;
145+
--> statement-breakpoint
146+
147+
drop trigger if exists trg_shipping_shipments_require_shippable_order_guardrail on shipping_shipments;
148+
--> statement-breakpoint
149+
150+
create trigger trg_shipping_shipments_require_shippable_order_guardrail
151+
before insert or update of status, order_id
152+
on shipping_shipments
153+
for each row
154+
execute function shop_shipping_shipments_require_shippable_order_guardrail();

0 commit comments

Comments
 (0)