@openlup/platform 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +327 -269
  2. package/README.md +123 -12
  3. package/dist/communications/contracts/subscriptionPaymentExpired.d.ts +3 -2
  4. package/dist/communications/contracts/subscriptionPaymentExpired.js +1 -1
  5. package/dist/communications/contracts/subscriptionPaymentFailed.d.ts +4 -3
  6. package/dist/communications/contracts/subscriptionPaymentFailed.js +4 -5
  7. package/dist/composition/index.d.ts +1 -0
  8. package/dist/composition/index.js +1 -0
  9. package/dist/composition/subscriptionContribution.d.ts +18 -0
  10. package/dist/composition/subscriptionContribution.js +19 -0
  11. package/dist/fulfillment/contracts/dispatch.d.ts +4 -0
  12. package/dist/fulfillment/dispatchProjection.d.ts +1 -1
  13. package/dist/fulfillment/dispatchProjection.js +4 -2
  14. package/dist/fulfillment/omnipack/client.js +14 -6
  15. package/dist/fulfillment/omnipack/index.js +5 -2
  16. package/dist/fulfillment/providerDispatchWorker.d.ts +38 -0
  17. package/dist/fulfillment/providerDispatchWorker.js +19 -1
  18. package/dist/inventory/index.d.ts +2 -0
  19. package/dist/inventory/index.js +1 -0
  20. package/dist/inventory/postgresStockAvailability.d.ts +24 -0
  21. package/dist/inventory/postgresStockAvailability.js +48 -0
  22. package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
  23. package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
  24. package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
  25. package/dist/invoicing/accountingLineAllocation.js +69 -0
  26. package/dist/invoicing/adapterConformance.d.ts +42 -0
  27. package/dist/invoicing/adapterConformance.js +296 -0
  28. package/dist/invoicing/artifacts.d.ts +7 -0
  29. package/dist/invoicing/artifacts.js +20 -0
  30. package/dist/invoicing/bindings.d.ts +46 -0
  31. package/dist/invoicing/bindings.js +69 -0
  32. package/dist/invoicing/channelPolicy.d.ts +28 -0
  33. package/dist/invoicing/channelPolicy.js +27 -0
  34. package/dist/invoicing/continuity.d.ts +12 -0
  35. package/dist/invoicing/continuity.js +18 -0
  36. package/dist/invoicing/contracts/facts.d.ts +11 -1
  37. package/dist/invoicing/contracts/host.d.ts +3 -4
  38. package/dist/invoicing/contracts/index.d.ts +1 -1
  39. package/dist/invoicing/contracts/store.d.ts +15 -5
  40. package/dist/invoicing/contribution.d.ts +21 -0
  41. package/dist/invoicing/contribution.js +19 -0
  42. package/dist/invoicing/correction.d.ts +42 -0
  43. package/dist/invoicing/correction.js +44 -0
  44. package/dist/invoicing/delivery.d.ts +85 -0
  45. package/dist/invoicing/delivery.js +79 -0
  46. package/dist/invoicing/documentHistory.d.ts +50 -0
  47. package/dist/invoicing/documentHistory.js +213 -0
  48. package/dist/invoicing/documentSync.d.ts +24 -0
  49. package/dist/invoicing/documentSync.js +26 -0
  50. package/dist/invoicing/dutyCollector.d.ts +15 -0
  51. package/dist/invoicing/dutyCollector.js +35 -0
  52. package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
  53. package/dist/invoicing/fulfillmentTrigger.js +90 -0
  54. package/dist/invoicing/handoff.d.ts +37 -0
  55. package/dist/invoicing/handoff.js +62 -0
  56. package/dist/invoicing/index.d.ts +38 -0
  57. package/dist/invoicing/index.js +36 -0
  58. package/dist/invoicing/issue.d.ts +39 -0
  59. package/dist/invoicing/issue.js +55 -0
  60. package/dist/invoicing/migrations.d.ts +48 -0
  61. package/dist/invoicing/migrations.js +25 -0
  62. package/dist/invoicing/nativeUnsupported.d.ts +9 -0
  63. package/dist/invoicing/nativeUnsupported.js +22 -0
  64. package/dist/invoicing/paymentReadback.d.ts +20 -0
  65. package/dist/invoicing/paymentReadback.js +47 -0
  66. package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
  67. package/dist/invoicing/postgresInvoiceStore.js +344 -0
  68. package/dist/invoicing/readModel.d.ts +74 -0
  69. package/dist/invoicing/readModel.js +90 -0
  70. package/dist/invoicing/readback.d.ts +4 -0
  71. package/dist/invoicing/readback.js +30 -0
  72. package/dist/invoicing/reversal.d.ts +37 -0
  73. package/dist/invoicing/reversal.js +28 -0
  74. package/dist/invoicing/sellerRevision.d.ts +19 -0
  75. package/dist/invoicing/sellerRevision.js +14 -0
  76. package/dist/invoicing/settlementSync.d.ts +18 -0
  77. package/dist/invoicing/settlementSync.js +32 -0
  78. package/dist/invoicing/snapshot.d.ts +52 -0
  79. package/dist/invoicing/snapshot.js +141 -0
  80. package/dist/subscriptions/commandExamples.d.ts +6 -0
  81. package/dist/subscriptions/commandExamples.js +20 -0
  82. package/dist/subscriptions/contracts/commands.d.ts +8 -1
  83. package/dist/subscriptions/contracts/index.d.ts +2 -2
  84. package/dist/subscriptions/contracts/stockWait.d.ts +12 -1
  85. package/dist/subscriptions/index.d.ts +22 -4
  86. package/dist/subscriptions/index.js +22 -3
  87. package/dist/subscriptions/lifecycleMessages.d.ts +9 -0
  88. package/dist/subscriptions/lifecycleMessages.js +38 -0
  89. package/dist/subscriptions/paymentOutcomes.js +1 -1
  90. package/dist/subscriptions/postgresCommandStore.d.ts +44 -0
  91. package/dist/subscriptions/postgresCommandStore.js +172 -0
  92. package/dist/subscriptions/postgresRetryStore.d.ts +6 -0
  93. package/dist/subscriptions/postgresRetryStore.js +11 -0
  94. package/dist/subscriptions/postgresStockWaitStore.d.ts +62 -0
  95. package/dist/subscriptions/postgresStockWaitStore.js +174 -0
  96. package/dist/subscriptions/services.d.ts +31 -0
  97. package/dist/subscriptions/services.js +15 -0
  98. package/package.json +3 -3
  99. package/release-gates.json +21 -7
  100. package/sql/20260809210000_accounting_document_rail.sql +889 -0
  101. package/sql/20260811150000_accounting_payout_rail.sql +333 -0
  102. package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
  103. package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
  104. package/sql/20261022191000_invoicing_document_effects.sql +249 -0
  105. package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
  106. package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
  107. package/sql/20261024170000_subscription_command_lifecycle.sql +404 -0
  108. package/sql/20261024180000_subscription_stock_wait.sql +330 -0
  109. package/sql/20261024183000_inventory_available_to_promise.sql +70 -0
  110. package/sql/20261024184000_subscription_renewal_reserve_uses_seam.sql +147 -0
  111. package/sql/20261024184500_subscription_retry_late_positive_stock.sql +59 -0
  112. package/sql/20261024184600_subscription_retry_candidate_payment_filter.sql +22 -0
  113. package/src/communications/contracts/subscriptionPaymentExpired.ts +4 -3
  114. package/src/communications/contracts/subscriptionPaymentFailed.ts +8 -8
  115. package/src/composition/index.ts +1 -0
  116. package/src/composition/subscriptionContribution.ts +25 -0
  117. package/src/fulfillment/contracts/dispatch.ts +4 -0
  118. package/src/fulfillment/dispatchProjection.ts +4 -2
  119. package/src/fulfillment/omnipack/client.ts +13 -6
  120. package/src/fulfillment/omnipack/index.ts +5 -2
  121. package/src/fulfillment/providerDispatchWorker.ts +14 -1
  122. package/src/inventory/index.ts +2 -0
  123. package/src/inventory/postgresStockAvailability.ts +54 -0
  124. package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
  125. package/src/invoicing/accountingLineAllocation.ts +69 -0
  126. package/src/invoicing/adapterConformance.ts +249 -0
  127. package/src/invoicing/artifacts.ts +19 -0
  128. package/src/invoicing/bindings.ts +80 -0
  129. package/src/invoicing/channelPolicy.ts +50 -0
  130. package/src/invoicing/continuity.ts +15 -0
  131. package/src/invoicing/contracts/facts.ts +12 -1
  132. package/src/invoicing/contracts/host.ts +3 -4
  133. package/src/invoicing/contracts/index.ts +1 -1
  134. package/src/invoicing/contracts/store.ts +11 -7
  135. package/src/invoicing/contribution.ts +28 -0
  136. package/src/invoicing/correction.ts +56 -0
  137. package/src/invoicing/delivery.ts +122 -0
  138. package/src/invoicing/documentHistory.ts +301 -0
  139. package/src/invoicing/documentSync.ts +51 -0
  140. package/src/invoicing/dutyCollector.ts +31 -0
  141. package/src/invoicing/fulfillmentTrigger.ts +144 -0
  142. package/src/invoicing/handoff.ts +71 -0
  143. package/src/invoicing/index.ts +38 -0
  144. package/src/invoicing/issue.ts +64 -0
  145. package/src/invoicing/migrations.ts +25 -0
  146. package/src/invoicing/nativeUnsupported.ts +24 -0
  147. package/src/invoicing/paymentReadback.ts +70 -0
  148. package/src/invoicing/postgresInvoiceStore.ts +334 -0
  149. package/src/invoicing/readModel.ts +80 -0
  150. package/src/invoicing/readback.ts +26 -0
  151. package/src/invoicing/reversal.ts +34 -0
  152. package/src/invoicing/sellerRevision.ts +20 -0
  153. package/src/invoicing/settlementSync.ts +37 -0
  154. package/src/invoicing/snapshot.ts +192 -0
  155. package/src/subscriptions/commandExamples.ts +19 -0
  156. package/src/subscriptions/contracts/commands.ts +35 -2
  157. package/src/subscriptions/contracts/index.ts +2 -2
  158. package/src/subscriptions/contracts/stockWait.ts +14 -2
  159. package/src/subscriptions/index.ts +30 -3
  160. package/src/subscriptions/lifecycleMessages.ts +37 -0
  161. package/src/subscriptions/paymentOutcomes.ts +1 -1
  162. package/src/subscriptions/postgresCommandStore.ts +183 -0
  163. package/src/subscriptions/postgresRetryStore.ts +11 -0
  164. package/src/subscriptions/postgresStockWaitStore.ts +170 -0
  165. package/src/subscriptions/services.ts +21 -0
