@openlup/platform 0.22.0 → 0.24.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 (241) hide show
  1. package/CHANGELOG.md +440 -158
  2. package/README.md +141 -589
  3. package/dist/fulfillment/adapterConformance.js +1 -1
  4. package/dist/fulfillment/bindings.js +1 -1
  5. package/dist/fulfillment/contracts/conformance.d.ts +3 -1
  6. package/dist/fulfillment/contracts/dispatch.d.ts +45 -3
  7. package/dist/fulfillment/contracts/index.d.ts +3 -3
  8. package/dist/fulfillment/contracts/observation.d.ts +21 -1
  9. package/dist/fulfillment/contracts/readback.d.ts +21 -0
  10. package/dist/fulfillment/contracts/store.d.ts +3 -0
  11. package/dist/fulfillment/contracts/sync.d.ts +58 -2
  12. package/dist/fulfillment/dispatchProjection.d.ts +20 -1
  13. package/dist/fulfillment/dispatchProjection.js +97 -6
  14. package/dist/fulfillment/index.d.ts +3 -2
  15. package/dist/fulfillment/index.js +2 -2
  16. package/dist/fulfillment/migrations.d.ts +5 -0
  17. package/dist/fulfillment/migrations.js +1 -0
  18. package/dist/fulfillment/observationProjection.d.ts +24 -3
  19. package/dist/fulfillment/observationProjection.js +65 -12
  20. package/dist/fulfillment/omnipack/client.d.ts +3 -1
  21. package/dist/fulfillment/omnipack/client.js +31 -13
  22. package/dist/fulfillment/omnipack/index.d.ts +4 -1
  23. package/dist/fulfillment/omnipack/index.js +107 -45
  24. package/dist/fulfillment/omnipack/mappers.d.ts +7 -1
  25. package/dist/fulfillment/omnipack/mappers.js +24 -2
  26. package/dist/fulfillment/omnipack/observation.d.ts +11 -4
  27. package/dist/fulfillment/omnipack/observation.js +44 -29
  28. package/dist/fulfillment/omnipack/reconciliation.d.ts +15 -0
  29. package/dist/fulfillment/omnipack/reconciliation.js +55 -0
  30. package/dist/fulfillment/postgresDispatchStore.js +1 -1
  31. package/dist/fulfillment/postgresShipmentSpine.d.ts +8 -1
  32. package/dist/fulfillment/postgresShipmentSpine.js +17 -3
  33. package/dist/fulfillment/postgresStockSyncPort.js +6 -3
  34. package/dist/fulfillment/providerDispatchAcceptance.d.ts +1 -0
  35. package/dist/fulfillment/providerDispatchAcceptance.js +8 -2
  36. package/dist/fulfillment/providerDispatchWorker.d.ts +51 -0
  37. package/dist/fulfillment/providerDispatchWorker.js +65 -20
  38. package/dist/fulfillment/providerReconciliationWorker.d.ts +27 -19
  39. package/dist/fulfillment/providerReconciliationWorker.js +75 -19
  40. package/dist/fulfillment/providerStockSyncWorker.d.ts +5 -1
  41. package/dist/fulfillment/providerStockSyncWorker.js +229 -120
  42. package/dist/fulfillment/providerTrackingReferences.d.ts +3 -2
  43. package/dist/fulfillment/verifiedIngress.d.ts +38 -11
  44. package/dist/fulfillment/verifiedIngress.js +36 -8
  45. package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
  46. package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
  47. package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
  48. package/dist/invoicing/accountingLineAllocation.js +69 -0
  49. package/dist/invoicing/adapterConformance.d.ts +42 -0
  50. package/dist/invoicing/adapterConformance.js +296 -0
  51. package/dist/invoicing/artifacts.d.ts +7 -0
  52. package/dist/invoicing/artifacts.js +20 -0
  53. package/dist/invoicing/bindings.d.ts +46 -0
  54. package/dist/invoicing/bindings.js +69 -0
  55. package/dist/invoicing/capabilities.d.ts +11 -0
  56. package/dist/invoicing/capabilities.js +10 -0
  57. package/dist/invoicing/channelPolicy.d.ts +28 -0
  58. package/dist/invoicing/channelPolicy.js +27 -0
  59. package/dist/invoicing/continuity.d.ts +12 -0
  60. package/dist/invoicing/continuity.js +18 -0
  61. package/dist/invoicing/contracts/capabilities.d.ts +40 -0
  62. package/dist/invoicing/contracts/capabilities.js +1 -0
  63. package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
  64. package/dist/invoicing/contracts/companyLookup.js +1 -0
  65. package/dist/invoicing/contracts/delivery.d.ts +74 -0
  66. package/dist/invoicing/contracts/delivery.js +1 -0
  67. package/dist/invoicing/contracts/facts.d.ts +110 -0
  68. package/dist/invoicing/contracts/facts.js +1 -0
  69. package/dist/invoicing/contracts/host.d.ts +245 -0
  70. package/dist/invoicing/contracts/host.js +1 -0
  71. package/dist/invoicing/contracts/index.d.ts +16 -0
  72. package/dist/invoicing/contracts/index.js +1 -0
  73. package/dist/invoicing/contracts/provider.d.ts +126 -0
  74. package/dist/invoicing/contracts/provider.js +1 -0
  75. package/dist/invoicing/contracts/settlement.d.ts +66 -0
  76. package/dist/invoicing/contracts/settlement.js +1 -0
  77. package/dist/invoicing/contracts/store.d.ts +277 -0
  78. package/dist/invoicing/contracts/store.js +1 -0
  79. package/dist/invoicing/contribution.d.ts +21 -0
  80. package/dist/invoicing/contribution.js +19 -0
  81. package/dist/invoicing/correction.d.ts +42 -0
  82. package/dist/invoicing/correction.js +44 -0
  83. package/dist/invoicing/delivery.d.ts +85 -0
  84. package/dist/invoicing/delivery.js +79 -0
  85. package/dist/invoicing/documentHistory.d.ts +50 -0
  86. package/dist/invoicing/documentHistory.js +213 -0
  87. package/dist/invoicing/documentSync.d.ts +24 -0
  88. package/dist/invoicing/documentSync.js +26 -0
  89. package/dist/invoicing/dutyCollector.d.ts +15 -0
  90. package/dist/invoicing/dutyCollector.js +35 -0
  91. package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
  92. package/dist/invoicing/fulfillmentTrigger.js +90 -0
  93. package/dist/invoicing/handoff.d.ts +37 -0
  94. package/dist/invoicing/handoff.js +62 -0
  95. package/dist/invoicing/index.d.ts +39 -0
  96. package/dist/invoicing/index.js +37 -0
  97. package/dist/invoicing/issue.d.ts +39 -0
  98. package/dist/invoicing/issue.js +55 -0
  99. package/dist/invoicing/migrations.d.ts +48 -0
  100. package/dist/invoicing/migrations.js +25 -0
  101. package/dist/invoicing/nativeUnsupported.d.ts +9 -0
  102. package/dist/invoicing/nativeUnsupported.js +22 -0
  103. package/dist/invoicing/paymentReadback.d.ts +20 -0
  104. package/dist/invoicing/paymentReadback.js +47 -0
  105. package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
  106. package/dist/invoicing/postgresInvoiceStore.js +344 -0
  107. package/dist/invoicing/readModel.d.ts +74 -0
  108. package/dist/invoicing/readModel.js +90 -0
  109. package/dist/invoicing/readback.d.ts +4 -0
  110. package/dist/invoicing/readback.js +30 -0
  111. package/dist/invoicing/reversal.d.ts +37 -0
  112. package/dist/invoicing/reversal.js +28 -0
  113. package/dist/invoicing/sellerRevision.d.ts +19 -0
  114. package/dist/invoicing/sellerRevision.js +14 -0
  115. package/dist/invoicing/settlementSync.d.ts +18 -0
  116. package/dist/invoicing/settlementSync.js +32 -0
  117. package/dist/invoicing/snapshot.d.ts +52 -0
  118. package/dist/invoicing/snapshot.js +141 -0
  119. package/dist/subscriptions/automaticRenewal.d.ts +44 -0
  120. package/dist/subscriptions/automaticRenewal.js +81 -0
  121. package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
  122. package/dist/subscriptions/automaticRenewalExecution.js +44 -0
  123. package/dist/subscriptions/contracts/commands.d.ts +115 -0
  124. package/dist/subscriptions/contracts/commands.js +1 -0
  125. package/dist/subscriptions/contracts/dunning.d.ts +133 -0
  126. package/dist/subscriptions/contracts/dunning.js +1 -0
  127. package/dist/subscriptions/contracts/index.d.ts +6 -0
  128. package/dist/subscriptions/contracts/index.js +1 -0
  129. package/dist/subscriptions/contracts/messages.d.ts +43 -0
  130. package/dist/subscriptions/contracts/messages.js +1 -0
  131. package/dist/subscriptions/contracts/renewal.d.ts +533 -0
  132. package/dist/subscriptions/contracts/renewal.js +1 -0
  133. package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
  134. package/dist/subscriptions/contracts/stockWait.js +1 -0
  135. package/dist/subscriptions/index.d.ts +39 -0
  136. package/dist/subscriptions/index.js +17 -0
  137. package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
  138. package/dist/subscriptions/paymentOutcomes.js +27 -0
  139. package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
  140. package/dist/subscriptions/postgresRenewalStore.js +61 -0
  141. package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
  142. package/dist/subscriptions/postgresRetryStore.js +142 -0
  143. package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
  144. package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
  145. package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
  146. package/dist/subscriptions/renewalCycleMechanism.js +157 -0
  147. package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
  148. package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
  149. package/dist/subscriptions/renewalIdentity.d.ts +3 -0
  150. package/dist/subscriptions/renewalIdentity.js +4 -0
  151. package/package.json +23 -3
  152. package/release-gates.json +74 -7
  153. package/sql/20260809210000_accounting_document_rail.sql +889 -0
  154. package/sql/20260811150000_accounting_payout_rail.sql +333 -0
  155. package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
  156. package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
  157. package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
  158. package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
  159. package/sql/20261022191000_invoicing_document_effects.sql +249 -0
  160. package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
  161. package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
  162. package/src/fulfillment/adapterConformance.ts +1 -1
  163. package/src/fulfillment/bindings.ts +1 -1
  164. package/src/fulfillment/contracts/conformance.ts +3 -1
  165. package/src/fulfillment/contracts/dispatch.ts +24 -5
  166. package/src/fulfillment/contracts/index.ts +3 -3
  167. package/src/fulfillment/contracts/observation.ts +11 -3
  168. package/src/fulfillment/contracts/readback.ts +13 -0
  169. package/src/fulfillment/contracts/store.ts +1 -0
  170. package/src/fulfillment/contracts/sync.ts +13 -2
  171. package/src/fulfillment/dispatchProjection.ts +94 -7
  172. package/src/fulfillment/index.ts +3 -2
  173. package/src/fulfillment/migrations.ts +1 -0
  174. package/src/fulfillment/observationProjection.ts +64 -13
  175. package/src/fulfillment/omnipack/client.ts +32 -13
  176. package/src/fulfillment/omnipack/index.ts +99 -39
  177. package/src/fulfillment/omnipack/mappers.ts +23 -2
  178. package/src/fulfillment/omnipack/observation.ts +45 -27
  179. package/src/fulfillment/omnipack/reconciliation.ts +56 -0
  180. package/src/fulfillment/postgresDispatchStore.ts +1 -1
  181. package/src/fulfillment/postgresShipmentSpine.ts +21 -4
  182. package/src/fulfillment/postgresStockSyncPort.ts +6 -3
  183. package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
  184. package/src/fulfillment/providerDispatchWorker.ts +61 -20
  185. package/src/fulfillment/providerReconciliationWorker.ts +81 -25
  186. package/src/fulfillment/providerStockSyncWorker.ts +222 -126
  187. package/src/fulfillment/providerTrackingReferences.ts +3 -2
  188. package/src/fulfillment/verifiedIngress.ts +44 -11
  189. package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
  190. package/src/invoicing/accountingLineAllocation.ts +69 -0
  191. package/src/invoicing/adapterConformance.ts +249 -0
  192. package/src/invoicing/artifacts.ts +19 -0
  193. package/src/invoicing/bindings.ts +80 -0
  194. package/src/invoicing/capabilities.ts +9 -0
  195. package/src/invoicing/channelPolicy.ts +50 -0
  196. package/src/invoicing/continuity.ts +15 -0
  197. package/src/invoicing/contracts/capabilities.ts +15 -0
  198. package/src/invoicing/contracts/companyLookup.ts +13 -0
  199. package/src/invoicing/contracts/delivery.ts +24 -0
  200. package/src/invoicing/contracts/facts.ts +71 -0
  201. package/src/invoicing/contracts/host.ts +84 -0
  202. package/src/invoicing/contracts/index.ts +16 -0
  203. package/src/invoicing/contracts/provider.ts +78 -0
  204. package/src/invoicing/contracts/settlement.ts +39 -0
  205. package/src/invoicing/contracts/store.ts +102 -0
  206. package/src/invoicing/contribution.ts +28 -0
  207. package/src/invoicing/correction.ts +56 -0
  208. package/src/invoicing/delivery.ts +122 -0
  209. package/src/invoicing/documentHistory.ts +301 -0
  210. package/src/invoicing/documentSync.ts +51 -0
  211. package/src/invoicing/dutyCollector.ts +31 -0
  212. package/src/invoicing/fulfillmentTrigger.ts +144 -0
  213. package/src/invoicing/handoff.ts +71 -0
  214. package/src/invoicing/index.ts +39 -0
  215. package/src/invoicing/issue.ts +64 -0
  216. package/src/invoicing/migrations.ts +25 -0
  217. package/src/invoicing/nativeUnsupported.ts +24 -0
  218. package/src/invoicing/paymentReadback.ts +70 -0
  219. package/src/invoicing/postgresInvoiceStore.ts +334 -0
  220. package/src/invoicing/readModel.ts +80 -0
  221. package/src/invoicing/readback.ts +26 -0
  222. package/src/invoicing/reversal.ts +34 -0
  223. package/src/invoicing/sellerRevision.ts +20 -0
  224. package/src/invoicing/settlementSync.ts +37 -0
  225. package/src/invoicing/snapshot.ts +192 -0
  226. package/src/subscriptions/automaticRenewal.ts +87 -0
  227. package/src/subscriptions/automaticRenewalExecution.ts +39 -0
  228. package/src/subscriptions/contracts/commands.ts +46 -0
  229. package/src/subscriptions/contracts/dunning.ts +48 -0
  230. package/src/subscriptions/contracts/index.ts +6 -0
  231. package/src/subscriptions/contracts/messages.ts +18 -0
  232. package/src/subscriptions/contracts/renewal.ts +325 -0
  233. package/src/subscriptions/contracts/stockWait.ts +34 -0
  234. package/src/subscriptions/index.ts +24 -0
  235. package/src/subscriptions/paymentOutcomes.ts +23 -0
  236. package/src/subscriptions/postgresRenewalStore.ts +76 -0
  237. package/src/subscriptions/postgresRetryStore.ts +174 -0
  238. package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
  239. package/src/subscriptions/renewalCycleMechanism.ts +225 -0
  240. package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
  241. package/src/subscriptions/renewalIdentity.ts +5 -0
