@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.
- package/CHANGELOG.md +440 -158
- package/README.md +141 -589
- package/dist/fulfillment/adapterConformance.js +1 -1
- package/dist/fulfillment/bindings.js +1 -1
- package/dist/fulfillment/contracts/conformance.d.ts +3 -1
- package/dist/fulfillment/contracts/dispatch.d.ts +45 -3
- package/dist/fulfillment/contracts/index.d.ts +3 -3
- package/dist/fulfillment/contracts/observation.d.ts +21 -1
- package/dist/fulfillment/contracts/readback.d.ts +21 -0
- package/dist/fulfillment/contracts/store.d.ts +3 -0
- package/dist/fulfillment/contracts/sync.d.ts +58 -2
- package/dist/fulfillment/dispatchProjection.d.ts +20 -1
- package/dist/fulfillment/dispatchProjection.js +97 -6
- package/dist/fulfillment/index.d.ts +3 -2
- package/dist/fulfillment/index.js +2 -2
- package/dist/fulfillment/migrations.d.ts +5 -0
- package/dist/fulfillment/migrations.js +1 -0
- package/dist/fulfillment/observationProjection.d.ts +24 -3
- package/dist/fulfillment/observationProjection.js +65 -12
- package/dist/fulfillment/omnipack/client.d.ts +3 -1
- package/dist/fulfillment/omnipack/client.js +31 -13
- package/dist/fulfillment/omnipack/index.d.ts +4 -1
- package/dist/fulfillment/omnipack/index.js +107 -45
- package/dist/fulfillment/omnipack/mappers.d.ts +7 -1
- package/dist/fulfillment/omnipack/mappers.js +24 -2
- package/dist/fulfillment/omnipack/observation.d.ts +11 -4
- package/dist/fulfillment/omnipack/observation.js +44 -29
- package/dist/fulfillment/omnipack/reconciliation.d.ts +15 -0
- package/dist/fulfillment/omnipack/reconciliation.js +55 -0
- package/dist/fulfillment/postgresDispatchStore.js +1 -1
- package/dist/fulfillment/postgresShipmentSpine.d.ts +8 -1
- package/dist/fulfillment/postgresShipmentSpine.js +17 -3
- package/dist/fulfillment/postgresStockSyncPort.js +6 -3
- package/dist/fulfillment/providerDispatchAcceptance.d.ts +1 -0
- package/dist/fulfillment/providerDispatchAcceptance.js +8 -2
- package/dist/fulfillment/providerDispatchWorker.d.ts +51 -0
- package/dist/fulfillment/providerDispatchWorker.js +65 -20
- package/dist/fulfillment/providerReconciliationWorker.d.ts +27 -19
- package/dist/fulfillment/providerReconciliationWorker.js +75 -19
- package/dist/fulfillment/providerStockSyncWorker.d.ts +5 -1
- package/dist/fulfillment/providerStockSyncWorker.js +229 -120
- package/dist/fulfillment/providerTrackingReferences.d.ts +3 -2
- package/dist/fulfillment/verifiedIngress.d.ts +38 -11
- package/dist/fulfillment/verifiedIngress.js +36 -8
- 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/capabilities.d.ts +11 -0
- package/dist/invoicing/capabilities.js +10 -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/capabilities.d.ts +40 -0
- package/dist/invoicing/contracts/capabilities.js +1 -0
- package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
- package/dist/invoicing/contracts/companyLookup.js +1 -0
- package/dist/invoicing/contracts/delivery.d.ts +74 -0
- package/dist/invoicing/contracts/delivery.js +1 -0
- package/dist/invoicing/contracts/facts.d.ts +110 -0
- package/dist/invoicing/contracts/facts.js +1 -0
- package/dist/invoicing/contracts/host.d.ts +245 -0
- package/dist/invoicing/contracts/host.js +1 -0
- package/dist/invoicing/contracts/index.d.ts +16 -0
- package/dist/invoicing/contracts/index.js +1 -0
- package/dist/invoicing/contracts/provider.d.ts +126 -0
- package/dist/invoicing/contracts/provider.js +1 -0
- package/dist/invoicing/contracts/settlement.d.ts +66 -0
- package/dist/invoicing/contracts/settlement.js +1 -0
- package/dist/invoicing/contracts/store.d.ts +277 -0
- package/dist/invoicing/contracts/store.js +1 -0
- 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 +39 -0
- package/dist/invoicing/index.js +37 -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/automaticRenewal.d.ts +44 -0
- package/dist/subscriptions/automaticRenewal.js +81 -0
- package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
- package/dist/subscriptions/automaticRenewalExecution.js +44 -0
- package/dist/subscriptions/contracts/commands.d.ts +115 -0
- package/dist/subscriptions/contracts/commands.js +1 -0
- package/dist/subscriptions/contracts/dunning.d.ts +133 -0
- package/dist/subscriptions/contracts/dunning.js +1 -0
- package/dist/subscriptions/contracts/index.d.ts +6 -0
- package/dist/subscriptions/contracts/index.js +1 -0
- package/dist/subscriptions/contracts/messages.d.ts +43 -0
- package/dist/subscriptions/contracts/messages.js +1 -0
- package/dist/subscriptions/contracts/renewal.d.ts +533 -0
- package/dist/subscriptions/contracts/renewal.js +1 -0
- package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
- package/dist/subscriptions/contracts/stockWait.js +1 -0
- package/dist/subscriptions/index.d.ts +39 -0
- package/dist/subscriptions/index.js +17 -0
- package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
- package/dist/subscriptions/paymentOutcomes.js +27 -0
- package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
- package/dist/subscriptions/postgresRenewalStore.js +61 -0
- package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
- package/dist/subscriptions/postgresRetryStore.js +142 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
- package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
- package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
- package/dist/subscriptions/renewalCycleMechanism.js +157 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
- package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
- package/dist/subscriptions/renewalIdentity.d.ts +3 -0
- package/dist/subscriptions/renewalIdentity.js +4 -0
- package/package.json +23 -3
- package/release-gates.json +74 -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/20261022160000_subscription_retry_lifecycle.sql +328 -0
- package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -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/src/fulfillment/adapterConformance.ts +1 -1
- package/src/fulfillment/bindings.ts +1 -1
- package/src/fulfillment/contracts/conformance.ts +3 -1
- package/src/fulfillment/contracts/dispatch.ts +24 -5
- package/src/fulfillment/contracts/index.ts +3 -3
- package/src/fulfillment/contracts/observation.ts +11 -3
- package/src/fulfillment/contracts/readback.ts +13 -0
- package/src/fulfillment/contracts/store.ts +1 -0
- package/src/fulfillment/contracts/sync.ts +13 -2
- package/src/fulfillment/dispatchProjection.ts +94 -7
- package/src/fulfillment/index.ts +3 -2
- package/src/fulfillment/migrations.ts +1 -0
- package/src/fulfillment/observationProjection.ts +64 -13
- package/src/fulfillment/omnipack/client.ts +32 -13
- package/src/fulfillment/omnipack/index.ts +99 -39
- package/src/fulfillment/omnipack/mappers.ts +23 -2
- package/src/fulfillment/omnipack/observation.ts +45 -27
- package/src/fulfillment/omnipack/reconciliation.ts +56 -0
- package/src/fulfillment/postgresDispatchStore.ts +1 -1
- package/src/fulfillment/postgresShipmentSpine.ts +21 -4
- package/src/fulfillment/postgresStockSyncPort.ts +6 -3
- package/src/fulfillment/providerDispatchAcceptance.ts +9 -3
- package/src/fulfillment/providerDispatchWorker.ts +61 -20
- package/src/fulfillment/providerReconciliationWorker.ts +81 -25
- package/src/fulfillment/providerStockSyncWorker.ts +222 -126
- package/src/fulfillment/providerTrackingReferences.ts +3 -2
- package/src/fulfillment/verifiedIngress.ts +44 -11
- 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/capabilities.ts +9 -0
- package/src/invoicing/channelPolicy.ts +50 -0
- package/src/invoicing/continuity.ts +15 -0
- package/src/invoicing/contracts/capabilities.ts +15 -0
- package/src/invoicing/contracts/companyLookup.ts +13 -0
- package/src/invoicing/contracts/delivery.ts +24 -0
- package/src/invoicing/contracts/facts.ts +71 -0
- package/src/invoicing/contracts/host.ts +84 -0
- package/src/invoicing/contracts/index.ts +16 -0
- package/src/invoicing/contracts/provider.ts +78 -0
- package/src/invoicing/contracts/settlement.ts +39 -0
- package/src/invoicing/contracts/store.ts +102 -0
- 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 +39 -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/automaticRenewal.ts +87 -0
- package/src/subscriptions/automaticRenewalExecution.ts +39 -0
- package/src/subscriptions/contracts/commands.ts +46 -0
- package/src/subscriptions/contracts/dunning.ts +48 -0
- package/src/subscriptions/contracts/index.ts +6 -0
- package/src/subscriptions/contracts/messages.ts +18 -0
- package/src/subscriptions/contracts/renewal.ts +325 -0
- package/src/subscriptions/contracts/stockWait.ts +34 -0
- package/src/subscriptions/index.ts +24 -0
- package/src/subscriptions/paymentOutcomes.ts +23 -0
- package/src/subscriptions/postgresRenewalStore.ts +76 -0
- package/src/subscriptions/postgresRetryStore.ts +174 -0
- package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
- package/src/subscriptions/renewalCycleMechanism.ts +225 -0
- package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
- package/src/subscriptions/renewalIdentity.ts +5 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
-- Public platform accounting rail, second forward: the payout. Two relations, one mutation,
|
|
2
|
+
-- one derived verdict and one deferred invariant -- the way a deployment says "the transport
|
|
3
|
+
-- that took our customers' money has now moved some of it to us, and here is which payments it
|
|
4
|
+
-- was for".
|
|
5
|
+
--
|
|
6
|
+
-- WHAT THIS FORWARD SHIPS, AND WHY IT IS NOT THE SETTLEMENT RAIL. The settlement forward gave
|
|
7
|
+
-- this kernel the customer side of money: an order declares a total, an intent asks for it, one
|
|
8
|
+
-- accepted settlement marks the order paid. None of that says anything about the money
|
|
9
|
+
-- ARRIVING. Between "the customer paid" and "we were paid" sits a batch the transport assembles
|
|
10
|
+
-- on its own schedule, out of many payments, minus a fee it decides, landing as one amount. A
|
|
11
|
+
-- deployment that cannot record that batch cannot answer the only question an accountant asks
|
|
12
|
+
-- of a shop -- does the money that arrived equal the money the orders say was charged -- and
|
|
13
|
+
-- until this forward the second bundle could not express the question, let alone the answer.
|
|
14
|
+
--
|
|
15
|
+
-- THE BATCH IS EVIDENCE, NOT AN INSTRUCTION. Nothing below moves money, asks anyone for money,
|
|
16
|
+
-- or talks to anything. Every row here is a record of something that already happened
|
|
17
|
+
-- elsewhere, which is why there is no state machine, no retry, no schedule and no queue: a
|
|
18
|
+
-- payout is observed, and the only thing this kernel decides is whether what was observed
|
|
19
|
+
-- agrees with itself.
|
|
20
|
+
--
|
|
21
|
+
-- WHERE THE SEMANTICS COME FROM, AND EVERY DEPARTURE. The behaviour was derived from the
|
|
22
|
+
-- managed chain by reading its settlement-sync plane end to end -- the batch upsert, the item
|
|
23
|
+
-- insert that replays on a constraint, the totals recomputed from the items, and the batch
|
|
24
|
+
-- verdict derived from the worst item it holds. Six departures, each named, because a silent
|
|
25
|
+
-- one is how a kernel and its origin quietly stop being the same machine:
|
|
26
|
+
--
|
|
27
|
+
-- 1. THE BATCH TOTALS ARE A DEFERRED CONSTRAINT, NOT A CONVENTION. Upstream recomputes the
|
|
28
|
+
-- three totals inside the mutation and nothing defends them afterwards: one hand-written
|
|
29
|
+
-- INSERT into the item relation leaves a batch whose declared amount is a fiction, and
|
|
30
|
+
-- nothing anywhere notices. Here the agreement between a batch and its items is a
|
|
31
|
+
-- constraint trigger on BOTH sides, deferred to the end of the transaction that touched
|
|
32
|
+
-- either -- the same device the document rail's position sum uses, for the same reason.
|
|
33
|
+
-- 2. CURRENCY LIVES ON THE BATCH, NOT ON EVERY ITEM. Upstream carries it on both and checks
|
|
34
|
+
-- neither against the other, so one batch can hold items in disagreeing currencies. A
|
|
35
|
+
-- payout is one wire in one currency; that is the fact, so it is stored once and a second
|
|
36
|
+
-- item that disagrees with it is refused by name. As in the settlement forward, currency
|
|
37
|
+
-- is a COLUMN WITH A CHECKED SHAPE and never a value: three characters, whichever three
|
|
38
|
+
-- the deployment declared.
|
|
39
|
+
-- 3. MINOR UNITS, NOT CENTS, matching every forward this catalogue carries. `cents` is the
|
|
40
|
+
-- subdivision of two particular currencies.
|
|
41
|
+
-- 4. EACH REFUSAL HAS ITS OWN NAME. Upstream raises one error for a key that is too short, a
|
|
42
|
+
-- negative amount, a status it does not recognise and arithmetic that does not add up.
|
|
43
|
+
-- Four different mistakes answered with one word is a diagnostic this kernel refuses to
|
|
44
|
+
-- inherit, so each is named below. The cross-bundle harness therefore compares exactly one
|
|
45
|
+
-- refusal -- the arithmetic one -- and the rest are proved on this bundle alone.
|
|
46
|
+
-- 5. THE LINK TO A DOCUMENT AND AN ORDER IS OPTIONAL AND REAL. Upstream declares the same two
|
|
47
|
+
-- keys and its only shipped caller passes NULL for both in production, so on that side
|
|
48
|
+
-- they are columns nothing fills. They are kept here, as `ON DELETE SET NULL` keys onto
|
|
49
|
+
-- relations earlier forwards published, because "settled against the ledger" is the whole
|
|
50
|
+
-- point of the capability -- but they are optional, because a transport that reports a
|
|
51
|
+
-- payment this deployment cannot resolve must still be recordable. A payout that cannot be
|
|
52
|
+
-- matched is EXACTLY the row an operator needs to see.
|
|
53
|
+
-- 6. NO `SECURITY DEFINER`, NO `GRANT`/`REVOKE`, NO ROW-LEVEL SECURITY, matching every forward
|
|
54
|
+
-- this catalogue carries and unlike upstream, where the same function is `SECURITY
|
|
55
|
+
-- DEFINER` behind four policies. This kernel creates zero roles; the functions below are
|
|
56
|
+
-- not a security boundary and the host application is what authenticates. `SET search_path`
|
|
57
|
+
-- is kept, because that one is a correctness property. Every constraint is named, and
|
|
58
|
+
-- `CREATE` is unconditional, because a manifest-ordered forward runs once against a known
|
|
59
|
+
-- prefix and `IF NOT EXISTS` would hide the drift the migration ledger exists to catch.
|
|
60
|
+
--
|
|
61
|
+
-- WHAT THIS FORWARD DELIBERATELY DOES NOT SHIP, enumerated so nobody has to infer it:
|
|
62
|
+
-- * no transport. Not a name, not an enum, not a branch keyed on who paid out. `provider_kind`
|
|
63
|
+
-- is an opaque label this kernel stores, compares for equality, and never parses;
|
|
64
|
+
-- * no protocol, no credential, no polling and no inbound event relation. Whoever observed the
|
|
65
|
+
-- payout is the caller's problem;
|
|
66
|
+
-- * no bank account, no payout schedule, no fee model, no tax treatment of a fee;
|
|
67
|
+
-- * no reconciliation verdict about an ORDER. This forward says whether a batch agrees with
|
|
68
|
+
-- its own items. Whether a shop's takings agree with its documents is a report, and a report
|
|
69
|
+
-- is not platform data;
|
|
70
|
+
-- * no runtime binding. Not one line of the application is rewired onto these relations by
|
|
71
|
+
-- this forward; the falsifier is its own cross-bundle parity harness.
|
|
72
|
+
|
|
73
|
+
CREATE TABLE public.accounting_payout_batches (
|
|
74
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
75
|
+
provider_kind text NOT NULL,
|
|
76
|
+
provider_batch_id text NOT NULL,
|
|
77
|
+
status text NOT NULL DEFAULT 'observed',
|
|
78
|
+
currency text NOT NULL,
|
|
79
|
+
gross_minor bigint NOT NULL DEFAULT 0,
|
|
80
|
+
fee_minor bigint NOT NULL DEFAULT 0,
|
|
81
|
+
net_minor bigint NOT NULL DEFAULT 0,
|
|
82
|
+
settled_at timestamptz,
|
|
83
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
84
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
85
|
+
evidence jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
86
|
+
CONSTRAINT accounting_payout_batches_reference_key UNIQUE (provider_kind, provider_batch_id),
|
|
87
|
+
CONSTRAINT accounting_payout_batches_provider_kind_nonempty_check CHECK (btrim(provider_kind) <> ''),
|
|
88
|
+
CONSTRAINT accounting_payout_batches_provider_batch_id_nonempty_check CHECK (btrim(provider_batch_id) <> ''),
|
|
89
|
+
-- Departure 2: a shape, never a value.
|
|
90
|
+
CONSTRAINT accounting_payout_batches_currency_shape_check CHECK (char_length(currency) = 3),
|
|
91
|
+
-- The four verdicts a batch can carry. Three of them are the observation still being
|
|
92
|
+
-- incomplete in three different ways, and only one of them is "this agrees".
|
|
93
|
+
CONSTRAINT accounting_payout_batches_status_check CHECK (status IN (
|
|
94
|
+
'observed', 'reconciled', 'mismatch', 'bank_pending'
|
|
95
|
+
)),
|
|
96
|
+
CONSTRAINT accounting_payout_batches_amounts_check CHECK (
|
|
97
|
+
gross_minor >= 0 AND fee_minor >= 0 AND net_minor >= 0
|
|
98
|
+
),
|
|
99
|
+
CONSTRAINT accounting_payout_batches_net_check CHECK (net_minor = gross_minor - fee_minor)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE public.accounting_payout_items (
|
|
103
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
104
|
+
batch_id uuid NOT NULL REFERENCES public.accounting_payout_batches(id) ON DELETE CASCADE,
|
|
105
|
+
provider_payment_id text NOT NULL,
|
|
106
|
+
-- Departure 5: optional on purpose, and the reason is in the header.
|
|
107
|
+
document_id uuid REFERENCES public.accounting_documents(id) ON DELETE SET NULL,
|
|
108
|
+
order_id uuid REFERENCES public.commerce_orders(id) ON DELETE SET NULL,
|
|
109
|
+
status text NOT NULL,
|
|
110
|
+
gross_minor bigint NOT NULL,
|
|
111
|
+
fee_minor bigint NOT NULL,
|
|
112
|
+
net_minor bigint NOT NULL,
|
|
113
|
+
settled_at timestamptz,
|
|
114
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
115
|
+
evidence jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
116
|
+
CONSTRAINT accounting_payout_items_reference_key UNIQUE (batch_id, provider_payment_id),
|
|
117
|
+
CONSTRAINT accounting_payout_items_provider_payment_id_nonempty_check
|
|
118
|
+
CHECK (btrim(provider_payment_id) <> ''),
|
|
119
|
+
CONSTRAINT accounting_payout_items_status_check CHECK (status IN (
|
|
120
|
+
'matched', 'unmatched', 'mismatch', 'bank_pending'
|
|
121
|
+
)),
|
|
122
|
+
CONSTRAINT accounting_payout_items_amounts_check CHECK (
|
|
123
|
+
gross_minor >= 0 AND fee_minor >= 0 AND net_minor >= 0
|
|
124
|
+
),
|
|
125
|
+
CONSTRAINT accounting_payout_items_net_check CHECK (net_minor = gross_minor - fee_minor)
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
-- The lookup an operator actually performs: which payout paid for this document. Partial,
|
|
129
|
+
-- because the majority of items on a young deployment carry no link at all.
|
|
130
|
+
CREATE INDEX idx_accounting_payout_items_document
|
|
131
|
+
ON public.accounting_payout_items (document_id)
|
|
132
|
+
WHERE document_id IS NOT NULL;
|
|
133
|
+
|
|
134
|
+
-- Departure 1. The batch's three totals are the sums of its items, checked at the end of the
|
|
135
|
+
-- transaction that touched either side, so a batch and its items can only ever be committed in
|
|
136
|
+
-- agreement. An argument check inside the mutation would be satisfied once and then bypassed by
|
|
137
|
+
-- the next write to the item relation.
|
|
138
|
+
CREATE FUNCTION public.accounting_payout_assert_batch_totals()
|
|
139
|
+
RETURNS trigger
|
|
140
|
+
LANGUAGE plpgsql
|
|
141
|
+
SET search_path = public, pg_catalog
|
|
142
|
+
AS $$
|
|
143
|
+
DECLARE
|
|
144
|
+
v_batch_id uuid;
|
|
145
|
+
v_batch public.accounting_payout_batches%ROWTYPE;
|
|
146
|
+
v_gross bigint;
|
|
147
|
+
v_fee bigint;
|
|
148
|
+
v_net bigint;
|
|
149
|
+
BEGIN
|
|
150
|
+
-- Branching statements rather than one CASE expression, for the reason the document rail's
|
|
151
|
+
-- position-sum trigger records: the two row types this trigger can see differ.
|
|
152
|
+
IF TG_TABLE_NAME = 'accounting_payout_batches' THEN
|
|
153
|
+
v_batch_id := NEW.id;
|
|
154
|
+
ELSIF TG_OP = 'DELETE' THEN
|
|
155
|
+
v_batch_id := OLD.batch_id;
|
|
156
|
+
ELSE
|
|
157
|
+
v_batch_id := NEW.batch_id;
|
|
158
|
+
END IF;
|
|
159
|
+
|
|
160
|
+
SELECT * INTO v_batch FROM public.accounting_payout_batches WHERE id = v_batch_id;
|
|
161
|
+
-- The batch is gone: a cascade took it, and there is nothing left to be consistent with.
|
|
162
|
+
IF NOT FOUND THEN
|
|
163
|
+
RETURN NULL;
|
|
164
|
+
END IF;
|
|
165
|
+
|
|
166
|
+
SELECT COALESCE(sum(gross_minor), 0), COALESCE(sum(fee_minor), 0), COALESCE(sum(net_minor), 0)
|
|
167
|
+
INTO v_gross, v_fee, v_net
|
|
168
|
+
FROM public.accounting_payout_items
|
|
169
|
+
WHERE batch_id = v_batch_id;
|
|
170
|
+
|
|
171
|
+
IF v_batch.gross_minor IS DISTINCT FROM v_gross
|
|
172
|
+
OR v_batch.fee_minor IS DISTINCT FROM v_fee
|
|
173
|
+
OR v_batch.net_minor IS DISTINCT FROM v_net THEN
|
|
174
|
+
RAISE EXCEPTION 'accounting_payout_batch_total_mismatch' USING ERRCODE = '23514',
|
|
175
|
+
DETAIL = format('batch %s declares %s/%s/%s and its items sum to %s/%s/%s',
|
|
176
|
+
v_batch_id, v_batch.gross_minor, v_batch.fee_minor, v_batch.net_minor,
|
|
177
|
+
v_gross, v_fee, v_net);
|
|
178
|
+
END IF;
|
|
179
|
+
|
|
180
|
+
RETURN NULL;
|
|
181
|
+
END;
|
|
182
|
+
$$;
|
|
183
|
+
|
|
184
|
+
CREATE CONSTRAINT TRIGGER trg_accounting_payout_batches_totals
|
|
185
|
+
AFTER INSERT OR UPDATE ON public.accounting_payout_batches
|
|
186
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
187
|
+
FOR EACH ROW
|
|
188
|
+
EXECUTE FUNCTION public.accounting_payout_assert_batch_totals();
|
|
189
|
+
|
|
190
|
+
CREATE CONSTRAINT TRIGGER trg_accounting_payout_items_totals
|
|
191
|
+
AFTER INSERT OR UPDATE OR DELETE ON public.accounting_payout_items
|
|
192
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
193
|
+
FOR EACH ROW
|
|
194
|
+
EXECUTE FUNCTION public.accounting_payout_assert_batch_totals();
|
|
195
|
+
|
|
196
|
+
-- The verdict, in one place, so the mutation and any later reader cannot disagree about what a
|
|
197
|
+
-- batch of a given shape means. Worst item wins: an item nobody could match, or one whose
|
|
198
|
+
-- amounts disagree with the payment they claim, makes the whole batch a mismatch; an item the
|
|
199
|
+
-- transport says it has not wired yet holds the batch open; only a batch whose every item is
|
|
200
|
+
-- matched has reconciled.
|
|
201
|
+
CREATE FUNCTION public.accounting_payout_batch_verdict(p_batch_id uuid)
|
|
202
|
+
RETURNS text
|
|
203
|
+
LANGUAGE sql
|
|
204
|
+
STABLE
|
|
205
|
+
SET search_path = public, pg_catalog
|
|
206
|
+
AS $$
|
|
207
|
+
SELECT CASE
|
|
208
|
+
WHEN count(*) = 0 THEN 'observed'
|
|
209
|
+
WHEN count(*) FILTER (WHERE status IN ('mismatch', 'unmatched')) > 0 THEN 'mismatch'
|
|
210
|
+
WHEN count(*) FILTER (WHERE status = 'bank_pending') > 0 THEN 'bank_pending'
|
|
211
|
+
ELSE 'reconciled'
|
|
212
|
+
END
|
|
213
|
+
FROM public.accounting_payout_items
|
|
214
|
+
WHERE batch_id = p_batch_id;
|
|
215
|
+
$$;
|
|
216
|
+
|
|
217
|
+
-- The one mutation. It records a single line of a payout, creating the batch that line belongs
|
|
218
|
+
-- to on first contact, and answers with the batch's verdict as of this observation.
|
|
219
|
+
CREATE FUNCTION public.accounting_payout_record_item(
|
|
220
|
+
p_idempotency_key text,
|
|
221
|
+
p_provider_kind text,
|
|
222
|
+
p_provider_batch_id text,
|
|
223
|
+
p_provider_payment_id text,
|
|
224
|
+
p_currency text,
|
|
225
|
+
p_status text,
|
|
226
|
+
p_gross_minor bigint,
|
|
227
|
+
p_fee_minor bigint,
|
|
228
|
+
p_net_minor bigint,
|
|
229
|
+
p_document_id uuid DEFAULT NULL,
|
|
230
|
+
p_order_id uuid DEFAULT NULL,
|
|
231
|
+
p_settled_at timestamptz DEFAULT NULL,
|
|
232
|
+
p_evidence jsonb DEFAULT '{}'::jsonb,
|
|
233
|
+
p_observed_at timestamptz DEFAULT now()
|
|
234
|
+
)
|
|
235
|
+
RETURNS jsonb
|
|
236
|
+
LANGUAGE plpgsql
|
|
237
|
+
SET search_path = public, pg_catalog
|
|
238
|
+
AS $$
|
|
239
|
+
DECLARE
|
|
240
|
+
v_batch public.accounting_payout_batches%ROWTYPE;
|
|
241
|
+
v_item public.accounting_payout_items%ROWTYPE;
|
|
242
|
+
v_replayed boolean := false;
|
|
243
|
+
v_verdict text;
|
|
244
|
+
BEGIN
|
|
245
|
+
IF p_idempotency_key IS NULL OR length(btrim(p_idempotency_key)) < 8
|
|
246
|
+
OR p_provider_kind IS NULL OR btrim(p_provider_kind) = ''
|
|
247
|
+
OR p_provider_batch_id IS NULL OR btrim(p_provider_batch_id) = ''
|
|
248
|
+
OR p_provider_payment_id IS NULL OR btrim(p_provider_payment_id) = ''
|
|
249
|
+
OR p_currency IS NULL OR char_length(p_currency) <> 3
|
|
250
|
+
OR p_gross_minor IS NULL OR p_fee_minor IS NULL OR p_net_minor IS NULL THEN
|
|
251
|
+
RAISE EXCEPTION 'accounting_payout_record_invalid_input' USING ERRCODE = '22023';
|
|
252
|
+
END IF;
|
|
253
|
+
|
|
254
|
+
-- Departure 4: four mistakes, four names.
|
|
255
|
+
IF p_status IS NULL OR p_status NOT IN ('matched', 'unmatched', 'mismatch', 'bank_pending') THEN
|
|
256
|
+
RAISE EXCEPTION 'accounting_payout_record_status_invalid' USING ERRCODE = '22023';
|
|
257
|
+
END IF;
|
|
258
|
+
|
|
259
|
+
IF p_gross_minor < 0 OR p_fee_minor < 0 OR p_net_minor < 0 THEN
|
|
260
|
+
RAISE EXCEPTION 'accounting_payout_record_amount_negative' USING ERRCODE = '22023';
|
|
261
|
+
END IF;
|
|
262
|
+
|
|
263
|
+
IF p_net_minor <> p_gross_minor - p_fee_minor THEN
|
|
264
|
+
RAISE EXCEPTION 'accounting_payout_record_net_mismatch' USING ERRCODE = '22023',
|
|
265
|
+
DETAIL = format('item %s declares %s net of %s and %s', p_provider_payment_id,
|
|
266
|
+
p_net_minor, p_gross_minor, p_fee_minor);
|
|
267
|
+
END IF;
|
|
268
|
+
|
|
269
|
+
INSERT INTO public.accounting_payout_batches (
|
|
270
|
+
provider_kind, provider_batch_id, currency, settled_at, created_at, updated_at, evidence
|
|
271
|
+
)
|
|
272
|
+
VALUES (
|
|
273
|
+
p_provider_kind, p_provider_batch_id, p_currency, p_settled_at, p_observed_at, p_observed_at,
|
|
274
|
+
COALESCE(p_evidence, '{}'::jsonb)
|
|
275
|
+
)
|
|
276
|
+
ON CONFLICT ON CONSTRAINT accounting_payout_batches_reference_key DO UPDATE
|
|
277
|
+
SET updated_at = p_observed_at,
|
|
278
|
+
settled_at = COALESCE(EXCLUDED.settled_at, accounting_payout_batches.settled_at),
|
|
279
|
+
evidence = accounting_payout_batches.evidence || EXCLUDED.evidence
|
|
280
|
+
RETURNING * INTO v_batch;
|
|
281
|
+
|
|
282
|
+
-- Departure 2. Equality, not coercion: a batch that already knows what it was paid in is not
|
|
283
|
+
-- quietly re-denominated by the next line somebody reports against it.
|
|
284
|
+
IF v_batch.currency <> p_currency THEN
|
|
285
|
+
RAISE EXCEPTION 'accounting_payout_batch_currency_conflict' USING ERRCODE = '23514',
|
|
286
|
+
DETAIL = format('batch %s was observed in %s', v_batch.id, v_batch.currency);
|
|
287
|
+
END IF;
|
|
288
|
+
|
|
289
|
+
INSERT INTO public.accounting_payout_items (
|
|
290
|
+
batch_id, provider_payment_id, document_id, order_id, status,
|
|
291
|
+
gross_minor, fee_minor, net_minor, settled_at, created_at, evidence
|
|
292
|
+
)
|
|
293
|
+
VALUES (
|
|
294
|
+
v_batch.id, p_provider_payment_id, p_document_id, p_order_id, p_status,
|
|
295
|
+
p_gross_minor, p_fee_minor, p_net_minor, p_settled_at, p_observed_at,
|
|
296
|
+
COALESCE(p_evidence, '{}'::jsonb) || jsonb_build_object('recordIdempotencyKey', p_idempotency_key)
|
|
297
|
+
)
|
|
298
|
+
ON CONFLICT ON CONSTRAINT accounting_payout_items_reference_key DO NOTHING
|
|
299
|
+
RETURNING * INTO v_item;
|
|
300
|
+
|
|
301
|
+
IF v_item.id IS NULL THEN
|
|
302
|
+
-- Replay is a constraint, not a timestamp comparison: the second caller reads the row the
|
|
303
|
+
-- first one wrote and is told so.
|
|
304
|
+
v_replayed := true;
|
|
305
|
+
SELECT * INTO v_item
|
|
306
|
+
FROM public.accounting_payout_items
|
|
307
|
+
WHERE batch_id = v_batch.id AND provider_payment_id = p_provider_payment_id;
|
|
308
|
+
END IF;
|
|
309
|
+
|
|
310
|
+
UPDATE public.accounting_payout_batches
|
|
311
|
+
SET updated_at = p_observed_at,
|
|
312
|
+
gross_minor = totals.gross_minor,
|
|
313
|
+
fee_minor = totals.fee_minor,
|
|
314
|
+
net_minor = totals.net_minor,
|
|
315
|
+
status = public.accounting_payout_batch_verdict(v_batch.id)
|
|
316
|
+
FROM (
|
|
317
|
+
SELECT COALESCE(sum(gross_minor), 0) AS gross_minor,
|
|
318
|
+
COALESCE(sum(fee_minor), 0) AS fee_minor,
|
|
319
|
+
COALESCE(sum(net_minor), 0) AS net_minor
|
|
320
|
+
FROM public.accounting_payout_items
|
|
321
|
+
WHERE batch_id = v_batch.id
|
|
322
|
+
) AS totals
|
|
323
|
+
WHERE id = v_batch.id
|
|
324
|
+
RETURNING status INTO v_verdict;
|
|
325
|
+
|
|
326
|
+
RETURN jsonb_build_object(
|
|
327
|
+
'batchId', v_batch.id,
|
|
328
|
+
'itemId', v_item.id,
|
|
329
|
+
'status', v_verdict,
|
|
330
|
+
'replayed', v_replayed
|
|
331
|
+
);
|
|
332
|
+
END;
|
|
333
|
+
$$;
|