@@ -0,0 +1,330 @@
1
+ -- Frozen stock waits use native M06 availability and reservations. Resumed means
2
+ -- paid realization admitted, never parcel arrival. No background consumer,
3
+ -- payment executor, pause, replacement confirmation or automatic delivery fact.
4
+ -- Disable the wait driver before rollback and retain captured obligations.
5
+ -- Donor: supabase/migrations/20261003123000_subscription_stock_wait.sql (87-126,
6
+ -- 260-298, 628-650), 20261003142320_subscription_stock_wait_unpaid_discovery.sql
7
+ -- (20-23, 116-195). Uncaptured selection rotates on operation.updated_at.
8
+ -- Native frozen recovery is pre-cycle only: operation snapshots
9
+ -- replace the managed unpaid cycle/order/payment graph; provider-owned graphs refuse.
10
+ CREATE TABLE public.subscription_stock_wait_cases (
11
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
12
+ subscription_id uuid NOT NULL REFERENCES public.subscriptions(id),
13
+ client_id uuid NOT NULL REFERENCES public.clients(id),
14
+ renewal_operation_id uuid NOT NULL UNIQUE REFERENCES public.subscription_renewal_operations(id),
15
+ scheduled_at timestamptz NOT NULL,
16
+ cycle_number integer NOT NULL CHECK (cycle_number>0),
17
+ state text NOT NULL CHECK (state IN ('waiting','stock_secured','resumed','closed')),
18
+ due_snapshot jsonb NOT NULL,
19
+ cycle_snapshot jsonb NOT NULL,
20
+ fingerprint text NOT NULL CHECK (fingerprint ~ '^[0-9a-f]{64}$'),
21
+ identity_key text NOT NULL,
22
+ deficits jsonb NOT NULL CHECK (jsonb_typeof(deficits)='array'),
23
+ notice_messages jsonb NOT NULL DEFAULT '{}',
24
+ refusal_observations jsonb NOT NULL DEFAULT '[]',
25
+ cycle_id uuid REFERENCES public.subscription_cycles(id),
26
+ order_id uuid REFERENCES public.commerce_orders(id),
27
+ fulfillment_id uuid REFERENCES public.fulfillment_shipments(id),
28
+ opened_at timestamptz NOT NULL DEFAULT now(),
29
+ last_checked_at timestamptz NOT NULL DEFAULT now(),
30
+ last_selected_at timestamptz,
31
+ stock_secured_at timestamptz,
32
+ resumed_at timestamptz,
33
+ closed_at timestamptz,
34
+ block_reason text,
35
+ CONSTRAINT subscription_stock_wait_schedule_key UNIQUE(subscription_id,scheduled_at)
36
+ );
37
+ -- squawk-ignore require-concurrent-index-creation
38
+ CREATE INDEX subscription_stock_wait_candidates_idx ON public.subscription_stock_wait_cases(state,last_selected_at,id);
39
+ REVOKE ALL ON TABLE public.subscription_stock_wait_cases FROM PUBLIC, anon, authenticated;
40
+ CREATE FUNCTION public.subscription_stock_wait_projection(p_case public.subscription_stock_wait_cases)
41
+ RETURNS jsonb LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $$
42
+ SELECT to_jsonb(p_case)-'due_snapshot'-'cycle_snapshot'-'notice_messages';
43
+ $$;
44
+ CREATE FUNCTION public.subscription_stock_wait_read(p_case_id uuid)
45
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
46
+ SELECT public.subscription_stock_wait_projection(c)||jsonb_build_object('waitingNoticeRelevant',s.status='active' AND s.next_cycle_at=c.scheduled_at)
47
+ FROM public.subscription_stock_wait_cases c JOIN public.subscriptions s ON s.id=c.subscription_id WHERE c.id=p_case_id;
48
+ $$;
49
+ CREATE FUNCTION public.subscription_stock_wait_frozen(p_subscription_id uuid,p_scheduled_at timestamptz)
50
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
51
+ SELECT jsonb_build_object('caseId',c.id,'operationId',op.id,'identityKey',op.identity_key,
52
+ 'operationFingerprint',op.operation_fingerprint,'snapshot',op.cycle_snapshot,'cycleNumber',op.cycle_number,
53
+ 'binding',jsonb_build_object('caseId',c.id,'cycleId',cycle.id,'orderId',ord.id,'paymentId',intent.id,'engineIdempotencyKey',op.identity_key))
54
+ FROM public.subscription_renewal_operations op
55
+ LEFT JOIN public.subscription_stock_wait_cases c ON c.renewal_operation_id=op.id
56
+ LEFT JOIN public.subscription_cycles cycle ON cycle.renewal_operation_id=op.id
57
+ LEFT JOIN public.commerce_orders ord ON ord.renewal_operation_id=op.id
58
+ LEFT JOIN public.commerce_settlement_intents intent ON intent.renewal_operation_id=op.id
59
+ WHERE op.subscription_id=p_subscription_id AND op.scheduled_at=p_scheduled_at
60
+ AND EXISTS(SELECT 1 FROM public.subscriptions s WHERE s.id=op.subscription_id
61
+ AND s.status='active' AND s.next_cycle_at=p_scheduled_at)
62
+ AND op.attempt_prepared_at IS NULL AND op.external_attempt_ref IS NULL
63
+ AND cycle.id IS NULL AND ord.id IS NULL AND intent.id IS NULL
64
+ AND (c.id IS NULL OR c.state IN ('waiting','stock_secured'))
65
+ AND NOT EXISTS(SELECT 1 FROM public.subscription_renewal_outcomes outcome WHERE outcome.renewal_operation_id=op.id
66
+ AND (outcome.outcome<>'refused' OR outcome.reason NOT IN ('inventory_unavailable','reservation_expired') OR outcome.external_ref IS NOT NULL));
67
+ $$;
68
+
69
+ -- Credit the operation's live commitment exactly once, subtract every other
70
+ -- commitment, and preserve the complete frozen basket in the wait ledger.
71
+ CREATE FUNCTION public.subscription_stock_wait_deficits(p_snapshot jsonb,p_operation_id uuid,p_now timestamptz)
72
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
73
+ DECLARE line record; stock public.fulfillment_stock_current; required bigint; own_qty bigint;
74
+ available bigint; promise jsonb; result jsonb:='[]'; source text:=p_snapshot->>'stockSourceKey';
75
+ BEGIN
76
+ IF jsonb_typeof(p_snapshot->'lines') IS DISTINCT FROM 'array' OR jsonb_array_length(p_snapshot->'lines')=0 OR source IS NULL THEN
77
+ RAISE EXCEPTION 'stock_wait_invalid_basket' USING ERRCODE='22023'; END IF;
78
+ PERFORM s.sku FROM public.fulfillment_stock_current s JOIN jsonb_array_elements(p_snapshot->'lines') l ON l->>'sku'=s.sku
79
+ WHERE s.source_key=source ORDER BY s.sku FOR UPDATE OF s;
80
+ FOR line IN SELECT l->>'sku' sku,sum((l->>'quantity')::integer) quantity FROM jsonb_array_elements(p_snapshot->'lines') l GROUP BY l->>'sku' ORDER BY l->>'sku' LOOP
81
+ required:=line.quantity;
82
+ IF required IS NULL OR required<1 OR btrim(COALESCE(line.sku,''))='' THEN RAISE EXCEPTION 'stock_wait_invalid_quantity'; END IF;
83
+ SELECT * INTO stock FROM public.fulfillment_stock_current WHERE source_key=source AND sku=line.sku;
84
+ IF stock.sku IS NULL THEN RAISE EXCEPTION 'stock_wait_stock_missing'; END IF;
85
+ IF stock.stale_after<=p_now THEN RAISE EXCEPTION 'stock_wait_stock_stale'; END IF;
86
+ SELECT COALESCE(sum(l.quantity),0) INTO own_qty
87
+ FROM public.fulfillment_inventory_reservations r JOIN public.fulfillment_inventory_reservation_lines l ON l.reservation_id=r.id
88
+ WHERE r.source_key=source AND l.sku=line.sku AND r.renewal_operation_id=p_operation_id
89
+ AND (r.status='committed' OR (r.status='held' AND r.expires_at>p_now));
90
+ promise:=public.inventory_available_to_promise(source,line.sku,p_now,true);
91
+ available:=CASE WHEN stock.inventory_class='sellable' THEN (promise->>'available_quantity')::bigint ELSE 0 END;
92
+ IF own_qty+available<required THEN
93
+ result:=result||jsonb_build_array(jsonb_build_object('sku',line.sku,'requiredUnits',required,'reservedUnits',own_qty,
94
+ 'availableUnits',available,'missingUnits',required-own_qty-available));
95
+ END IF;
96
+ END LOOP;
97
+ RETURN result;
98
+ END;
99
+ $$;
100
+ CREATE FUNCTION public.subscription_stock_wait_enqueue(p_case public.subscription_stock_wait_cases,p_kind text)
101
+ RETURNS void LANGUAGE plpgsql SET search_path = pg_catalog AS $$
102
+ BEGIN
103
+ IF p_kind NOT IN ('started','resumed') THEN RAISE EXCEPTION 'stock_wait_event_invalid'; END IF;
104
+ INSERT INTO public.outbox_events(aggregate_type,aggregate_id,event_type,idempotency_key,payload,metadata)
105
+ VALUES('subscription_stock_wait',p_case.id,'subscription_stock_wait.'||p_kind,'subscription_stock_wait:'||p_case.id::text||':'||p_kind,
106
+ jsonb_build_object('caseId',p_case.id,'clientId',p_case.client_id,'subscriptionId',p_case.subscription_id,
107
+ 'scheduledAt',p_case.scheduled_at,'deficits',p_case.deficits,'orderId',p_case.order_id),jsonb_build_object('source','subscription_stock_wait'))
108
+ ON CONFLICT(event_type,idempotency_key) DO NOTHING;
109
+ END;
110
+ $$;
111
+
112
+ -- This is a bounded stock-only reset. Stock-refusal observations are not payment
113
+ -- outcomes; any other outcome or business artifact refuses before mutation.
114
+ CREATE FUNCTION public.subscription_stock_wait_recover_refusal(p_operation_id uuid,p_fingerprint text,p_worker text,p_now timestamptz DEFAULT now())
115
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
116
+ DECLARE s public.subscriptions; op public.subscription_renewal_operations; c public.subscription_stock_wait_cases;
117
+ r public.fulfillment_inventory_reservations; deficits jsonb; token uuid:=gen_random_uuid();
118
+ BEGIN
119
+ SELECT * INTO s FROM public.subscriptions WHERE id=(SELECT subscription_id FROM public.subscription_renewal_operations WHERE id=p_operation_id) FOR UPDATE;
120
+ SELECT * INTO op FROM public.subscription_renewal_operations WHERE id=p_operation_id FOR UPDATE;
121
+ SELECT * INTO c FROM public.subscription_stock_wait_cases WHERE renewal_operation_id=p_operation_id FOR UPDATE;
122
+ SELECT * INTO r FROM public.fulfillment_inventory_reservations WHERE renewal_operation_id=p_operation_id FOR UPDATE;
123
+ IF op.id IS NULL OR s.status<>'active' OR s.next_cycle_at IS DISTINCT FROM op.scheduled_at OR
124
+ op.operation_fingerprint IS DISTINCT FROM p_fingerprint OR btrim(COALESCE(p_worker,''))='' OR
125
+ (c.id IS NOT NULL AND (c.fingerprint<>op.operation_fingerprint OR c.cycle_snapshot<>op.cycle_snapshot OR c.identity_key<>op.identity_key OR c.state='closed')) THEN
126
+ RAISE EXCEPTION 'stock_wait_binding_changed' USING ERRCODE='22023'; END IF;
127
+ IF c.state='stock_secured' AND op.state='reserved' AND r.status='held' AND r.expires_at>p_now AND op.lease_expires_at>p_now THEN
128
+ RETURN public.subscription_renewal_operation_response(op,false,true,'stock_secured'); END IF;
129
+ IF NOT COALESCE(((op.state='refused' AND op.terminal_reason IN ('inventory_unavailable','reservation_expired')) OR
130
+ (op.state='reserved' AND c.state='stock_secured' AND r.status='held')),false) OR op.lease_expires_at>p_now OR
131
+ op.attempt_prepared_at IS NOT NULL OR op.external_attempt_ref IS NOT NULL OR op.external_attempt_acknowledged_at IS NOT NULL OR
132
+ EXISTS(SELECT 1 FROM public.subscription_cycles WHERE renewal_operation_id=op.id) OR
133
+ EXISTS(SELECT 1 FROM public.commerce_orders WHERE renewal_operation_id=op.id) OR
134
+ EXISTS(SELECT 1 FROM public.commerce_settlement_intents WHERE renewal_operation_id=op.id) OR
135
+ EXISTS(SELECT 1 FROM public.subscription_renewal_outcomes WHERE renewal_operation_id=op.id AND
136
+ (outcome<>'refused' OR reason IS NULL OR reason NOT IN ('inventory_unavailable','reservation_expired') OR external_ref IS NOT NULL
137
+ OR operation_fingerprint<>op.operation_fingerprint OR event_key NOT IN (op.identity_key||':inventory',op.identity_key||':reservation-expired'))) OR
138
+ (r.id IS NOT NULL AND (r.operation_fingerprint<>p_fingerprint OR r.source_key IS DISTINCT FROM op.cycle_snapshot->>'stockSourceKey' OR (r.status NOT IN ('expired','released') AND NOT (r.status='held' AND c.state='stock_secured')))) THEN
139
+ RAISE EXCEPTION 'stock_wait_recovery_provider_owned_or_not_eligible' USING ERRCODE='22023'; END IF;
140
+ deficits:=public.subscription_stock_wait_deficits(op.cycle_snapshot,op.id,p_now);
141
+ INSERT INTO public.subscription_stock_wait_cases(subscription_id,client_id,renewal_operation_id,scheduled_at,cycle_number,state,due_snapshot,cycle_snapshot,fingerprint,identity_key,deficits,opened_at,last_checked_at)
142
+ VALUES(s.id,s.client_id,op.id,op.scheduled_at,op.cycle_number,'waiting',to_jsonb(op),op.cycle_snapshot,op.operation_fingerprint,op.identity_key,deficits,p_now,p_now)
143
+ ON CONFLICT(subscription_id,scheduled_at) DO UPDATE SET deficits=EXCLUDED.deficits,last_checked_at=p_now RETURNING * INTO c;
144
+ IF jsonb_array_length(deficits)>0 THEN
145
+ UPDATE public.subscription_stock_wait_cases SET state='waiting',block_reason='inventory_unavailable' WHERE id=c.id RETURNING * INTO c;
146
+ PERFORM public.subscription_stock_wait_enqueue(c,'started');
147
+ RETURN jsonb_build_object('waiting',true,'caseId',c.id,'deficits',deficits);
148
+ END IF;
149
+ -- All effects inside this block roll back on a stock shortage. The wait row
150
+ -- remains outside it; prior commitments are never released to try another SKU.
151
+ BEGIN
152
+ -- The base stock rail uses one local refusal key per schedule/reason. Archive
153
+ -- those exact non-payment receipts before clearing them for this bounded
154
+ -- reset, so a later expiry can reuse the original key without a unique-key
155
+ -- failure. Provider/money outcomes are rejected above and never removed.
156
+ UPDATE public.subscription_stock_wait_cases SET refusal_observations=refusal_observations||
157
+ COALESCE((SELECT jsonb_agg(to_jsonb(observation) ORDER BY occurred_at,id) FROM public.subscription_renewal_outcomes observation WHERE renewal_operation_id=op.id),'[]'::jsonb)
158
+ WHERE id=c.id;
159
+ DELETE FROM public.subscription_renewal_outcomes WHERE renewal_operation_id=op.id;
160
+ UPDATE public.subscription_renewal_operations SET state='claimed',terminal_outcome=NULL,terminal_reason=NULL,terminal_at=NULL,
161
+ claim_token=token,claim_generation=claim_generation+1,claimed_by=p_worker,claimed_at=p_now,lease_expires_at=p_now+interval '5 minutes',updated_at=p_now WHERE id=op.id;
162
+ IF r.id IS NULL THEN
163
+ IF NOT (public.fulfillment_reserve_subscription_renewal(op.id,token,900,p_now)->>'reserved')::boolean THEN
164
+ RAISE EXCEPTION 'stock_wait_atomic_shortage' USING ERRCODE='23514'; END IF;
165
+ ELSE
166
+ -- Restore exactly the expired reservation's original frozen lines.
167
+ IF (SELECT jsonb_agg(jsonb_build_object('sku',sku,'quantity',quantity) ORDER BY sku) FROM public.fulfillment_inventory_reservation_lines WHERE reservation_id=r.id)
168
+ IS DISTINCT FROM (SELECT jsonb_agg(jsonb_build_object('sku',l->>'sku','quantity',(l->>'quantity')::integer) ORDER BY l->>'sku') FROM jsonb_array_elements(op.cycle_snapshot->'lines') l) THEN
169
+ RAISE EXCEPTION 'stock_wait_reservation_snapshot_mismatch'; END IF;
170
+ UPDATE public.fulfillment_inventory_reservations SET status='held',expires_at=p_now+interval '15 minutes',terminal_reason=NULL,updated_at=p_now WHERE id=r.id;
171
+ UPDATE public.subscription_renewal_operations SET state='reserved' WHERE id=op.id;
172
+ END IF;
173
+ EXCEPTION WHEN check_violation THEN
174
+ IF SQLERRM<>'stock_wait_atomic_shortage' THEN RAISE; END IF;
175
+ UPDATE public.subscription_stock_wait_cases SET block_reason='inventory_unavailable',state='waiting' WHERE id=c.id;
176
+ RETURN jsonb_build_object('waiting',true,'caseId',c.id);
177
+ END;
178
+ UPDATE public.subscription_stock_wait_cases SET state='stock_secured',deficits='[]',block_reason=NULL,
179
+ stock_secured_at=COALESCE(stock_secured_at,p_now),last_checked_at=p_now WHERE id=c.id;
180
+ SELECT * INTO op FROM public.subscription_renewal_operations WHERE id=op.id;
181
+ RETURN public.subscription_renewal_operation_response(op,true,false,'stock_secured')||jsonb_build_object('waiting',false,'caseId',c.id);
182
+ END;
183
+ $$;
184
+ CREATE FUNCTION public.subscription_stock_wait_admit(p_input jsonb,p_due jsonb,p_now timestamptz DEFAULT now())
185
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
186
+ DECLARE frozen jsonb; op public.subscription_renewal_operations; result jsonb; c public.subscription_stock_wait_cases;
187
+ BEGIN
188
+ SELECT * INTO op FROM public.subscription_renewal_operations WHERE id=(p_input->>'operationId')::uuid;
189
+ frozen:=public.subscription_stock_wait_frozen(op.subscription_id,op.scheduled_at);
190
+ IF frozen IS NULL OR frozen->'snapshot' IS DISTINCT FROM p_input->'snapshot' OR
191
+ frozen->>'operationFingerprint' IS DISTINCT FROM p_input->>'operationFingerprint' OR
192
+ frozen->>'identityKey' IS DISTINCT FROM p_input->>'identityKey' OR
193
+ (p_due ? 'stockWaitRecovery' AND frozen->'binding' IS DISTINCT FROM p_due->'stockWaitRecovery') THEN
194
+ RAISE EXCEPTION 'stock_wait_frozen_binding_changed' USING ERRCODE='23505'; END IF;
195
+ IF op.state='refused' OR (op.state='reserved' AND op.lease_expires_at<=p_now) THEN
196
+ RETURN public.subscription_stock_wait_recover_refusal(op.id,op.operation_fingerprint,p_input->>'worker',p_now);
197
+ END IF;
198
+ IF op.state='reserved' THEN RETURN public.subscription_renewal_operation_response(op,false,true,'stock_secured'); END IF;
199
+ result:=public.fulfillment_reserve_subscription_renewal(op.id,(p_input->>'claimToken')::uuid,900,p_now);
200
+ IF (result->>'reserved')::boolean THEN RETURN result||jsonb_build_object('waiting',false); END IF;
201
+ -- Record the wait immediately; lease expiry is still required before reset.
202
+ INSERT INTO public.subscription_stock_wait_cases(subscription_id,client_id,renewal_operation_id,scheduled_at,cycle_number,state,due_snapshot,cycle_snapshot,fingerprint,identity_key,deficits,opened_at,last_checked_at)
203
+ SELECT op.subscription_id,s.client_id,op.id,op.scheduled_at,op.cycle_number,'waiting',p_due,op.cycle_snapshot,
204
+ op.operation_fingerprint,op.identity_key,public.subscription_stock_wait_deficits(op.cycle_snapshot,op.id,p_now),p_now,p_now
205
+ FROM public.subscriptions s WHERE s.id=op.subscription_id
206
+ ON CONFLICT(subscription_id,scheduled_at) DO UPDATE SET last_checked_at=p_now RETURNING * INTO c;
207
+ PERFORM public.subscription_stock_wait_enqueue(c,'started');
208
+ RETURN result||jsonb_build_object('waiting',true,'caseId',c.id);
209
+ END;
210
+ $$;
211
+ -- Discover refused schedules even without a prior wait row. Oldest selected
212
+ -- waits rotate independently of the normal due cohort, preserving fairness.
213
+ CREATE FUNCTION public.subscription_stock_wait_candidates(p_limit integer DEFAULT 50,p_now timestamptz DEFAULT now())
214
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
215
+ DECLARE result jsonb; row record;
216
+ BEGIN
217
+ result:='[]';
218
+ FOR row IN SELECT op.id,op.subscription_id,op.scheduled_at,c.id case_id FROM public.subscription_renewal_operations op
219
+ JOIN public.subscriptions s ON s.id=op.subscription_id LEFT JOIN public.subscription_stock_wait_cases c ON c.renewal_operation_id=op.id
220
+ WHERE s.status='active' AND s.next_cycle_at=op.scheduled_at AND op.lease_expires_at<=p_now
221
+ AND ((op.state='refused' AND op.terminal_reason IN ('inventory_unavailable','reservation_expired')) OR (op.state='reserved' AND c.state='stock_secured'))
222
+ AND (c.id IS NULL OR c.state IN ('waiting','stock_secured'))
223
+ AND public.subscription_stock_wait_frozen(op.subscription_id,op.scheduled_at) IS NOT NULL
224
+ ORDER BY CASE WHEN c.id IS NULL THEN op.updated_at ELSE c.last_selected_at END NULLS FIRST,op.scheduled_at,op.id LIMIT LEAST(GREATEST(p_limit,1),500)
225
+ LOOP
226
+ IF public.subscription_stock_wait_frozen(row.subscription_id,row.scheduled_at) IS NOT NULL THEN
227
+ IF row.case_id IS NOT NULL THEN
228
+ UPDATE public.subscription_stock_wait_cases SET last_selected_at=p_now WHERE id=row.case_id;
229
+ ELSE
230
+ PERFORM 1 FROM public.subscription_renewal_operations WHERE id=row.id FOR UPDATE SKIP LOCKED;
231
+ IF NOT FOUND THEN CONTINUE; END IF;
232
+ UPDATE public.subscription_renewal_operations SET updated_at=p_now WHERE id=row.id;
233
+ END IF;
234
+ result:=result||jsonb_build_array(public.subscription_stock_wait_frozen(row.subscription_id,row.scheduled_at));
235
+ END IF;
236
+ END LOOP;
237
+ -- Superseded/cancelled unpaid waits close; provider-owned/paid cases never do.
238
+ UPDATE public.subscription_stock_wait_cases c SET state='closed',closed_at=p_now,block_reason='schedule_superseded'
239
+ FROM public.subscriptions s,public.subscription_renewal_operations op WHERE s.id=c.subscription_id AND op.id=c.renewal_operation_id
240
+ AND c.state IN ('waiting','stock_secured') AND (s.status='cancelled' OR s.next_cycle_at IS DISTINCT FROM c.scheduled_at)
241
+ AND op.attempt_prepared_at IS NULL AND op.external_attempt_ref IS NULL
242
+ AND NOT EXISTS(SELECT 1 FROM public.subscription_cycles WHERE renewal_operation_id=op.id);
243
+ RETURN result;
244
+ END;
245
+ $$;
246
+ CREATE FUNCTION public.subscription_stock_wait_normal_due(p_limit integer DEFAULT 50,p_now timestamptz DEFAULT now())
247
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
248
+ SELECT COALESCE(jsonb_agg(to_jsonb(d) ORDER BY d.scheduled_at,d.subscription_id),'[]'::jsonb) FROM (
249
+ SELECT subscription.id AS subscription_id, subscription.client_id, subscription.next_cycle_at AS scheduled_at,
250
+ subscription.cadence_days, terms.next_cycle_number AS cycle_number, terms.currency_code,
251
+ terms.stock_source_key, terms.settlement_channel_key, terms.authorization_ref,
252
+ terms.payer_account_ref, terms.unattended_charge_authorized_at, terms.delivery_admission,
253
+ terms.delivery_block_reason
254
+ FROM public.subscriptions subscription
255
+ JOIN public.subscription_renewal_terms terms ON terms.subscription_id=subscription.id
256
+ WHERE subscription.status='active' AND subscription.next_cycle_at IS NOT NULL
257
+ AND subscription.next_cycle_at<=COALESCE(p_now,now())
258
+ AND NOT EXISTS(SELECT 1 FROM public.subscription_renewal_operations operation
259
+ WHERE operation.subscription_id=subscription.id AND operation.scheduled_at=subscription.next_cycle_at
260
+ AND (operation.state IN ('succeeded','refused') OR operation.lease_expires_at>COALESCE(p_now,now())))
261
+ AND NOT EXISTS(SELECT 1 FROM public.subscription_stock_wait_cases c
262
+ WHERE c.subscription_id=subscription.id AND c.scheduled_at=subscription.next_cycle_at AND c.state IN ('waiting','stock_secured'))
263
+ ORDER BY subscription.next_cycle_at,subscription.id LIMIT LEAST(GREATEST(COALESCE(p_limit,50),1),500)
264
+ ) d;
265
+ $$;
266
+ CREATE FUNCTION public.subscription_stock_wait_mark_resumed(p_order_id uuid)
267
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
268
+ DECLARE c public.subscription_stock_wait_cases; shipment public.fulfillment_shipments;
269
+ BEGIN
270
+ PERFORM 1 FROM public.subscriptions WHERE id=(SELECT subscription_id FROM public.subscription_stock_wait_cases WHERE renewal_operation_id=(SELECT renewal_operation_id FROM public.commerce_orders WHERE id=p_order_id)) FOR UPDATE;
271
+ SELECT * INTO c FROM public.subscription_stock_wait_cases WHERE renewal_operation_id=(SELECT renewal_operation_id FROM public.commerce_orders WHERE id=p_order_id) FOR UPDATE;
272
+ IF c.id IS NULL THEN RETURN NULL; END IF;
273
+ SELECT * INTO shipment FROM public.fulfillment_shipments WHERE order_id=p_order_id AND replaces_shipment_id IS NULL ORDER BY id LIMIT 1 FOR UPDATE;
274
+ IF c.state='closed' OR EXISTS(SELECT 1 FROM public.commerce_orders WHERE id=p_order_id AND status='pending_payment') THEN RETURN NULL; END IF;
275
+ IF NOT EXISTS(SELECT 1 FROM public.commerce_orders WHERE id=p_order_id AND status IN ('paid','fulfillment_pending','fulfilled')) OR
276
+ NOT EXISTS(SELECT 1 FROM public.commerce_settlement_intents WHERE order_id=p_order_id AND status='settled') OR
277
+ NOT EXISTS(SELECT 1 FROM public.subscription_cycles WHERE renewal_operation_id=c.renewal_operation_id AND status='paid') OR
278
+ NOT EXISTS(SELECT 1 FROM public.fulfillment_inventory_reservations WHERE renewal_operation_id=c.renewal_operation_id AND status='consumed') OR
279
+ shipment.id IS NULL OR shipment.status IN ('cancelled','exception') OR
280
+ EXISTS(SELECT 1 FROM public.commerce_order_holds WHERE order_id=p_order_id AND status='active') THEN
281
+ RAISE EXCEPTION 'stock_wait_realization_admission_missing' USING ERRCODE='22023'; END IF;
282
+ IF c.state='resumed' THEN
283
+ IF c.fulfillment_id IS DISTINCT FROM shipment.id THEN RAISE EXCEPTION 'stock_wait_fulfillment_replay_mismatch'; END IF;
284
+ RETURN public.subscription_stock_wait_projection(c);
285
+ END IF;
286
+ IF c.state<>'stock_secured' THEN RAISE EXCEPTION 'stock_wait_stock_not_secured'; END IF;
287
+ UPDATE public.subscription_stock_wait_cases SET state='resumed',resumed_at=now(),cycle_id=(SELECT subscription_cycle_id FROM public.commerce_orders WHERE id=p_order_id),
288
+ order_id=p_order_id,fulfillment_id=shipment.id WHERE id=c.id RETURNING * INTO c;
289
+ PERFORM public.subscription_stock_wait_enqueue(c,'resumed');
290
+ RETURN public.subscription_stock_wait_projection(c);
291
+ END;
292
+ $$;
293
+ CREATE FUNCTION public.subscription_stock_wait_summary(p_state text)
294
+ RETURNS jsonb LANGUAGE plpgsql STABLE SET search_path = pg_catalog AS $$
295
+ BEGIN
296
+ IF p_state NOT IN ('all','waiting','stock_secured','resumed','closed') THEN RAISE EXCEPTION 'stock_wait_summary_state_invalid'; END IF;
297
+ RETURN jsonb_build_object('totalCount',(SELECT count(*) FROM public.subscription_stock_wait_cases WHERE p_state='all' OR state=p_state),
298
+ 'productTotals',COALESCE((SELECT jsonb_agg(to_jsonb(t)) FROM (SELECT l->>'sku' sku,sum((l->>'missingUnits')::bigint) missing_units,
299
+ count(DISTINCT c.id) affected_cases FROM public.subscription_stock_wait_cases c CROSS JOIN LATERAL jsonb_array_elements(c.deficits) l
300
+ WHERE p_state='all' OR c.state=p_state GROUP BY l->>'sku') t),'[]'::jsonb));
301
+ END;
302
+ $$;
303
+ CREATE FUNCTION public.subscription_stock_wait_bind_notice(p_case_id uuid,p_event_id uuid,p_message jsonb)
304
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
305
+ DECLARE c public.subscription_stock_wait_cases; existing jsonb;
306
+ BEGIN
307
+ SELECT * INTO c FROM public.subscription_stock_wait_cases WHERE id=p_case_id FOR UPDATE;
308
+ IF c.id IS NULL OR NOT EXISTS(SELECT 1 FROM public.outbox_events WHERE id=p_event_id AND aggregate_id=c.id
309
+ AND aggregate_type='subscription_stock_wait' AND event_type IN ('subscription_stock_wait.started','subscription_stock_wait.resumed')) THEN
310
+ RAISE EXCEPTION 'stock_wait_notice_binding_invalid'; END IF;
311
+ existing:=c.notice_messages->p_event_id::text;
312
+ IF existing IS NOT NULL THEN RETURN existing; END IF;
313
+ IF jsonb_typeof(p_message) IS DISTINCT FROM 'object' OR EXISTS(SELECT 1 FROM unnest(ARRAY['from','to','subject','html']) k WHERE btrim(COALESCE(p_message->>k,''))='') THEN
314
+ RAISE EXCEPTION 'stock_wait_notice_message_invalid'; END IF;
315
+ UPDATE public.subscription_stock_wait_cases SET notice_messages=jsonb_set(notice_messages,ARRAY[p_event_id::text],p_message) WHERE id=c.id;
316
+ RETURN p_message;
317
+ END;
318
+ $$;
319
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_projection(public.subscription_stock_wait_cases) FROM PUBLIC, anon, authenticated;
320
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_read(uuid) FROM PUBLIC, anon, authenticated;
321
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_frozen(uuid,timestamptz) FROM PUBLIC, anon, authenticated;
322
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_deficits(jsonb,uuid,timestamptz) FROM PUBLIC, anon, authenticated;
323
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_enqueue(public.subscription_stock_wait_cases,text) FROM PUBLIC, anon, authenticated;
324
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_admit(jsonb,jsonb,timestamptz) FROM PUBLIC, anon, authenticated;
325
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_candidates(integer,timestamptz) FROM PUBLIC, anon, authenticated;
326
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_normal_due(integer,timestamptz) FROM PUBLIC, anon, authenticated;
327
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_mark_resumed(uuid) FROM PUBLIC, anon, authenticated;
328
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_summary(text) FROM PUBLIC, anon, authenticated;
329
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_bind_notice(uuid,uuid,jsonb) FROM PUBLIC, anon, authenticated;
330
+ REVOKE ALL ON FUNCTION public.subscription_stock_wait_recover_refusal(uuid,text,text,timestamptz) FROM PUBLIC, anon, authenticated;
@@ -0,0 +1,70 @@
1
+ -- Donor: supabase/migrations/20260928170000_new_purchase_stock_availability.sql (26-76).
2
+ -- Native provider-stock subset: no lots/safety/excluded order; channel exposure is deferred.
3
+ -- M06 observed-stock ATP. Native stock has no separate safety balance.
4
+ -- Admission callers keep their stock-row locks; this is a read, not a hold.
5
+ -- Only actual native renewal allocation skips the fixed 14-day soft priority.
6
+ CREATE FUNCTION public.inventory_channel_exposure_quantity(p_source_key text, p_sku text)
7
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
8
+ SELECT '0'::jsonb;
9
+ $$;
10
+ REVOKE ALL ON FUNCTION public.inventory_channel_exposure_quantity(text,text) FROM PUBLIC, anon, authenticated;
11
+
12
+ CREATE FUNCTION public.inventory_available_to_promise(
13
+ p_source_key text, p_sku text, p_as_of timestamptz, p_for_subscription_renewal boolean
14
+ )
15
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
16
+ WITH live_holds AS (
17
+ SELECT r.renewal_operation_id,l.quantity
18
+ FROM public.fulfillment_inventory_reservations r
19
+ JOIN public.fulfillment_inventory_reservation_lines l ON l.reservation_id=r.id
20
+ WHERE r.source_key=p_source_key AND l.sku=p_sku
21
+ AND (r.status='committed' OR (r.status='held' AND r.expires_at>p_as_of))
22
+ ), components AS (
23
+ SELECT COALESCE((SELECT stock.for_sale_quantity FROM public.fulfillment_stock_current stock
24
+ WHERE stock.source_key=p_source_key AND stock.sku=p_sku
25
+ AND stock.inventory_class='sellable' AND stock.stale_after>p_as_of),0)::bigint for_sale,
26
+ (SELECT COALESCE(sum(quantity),0) FROM live_holds)::bigint hard_holds,
27
+ (SELECT COALESCE(sum(GREATEST(0,due.quantity-credited.quantity)),0)::bigint
28
+ FROM (
29
+ SELECT s.id,s.next_cycle_at,sum(l.quantity) quantity
30
+ FROM public.subscriptions s
31
+ JOIN public.subscription_renewal_terms t ON t.subscription_id=s.id
32
+ JOIN public.subscription_renewal_term_lines l ON l.subscription_id=s.id
33
+ WHERE s.status='active' AND s.next_cycle_at<=p_as_of+interval '14 days'
34
+ AND t.stock_source_key=p_source_key AND l.sku=p_sku
35
+ GROUP BY s.id,s.next_cycle_at
36
+ ) due CROSS JOIN LATERAL (
37
+ SELECT COALESCE(sum(h.quantity),0) quantity FROM live_holds h
38
+ JOIN public.subscription_renewal_operations op ON op.id=h.renewal_operation_id
39
+ WHERE op.subscription_id=due.id AND op.scheduled_at=due.next_cycle_at
40
+ AND NOT EXISTS(SELECT 1 FROM public.subscription_cycles c
41
+ WHERE c.renewal_operation_id=op.id AND c.status IN ('paid','skipped','cancelled'))
42
+ ) credited) soft_demand,
43
+ (public.inventory_channel_exposure_quantity(p_source_key,p_sku)#>>'{}')::bigint exposure
44
+ )
45
+ SELECT jsonb_build_object('available_quantity',GREATEST(0,for_sale-hard_holds
46
+ -CASE WHEN p_for_subscription_renewal IS TRUE THEN 0 ELSE soft_demand END-exposure),
47
+ 'for_sale_quantity',for_sale,'hard_hold_quantity',hard_holds,'safety_quantity',0,
48
+ 'soft_subscription_demand_quantity',soft_demand,'channel_exposure_quantity',exposure)
49
+ FROM components;
50
+ $$;
51
+ REVOKE ALL ON FUNCTION public.inventory_available_to_promise(text,text,timestamptz,boolean) FROM PUBLIC, anon, authenticated;
52
+
53
+ -- M06 parity completion. Both availability fields protect subscription demand;
54
+ -- this one-time-purchase read never accepts a caller-supplied renewal exemption.
55
+ CREATE FUNCTION public.inventory_new_purchase_stock_read(p_source_key text, p_skus jsonb, p_as_of timestamptz)
56
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
57
+ SELECT COALESCE(jsonb_agg(jsonb_build_object('sku',requested.sku,'catalogSkuId',sku.id,
58
+ 'source',CASE WHEN stock.sku IS NULL THEN 'missing' ELSE 'provider' END,
59
+ 'providerForSale',stock.for_sale_quantity,'activeHolds',atp->'hard_hold_quantity',
60
+ 'safetyStock',atp->'safety_quantity','available',atp->'available_quantity',
61
+ 'stale',stock.sku IS NOT NULL AND stock.stale_after<=p_as_of,
62
+ 'renewalDemand',atp->'soft_subscription_demand_quantity',
63
+ 'availableForNewPurchases',atp->'available_quantity','excludedOrderHolds',0,
64
+ 'channelExposure',atp->'channel_exposure_quantity') ORDER BY requested.sku),'[]'::jsonb)
65
+ FROM (SELECT DISTINCT value sku FROM jsonb_array_elements_text(p_skus)) requested
66
+ LEFT JOIN public.catalog_skus sku ON sku.sku=requested.sku
67
+ LEFT JOIN public.fulfillment_stock_current stock ON stock.source_key=p_source_key AND stock.sku=requested.sku
68
+ CROSS JOIN LATERAL (SELECT public.inventory_available_to_promise(p_source_key,requested.sku,p_as_of,false) atp) promise;
69
+ $$;
70
+ REVOKE ALL ON FUNCTION public.inventory_new_purchase_stock_read(text,jsonb,timestamptz) FROM PUBLIC, anon, authenticated;
@@ -0,0 +1,147 @@
1
+ -- Donor: supabase/migrations/20260710131002_no_cron_inventory_reservation_leases.sql
2
+ -- (inventory_reserve_order provider branch); native operation/claim receipts replace managed order holds.
3
+ CREATE OR REPLACE FUNCTION public.fulfillment_reserve_subscription_renewal(
4
+ p_operation_id uuid,
5
+ p_claim_token uuid,
6
+ p_hold_seconds integer DEFAULT 900,
7
+ p_now timestamptz DEFAULT now()
8
+ )
9
+ RETURNS jsonb
10
+ LANGUAGE plpgsql
11
+ SET search_path = pg_catalog
12
+ AS $$
13
+ DECLARE
14
+ v_operation public.subscription_renewal_operations%ROWTYPE;
15
+ v_reservation public.fulfillment_inventory_reservations%ROWTYPE;
16
+ v_line jsonb;
17
+ v_source text;
18
+ v_sku text;
19
+ v_quantity integer;
20
+ v_available integer;
21
+ BEGIN
22
+ IF p_operation_id IS NULL OR p_claim_token IS NULL
23
+ OR p_hold_seconds IS NULL OR p_hold_seconds NOT BETWEEN 60 AND 86400
24
+ THEN
25
+ RAISE EXCEPTION 'fulfillment_renewal_reservation_invalid_input' USING ERRCODE = '22023';
26
+ END IF;
27
+
28
+ SELECT * INTO v_operation FROM public.subscription_renewal_operations
29
+ WHERE id = p_operation_id FOR UPDATE;
30
+ IF NOT FOUND THEN
31
+ RAISE EXCEPTION 'fulfillment_renewal_reservation_operation_not_found' USING ERRCODE = '22023';
32
+ END IF;
33
+ IF v_operation.claim_token <> p_claim_token
34
+ OR v_operation.lease_expires_at <= COALESCE(p_now, now())
35
+ THEN
36
+ RAISE EXCEPTION 'fulfillment_renewal_reservation_stale_claim' USING ERRCODE = '40001';
37
+ END IF;
38
+
39
+ SELECT * INTO v_reservation FROM public.fulfillment_inventory_reservations
40
+ WHERE renewal_operation_id = p_operation_id FOR UPDATE;
41
+ IF FOUND THEN
42
+ IF v_reservation.operation_fingerprint <> v_operation.operation_fingerprint THEN
43
+ RAISE EXCEPTION 'fulfillment_renewal_reservation_fingerprint_conflict' USING ERRCODE = '23505';
44
+ END IF;
45
+ IF v_reservation.status = 'held'
46
+ AND v_reservation.expires_at <= COALESCE(p_now, now())
47
+ AND v_operation.state = 'reserved'
48
+ THEN
49
+ UPDATE public.fulfillment_inventory_reservations
50
+ SET status = 'expired', expires_at = NULL,
51
+ terminal_reason = 'reservation_expired', updated_at = COALESCE(p_now, now())
52
+ WHERE id = v_reservation.id
53
+ RETURNING * INTO v_reservation;
54
+ UPDATE public.subscription_renewal_operations
55
+ SET state = 'refused', terminal_outcome = 'refused',
56
+ terminal_reason = 'reservation_expired', terminal_at = COALESCE(p_now, now()),
57
+ updated_at = COALESCE(p_now, now())
58
+ WHERE id = p_operation_id RETURNING * INTO v_operation;
59
+ INSERT INTO public.subscription_renewal_outcomes (
60
+ renewal_operation_id, operation_fingerprint, event_key, outcome, reason, occurred_at
61
+ ) VALUES (
62
+ p_operation_id, v_operation.operation_fingerprint,
63
+ v_operation.identity_key || ':reservation-expired', 'refused',
64
+ 'reservation_expired', COALESCE(p_now, now()));
65
+ RETURN jsonb_build_object(
66
+ 'reserved', false, 'replayed', true, 'reservationId', v_reservation.id,
67
+ 'status', 'expired', 'reason', 'reservation_expired');
68
+ END IF;
69
+ RETURN jsonb_build_object(
70
+ 'reserved', v_reservation.status IN ('held', 'committed', 'consumed'),
71
+ 'replayed', true, 'reservationId', v_reservation.id,
72
+ 'status', v_reservation.status, 'reason', v_reservation.terminal_reason);
73
+ END IF;
74
+
75
+ IF v_operation.state <> 'claimed' THEN
76
+ RAISE EXCEPTION 'fulfillment_renewal_reservation_invalid_state' USING ERRCODE = '22023';
77
+ END IF;
78
+ v_source := v_operation.cycle_snapshot->>'stockSourceKey';
79
+ IF v_source IS NULL OR v_source !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$' THEN
80
+ RAISE EXCEPTION 'fulfillment_renewal_reservation_invalid_snapshot' USING ERRCODE = '22023';
81
+ END IF;
82
+
83
+ -- Lock all stock rows in a deterministic order before computing ATP, so two different
84
+ -- operations containing the same SKUs cannot both spend the same observed availability.
85
+ PERFORM stock.sku
86
+ FROM public.fulfillment_stock_current stock
87
+ JOIN jsonb_array_elements(v_operation.cycle_snapshot->'lines') line
88
+ ON stock.source_key = v_source AND stock.sku = line->>'sku'
89
+ ORDER BY stock.sku
90
+ FOR UPDATE OF stock;
91
+
92
+ FOR v_line IN SELECT value FROM jsonb_array_elements(v_operation.cycle_snapshot->'lines')
93
+ LOOP
94
+ v_sku := v_line->>'sku';
95
+ BEGIN v_quantity := (v_line->>'quantity')::integer;
96
+ EXCEPTION WHEN invalid_text_representation OR numeric_value_out_of_range THEN
97
+ RAISE EXCEPTION 'fulfillment_renewal_reservation_invalid_snapshot' USING ERRCODE = '22023';
98
+ END;
99
+ IF btrim(COALESCE(v_sku, '')) = '' OR v_quantity < 1 THEN
100
+ RAISE EXCEPTION 'fulfillment_renewal_reservation_invalid_snapshot' USING ERRCODE = '22023';
101
+ END IF;
102
+
103
+ SELECT (public.inventory_available_to_promise(v_source,v_sku,COALESCE(p_now,now()),true)
104
+ ->>'available_quantity')::integer INTO v_available;
105
+ IF v_available < v_quantity THEN
106
+ UPDATE public.subscription_renewal_operations
107
+ SET state = 'refused', terminal_outcome = 'refused',
108
+ terminal_reason = 'inventory_unavailable', terminal_at = COALESCE(p_now, now()),
109
+ updated_at = COALESCE(p_now, now())
110
+ WHERE id = p_operation_id RETURNING * INTO v_operation;
111
+ INSERT INTO public.subscription_renewal_outcomes (
112
+ renewal_operation_id, operation_fingerprint, event_key, outcome, reason, occurred_at
113
+ ) VALUES (
114
+ p_operation_id, v_operation.operation_fingerprint,
115
+ v_operation.identity_key || ':inventory', 'refused', 'inventory_unavailable',
116
+ COALESCE(p_now, now()));
117
+ RETURN jsonb_build_object(
118
+ 'reserved', false, 'replayed', false, 'reservationId', NULL,
119
+ 'status', 'refused', 'reason', 'inventory_unavailable');
120
+ END IF;
121
+ END LOOP;
122
+
123
+ INSERT INTO public.fulfillment_inventory_reservations (
124
+ renewal_operation_id, operation_fingerprint, source_key, expires_at, created_at, updated_at
125
+ ) VALUES (
126
+ p_operation_id, v_operation.operation_fingerprint, v_source,
127
+ COALESCE(p_now, now()) + make_interval(secs => p_hold_seconds),
128
+ COALESCE(p_now, now()), COALESCE(p_now, now())
129
+ ) RETURNING * INTO v_reservation;
130
+
131
+ FOR v_line IN SELECT value FROM jsonb_array_elements(v_operation.cycle_snapshot->'lines')
132
+ LOOP
133
+ INSERT INTO public.fulfillment_inventory_reservation_lines (
134
+ reservation_id, operation_fingerprint, sku, quantity
135
+ ) VALUES (
136
+ v_reservation.id, v_operation.operation_fingerprint,
137
+ v_line->>'sku', (v_line->>'quantity')::integer);
138
+ END LOOP;
139
+
140
+ UPDATE public.subscription_renewal_operations
141
+ SET state = 'reserved', updated_at = COALESCE(p_now, now())
142
+ WHERE id = p_operation_id;
143
+ RETURN jsonb_build_object(
144
+ 'reserved', true, 'replayed', false, 'reservationId', v_reservation.id,
145
+ 'status', 'held', 'reason', NULL);
146
+ END;
147
+ $$;
@@ -0,0 +1,59 @@
1
+ -- Donor: supabase/migrations/20261009110000_subscription_dunning_active_repair.sql
2
+ -- (payment-result recovery). Native released-to-committed ATP accounting has no managed row-state twin.
3
+ -- Native CTL-2 X: verified waiting-state late payment restores the original
4
+ -- released reservation as an ATP-counted committed hold, without a new charge.
5
+ -- Requires the native N1/N2/N3, 20261024183000 ATP seam and 20261024184000 allocator prefix.
6
+ -- Body-only replacement: original signature, invoker rights, search_path and
7
+ -- effective ACLs remain unchanged; no grants, tables, seeds or driver activation.
8
+ -- Prepared/committed recovery retains its existing consumed transition. A restored
9
+ -- hold remains counted until an explicit fulfillment/inventory reconciliation.
10
+ -- Native only; no managed twin is installed. Keep this forward on code rollback.
11
+ CREATE OR REPLACE FUNCTION public.subscription_retry_record_recovery(p_operation_id uuid,p_key text,p_input jsonb)
12
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
13
+ DECLARE o public.subscription_retry_obligations; r public.subscription_retry_receipts; prep public.subscription_retry_receipts;
14
+ i public.commerce_settlement_intents; result jsonb; at_time timestamptz:=(p_input->>'occurredAt')::timestamptz;
15
+ BEGIN
16
+ PERFORM 1 FROM public.subscriptions WHERE id=(SELECT subscription_id FROM public.subscription_renewal_operations WHERE id=p_operation_id) FOR UPDATE;
17
+ SELECT * INTO o FROM public.subscription_retry_obligations WHERE renewal_operation_id=p_operation_id FOR UPDATE;
18
+ SELECT * INTO r FROM public.subscription_retry_receipts WHERE receipt_key=p_key;
19
+ IF r.id IS NOT NULL THEN
20
+ IF r.obligation_id<>o.id OR r.kind<>'recovery' OR r.input<>p_input THEN RAISE EXCEPTION 'subscription_retry_replay_conflict' USING ERRCODE='23505'; END IF;
21
+ RETURN r.result;
22
+ END IF;
23
+ SELECT * INTO i FROM public.commerce_settlement_intents WHERE id=o.settlement_intent_id FOR UPDATE;
24
+ SELECT * INTO prep FROM public.subscription_retry_receipts WHERE obligation_id=o.id AND kind='prepare' AND attempt_sequence=o.attempt_sequence;
25
+ IF o.id IS NULL OR o.state='recovered' OR at_time IS NULL OR (p_input->>'firstRecordedAt')::timestamptz IS NULL OR
26
+ btrim(COALESCE(p_input->>'evidenceKey',''))='' OR btrim(COALESCE(p_input->>'externalRef',''))='' OR
27
+ (p_input->>'amountMinor')::bigint IS DISTINCT FROM i.amount_minor OR p_input->>'currency' IS DISTINCT FROM i.currency_code OR
28
+ (o.state='prepared' AND prep.receipt_key IS DISTINCT FROM p_input->>'prepareKey') THEN
29
+ RAISE EXCEPTION 'subscription_retry_positive_evidence_required' USING ERRCODE='22023'; END IF;
30
+ PERFORM public.payment_truth_ingest_event(p_input->>'evidenceKey',
31
+ encode(sha256(convert_to(jsonb_build_object('operationId',p_operation_id,'input',p_input)::text,'UTF8')),'hex'),
32
+ p_key,i.id,'captured',i.amount_minor,i.currency_code,at_time,NULL,NULL,
33
+ p_input||jsonb_build_object('renewalOperationId',p_operation_id,'operationFingerprint',o.operation_fingerprint),
34
+ NULL,NULL,p_input->>'template');
35
+ UPDATE public.subscription_renewal_terms SET next_cycle_number=GREATEST(next_cycle_number,
36
+ (SELECT cycle_number+1 FROM public.subscription_renewal_operations WHERE id=p_operation_id)),updated_at=at_time WHERE subscription_id=o.subscription_id;
37
+ -- A waiting-state refusal released this obligation's stock. Paid lines must
38
+ -- re-enter the existing ATP hard holds; consumed rows are not counted there.
39
+ -- Serialize with the native allocator using its deterministic stock-row locks.
40
+ IF o.state='waiting' THEN
41
+ PERFORM stock.sku FROM public.fulfillment_stock_current stock
42
+ JOIN public.fulfillment_inventory_reservations reservation ON reservation.source_key=stock.source_key
43
+ JOIN public.fulfillment_inventory_reservation_lines line ON line.reservation_id=reservation.id AND line.sku=stock.sku
44
+ WHERE reservation.renewal_operation_id=p_operation_id AND reservation.status='released'
45
+ ORDER BY stock.sku FOR UPDATE OF stock;
46
+ END IF;
47
+ UPDATE public.fulfillment_inventory_reservations
48
+ SET status=CASE WHEN status='released' THEN 'committed' ELSE 'consumed' END,
49
+ expires_at=CASE WHEN status='released' THEN NULL ELSE expires_at END,terminal_reason=NULL,updated_at=at_time
50
+ WHERE renewal_operation_id=p_operation_id AND
51
+ (status='committed' OR (o.state='waiting' AND status='released'));
52
+ IF o.case_id IS NOT NULL THEN PERFORM public.dunning_lifecycle_record_recovery(o.case_id,at_time,p_input->>'template'); END IF;
53
+ UPDATE public.subscription_retry_obligations SET state='recovered',next_retry_at=NULL,updated_at=at_time WHERE id=o.id RETURNING * INTO o;
54
+ result:=to_jsonb(o);
55
+ INSERT INTO public.subscription_retry_receipts(obligation_id,receipt_key,kind,attempt_sequence,input,result)
56
+ VALUES(o.id,p_key,'recovery',o.attempt_sequence,p_input,result);
57
+ RETURN result;
58
+ END;
59
+ $$;
@@ -0,0 +1,22 @@
1
+ -- Donor: supabase/migrations/20261009110000_subscription_dunning_active_repair.sql
2
+ -- (due-reader paid exclusion); 20260912164500_retry_admission_observed_provider_success.sql.
3
+ -- Native waiting-obligation selection translates managed unpaid-cycle/intent admission.
4
+ -- Native CTL-2 Y: omit paid cycles and settled intents before retry LIMIT so
5
+ -- stale waiting obligations cannot starve genuinely due declines.
6
+ -- Requires the native prefix through CTL-2 X (20261024184500). This read-only body
7
+ -- replacement retains the exact arguments/defaults, STABLE volatility, invoker
8
+ -- rights, search_path, ordering, limit clamp and effective ACLs. No close route,
9
+ -- state rewrite, grant, seed, subscription pause or driver activation is added.
10
+ -- Native only; no managed twin is installed. Keep this forward on code rollback.
11
+ CREATE OR REPLACE FUNCTION public.subscription_retry_candidates(p_limit integer DEFAULT 50,p_now timestamptz DEFAULT now())
12
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
13
+ SELECT COALESCE(jsonb_agg(to_jsonb(c)), '[]'::jsonb) FROM (
14
+ SELECT o.* FROM public.subscription_retry_obligations o JOIN public.subscriptions s ON s.id=o.subscription_id
15
+ WHERE s.status='active' AND o.state='waiting' AND o.next_retry_at<=p_now
16
+ AND EXISTS(SELECT 1 FROM public.subscription_cycles cycle
17
+ WHERE cycle.id=o.cycle_id AND cycle.status<>'paid')
18
+ AND EXISTS(SELECT 1 FROM public.commerce_settlement_intents intent
19
+ WHERE intent.id=o.settlement_intent_id AND intent.status<>'settled')
20
+ ORDER BY o.next_retry_at,o.id LIMIT LEAST(GREATEST(p_limit,1),500)
21
+ ) c;
22
+ $$;