@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.
- package/CHANGELOG.md +327 -269
- package/README.md +123 -12
- package/dist/communications/contracts/subscriptionPaymentExpired.d.ts +3 -2
- package/dist/communications/contracts/subscriptionPaymentExpired.js +1 -1
- package/dist/communications/contracts/subscriptionPaymentFailed.d.ts +4 -3
- package/dist/communications/contracts/subscriptionPaymentFailed.js +4 -5
- package/dist/composition/index.d.ts +1 -0
- package/dist/composition/index.js +1 -0
- package/dist/composition/subscriptionContribution.d.ts +18 -0
- package/dist/composition/subscriptionContribution.js +19 -0
- package/dist/fulfillment/contracts/dispatch.d.ts +4 -0
- package/dist/fulfillment/dispatchProjection.d.ts +1 -1
- package/dist/fulfillment/dispatchProjection.js +4 -2
- package/dist/fulfillment/omnipack/client.js +14 -6
- package/dist/fulfillment/omnipack/index.js +5 -2
- package/dist/fulfillment/providerDispatchWorker.d.ts +38 -0
- package/dist/fulfillment/providerDispatchWorker.js +19 -1
- package/dist/inventory/index.d.ts +2 -0
- package/dist/inventory/index.js +1 -0
- package/dist/inventory/postgresStockAvailability.d.ts +24 -0
- package/dist/inventory/postgresStockAvailability.js +48 -0
- package/dist/invoicing/accountingCanonicalProviderLines.d.ts +5 -0
- package/dist/invoicing/accountingCanonicalProviderLines.js +104 -0
- package/dist/invoicing/accountingLineAllocation.d.ts +2 -0
- package/dist/invoicing/accountingLineAllocation.js +69 -0
- package/dist/invoicing/adapterConformance.d.ts +42 -0
- package/dist/invoicing/adapterConformance.js +296 -0
- package/dist/invoicing/artifacts.d.ts +7 -0
- package/dist/invoicing/artifacts.js +20 -0
- package/dist/invoicing/bindings.d.ts +46 -0
- package/dist/invoicing/bindings.js +69 -0
- package/dist/invoicing/channelPolicy.d.ts +28 -0
- package/dist/invoicing/channelPolicy.js +27 -0
- package/dist/invoicing/continuity.d.ts +12 -0
- package/dist/invoicing/continuity.js +18 -0
- package/dist/invoicing/contracts/facts.d.ts +11 -1
- package/dist/invoicing/contracts/host.d.ts +3 -4
- package/dist/invoicing/contracts/index.d.ts +1 -1
- package/dist/invoicing/contracts/store.d.ts +15 -5
- package/dist/invoicing/contribution.d.ts +21 -0
- package/dist/invoicing/contribution.js +19 -0
- package/dist/invoicing/correction.d.ts +42 -0
- package/dist/invoicing/correction.js +44 -0
- package/dist/invoicing/delivery.d.ts +85 -0
- package/dist/invoicing/delivery.js +79 -0
- package/dist/invoicing/documentHistory.d.ts +50 -0
- package/dist/invoicing/documentHistory.js +213 -0
- package/dist/invoicing/documentSync.d.ts +24 -0
- package/dist/invoicing/documentSync.js +26 -0
- package/dist/invoicing/dutyCollector.d.ts +15 -0
- package/dist/invoicing/dutyCollector.js +35 -0
- package/dist/invoicing/fulfillmentTrigger.d.ts +56 -0
- package/dist/invoicing/fulfillmentTrigger.js +90 -0
- package/dist/invoicing/handoff.d.ts +37 -0
- package/dist/invoicing/handoff.js +62 -0
- package/dist/invoicing/index.d.ts +38 -0
- package/dist/invoicing/index.js +36 -0
- package/dist/invoicing/issue.d.ts +39 -0
- package/dist/invoicing/issue.js +55 -0
- package/dist/invoicing/migrations.d.ts +48 -0
- package/dist/invoicing/migrations.js +25 -0
- package/dist/invoicing/nativeUnsupported.d.ts +9 -0
- package/dist/invoicing/nativeUnsupported.js +22 -0
- package/dist/invoicing/paymentReadback.d.ts +20 -0
- package/dist/invoicing/paymentReadback.js +47 -0
- package/dist/invoicing/postgresInvoiceStore.d.ts +55 -0
- package/dist/invoicing/postgresInvoiceStore.js +344 -0
- package/dist/invoicing/readModel.d.ts +74 -0
- package/dist/invoicing/readModel.js +90 -0
- package/dist/invoicing/readback.d.ts +4 -0
- package/dist/invoicing/readback.js +30 -0
- package/dist/invoicing/reversal.d.ts +37 -0
- package/dist/invoicing/reversal.js +28 -0
- package/dist/invoicing/sellerRevision.d.ts +19 -0
- package/dist/invoicing/sellerRevision.js +14 -0
- package/dist/invoicing/settlementSync.d.ts +18 -0
- package/dist/invoicing/settlementSync.js +32 -0
- package/dist/invoicing/snapshot.d.ts +52 -0
- package/dist/invoicing/snapshot.js +141 -0
- package/dist/subscriptions/commandExamples.d.ts +6 -0
- package/dist/subscriptions/commandExamples.js +20 -0
- package/dist/subscriptions/contracts/commands.d.ts +8 -1
- package/dist/subscriptions/contracts/index.d.ts +2 -2
- package/dist/subscriptions/contracts/stockWait.d.ts +12 -1
- package/dist/subscriptions/index.d.ts +22 -4
- package/dist/subscriptions/index.js +22 -3
- package/dist/subscriptions/lifecycleMessages.d.ts +9 -0
- package/dist/subscriptions/lifecycleMessages.js +38 -0
- package/dist/subscriptions/paymentOutcomes.js +1 -1
- package/dist/subscriptions/postgresCommandStore.d.ts +44 -0
- package/dist/subscriptions/postgresCommandStore.js +172 -0
- package/dist/subscriptions/postgresRetryStore.d.ts +6 -0
- package/dist/subscriptions/postgresRetryStore.js +11 -0
- package/dist/subscriptions/postgresStockWaitStore.d.ts +62 -0
- package/dist/subscriptions/postgresStockWaitStore.js +174 -0
- package/dist/subscriptions/services.d.ts +31 -0
- package/dist/subscriptions/services.js +15 -0
- package/package.json +3 -3
- package/release-gates.json +21 -7
- package/sql/20260809210000_accounting_document_rail.sql +889 -0
- package/sql/20260811150000_accounting_payout_rail.sql +333 -0
- package/sql/20260812130000_accounting_document_lifecycle_rail.sql +460 -0
- package/sql/20261022190000_invoicing_durable_duties.sql +494 -0
- package/sql/20261022191000_invoicing_document_effects.sql +249 -0
- package/sql/20261022192000_invoicing_payment_decoupling.sql +144 -0
- package/sql/20261022195000_invoicing_seller_revision.sql +65 -0
- package/sql/20261024170000_subscription_command_lifecycle.sql +404 -0
- package/sql/20261024180000_subscription_stock_wait.sql +330 -0
- package/sql/20261024183000_inventory_available_to_promise.sql +70 -0
- package/sql/20261024184000_subscription_renewal_reserve_uses_seam.sql +147 -0
- package/sql/20261024184500_subscription_retry_late_positive_stock.sql +59 -0
- package/sql/20261024184600_subscription_retry_candidate_payment_filter.sql +22 -0
- package/src/communications/contracts/subscriptionPaymentExpired.ts +4 -3
- package/src/communications/contracts/subscriptionPaymentFailed.ts +8 -8
- package/src/composition/index.ts +1 -0
- package/src/composition/subscriptionContribution.ts +25 -0
- package/src/fulfillment/contracts/dispatch.ts +4 -0
- package/src/fulfillment/dispatchProjection.ts +4 -2
- package/src/fulfillment/omnipack/client.ts +13 -6
- package/src/fulfillment/omnipack/index.ts +5 -2
- package/src/fulfillment/providerDispatchWorker.ts +14 -1
- package/src/inventory/index.ts +2 -0
- package/src/inventory/postgresStockAvailability.ts +54 -0
- package/src/invoicing/accountingCanonicalProviderLines.ts +134 -0
- package/src/invoicing/accountingLineAllocation.ts +69 -0
- package/src/invoicing/adapterConformance.ts +249 -0
- package/src/invoicing/artifacts.ts +19 -0
- package/src/invoicing/bindings.ts +80 -0
- package/src/invoicing/channelPolicy.ts +50 -0
- package/src/invoicing/continuity.ts +15 -0
- package/src/invoicing/contracts/facts.ts +12 -1
- package/src/invoicing/contracts/host.ts +3 -4
- package/src/invoicing/contracts/index.ts +1 -1
- package/src/invoicing/contracts/store.ts +11 -7
- package/src/invoicing/contribution.ts +28 -0
- package/src/invoicing/correction.ts +56 -0
- package/src/invoicing/delivery.ts +122 -0
- package/src/invoicing/documentHistory.ts +301 -0
- package/src/invoicing/documentSync.ts +51 -0
- package/src/invoicing/dutyCollector.ts +31 -0
- package/src/invoicing/fulfillmentTrigger.ts +144 -0
- package/src/invoicing/handoff.ts +71 -0
- package/src/invoicing/index.ts +38 -0
- package/src/invoicing/issue.ts +64 -0
- package/src/invoicing/migrations.ts +25 -0
- package/src/invoicing/nativeUnsupported.ts +24 -0
- package/src/invoicing/paymentReadback.ts +70 -0
- package/src/invoicing/postgresInvoiceStore.ts +334 -0
- package/src/invoicing/readModel.ts +80 -0
- package/src/invoicing/readback.ts +26 -0
- package/src/invoicing/reversal.ts +34 -0
- package/src/invoicing/sellerRevision.ts +20 -0
- package/src/invoicing/settlementSync.ts +37 -0
- package/src/invoicing/snapshot.ts +192 -0
- package/src/subscriptions/commandExamples.ts +19 -0
- package/src/subscriptions/contracts/commands.ts +35 -2
- package/src/subscriptions/contracts/index.ts +2 -2
- package/src/subscriptions/contracts/stockWait.ts +14 -2
- package/src/subscriptions/index.ts +30 -3
- package/src/subscriptions/lifecycleMessages.ts +37 -0
- package/src/subscriptions/paymentOutcomes.ts +1 -1
- package/src/subscriptions/postgresCommandStore.ts +183 -0
- package/src/subscriptions/postgresRetryStore.ts +11 -0
- package/src/subscriptions/postgresStockWaitStore.ts +170 -0
- package/src/subscriptions/services.ts +21 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
-- Fenced document effects. No payment, subscription, shipment or outbox trigger.
|
|
2
|
+
CREATE TABLE public.invoicing_document_observations (
|
|
3
|
+
event_key text PRIMARY KEY,
|
|
4
|
+
document_id uuid NOT NULL REFERENCES public.accounting_documents(id),
|
|
5
|
+
observation jsonb NOT NULL,
|
|
6
|
+
observed_at timestamptz NOT NULL
|
|
7
|
+
);
|
|
8
|
+
CREATE TABLE public.invoicing_delivery_receipts (
|
|
9
|
+
delivery_id uuid PRIMARY KEY REFERENCES public.accounting_document_deliveries(id),
|
|
10
|
+
generation integer NOT NULL DEFAULT 0,
|
|
11
|
+
claim_token uuid,
|
|
12
|
+
lease_until timestamptz,
|
|
13
|
+
state text NOT NULL DEFAULT 'ready' CHECK (state IN ('ready','processing','sent','uncertain','failed')),
|
|
14
|
+
result jsonb
|
|
15
|
+
);
|
|
16
|
+
CREATE FUNCTION public.invoicing_request_reversal(p_key text,p_order uuid,p_reason text,p_evidence jsonb)
|
|
17
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
|
|
18
|
+
DECLARE
|
|
19
|
+
v_base public.invoicing_duties%ROWTYPE;
|
|
20
|
+
v_child public.invoicing_duties%ROWTYPE;
|
|
21
|
+
v_document public.accounting_documents%ROWTYPE;
|
|
22
|
+
v_reversal jsonb;
|
|
23
|
+
v_result jsonb;
|
|
24
|
+
v_facts jsonb;
|
|
25
|
+
v_prior public.invoicing_effect_receipts%ROWTYPE;
|
|
26
|
+
v_existing_document uuid;
|
|
27
|
+
v_ready uuid;
|
|
28
|
+
v_safe_void boolean;
|
|
29
|
+
BEGIN
|
|
30
|
+
IF nullif(btrim(p_key),'') IS NULL OR nullif(btrim(p_reason),'') IS NULL OR jsonb_typeof(p_evidence) IS DISTINCT FROM 'object' THEN
|
|
31
|
+
RAISE EXCEPTION 'invoicing_reversal_invalid' USING ERRCODE='22023'; END IF;
|
|
32
|
+
IF NOT pg_try_advisory_xact_lock(hashtextextended('invoicing:'||p_order::text,0)) THEN RAISE EXCEPTION 'invoicing_capture_busy' USING ERRCODE='55P03'; END IF;
|
|
33
|
+
PERFORM 1 FROM public.accounting_document_requests WHERE id=coalesce(
|
|
34
|
+
(SELECT request_id FROM public.invoicing_duties WHERE order_id=p_order AND parent_duty_id IS NULL),
|
|
35
|
+
(SELECT r.id FROM public.accounting_document_requests r JOIN public.accounting_document_operations o ON o.document_id=r.document_id
|
|
36
|
+
JOIN public.invoicing_duties d ON o.idempotency_key='invoicing:request:'||d.id::text
|
|
37
|
+
WHERE d.order_id=p_order AND d.parent_duty_id IS NULL AND r.request_kind='issue')) FOR UPDATE;
|
|
38
|
+
SELECT * INTO v_base FROM public.invoicing_duties WHERE order_id=p_order AND parent_duty_id IS NULL FOR UPDATE;
|
|
39
|
+
IF NOT FOUND THEN RETURN jsonb_build_object('kind','missing'); END IF;
|
|
40
|
+
v_reversal:=jsonb_build_object('key',p_key,'reason',p_reason,'evidence',p_evidence);
|
|
41
|
+
SELECT * INTO v_prior FROM public.invoicing_effect_receipts WHERE receipt_key='reversal-key:'||p_key;
|
|
42
|
+
IF FOUND AND (v_prior.duty_id IS DISTINCT FROM v_base.id OR v_prior.payload IS DISTINCT FROM v_reversal) THEN
|
|
43
|
+
RAISE EXCEPTION 'invoicing_reversal_key_conflict' USING ERRCODE='23505'; END IF;
|
|
44
|
+
INSERT INTO public.invoicing_effect_receipts(receipt_key,duty_id,payload) VALUES('reversal-key:'||p_key,v_base.id,v_reversal) ON CONFLICT DO NOTHING;
|
|
45
|
+
SELECT * INTO v_prior FROM public.invoicing_effect_receipts WHERE receipt_key='reversal-key:'||p_key;
|
|
46
|
+
IF v_prior.duty_id IS DISTINCT FROM v_base.id OR v_prior.payload IS DISTINCT FROM v_reversal THEN
|
|
47
|
+
RAISE EXCEPTION 'invoicing_reversal_key_conflict' USING ERRCODE='23505'; END IF;
|
|
48
|
+
-- Once a compensation intent exists, later aliases cannot change its reason/evidence.
|
|
49
|
+
IF v_base.reversal IS NOT NULL AND (v_base.reversal-'key') IS DISTINCT FROM (v_reversal-'key') THEN
|
|
50
|
+
RAISE EXCEPTION 'invoicing_reversal_conflict' USING ERRCODE='23505'; END IF;
|
|
51
|
+
UPDATE public.invoicing_duties SET reversal=coalesce(reversal,v_reversal) WHERE id=v_base.id;
|
|
52
|
+
IF v_base.status='voided' THEN RETURN jsonb_build_object('kind','voided','replayed',true); END IF;
|
|
53
|
+
IF v_base.status='blocked' THEN RETURN jsonb_build_object('kind','unsupported'); END IF;
|
|
54
|
+
IF v_base.status IN ('processing','unknown') OR v_base.claim_token IS NOT NULL OR v_base.lease_until IS NOT NULL THEN RETURN jsonb_build_object('kind','pending_readback'); END IF;
|
|
55
|
+
IF v_base.document_id IS NULL THEN
|
|
56
|
+
SELECT o.document_id,r.id INTO v_base.document_id,v_base.request_id FROM public.accounting_document_operations o
|
|
57
|
+
JOIN public.accounting_document_requests r ON r.document_id=o.document_id AND r.request_kind='issue'
|
|
58
|
+
WHERE o.idempotency_key='invoicing:request:'||v_base.id::text;
|
|
59
|
+
END IF;
|
|
60
|
+
-- A live unlinked local document is still an outstanding obligation.
|
|
61
|
+
IF EXISTS (SELECT 1 FROM public.accounting_documents a WHERE a.order_id=p_order
|
|
62
|
+
AND a.correction_of_document_id IS NULL AND a.status<>'voided'
|
|
63
|
+
AND a.id IS DISTINCT FROM v_base.document_id) THEN RETURN jsonb_build_object('kind','unsupported'); END IF;
|
|
64
|
+
v_safe_void:=coalesce(
|
|
65
|
+
(v_base.last_result->>'kind'='retryable' AND v_base.last_result->'evidence'->>'phase'='before_send')
|
|
66
|
+
OR (v_base.last_result->>'kind'='absent' AND v_base.last_result->'evidence'->>'kind'='authoritative_absence'
|
|
67
|
+
AND v_base.last_result->'evidence'->>'providerCreateIdentity'='invoicing:'||v_base.id::text),false);
|
|
68
|
+
IF v_base.status<>'issued' AND EXISTS (SELECT 1 FROM public.accounting_document_requests
|
|
69
|
+
WHERE id=v_base.request_id AND (status='processing' OR claim_token IS NOT NULL
|
|
70
|
+
OR (attempts>0 AND NOT v_safe_void))) THEN RETURN jsonb_build_object('kind','pending_readback'); END IF;
|
|
71
|
+
IF v_base.document_id IS NULL OR (v_base.status<>'issued' AND (v_base.generation=0 OR v_safe_void)) THEN
|
|
72
|
+
IF v_base.document_id IS NOT NULL THEN
|
|
73
|
+
PERFORM public.accounting_document_advance('invoicing:void:'||v_base.id::text,v_base.document_id,'voided');
|
|
74
|
+
UPDATE public.accounting_document_requests SET status='discarded',claim_token=NULL,updated_at=now() WHERE id=v_base.request_id;
|
|
75
|
+
END IF;
|
|
76
|
+
UPDATE public.invoicing_duties SET status='voided' WHERE id=v_base.id;
|
|
77
|
+
RETURN jsonb_build_object('kind','voided','replayed',false);
|
|
78
|
+
END IF;
|
|
79
|
+
SELECT * INTO v_child FROM public.invoicing_duties WHERE parent_duty_id=v_base.id;
|
|
80
|
+
IF FOUND THEN RETURN jsonb_build_object('kind','correction_requested','dutyId',v_child.id,'replayed',true); END IF;
|
|
81
|
+
SELECT * INTO v_document FROM public.accounting_documents WHERE id=v_base.document_id FOR UPDATE;
|
|
82
|
+
IF v_document.provider_reference IS NULL THEN RETURN jsonb_build_object('kind','pending_readback'); END IF;
|
|
83
|
+
SELECT id INTO v_existing_document FROM public.accounting_documents WHERE correction_of_document_id=v_base.document_id;
|
|
84
|
+
IF FOUND THEN
|
|
85
|
+
-- An old native child/request is an existing obligation, even when its M09
|
|
86
|
+
-- sidecar is new. Do not call the constructor again or refreeze its seller.
|
|
87
|
+
v_result:=jsonb_build_object('document',jsonb_build_object('id',v_existing_document));
|
|
88
|
+
ELSE
|
|
89
|
+
v_result:=public.accounting_document_request_correction('invoicing:correction:'||v_base.id::text,v_base.document_id,p_reason,p_evidence);
|
|
90
|
+
END IF;
|
|
91
|
+
INSERT INTO public.invoicing_duties(order_id,parent_duty_id,document_id,request_id,original_key,status,seller_policy_version)
|
|
92
|
+
SELECT p_order,v_base.id,(v_result->'document'->>'id')::uuid,r.id,p_key,CASE WHEN doc.provider_reference IS NOT NULL THEN 'issued' WHEN doc.status='voided' THEN 'voided'
|
|
93
|
+
WHEN r.attempts>0 OR r.status<>'pending' OR r.claim_token IS NOT NULL THEN 'unsupported' ELSE 'ready' END,
|
|
94
|
+
CASE WHEN v_existing_document IS NULL AND p_evidence->>'sellerPolicyVersion'='invoicing.seller.v1' THEN 'invoicing.seller.v1' END
|
|
95
|
+
FROM public.accounting_document_requests r JOIN public.accounting_documents doc ON doc.id=r.document_id WHERE r.document_id=(v_result->'document'->>'id')::uuid AND r.request_kind='correction'
|
|
96
|
+
RETURNING * INTO v_child;
|
|
97
|
+
UPDATE public.accounting_document_requests SET available_at='infinity'
|
|
98
|
+
WHERE id=v_child.request_id AND v_child.status='ready' AND status='pending' AND attempts=0 AND claim_token IS NULL
|
|
99
|
+
RETURNING id INTO v_ready;
|
|
100
|
+
IF v_child.status='ready' AND v_ready IS NULL THEN
|
|
101
|
+
UPDATE public.invoicing_duties SET status='unsupported' WHERE id=v_child.id RETURNING * INTO v_child;
|
|
102
|
+
END IF;
|
|
103
|
+
SELECT facts INTO v_facts FROM public.invoicing_fact_revisions WHERE duty_id=v_base.id;
|
|
104
|
+
INSERT INTO public.invoicing_fact_revisions(duty_id,revision_ref,facts) VALUES(v_child.id,'correction:'||v_base.id::text,
|
|
105
|
+
v_facts||jsonb_build_object('before',v_facts,'after',jsonb_build_object('totalGrossMinor',0,'totalNetMinor',0,'totalTaxMinor',0),
|
|
106
|
+
'parentDocumentId',v_base.document_id,'parentExternalId',v_document.provider_reference,'reason',p_reason));
|
|
107
|
+
RETURN jsonb_build_object('kind',CASE WHEN v_child.status='unsupported' THEN 'unsupported' ELSE 'correction_requested' END,'dutyId',v_child.id,'replayed',false);
|
|
108
|
+
END;
|
|
109
|
+
$$;
|
|
110
|
+
CREATE FUNCTION public.invoicing_finish_correction(p_duty uuid,p_token uuid,p_generation integer,p_external text,p_number text,p_proof jsonb)
|
|
111
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
|
|
112
|
+
DECLARE v_duty public.invoicing_duties%ROWTYPE; v_base public.invoicing_duties%ROWTYPE; v_payload jsonb; v_prior jsonb; v_parent_external text;
|
|
113
|
+
BEGIN
|
|
114
|
+
-- Parent request precedes child request, then the corresponding duties.
|
|
115
|
+
SELECT * INTO v_base FROM public.invoicing_duties WHERE id=(SELECT parent_duty_id FROM public.invoicing_duties WHERE id=p_duty);
|
|
116
|
+
PERFORM 1 FROM public.accounting_document_requests WHERE id=v_base.request_id FOR UPDATE;
|
|
117
|
+
PERFORM 1 FROM public.accounting_document_requests WHERE id=(SELECT request_id FROM public.invoicing_duties WHERE id=p_duty) FOR UPDATE;
|
|
118
|
+
PERFORM 1 FROM public.invoicing_duties WHERE id=v_base.id FOR UPDATE;
|
|
119
|
+
SELECT * INTO v_duty FROM public.invoicing_duties WHERE id=p_duty FOR UPDATE;
|
|
120
|
+
IF NOT FOUND OR v_duty.parent_duty_id IS NULL THEN RAISE EXCEPTION 'invoicing_correction_missing' USING ERRCODE='22023'; END IF;
|
|
121
|
+
v_payload:=jsonb_build_object('externalId',p_external,'number',p_number,'proof',p_proof);
|
|
122
|
+
SELECT payload INTO v_prior FROM public.invoicing_effect_receipts WHERE receipt_key='correction:'||p_duty::text;
|
|
123
|
+
IF FOUND THEN
|
|
124
|
+
IF v_prior->>'externalId' IS DISTINCT FROM p_external OR v_prior->>'number' IS DISTINCT FROM p_number
|
|
125
|
+
OR v_prior->'proof'->>'parentExternalId' IS DISTINCT FROM p_proof->>'parentExternalId' THEN RAISE EXCEPTION 'invoicing_correction_identity_conflict' USING ERRCODE='23505'; END IF;
|
|
126
|
+
RETURN jsonb_build_object('kind','issued','replayed',true);
|
|
127
|
+
END IF;
|
|
128
|
+
IF v_duty.status<>'processing' OR v_duty.claim_token IS DISTINCT FROM p_token OR v_duty.generation IS DISTINCT FROM p_generation OR v_duty.lease_until<=now()
|
|
129
|
+
THEN RETURN jsonb_build_object('kind','stale'); END IF;
|
|
130
|
+
SELECT provider_reference INTO v_parent_external FROM public.accounting_documents WHERE id=v_base.document_id;
|
|
131
|
+
IF nullif(btrim(p_external),'') IS NULL OR p_external=v_parent_external OR p_proof->>'kind' IS NULL OR p_proof->>'kind' NOT IN ('issued','readback_found')
|
|
132
|
+
OR p_proof->>'externalId' IS DISTINCT FROM p_external OR p_proof->>'parentExternalId' IS DISTINCT FROM v_parent_external
|
|
133
|
+
OR p_proof->>'providerCreateIdentity' IS DISTINCT FROM 'invoicing:'||p_duty::text
|
|
134
|
+
OR (p_proof->>'kind'='issued' AND (v_duty.preflight_generation IS DISTINCT FROM p_generation OR v_duty.last_result->>'action'<>'create'))
|
|
135
|
+
THEN RAISE EXCEPTION 'invoicing_correction_proof_invalid' USING ERRCODE='22023'; END IF;
|
|
136
|
+
PERFORM public.accounting_document_advance('invoicing:correction-request:'||p_duty::text,v_duty.document_id,'issue_requested');
|
|
137
|
+
PERFORM public.accounting_document_advance('invoicing:correction-issue:'||p_duty::text,v_duty.document_id,'issued',p_external,jsonb_build_object('number',p_number));
|
|
138
|
+
PERFORM public.accounting_document_advance('invoicing:corrected:'||p_duty::text,v_base.document_id,'corrected');
|
|
139
|
+
UPDATE public.accounting_document_requests SET status='succeeded',claim_token=NULL,error=NULL,updated_at=now() WHERE id=v_duty.request_id;
|
|
140
|
+
UPDATE public.invoicing_duties SET status='issued',claim_token=NULL,lease_until=NULL,last_result=v_payload WHERE id=p_duty;
|
|
141
|
+
INSERT INTO public.invoicing_effect_receipts(receipt_key,duty_id,payload) VALUES('correction:'||p_duty::text,p_duty,v_payload);
|
|
142
|
+
RETURN jsonb_build_object('kind','issued','replayed',false);
|
|
143
|
+
END;
|
|
144
|
+
$$;
|
|
145
|
+
CREATE FUNCTION public.invoicing_record_observation(p_key text,p_document uuid,p_observation jsonb)
|
|
146
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
|
|
147
|
+
DECLARE v_prior public.invoicing_document_observations%ROWTYPE; v_state text;
|
|
148
|
+
BEGIN
|
|
149
|
+
IF nullif(btrim(p_key),'') IS NULL OR p_observation->>'observedAt' IS NULL OR p_observation->>'kind' IS NULL
|
|
150
|
+
OR p_observation->>'kind' NOT IN ('clearance','artifacts','unsupported') THEN
|
|
151
|
+
RAISE EXCEPTION 'invoicing_observation_invalid' USING ERRCODE='22023'; END IF;
|
|
152
|
+
PERFORM 1 FROM public.accounting_documents WHERE id=p_document FOR UPDATE;
|
|
153
|
+
SELECT * INTO v_prior FROM public.invoicing_document_observations WHERE event_key=p_key;
|
|
154
|
+
IF FOUND THEN
|
|
155
|
+
IF v_prior.document_id IS DISTINCT FROM p_document OR v_prior.observation IS DISTINCT FROM p_observation THEN
|
|
156
|
+
RAISE EXCEPTION 'invoicing_observation_conflict' USING ERRCODE='23505'; END IF;
|
|
157
|
+
RETURN jsonb_build_object('replayed',true);
|
|
158
|
+
END IF;
|
|
159
|
+
IF p_observation->>'kind'='clearance' THEN
|
|
160
|
+
v_state:=p_observation->>'state';
|
|
161
|
+
IF v_state IS NULL OR v_state NOT IN ('not_submitted','pending','accepted','rejected','not_required')
|
|
162
|
+
OR (v_state='accepted' AND nullif(btrim(p_observation->>'reference'),'') IS NULL) THEN
|
|
163
|
+
RAISE EXCEPTION 'invoicing_clearance_invalid' USING ERRCODE='22023'; END IF;
|
|
164
|
+
-- Older observations remain evidence without replacing newer projection.
|
|
165
|
+
IF NOT EXISTS(SELECT 1 FROM public.invoicing_document_observations WHERE document_id=p_document
|
|
166
|
+
AND observation->>'kind'='clearance' AND observed_at>(p_observation->>'observedAt')::timestamptz) THEN
|
|
167
|
+
PERFORM public.accounting_document_record_submission(p_document,v_state,p_observation->>'reference',(p_observation->>'observedAt')::timestamptz);
|
|
168
|
+
END IF;
|
|
169
|
+
END IF;
|
|
170
|
+
INSERT INTO public.invoicing_document_observations VALUES(p_key,p_document,p_observation,(p_observation->>'observedAt')::timestamptz);
|
|
171
|
+
RETURN jsonb_build_object('replayed',false,'kind',p_observation->>'kind');
|
|
172
|
+
END;
|
|
173
|
+
$$;
|
|
174
|
+
CREATE FUNCTION public.invoicing_claim_deliveries(p_limit integer,p_visibility integer,p_require_clearance boolean,p_order_id uuid DEFAULT NULL)
|
|
175
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
|
|
176
|
+
DECLARE v_delivery public.accounting_document_deliveries%ROWTYPE; v_receipt public.invoicing_delivery_receipts%ROWTYPE; v_rows jsonb:='[]';
|
|
177
|
+
BEGIN
|
|
178
|
+
UPDATE public.invoicing_delivery_receipts SET state='uncertain',claim_token=NULL,lease_until=NULL,
|
|
179
|
+
result=jsonb_build_object('kind','uncertain','receipt',NULL,'evidence',jsonb_build_object('code','lease_expired'))
|
|
180
|
+
WHERE state='processing' AND lease_until<=clock_timestamp();
|
|
181
|
+
FOR v_delivery IN SELECT d.* FROM public.accounting_document_deliveries d
|
|
182
|
+
JOIN public.accounting_documents doc ON doc.id=d.document_id
|
|
183
|
+
WHERE (p_order_id IS NULL OR doc.order_id=p_order_id)
|
|
184
|
+
AND d.status IN ('pending','failed') AND d.available_at<=now() AND doc.provider_reference IS NOT NULL
|
|
185
|
+
AND doc.status IN ('issued','accepted','corrected')
|
|
186
|
+
AND (p_require_clearance IS NOT TRUE OR doc.submission_required IS NOT TRUE OR doc.submission_state='accepted')
|
|
187
|
+
AND NOT EXISTS (SELECT 1 FROM public.invoicing_delivery_receipts s WHERE s.delivery_id=d.id AND s.state IN ('processing','sent','uncertain'))
|
|
188
|
+
ORDER BY d.created_at,d.id LIMIT least(greatest(coalesce(p_limit,10),1),100) FOR UPDATE OF d, doc SKIP LOCKED LOOP
|
|
189
|
+
INSERT INTO public.invoicing_delivery_receipts(delivery_id,generation) VALUES(v_delivery.id,v_delivery.attempts) ON CONFLICT DO NOTHING;
|
|
190
|
+
SELECT * INTO v_receipt FROM public.invoicing_delivery_receipts WHERE delivery_id=v_delivery.id FOR UPDATE;
|
|
191
|
+
IF v_receipt.state IN ('processing','sent','uncertain') THEN CONTINUE; END IF;
|
|
192
|
+
UPDATE public.invoicing_delivery_receipts SET generation=greatest(generation,v_delivery.attempts)+1,claim_token=gen_random_uuid(),state='processing',
|
|
193
|
+
lease_until=now()+make_interval(secs=>least(greatest(coalesce(p_visibility,300),1),3600)) WHERE delivery_id=v_delivery.id RETURNING * INTO v_receipt;
|
|
194
|
+
UPDATE public.accounting_document_deliveries SET status='processing',attempts=v_receipt.generation,lease_until='infinity',updated_at=now() WHERE id=v_delivery.id;
|
|
195
|
+
v_rows:=v_rows||jsonb_build_array(to_jsonb(v_delivery)||to_jsonb(v_receipt));
|
|
196
|
+
END LOOP;
|
|
197
|
+
RETURN v_rows;
|
|
198
|
+
END;
|
|
199
|
+
$$;
|
|
200
|
+
CREATE FUNCTION public.invoicing_settle_delivery(p_delivery uuid,p_token uuid,p_generation integer,p_kind text,p_receipt text,p_evidence jsonb)
|
|
201
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
|
|
202
|
+
DECLARE v_row public.invoicing_delivery_receipts%ROWTYPE; v_payload jsonb;
|
|
203
|
+
BEGIN
|
|
204
|
+
PERFORM 1 FROM public.accounting_document_deliveries WHERE id=p_delivery FOR UPDATE;
|
|
205
|
+
SELECT * INTO v_row FROM public.invoicing_delivery_receipts WHERE delivery_id=p_delivery FOR UPDATE;
|
|
206
|
+
v_payload:=jsonb_build_object('kind',p_kind,'receipt',p_receipt,'evidence',p_evidence);
|
|
207
|
+
IF v_row.state='sent' THEN
|
|
208
|
+
IF v_row.result IS DISTINCT FROM v_payload THEN RAISE EXCEPTION 'invoicing_delivery_conflict' USING ERRCODE='23505'; END IF;
|
|
209
|
+
RETURN jsonb_build_object('kind','sent','replayed',true);
|
|
210
|
+
END IF;
|
|
211
|
+
IF v_row.delivery_id IS NULL OR v_row.state<>'processing' OR v_row.claim_token IS DISTINCT FROM p_token OR v_row.generation IS DISTINCT FROM p_generation
|
|
212
|
+
OR v_row.lease_until IS NULL OR v_row.lease_until<=clock_timestamp()
|
|
213
|
+
THEN RETURN jsonb_build_object('kind','stale'); END IF;
|
|
214
|
+
IF p_kind IS NULL OR p_kind NOT IN ('sent','uncertain','failed_before_send') OR jsonb_typeof(p_evidence) IS DISTINCT FROM 'object'
|
|
215
|
+
OR (p_kind='sent' AND nullif(btrim(p_receipt),'') IS NULL)
|
|
216
|
+
OR (p_kind<>'sent' AND p_receipt IS NOT NULL)
|
|
217
|
+
THEN RAISE EXCEPTION 'invoicing_delivery_evidence_invalid' USING ERRCODE='22023'; END IF;
|
|
218
|
+
UPDATE public.invoicing_delivery_receipts SET state=CASE WHEN p_kind='failed_before_send' THEN 'failed' ELSE p_kind END,
|
|
219
|
+
result=v_payload,claim_token=NULL,lease_until=NULL WHERE delivery_id=p_delivery;
|
|
220
|
+
IF p_kind='sent' THEN
|
|
221
|
+
UPDATE public.accounting_document_deliveries SET status='succeeded',receipt_reference=p_receipt,delivered_at=now(),lease_until=NULL,updated_at=now() WHERE id=p_delivery;
|
|
222
|
+
INSERT INTO public.accounting_document_delivery_attempts(delivery_id,attempt_ordinal,outcome,receipt_reference) VALUES(p_delivery,p_generation,'delivered',p_receipt);
|
|
223
|
+
ELSIF p_kind='failed_before_send' THEN
|
|
224
|
+
UPDATE public.accounting_document_deliveries SET status='failed',lease_until=NULL,
|
|
225
|
+
available_at=CASE WHEN p_evidence->'retryable'='false'::jsonb THEN 'infinity'::timestamptz
|
|
226
|
+
ELSE now()+make_interval(secs=>CASE WHEN jsonb_typeof(p_evidence->'retrySeconds')='number' THEN
|
|
227
|
+
CASE WHEN (p_evidence->>'retrySeconds')::numeric=trunc((p_evidence->>'retrySeconds')::numeric)
|
|
228
|
+
THEN least(greatest((p_evidence->>'retrySeconds')::numeric,60),86400)::integer ELSE 900 END
|
|
229
|
+
ELSE 900 END) END,
|
|
230
|
+
last_error=p_evidence,updated_at=now() WHERE id=p_delivery;
|
|
231
|
+
INSERT INTO public.accounting_document_delivery_attempts(delivery_id,attempt_ordinal,outcome,error) VALUES(p_delivery,p_generation,'failed',p_evidence);
|
|
232
|
+
END IF;
|
|
233
|
+
RETURN jsonb_build_object('kind',p_kind,'replayed',false);
|
|
234
|
+
END;
|
|
235
|
+
$$;
|
|
236
|
+
|
|
237
|
+
REVOKE ALL ON TABLE public.invoicing_document_observations FROM PUBLIC, anon, authenticated;
|
|
238
|
+
|
|
239
|
+
REVOKE ALL ON TABLE public.invoicing_delivery_receipts FROM PUBLIC, anon, authenticated;
|
|
240
|
+
|
|
241
|
+
REVOKE ALL ON FUNCTION public.invoicing_request_reversal(text,uuid,text,jsonb) FROM PUBLIC, anon, authenticated;
|
|
242
|
+
|
|
243
|
+
REVOKE ALL ON FUNCTION public.invoicing_finish_correction(uuid,uuid,integer,text,text,jsonb) FROM PUBLIC, anon, authenticated;
|
|
244
|
+
|
|
245
|
+
REVOKE ALL ON FUNCTION public.invoicing_record_observation(text,uuid,jsonb) FROM PUBLIC, anon, authenticated;
|
|
246
|
+
|
|
247
|
+
REVOKE ALL ON FUNCTION public.invoicing_claim_deliveries(integer,integer,boolean,uuid) FROM PUBLIC, anon, authenticated;
|
|
248
|
+
|
|
249
|
+
REVOKE ALL ON FUNCTION public.invoicing_settle_delivery(uuid,uuid,integer,text,text,jsonb) FROM PUBLIC, anon, authenticated;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
CREATE OR REPLACE FUNCTION public.payment_truth_apply_terminal(
|
|
2
|
+
p_event_id uuid,
|
|
3
|
+
p_idempotency_key text,
|
|
4
|
+
p_outcome text,
|
|
5
|
+
p_occurred_at timestamptz,
|
|
6
|
+
p_failure_class text,
|
|
7
|
+
p_failure_reason text,
|
|
8
|
+
p_source_evidence jsonb,
|
|
9
|
+
p_dunning_template_slug text,
|
|
10
|
+
p_recovery_url_path text,
|
|
11
|
+
p_recovery_template_slug text
|
|
12
|
+
)
|
|
13
|
+
RETURNS void
|
|
14
|
+
LANGUAGE plpgsql
|
|
15
|
+
SET search_path = public, pg_catalog
|
|
16
|
+
AS $$
|
|
17
|
+
DECLARE
|
|
18
|
+
v_event public.payment_truth_events%ROWTYPE;
|
|
19
|
+
v_intent public.commerce_settlement_intents%ROWTYPE;
|
|
20
|
+
v_order public.commerce_orders%ROWTYPE;
|
|
21
|
+
v_cycle public.subscription_cycles%ROWTYPE;
|
|
22
|
+
v_subscription public.subscriptions%ROWTYPE;
|
|
23
|
+
v_settlement jsonb;
|
|
24
|
+
v_dunning jsonb;
|
|
25
|
+
v_accounting jsonb;
|
|
26
|
+
v_positions jsonb;
|
|
27
|
+
v_open_case_id uuid;
|
|
28
|
+
v_transition_id uuid;
|
|
29
|
+
v_dunning_case_id uuid;
|
|
30
|
+
v_accounting_document_id uuid;
|
|
31
|
+
BEGIN
|
|
32
|
+
SELECT * INTO v_event FROM public.payment_truth_events WHERE id = p_event_id FOR UPDATE;
|
|
33
|
+
IF NOT FOUND THEN RAISE EXCEPTION 'payment_truth_event_not_found' USING ERRCODE = '22023'; END IF;
|
|
34
|
+
IF v_event.state = 'settled' THEN RETURN; END IF;
|
|
35
|
+
IF p_outcome NOT IN ('captured', 'refused') THEN
|
|
36
|
+
RAISE EXCEPTION 'payment_truth_terminal_outcome_invalid' USING ERRCODE = '22023';
|
|
37
|
+
END IF;
|
|
38
|
+
IF p_outcome = 'refused' AND p_failure_class IS NULL THEN
|
|
39
|
+
RAISE EXCEPTION 'payment_truth_refusal_classification_required' USING ERRCODE = '22023';
|
|
40
|
+
END IF;
|
|
41
|
+
IF p_failure_class IS NOT NULL AND p_failure_class NOT IN (
|
|
42
|
+
'authorization_refused', 'insufficient_funds', 'method_unavailable',
|
|
43
|
+
'risk_refused', 'unknown_refusal'
|
|
44
|
+
) THEN
|
|
45
|
+
RAISE EXCEPTION 'payment_truth_failure_class_invalid' USING ERRCODE = '22023';
|
|
46
|
+
END IF;
|
|
47
|
+
|
|
48
|
+
SELECT * INTO v_intent
|
|
49
|
+
FROM public.commerce_settlement_intents WHERE id = v_event.settlement_intent_id;
|
|
50
|
+
SELECT * INTO v_order FROM public.commerce_orders WHERE id = v_intent.order_id;
|
|
51
|
+
IF v_intent.amount_minor IS DISTINCT FROM v_event.amount_minor
|
|
52
|
+
OR v_intent.currency_code IS DISTINCT FROM v_event.currency_code
|
|
53
|
+
THEN
|
|
54
|
+
RAISE EXCEPTION 'payment_truth_settlement_money_mismatch' USING ERRCODE = '22023';
|
|
55
|
+
END IF;
|
|
56
|
+
|
|
57
|
+
v_settlement := public.commerce_record_settlement(
|
|
58
|
+
p_idempotency_key || ':settlement',
|
|
59
|
+
v_event.settlement_intent_id,
|
|
60
|
+
CASE WHEN p_outcome = 'captured' THEN 'succeeded' ELSE 'failed' END,
|
|
61
|
+
NULL,
|
|
62
|
+
jsonb_build_object(
|
|
63
|
+
'paymentTruthEventId', v_event.id,
|
|
64
|
+
'sourceEvidence', COALESCE(p_source_evidence, '{}'::jsonb),
|
|
65
|
+
'reason', p_failure_reason,
|
|
66
|
+
'failureClass', p_failure_class
|
|
67
|
+
),
|
|
68
|
+
p_occurred_at
|
|
69
|
+
);
|
|
70
|
+
v_transition_id := (v_settlement->>'transitionId')::uuid;
|
|
71
|
+
|
|
72
|
+
IF p_outcome = 'captured' THEN
|
|
73
|
+
-- Capture owed work only; no document construction or accounting call here.
|
|
74
|
+
DECLARE v_capture_timeout text := current_setting('lock_timeout');
|
|
75
|
+
BEGIN
|
|
76
|
+
PERFORM set_config('lock_timeout', '100ms', true);
|
|
77
|
+
PERFORM public.invoicing_capture_duty(
|
|
78
|
+
p_idempotency_key || ':accounting', 'paid', v_transition_id, v_order.id,
|
|
79
|
+
jsonb_build_object('settlementIntentId', v_intent.id, 'amountMinor', v_intent.amount_minor, 'currency', v_intent.currency_code),
|
|
80
|
+
p_occurred_at
|
|
81
|
+
);
|
|
82
|
+
PERFORM set_config('lock_timeout', v_capture_timeout, true);
|
|
83
|
+
EXCEPTION WHEN OTHERS OR query_canceled THEN
|
|
84
|
+
-- query_canceled during capture is absorbed so settlement/recovery commit.
|
|
85
|
+
-- Append-only, no FK locks or conflicting unique key. The retained paid
|
|
86
|
+
-- transition is also durable retry evidence if host ACLs deny this marker.
|
|
87
|
+
BEGIN
|
|
88
|
+
PERFORM set_config('lock_timeout','100ms',true);
|
|
89
|
+
INSERT INTO public.invoicing_capture_faults(order_id,source_kind,source_id,attempts,state,available_at)
|
|
90
|
+
VALUES(v_order.id,'paid',v_transition_id,1,'retryable',now());
|
|
91
|
+
PERFORM set_config('lock_timeout',v_capture_timeout,true);
|
|
92
|
+
EXCEPTION WHEN OTHERS OR query_canceled THEN NULL;
|
|
93
|
+
END;
|
|
94
|
+
END;
|
|
95
|
+
|
|
96
|
+
IF v_order.subscription_cycle_id IS NOT NULL THEN
|
|
97
|
+
SELECT id INTO v_open_case_id
|
|
98
|
+
FROM public.subscription_dunning_cases
|
|
99
|
+
WHERE cycle_id = v_order.subscription_cycle_id AND status = 'open'
|
|
100
|
+
FOR UPDATE;
|
|
101
|
+
IF v_open_case_id IS NOT NULL THEN
|
|
102
|
+
PERFORM public.dunning_lifecycle_record_recovery(
|
|
103
|
+
v_open_case_id, p_occurred_at, p_recovery_template_slug
|
|
104
|
+
);
|
|
105
|
+
END IF;
|
|
106
|
+
END IF;
|
|
107
|
+
ELSE
|
|
108
|
+
IF v_order.subscription_cycle_id IS NOT NULL THEN
|
|
109
|
+
SELECT * INTO v_cycle
|
|
110
|
+
FROM public.subscription_cycles WHERE id = v_order.subscription_cycle_id;
|
|
111
|
+
SELECT * INTO v_subscription
|
|
112
|
+
FROM public.subscriptions WHERE id = v_cycle.subscription_id;
|
|
113
|
+
v_dunning := public.dunning_lifecycle_open_case(
|
|
114
|
+
'payment-truth:dunning:' || v_cycle.id::text,
|
|
115
|
+
v_subscription.id,
|
|
116
|
+
v_cycle.id,
|
|
117
|
+
v_subscription.client_id,
|
|
118
|
+
1,
|
|
119
|
+
NULL,
|
|
120
|
+
p_failure_class,
|
|
121
|
+
p_failure_reason,
|
|
122
|
+
p_occurred_at,
|
|
123
|
+
v_event.amount_minor,
|
|
124
|
+
v_event.currency_code,
|
|
125
|
+
p_dunning_template_slug,
|
|
126
|
+
p_recovery_url_path
|
|
127
|
+
);
|
|
128
|
+
v_dunning_case_id := (v_dunning->>'caseId')::uuid;
|
|
129
|
+
END IF;
|
|
130
|
+
END IF;
|
|
131
|
+
|
|
132
|
+
UPDATE public.payment_truth_events
|
|
133
|
+
SET state = 'settled',
|
|
134
|
+
outcome = p_outcome,
|
|
135
|
+
failure_class = CASE WHEN p_outcome = 'refused' THEN p_failure_class ELSE NULL END,
|
|
136
|
+
failure_reason = CASE WHEN p_outcome = 'refused' THEN p_failure_reason ELSE NULL END,
|
|
137
|
+
settlement_transition_id = v_transition_id,
|
|
138
|
+
dunning_case_id = v_dunning_case_id,
|
|
139
|
+
accounting_document_id = v_accounting_document_id,
|
|
140
|
+
settled_at = p_occurred_at,
|
|
141
|
+
updated_at = p_occurred_at
|
|
142
|
+
WHERE id = v_event.id;
|
|
143
|
+
END;
|
|
144
|
+
$$;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- Eligibility is a creation marker, never a wall-clock inference or historical backfill.
|
|
2
|
+
CREATE TABLE public.invoicing_seller_revisions (
|
|
3
|
+
duty_id uuid PRIMARY KEY REFERENCES public.invoicing_duties(id),
|
|
4
|
+
eligibility_version text NOT NULL CHECK (eligibility_version='invoicing.seller.v1'),
|
|
5
|
+
revision_ref text,
|
|
6
|
+
seller_facts jsonb,
|
|
7
|
+
captured_at timestamptz,
|
|
8
|
+
capture_generation integer,
|
|
9
|
+
CHECK ((seller_facts IS NULL AND revision_ref IS NULL AND captured_at IS NULL AND capture_generation IS NULL)
|
|
10
|
+
OR (jsonb_typeof(seller_facts)='object' AND revision_ref IS NOT NULL AND captured_at IS NOT NULL AND capture_generation IS NOT NULL))
|
|
11
|
+
);
|
|
12
|
+
CREATE FUNCTION public.invoicing_request_with_seller_policy(p_key text,p_source_kind text,p_source_id uuid,p_order_id uuid,p_evidence jsonb,p_occurred_at timestamptz)
|
|
13
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
|
|
14
|
+
DECLARE v_result jsonb; v_existing boolean; v_prior_duty boolean; v_duty uuid; v_timeout text:=current_setting('lock_timeout');
|
|
15
|
+
BEGIN
|
|
16
|
+
IF p_source_kind='correction' THEN
|
|
17
|
+
RETURN public.invoicing_request_reversal(p_key,p_order_id,p_evidence->>'reason',p_evidence||jsonb_build_object('sellerPolicyVersion','invoicing.seller.v1'));
|
|
18
|
+
END IF;
|
|
19
|
+
IF NOT pg_try_advisory_xact_lock(hashtextextended('invoicing:'||p_order_id::text,0)) THEN RAISE EXCEPTION 'invoicing_capture_busy' USING ERRCODE='55P03'; END IF;
|
|
20
|
+
PERFORM set_config('lock_timeout','100ms',true);
|
|
21
|
+
PERFORM 1 FROM public.commerce_orders WHERE id=p_order_id FOR UPDATE;
|
|
22
|
+
-- A new sidecar around an older document/request is still a legacy obligation.
|
|
23
|
+
v_prior_duty:=EXISTS(SELECT 1 FROM public.invoicing_duties WHERE order_id=p_order_id AND parent_duty_id IS NULL);
|
|
24
|
+
v_existing:=EXISTS(SELECT 1 FROM public.invoicing_duties WHERE order_id=p_order_id AND parent_duty_id IS NULL)
|
|
25
|
+
OR EXISTS(SELECT 1 FROM public.accounting_documents WHERE order_id=p_order_id AND correction_of_document_id IS NULL);
|
|
26
|
+
v_result:=public.invoicing_capture_duty(p_key,p_source_kind,p_source_id,p_order_id,p_evidence,p_occurred_at);
|
|
27
|
+
v_duty:=(v_result->>'dutyId')::uuid;
|
|
28
|
+
IF v_existing AND NOT v_prior_duty THEN UPDATE public.invoicing_duties SET seller_policy_version=NULL WHERE id=v_duty; END IF;
|
|
29
|
+
IF NOT v_existing THEN INSERT INTO public.invoicing_seller_revisions(duty_id,eligibility_version) VALUES(v_duty,'invoicing.seller.v1') ON CONFLICT DO NOTHING; END IF;
|
|
30
|
+
PERFORM set_config('lock_timeout',v_timeout,true);
|
|
31
|
+
RETURN v_result;
|
|
32
|
+
END;
|
|
33
|
+
$$;
|
|
34
|
+
CREATE FUNCTION public.invoicing_claim_with_seller(p_limit integer,p_visibility integer,p_provider text,p_seller jsonb,p_revision text,p_order_id uuid DEFAULT NULL)
|
|
35
|
+
RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
|
|
36
|
+
DECLARE v_claim jsonb; v_seller public.invoicing_seller_revisions%ROWTYPE; v_rows jsonb:='[]';
|
|
37
|
+
BEGIN
|
|
38
|
+
IF jsonb_typeof(p_seller) IS DISTINCT FROM 'object' OR nullif(btrim(p_revision),'') IS NULL THEN
|
|
39
|
+
RAISE EXCEPTION 'invoicing_seller_required' USING ERRCODE='22023'; END IF;
|
|
40
|
+
FOR v_claim IN SELECT value FROM jsonb_array_elements(public.invoicing_claim_duties(p_limit,p_visibility,p_provider,p_order_id)) LOOP
|
|
41
|
+
-- Generation advances only in invoicing_claim_duties: 1 is the first-ever claim.
|
|
42
|
+
-- A pre-wrapper claim must never acquire a seller revision on retry.
|
|
43
|
+
INSERT INTO public.invoicing_seller_revisions(duty_id,eligibility_version)
|
|
44
|
+
SELECT id,'invoicing.seller.v1' FROM public.invoicing_duties WHERE id=(v_claim->>'id')::uuid AND seller_policy_version='invoicing.seller.v1' AND (v_claim->>'generation')::integer=1
|
|
45
|
+
ON CONFLICT DO NOTHING;
|
|
46
|
+
SELECT * INTO v_seller FROM public.invoicing_seller_revisions WHERE duty_id=(v_claim->>'id')::uuid FOR UPDATE;
|
|
47
|
+
IF FOUND AND (v_seller.seller_facts IS NOT NULL OR (v_claim->>'generation')::integer=1) THEN
|
|
48
|
+
UPDATE public.invoicing_seller_revisions SET seller_facts=coalesce(seller_facts,p_seller),revision_ref=coalesce(revision_ref,p_revision),
|
|
49
|
+
captured_at=coalesce(captured_at,now()),capture_generation=coalesce(capture_generation,(v_claim->>'generation')::integer)
|
|
50
|
+
WHERE duty_id=v_seller.duty_id RETURNING * INTO v_seller;
|
|
51
|
+
v_claim:=v_claim||jsonb_build_object('seller',v_seller.seller_facts,'sellerRevision',v_seller.revision_ref,'sellerMode','frozen');
|
|
52
|
+
ELSE
|
|
53
|
+
v_claim:=v_claim||jsonb_build_object('seller',p_seller,'sellerRevision',p_revision,'sellerMode','legacy_fallback');
|
|
54
|
+
END IF;
|
|
55
|
+
v_rows:=v_rows||jsonb_build_array(v_claim);
|
|
56
|
+
END LOOP;
|
|
57
|
+
RETURN v_rows;
|
|
58
|
+
END;
|
|
59
|
+
$$;
|
|
60
|
+
|
|
61
|
+
REVOKE ALL ON TABLE public.invoicing_seller_revisions FROM PUBLIC, anon, authenticated;
|
|
62
|
+
|
|
63
|
+
REVOKE ALL ON FUNCTION public.invoicing_request_with_seller_policy(text,text,uuid,uuid,jsonb,timestamptz) FROM PUBLIC, anon, authenticated;
|
|
64
|
+
|
|
65
|
+
REVOKE ALL ON FUNCTION public.invoicing_claim_with_seller(integer,integer,text,jsonb,text,uuid) FROM PUBLIC, anon, authenticated;
|