@@ -0,0 +1,328 @@
1
+ -- Native same-obligation retry bookkeeping. The original renewal remains terminal;
2
+ -- payment_pending, order, settlement, snapshot and identity never get recreated.
3
+ -- A prepared receipt is an irreversible dispatch fence: loss of ACK requires
4
+ -- readback, never another prepare. Hosts supply verified payment observations;
5
+ -- these invoker functions perform no payment I/O and grant no runtime authority.
6
+ -- Failure and case propagation retain their separate donor transaction units.
7
+ -- Disable the retry driver before rollback; retain these ledgers for reconciliation.
8
+ CREATE TABLE public.subscription_retry_obligations (
9
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
10
+ renewal_operation_id uuid NOT NULL UNIQUE REFERENCES public.subscription_renewal_operations(id),
11
+ subscription_id uuid NOT NULL REFERENCES public.subscriptions(id),
12
+ cycle_id uuid NOT NULL UNIQUE REFERENCES public.subscription_cycles(id),
13
+ order_id uuid NOT NULL UNIQUE REFERENCES public.commerce_orders(id),
14
+ settlement_intent_id uuid NOT NULL UNIQUE REFERENCES public.commerce_settlement_intents(id),
15
+ logical_cycle_number integer NOT NULL CHECK (logical_cycle_number > 0),
16
+ identity_key text NOT NULL UNIQUE,
17
+ operation_fingerprint text NOT NULL CHECK (operation_fingerprint ~ '^[0-9a-f]{64}$'),
18
+ scheduled_at timestamptz NOT NULL,
19
+ cycle_snapshot jsonb NOT NULL,
20
+ retry_attempt integer NOT NULL DEFAULT 0 CHECK (retry_attempt >= 0),
21
+ attempt_sequence integer NOT NULL DEFAULT 0 CHECK (attempt_sequence >= 0),
22
+ state text NOT NULL DEFAULT 'waiting' CHECK (state IN ('waiting','prepared','recovered')),
23
+ next_retry_at timestamptz,
24
+ failure_class text,
25
+ failure_reason text,
26
+ failed_at timestamptz,
27
+ case_id uuid REFERENCES public.subscription_dunning_cases(id),
28
+ created_at timestamptz NOT NULL DEFAULT now(),
29
+ updated_at timestamptz NOT NULL DEFAULT now(),
30
+ UNIQUE (subscription_id, scheduled_at)
31
+ );
32
+ -- squawk-ignore require-concurrent-index-creation
33
+ CREATE INDEX subscription_retry_due_idx ON public.subscription_retry_obligations(state,next_retry_at,id);
34
+ CREATE TABLE public.subscription_retry_receipts (
35
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
36
+ obligation_id uuid NOT NULL REFERENCES public.subscription_retry_obligations(id),
37
+ receipt_key text NOT NULL UNIQUE CHECK (btrim(receipt_key) <> ''),
38
+ kind text NOT NULL CHECK (kind IN ('failure','prepare','recovery','schedule')),
39
+ attempt_sequence integer NOT NULL CHECK (attempt_sequence >= 0),
40
+ input jsonb NOT NULL,
41
+ result jsonb NOT NULL,
42
+ created_at timestamptz NOT NULL DEFAULT now(),
43
+ UNIQUE (obligation_id, kind, attempt_sequence)
44
+ );
45
+ CREATE TABLE public.subscription_retry_recovery_authorizations (
46
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
47
+ obligation_id uuid NOT NULL REFERENCES public.subscription_retry_obligations(id),
48
+ token_id uuid NOT NULL REFERENCES public.commerce_checkout_recovery_tokens(id),
49
+ retry_attempt integer NOT NULL CHECK (retry_attempt > 0),
50
+ method_ref text NOT NULL CHECK (btrim(method_ref) <> ''),
51
+ method_evidence_ref text NOT NULL CHECK (btrim(method_evidence_ref) <> ''),
52
+ authorized_at timestamptz NOT NULL,
53
+ consumed_at timestamptz,
54
+ UNIQUE (token_id, retry_attempt),
55
+ UNIQUE (obligation_id, retry_attempt)
56
+ );
57
+ REVOKE ALL ON TABLE public.subscription_retry_obligations, public.subscription_retry_receipts, public.subscription_retry_recovery_authorizations FROM PUBLIC, anon, authenticated;
58
+
59
+ CREATE FUNCTION public.subscription_retry_read(p_operation_id uuid)
60
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
61
+ SELECT to_jsonb(o) FROM public.subscription_retry_obligations o WHERE renewal_operation_id=p_operation_id;
62
+ $$;
63
+ CREATE FUNCTION public.subscription_retry_candidates(p_limit integer DEFAULT 50,p_now timestamptz DEFAULT now())
64
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
65
+ SELECT COALESCE(jsonb_agg(to_jsonb(c)), '[]'::jsonb) FROM (
66
+ SELECT o.* FROM public.subscription_retry_obligations o JOIN public.subscriptions s ON s.id=o.subscription_id
67
+ WHERE s.status='active' AND o.state='waiting' AND o.next_retry_at<=p_now
68
+ ORDER BY o.next_retry_at,o.id LIMIT LEAST(GREATEST(p_limit,1),500)
69
+ ) c;
70
+ $$;
71
+
72
+ -- input binds the original logical cycle explicitly; the native renewal counter
73
+ -- is not evidence of activation-cycle numbering. Retry zero is the original.
74
+ CREATE FUNCTION public.subscription_retry_apply_failure(p_operation_id uuid,p_key text,p_input jsonb)
75
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
76
+ DECLARE op public.subscription_renewal_operations; o public.subscription_retry_obligations;
77
+ r public.subscription_retry_receipts; c public.subscription_cycles; i public.commerce_settlement_intents;
78
+ at_time timestamptz := (p_input->>'occurredAt')::timestamptz; seq integer := (p_input->>'attemptSequence')::integer;
79
+ next_time timestamptz; result jsonb;
80
+ BEGIN
81
+ PERFORM 1 FROM public.subscriptions WHERE id=(SELECT subscription_id FROM public.subscription_renewal_operations WHERE id=p_operation_id) FOR UPDATE;
82
+ SELECT * INTO op FROM public.subscription_renewal_operations WHERE id=p_operation_id FOR UPDATE;
83
+ IF op.id IS NULL OR at_time IS NULL OR seq IS NULL OR btrim(COALESCE(p_key,''))='' OR
84
+ btrim(COALESCE(p_input->>'evidenceKey',''))='' OR (p_input->>'firstRecordedAt')::timestamptz IS NULL OR
85
+ (p_input->>'logicalCycleNumber')::integer IS NULL OR (p_input->>'logicalCycleNumber')::integer<1 THEN
86
+ RAISE EXCEPTION 'subscription_retry_invalid_input' USING ERRCODE='22023'; END IF;
87
+ SELECT * INTO o FROM public.subscription_retry_obligations WHERE renewal_operation_id=op.id FOR UPDATE;
88
+ SELECT * INTO r FROM public.subscription_retry_receipts WHERE receipt_key=p_key;
89
+ IF r.id IS NOT NULL THEN
90
+ IF r.obligation_id IS DISTINCT FROM o.id OR r.kind<>'failure' OR r.input<>p_input THEN RAISE EXCEPTION 'subscription_retry_replay_conflict' USING ERRCODE='23505'; END IF;
91
+ RETURN r.result;
92
+ END IF;
93
+ SELECT * INTO c FROM public.subscription_cycles WHERE renewal_operation_id=op.id FOR UPDATE;
94
+ SELECT * INTO i FROM public.commerce_settlement_intents WHERE renewal_operation_id=op.id FOR UPDATE;
95
+ IF c.id IS NULL OR i.id IS NULL OR c.status<>'payment_pending' OR i.status='settled' THEN
96
+ RAISE EXCEPTION 'subscription_retry_not_unpaid' USING ERRCODE='22023'; END IF;
97
+ INSERT INTO public.subscription_retry_obligations(renewal_operation_id,subscription_id,cycle_id,order_id,settlement_intent_id,
98
+ logical_cycle_number,identity_key,operation_fingerprint,scheduled_at,cycle_snapshot)
99
+ VALUES(op.id,op.subscription_id,c.id,i.order_id,i.id,(p_input->>'logicalCycleNumber')::integer,
100
+ op.identity_key,op.operation_fingerprint,op.scheduled_at,op.cycle_snapshot)
101
+ ON CONFLICT (renewal_operation_id) DO NOTHING;
102
+ SELECT * INTO o FROM public.subscription_retry_obligations WHERE renewal_operation_id=op.id FOR UPDATE;
103
+ SELECT * INTO r FROM public.subscription_retry_receipts WHERE receipt_key=p_key;
104
+ IF r.id IS NOT NULL THEN
105
+ IF r.obligation_id<>o.id OR r.kind<>'failure' OR r.input<>p_input THEN RAISE EXCEPTION 'subscription_retry_replay_conflict' USING ERRCODE='23505'; END IF;
106
+ RETURN r.result;
107
+ END IF;
108
+ IF o.attempt_sequence<>seq OR o.logical_cycle_number<>(p_input->>'logicalCycleNumber')::integer OR o.state='recovered' OR
109
+ EXISTS(SELECT 1 FROM public.subscription_retry_receipts WHERE obligation_id=o.id AND kind='failure' AND
110
+ (attempt_sequence=seq OR input->>'evidenceKey'=p_input->>'evidenceKey')) THEN
111
+ RAISE EXCEPTION 'subscription_retry_attempt_conflict' USING ERRCODE='23505'; END IF;
112
+ IF seq=0 THEN
113
+ IF op.state='attempt_prepared' THEN
114
+ PERFORM public.commerce_record_subscription_renewal_outcome(op.id,p_key,op.operation_fingerprint,'refused',
115
+ op.external_attempt_ref,p_input->>'reason',at_time);
116
+ ELSIF op.state<>'refused' OR NOT EXISTS(SELECT 1 FROM public.subscription_renewal_outcomes
117
+ WHERE renewal_operation_id=op.id AND outcome='refused') OR op.attempt_prepared_at IS NULL THEN
118
+ RAISE EXCEPTION 'subscription_retry_definite_decline_required' USING ERRCODE='22023';
119
+ ELSE
120
+ at_time:=op.terminal_at;
121
+ END IF;
122
+ ELSE
123
+ IF o.state<>'prepared' OR btrim(COALESCE(p_input->>'externalRef',''))='' OR
124
+ btrim(COALESCE(p_input->>'evidenceKey',''))='' OR (p_input->>'firstRecordedAt')::timestamptz IS NULL THEN RAISE EXCEPTION 'subscription_retry_not_prepared' USING ERRCODE='22023'; END IF;
125
+ -- As on the M07 truth rail, attempt evidence does not replace the
126
+ -- aggregate's write-once external handle.
127
+ PERFORM public.commerce_record_settlement(p_key||':settlement',i.id,'failed',NULL,p_input,at_time);
128
+ UPDATE public.fulfillment_inventory_reservations SET status='released',terminal_reason='settlement_refused',updated_at=at_time
129
+ WHERE renewal_operation_id=op.id AND status='committed';
130
+ END IF;
131
+ next_time := CASE WHEN p_input->>'failureClass'='hard_do_not_retry' THEN NULL
132
+ WHEN o.retry_attempt=0 THEN at_time+interval '24 hours'
133
+ WHEN o.retry_attempt=1 THEN at_time+interval '72 hours'
134
+ WHEN o.retry_attempt=2 THEN at_time+interval '168 hours' ELSE NULL END;
135
+ UPDATE public.subscription_retry_obligations SET retry_attempt=retry_attempt+1,state='waiting',next_retry_at=next_time,
136
+ failure_class=p_input->>'failureClass',failure_reason=p_input->>'reason',failed_at=at_time,updated_at=at_time WHERE id=o.id RETURNING * INTO o;
137
+ result:=to_jsonb(o);
138
+ INSERT INTO public.subscription_retry_receipts(obligation_id,receipt_key,kind,attempt_sequence,input,result)
139
+ VALUES(o.id,p_key,'failure',seq,p_input,result);
140
+ RETURN result;
141
+ END;
142
+ $$;
143
+
144
+ CREATE FUNCTION public.subscription_retry_open_case(p_operation_id uuid,p_template text,p_recovery_path text)
145
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
146
+ DECLARE o public.subscription_retry_obligations; result jsonb; client uuid; i public.commerce_settlement_intents;
147
+ BEGIN
148
+ PERFORM 1 FROM public.subscriptions WHERE id=(SELECT subscription_id FROM public.subscription_renewal_operations WHERE id=p_operation_id) FOR UPDATE;
149
+ SELECT * INTO o FROM public.subscription_retry_obligations WHERE renewal_operation_id=p_operation_id FOR UPDATE;
150
+ IF o.id IS NULL OR o.retry_attempt=0 OR o.state='recovered' THEN RAISE EXCEPTION 'subscription_retry_failure_required'; END IF;
151
+ SELECT client_id INTO client FROM public.subscriptions WHERE id=o.subscription_id;
152
+ SELECT * INTO i FROM public.commerce_settlement_intents WHERE id=o.settlement_intent_id;
153
+ IF o.case_id IS NULL THEN
154
+ result:=public.dunning_lifecycle_open_case(o.identity_key||':retry-case',o.subscription_id,o.cycle_id,client,
155
+ o.retry_attempt,o.next_retry_at,o.failure_class,o.failure_reason,o.failed_at,i.amount_minor,i.currency_code,p_template,p_recovery_path);
156
+ UPDATE public.subscription_retry_obligations SET case_id=(result->>'caseId')::uuid WHERE id=o.id RETURNING * INTO o;
157
+ END IF;
158
+ RETURN public.dunning_lifecycle_handle_failure(o.case_id,o.failed_at,o.failure_class,o.failure_reason,
159
+ o.retry_attempt,o.next_retry_at,p_template,p_recovery_path);
160
+ END;
161
+ $$;
162
+
163
+ -- Token consumption and durable usable-method evidence precede scheduling. A
164
+ -- method webhook alone cannot reopen a class stop or an exhausted ladder.
165
+ CREATE FUNCTION public.subscription_retry_authorize_recovery(p_operation_id uuid,p_token_id uuid,p_method text,p_evidence text,p_now timestamptz DEFAULT now())
166
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
167
+ DECLARE o public.subscription_retry_obligations; t public.commerce_checkout_recovery_tokens;
168
+ a public.subscription_retry_recovery_authorizations;
169
+ BEGIN
170
+ PERFORM 1 FROM public.subscriptions WHERE id=(SELECT subscription_id FROM public.subscription_renewal_operations WHERE id=p_operation_id) FOR UPDATE;
171
+ SELECT * INTO o FROM public.subscription_retry_obligations WHERE renewal_operation_id=p_operation_id FOR UPDATE;
172
+ SELECT * INTO a FROM public.subscription_retry_recovery_authorizations WHERE obligation_id=o.id AND retry_attempt=o.retry_attempt;
173
+ IF a.id IS NOT NULL THEN
174
+ IF a.token_id<>p_token_id OR a.method_ref IS DISTINCT FROM p_method OR a.method_evidence_ref IS DISTINCT FROM p_evidence THEN
175
+ RAISE EXCEPTION 'subscription_retry_authorization_conflict' USING ERRCODE='23505'; END IF;
176
+ RETURN to_jsonb(a);
177
+ END IF;
178
+ SELECT * INTO t FROM public.commerce_checkout_recovery_tokens WHERE id=p_token_id FOR UPDATE;
179
+ IF o.id IS NULL OR o.case_id IS NULL OR o.state<>'waiting' OR t.id IS NULL OR t.order_id<>o.order_id OR
180
+ t.client_id IS DISTINCT FROM (SELECT client_id FROM public.subscriptions WHERE id=o.subscription_id) OR
181
+ t.revoked_at IS NOT NULL OR t.expires_at<=p_now OR
182
+ NOT EXISTS(SELECT 1 FROM public.subscription_method_states WHERE subscription_id=o.subscription_id AND health_state='healthy') OR btrim(COALESCE(p_method,''))='' OR btrim(COALESCE(p_evidence,''))='' OR
183
+ NOT EXISTS(SELECT 1 FROM public.subscription_dunning_cases WHERE id=o.case_id AND status='open') THEN
184
+ RAISE EXCEPTION 'subscription_retry_recovery_not_authorized' USING ERRCODE='22023'; END IF;
185
+ INSERT INTO public.subscription_retry_recovery_authorizations(obligation_id,token_id,retry_attempt,method_ref,method_evidence_ref,authorized_at)
186
+ VALUES(o.id,t.id,o.retry_attempt,p_method,p_evidence,p_now) RETURNING * INTO a;
187
+ UPDATE public.commerce_checkout_recovery_tokens SET revoked_at=p_now WHERE id=t.id;
188
+ RETURN to_jsonb(a);
189
+ END;
190
+ $$;
191
+ CREATE FUNCTION public.subscription_retry_schedule_recovery(p_operation_id uuid,p_key text,p_authorization_id uuid,p_now timestamptz DEFAULT now())
192
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
193
+ DECLARE o public.subscription_retry_obligations; a public.subscription_retry_recovery_authorizations; r public.subscription_retry_receipts; input jsonb;
194
+ BEGIN
195
+ PERFORM 1 FROM public.subscriptions WHERE id=(SELECT subscription_id FROM public.subscription_renewal_operations WHERE id=p_operation_id) FOR UPDATE;
196
+ SELECT * INTO o FROM public.subscription_retry_obligations WHERE renewal_operation_id=p_operation_id FOR UPDATE;
197
+ input:=jsonb_build_object('authorizationId',p_authorization_id,'now',p_now);
198
+ SELECT * INTO r FROM public.subscription_retry_receipts WHERE receipt_key=p_key;
199
+ IF r.id IS NOT NULL THEN
200
+ IF r.obligation_id<>o.id OR r.kind<>'schedule' OR r.input<>input THEN RAISE EXCEPTION 'subscription_retry_replay_conflict' USING ERRCODE='23505'; END IF;
201
+ RETURN r.result;
202
+ END IF;
203
+ SELECT * INTO a FROM public.subscription_retry_recovery_authorizations WHERE id=p_authorization_id FOR UPDATE;
204
+ IF o.id IS NULL OR o.state<>'waiting' OR a.obligation_id IS DISTINCT FROM o.id OR a.retry_attempt<>o.retry_attempt OR a.consumed_at IS NOT NULL OR
205
+ NOT EXISTS(SELECT 1 FROM public.subscription_method_states WHERE subscription_id=o.subscription_id AND health_state='healthy') THEN
206
+ RAISE EXCEPTION 'subscription_retry_recovery_not_authorized' USING ERRCODE='22023'; END IF;
207
+ -- Receipt first: a failed schedule write rolls the receipt back too.
208
+ INSERT INTO public.subscription_retry_receipts(obligation_id,receipt_key,kind,attempt_sequence,input,result)
209
+ VALUES(o.id,p_key,'schedule',o.attempt_sequence,input,jsonb_build_object('scheduled',true,'obligationId',o.id));
210
+ UPDATE public.subscription_retry_obligations SET next_retry_at=p_now,updated_at=p_now WHERE id=o.id;
211
+ UPDATE public.subscription_dunning_cases SET next_retry_at=p_now,updated_at=p_now WHERE id=o.case_id AND status='open';
212
+ RETURN jsonb_build_object('scheduled',true,'obligationId',o.id);
213
+ END;
214
+ $$;
215
+
216
+ -- Native M06 stock is locked in SKU order; all lines are checked before any
217
+ -- released reservation is recommitted. Existing unrelated commitments survive.
218
+ CREATE FUNCTION public.subscription_retry_prepare(p_operation_id uuid,p_key text,p_input jsonb,p_now timestamptz DEFAULT now())
219
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
220
+ DECLARE o public.subscription_retry_obligations; op public.subscription_renewal_operations; r public.subscription_retry_receipts;
221
+ a public.subscription_retry_recovery_authorizations; reservation public.fulfillment_inventory_reservations;
222
+ line record; available bigint; committed bigint; result jsonb;
223
+ BEGIN
224
+ PERFORM 1 FROM public.subscriptions WHERE id=(SELECT subscription_id FROM public.subscription_renewal_operations WHERE id=p_operation_id) FOR UPDATE;
225
+ SELECT * INTO op FROM public.subscription_renewal_operations WHERE id=p_operation_id FOR UPDATE;
226
+ SELECT * INTO o FROM public.subscription_retry_obligations WHERE renewal_operation_id=op.id FOR UPDATE;
227
+ SELECT * INTO r FROM public.subscription_retry_receipts WHERE receipt_key=p_key;
228
+ IF r.id IS NOT NULL THEN
229
+ IF r.obligation_id<>o.id OR r.kind<>'prepare' OR r.input<>p_input THEN RAISE EXCEPTION 'subscription_retry_replay_conflict' USING ERRCODE='23505'; END IF;
230
+ RETURN r.result||jsonb_build_object('execute',false,'replayed',true);
231
+ END IF;
232
+ IF o.id IS NULL OR o.state<>'waiting' OR o.next_retry_at IS NULL OR o.next_retry_at>p_now OR
233
+ NOT EXISTS(SELECT 1 FROM public.subscriptions WHERE id=o.subscription_id AND status='active') OR
234
+ NOT EXISTS(SELECT 1 FROM public.subscription_cycles WHERE id=o.cycle_id AND status='payment_pending') OR
235
+ NOT EXISTS(SELECT 1 FROM public.commerce_settlement_intents WHERE id=o.settlement_intent_id AND status='failed') OR
236
+ btrim(COALESCE(p_input->>'idempotencyKey',''))='' OR btrim(COALESCE(p_input->>'providerIdempotencyKey',''))='' OR
237
+ COALESCE(p_input->>'providerRequestFingerprint','') !~ '^[0-9a-f]{64}$' OR
238
+ p_input->>'paymentIntentId' IS DISTINCT FROM o.settlement_intent_id::text OR
239
+ p_input->>'orderRef' IS DISTINCT FROM o.order_id::text OR
240
+ (p_input->>'amountMinor')::bigint IS DISTINCT FROM (o.cycle_snapshot->>'totalAmountMinor')::bigint OR
241
+ p_input->>'currency' IS DISTINCT FROM o.cycle_snapshot->>'currency' OR
242
+ EXISTS(SELECT 1 FROM public.subscription_retry_receipts WHERE kind='prepare' AND (input->>'idempotencyKey'=p_input->>'idempotencyKey' OR input->>'providerIdempotencyKey'=p_input->>'providerIdempotencyKey')) OR btrim(COALESCE(p_input->>'paymentMethodRef',''))='' THEN
243
+ RAISE EXCEPTION 'subscription_retry_not_due_or_ambiguous' USING ERRCODE='22023'; END IF;
244
+ SELECT * INTO a FROM public.subscription_retry_recovery_authorizations WHERE obligation_id=o.id AND retry_attempt=o.retry_attempt FOR UPDATE;
245
+ IF (o.retry_attempt>=4 OR o.failure_class='hard_do_not_retry') AND (a.id IS NULL OR a.consumed_at IS NOT NULL) THEN
246
+ RAISE EXCEPTION 'subscription_retry_recovery_not_authorized' USING ERRCODE='22023'; END IF;
247
+ IF a.id IS NOT NULL AND (a.method_ref IS DISTINCT FROM p_input->>'paymentMethodRef' OR
248
+ NOT EXISTS(SELECT 1 FROM public.subscription_method_states WHERE subscription_id=o.subscription_id AND health_state='healthy')) THEN RAISE EXCEPTION 'subscription_retry_method_mismatch'; END IF;
249
+ SELECT * INTO reservation FROM public.fulfillment_inventory_reservations WHERE renewal_operation_id=op.id FOR UPDATE;
250
+ IF reservation.id IS NULL OR reservation.operation_fingerprint<>o.operation_fingerprint THEN RAISE EXCEPTION 'subscription_retry_stock_binding_missing'; END IF;
251
+ PERFORM stock.sku FROM public.fulfillment_stock_current stock JOIN public.fulfillment_inventory_reservation_lines l ON l.sku=stock.sku
252
+ WHERE l.reservation_id=reservation.id AND stock.source_key=reservation.source_key ORDER BY stock.sku FOR UPDATE OF stock;
253
+ FOR line IN SELECT * FROM public.fulfillment_inventory_reservation_lines WHERE reservation_id=reservation.id ORDER BY sku LOOP
254
+ SELECT for_sale_quantity INTO available FROM public.fulfillment_stock_current WHERE source_key=reservation.source_key AND sku=line.sku
255
+ AND inventory_class='sellable' AND stale_after>p_now;
256
+ SELECT COALESCE(sum(l.quantity),0) INTO committed FROM public.fulfillment_inventory_reservation_lines l
257
+ JOIN public.fulfillment_inventory_reservations rr ON rr.id=l.reservation_id WHERE rr.source_key=reservation.source_key AND l.sku=line.sku
258
+ AND rr.id<>reservation.id AND (rr.status='committed' OR (rr.status='held' AND rr.expires_at>p_now));
259
+ IF available IS NULL OR available-committed<line.quantity THEN RAISE EXCEPTION 'subscription_retry_inventory_unavailable' USING ERRCODE='23514'; END IF;
260
+ END LOOP;
261
+ UPDATE public.fulfillment_inventory_reservations SET status='committed',expires_at=NULL,terminal_reason=NULL,updated_at=p_now WHERE id=reservation.id;
262
+ UPDATE public.subscription_retry_obligations SET state='prepared',attempt_sequence=attempt_sequence+1,next_retry_at=NULL,updated_at=p_now WHERE id=o.id RETURNING * INTO o;
263
+ UPDATE public.subscription_retry_recovery_authorizations SET consumed_at=p_now WHERE id=a.id;
264
+ result:=jsonb_build_object('execute',true,'replayed',false,'obligationId',o.id,'attemptSequence',o.attempt_sequence,
265
+ 'cycleId',o.cycle_id,'orderId',o.order_id,'settlementIntentId',o.settlement_intent_id,'identityKey',o.identity_key,
266
+ 'operationFingerprint',o.operation_fingerprint,'snapshot',o.cycle_snapshot,'request',p_input);
267
+ INSERT INTO public.subscription_retry_receipts(obligation_id,receipt_key,kind,attempt_sequence,input,result)
268
+ VALUES(o.id,p_key,'prepare',o.attempt_sequence,p_input,result);
269
+ RETURN result;
270
+ END;
271
+ $$;
272
+
273
+ -- The M07 external settlement reference remains write-once across attempts.
274
+ -- M07 truth ingestion retains that aggregate handle while recording distinct
275
+ -- attempt evidence. Never clear or substitute the settlement reference.
276
+ -- Verified positive evidence can settle an original decline without resetting its
277
+ -- operation. An outstanding retry must be reconciled under its own prepare key.
278
+ CREATE FUNCTION public.subscription_retry_record_recovery(p_operation_id uuid,p_key text,p_input jsonb)
279
+ RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog AS $$
280
+ DECLARE o public.subscription_retry_obligations; r public.subscription_retry_receipts; prep public.subscription_retry_receipts;
281
+ i public.commerce_settlement_intents; result jsonb; at_time timestamptz:=(p_input->>'occurredAt')::timestamptz;
282
+ BEGIN
283
+ PERFORM 1 FROM public.subscriptions WHERE id=(SELECT subscription_id FROM public.subscription_renewal_operations WHERE id=p_operation_id) FOR UPDATE;
284
+ SELECT * INTO o FROM public.subscription_retry_obligations WHERE renewal_operation_id=p_operation_id FOR UPDATE;
285
+ SELECT * INTO r FROM public.subscription_retry_receipts WHERE receipt_key=p_key;
286
+ IF r.id IS NOT NULL THEN
287
+ 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;
288
+ RETURN r.result;
289
+ END IF;
290
+ SELECT * INTO i FROM public.commerce_settlement_intents WHERE id=o.settlement_intent_id FOR UPDATE;
291
+ SELECT * INTO prep FROM public.subscription_retry_receipts WHERE obligation_id=o.id AND kind='prepare' AND attempt_sequence=o.attempt_sequence;
292
+ IF o.id IS NULL OR o.state='recovered' OR at_time IS NULL OR (p_input->>'firstRecordedAt')::timestamptz IS NULL OR
293
+ btrim(COALESCE(p_input->>'evidenceKey',''))='' OR btrim(COALESCE(p_input->>'externalRef',''))='' OR
294
+ (p_input->>'amountMinor')::bigint IS DISTINCT FROM i.amount_minor OR p_input->>'currency' IS DISTINCT FROM i.currency_code OR
295
+ (o.state='prepared' AND prep.receipt_key IS DISTINCT FROM p_input->>'prepareKey') THEN
296
+ RAISE EXCEPTION 'subscription_retry_positive_evidence_required' USING ERRCODE='22023'; END IF;
297
+ PERFORM public.payment_truth_ingest_event(p_input->>'evidenceKey',
298
+ encode(sha256(convert_to(jsonb_build_object('operationId',p_operation_id,'input',p_input)::text,'UTF8')),'hex'),
299
+ p_key,i.id,'captured',i.amount_minor,i.currency_code,at_time,NULL,NULL,
300
+ p_input||jsonb_build_object('renewalOperationId',p_operation_id,'operationFingerprint',o.operation_fingerprint),
301
+ NULL,NULL,p_input->>'template');
302
+ UPDATE public.subscription_renewal_terms SET next_cycle_number=GREATEST(next_cycle_number,
303
+ (SELECT cycle_number+1 FROM public.subscription_renewal_operations WHERE id=p_operation_id)),updated_at=at_time WHERE subscription_id=o.subscription_id;
304
+ UPDATE public.fulfillment_inventory_reservations SET status='consumed',terminal_reason=NULL,updated_at=at_time
305
+ WHERE renewal_operation_id=p_operation_id AND status='committed';
306
+ IF o.case_id IS NOT NULL THEN PERFORM public.dunning_lifecycle_record_recovery(o.case_id,at_time,p_input->>'template'); END IF;
307
+ UPDATE public.subscription_retry_obligations SET state='recovered',next_retry_at=NULL,updated_at=at_time WHERE id=o.id RETURNING * INTO o;
308
+ result:=to_jsonb(o);
309
+ INSERT INTO public.subscription_retry_receipts(obligation_id,receipt_key,kind,attempt_sequence,input,result)
310
+ VALUES(o.id,p_key,'recovery',o.attempt_sequence,p_input,result);
311
+ RETURN result;
312
+ END;
313
+ $$;
314
+ CREATE FUNCTION public.subscription_retry_note_row_outcome(p_operation_id uuid,p_key text,p_input jsonb)
315
+ RETURNS jsonb LANGUAGE sql SET search_path = pg_catalog AS $$
316
+ SELECT CASE WHEN p_input->>'outcome'='succeeded' THEN public.subscription_retry_record_recovery(p_operation_id,p_key,p_input)
317
+ WHEN p_input->>'outcome'='refused' THEN public.subscription_retry_apply_failure(p_operation_id,p_key,p_input)
318
+ ELSE jsonb_build_object('awaitingProvider',true,'execute',false) END;
319
+ $$;
320
+ REVOKE ALL ON FUNCTION public.subscription_retry_read(uuid) FROM PUBLIC, anon, authenticated;
321
+ REVOKE ALL ON FUNCTION public.subscription_retry_candidates(integer,timestamptz) FROM PUBLIC, anon, authenticated;
322
+ REVOKE ALL ON FUNCTION public.subscription_retry_apply_failure(uuid,text,jsonb) FROM PUBLIC, anon, authenticated;
323
+ REVOKE ALL ON FUNCTION public.subscription_retry_open_case(uuid,text,text) FROM PUBLIC, anon, authenticated;
324
+ REVOKE ALL ON FUNCTION public.subscription_retry_authorize_recovery(uuid,uuid,text,text,timestamptz) FROM PUBLIC, anon, authenticated;
325
+ REVOKE ALL ON FUNCTION public.subscription_retry_schedule_recovery(uuid,text,uuid,timestamptz) FROM PUBLIC, anon, authenticated;
326
+ REVOKE ALL ON FUNCTION public.subscription_retry_prepare(uuid,text,jsonb,timestamptz) FROM PUBLIC, anon, authenticated;
327
+ REVOKE ALL ON FUNCTION public.subscription_retry_record_recovery(uuid,text,jsonb) FROM PUBLIC, anon, authenticated;
328
+ REVOKE ALL ON FUNCTION public.subscription_retry_note_row_outcome(uuid,text,jsonb) FROM PUBLIC, anon, authenticated;
@@ -0,0 +1,30 @@
1
+ CREATE OR REPLACE FUNCTION public.fulfillment_record_stock_evidence(
2
+ p_source_key text, p_idempotency_key text, p_fingerprint text, p_sku text,
3
+ p_threshold_kind text, p_severity text, p_provider_for_sale integer,
4
+ p_local_available integer
5
+ ) RETURNS jsonb LANGUAGE plpgsql SET search_path = pg_catalog, public AS $$
6
+ DECLARE v_existing public.fulfillment_stock_evidence%ROWTYPE;
7
+ BEGIN
8
+ SELECT * INTO v_existing FROM public.fulfillment_stock_evidence
9
+ WHERE idempotency_key = p_idempotency_key FOR UPDATE;
10
+ IF FOUND THEN
11
+ IF v_existing.operation_fingerprint <> p_fingerprint THEN
12
+ RAISE EXCEPTION 'fulfillment_stock_evidence_idempotency_conflict' USING ERRCODE = '23505';
13
+ END IF;
14
+ UPDATE public.fulfillment_stock_evidence SET
15
+ status = 'open', severity = p_severity,
16
+ provider_for_sale_quantity = p_provider_for_sale,
17
+ local_available_quantity = p_local_available,
18
+ last_seen_at = clock_timestamp(), resolved_at = NULL
19
+ WHERE idempotency_key = p_idempotency_key;
20
+ RETURN jsonb_build_object('replayed', true);
21
+ END IF;
22
+ INSERT INTO public.fulfillment_stock_evidence(
23
+ idempotency_key, operation_fingerprint, source_key, sku, threshold_kind,
24
+ severity, provider_for_sale_quantity, local_available_quantity
25
+ ) VALUES (
26
+ p_idempotency_key, p_fingerprint, p_source_key, p_sku, p_threshold_kind,
27
+ p_severity, p_provider_for_sale, p_local_available
28
+ );
29
+ RETURN jsonb_build_object('replayed', false);
30
+ END $$;