@happyvertical/smrt-commerce 0.57.3 → 0.57.5

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 (168) hide show
  1. package/AGENTS.md +20 -0
  2. package/README.md +130 -0
  3. package/agents/accounting-snapshot-mapper.md +11 -0
  4. package/agents/contract-billing-schedules.md +67 -0
  5. package/agents/contract-money.md +81 -0
  6. package/agents/customer-funds.md +96 -0
  7. package/agents/historical-sales.md +150 -0
  8. package/agents/money-calculator.md +36 -0
  9. package/agents/money-model.md +6 -3
  10. package/agents/observed-purchase-prices.md +37 -0
  11. package/agents/ordinary-accounting.md +72 -0
  12. package/agents/payment-allocations.md +59 -0
  13. package/agents/received-sales-contract-money.md +38 -0
  14. package/dist/accounting-snapshot.d.ts +8 -1
  15. package/dist/accounting-snapshot.d.ts.map +1 -1
  16. package/dist/accounting-steps-internal.d.ts +49 -0
  17. package/dist/accounting-steps-internal.d.ts.map +1 -0
  18. package/dist/allocation-integrity.d.ts.map +1 -1
  19. package/dist/chunks/{ContractLineItemCollection-BZcrlrrY.js → ContractLineItemCollection-D5hBzrct.js} +6 -6
  20. package/dist/chunks/{ContractLineItemCollection-BZcrlrrY.js.map → ContractLineItemCollection-D5hBzrct.js.map} +1 -1
  21. package/dist/chunks/DocumentTaxCollection-C6cI5U89.js +4395 -0
  22. package/dist/chunks/DocumentTaxCollection-C6cI5U89.js.map +1 -0
  23. package/dist/chunks/HistoricalCustomerFundsCollections-B5x8gTW3.js +170 -0
  24. package/dist/chunks/HistoricalCustomerFundsCollections-B5x8gTW3.js.map +1 -0
  25. package/dist/chunks/{InvoiceCollection-cinzAqt6.js → InvoiceCollection-BolwughC.js} +133 -18
  26. package/dist/chunks/InvoiceCollection-BolwughC.js.map +1 -0
  27. package/dist/chunks/{TaxCodeCollection-DaBOPG9c.js → TaxCodeCollection-CW-tWjkU.js} +3 -4
  28. package/dist/chunks/TaxCodeCollection-CW-tWjkU.js.map +1 -0
  29. package/dist/chunks/{TaxComponentCollection-C4zxnyVu.js → TaxComponentCollection-BiJ9aAe8.js} +3 -3
  30. package/dist/chunks/{TaxComponentCollection-C4zxnyVu.js.map → TaxComponentCollection-BiJ9aAe8.js.map} +1 -1
  31. package/dist/chunks/{allocation-integrity-WmWyXhtG.js → allocation-integrity-CH-i-hsY.js} +7 -7
  32. package/dist/chunks/{allocation-integrity-WmWyXhtG.js.map → allocation-integrity-CH-i-hsY.js.map} +1 -1
  33. package/dist/chunks/{contract-money-EuToOghh.js → contract-money-BMsq4cpL.js} +45 -12
  34. package/dist/chunks/contract-money-BMsq4cpL.js.map +1 -0
  35. package/dist/chunks/customer-funds-internal-BQzoJXK5.js +24 -0
  36. package/dist/chunks/customer-funds-internal-BQzoJXK5.js.map +1 -0
  37. package/dist/chunks/{detect-CY9ufaFb.js → detect-DAE-_NAk.js} +5 -2
  38. package/dist/chunks/{detect-CY9ufaFb.js.map → detect-DAE-_NAk.js.map} +1 -1
  39. package/dist/chunks/historical-commerce-internal-C9ButfuH.js +18 -0
  40. package/dist/chunks/historical-commerce-internal-C9ButfuH.js.map +1 -0
  41. package/dist/chunks/{invoice-posting-C2i5dOVZ.js → invoice-posting-IXic4UEP.js} +5 -5
  42. package/dist/chunks/invoice-posting-IXic4UEP.js.map +1 -0
  43. package/dist/chunks/retention-accounting-c7kY7gFU.js +2738 -0
  44. package/dist/chunks/retention-accounting-c7kY7gFU.js.map +1 -0
  45. package/dist/chunks/retention-accounting-internal-DNxLLVvK.js +30 -0
  46. package/dist/chunks/retention-accounting-internal-DNxLLVvK.js.map +1 -0
  47. package/dist/chunks/{tenant-money-barrier-6TlqUeVj.js → tenant-money-barrier-DEVLrpaB.js} +5 -5
  48. package/dist/chunks/{tenant-money-barrier-6TlqUeVj.js.map → tenant-money-barrier-DEVLrpaB.js.map} +1 -1
  49. package/dist/chunks/types-Bbm0Xflc.js.map +1 -1
  50. package/dist/collections/DocumentTaxCollection.d.ts +4 -1
  51. package/dist/collections/DocumentTaxCollection.d.ts.map +1 -1
  52. package/dist/collections/HistoricalCustomerFundsCollections.d.ts +10 -0
  53. package/dist/collections/HistoricalCustomerFundsCollections.d.ts.map +1 -0
  54. package/dist/collections/HistoricalSalesLineCollection.d.ts +6 -0
  55. package/dist/collections/HistoricalSalesLineCollection.d.ts.map +1 -0
  56. package/dist/collections/PaymentAllocationCollection.d.ts +2 -6
  57. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
  58. package/dist/collections/PurchasePriceObservationCollection.d.ts +6 -0
  59. package/dist/collections/PurchasePriceObservationCollection.d.ts.map +1 -0
  60. package/dist/collections/index.d.ts +1 -0
  61. package/dist/collections/index.d.ts.map +1 -1
  62. package/dist/contract-billing-schedule.d.ts +83 -0
  63. package/dist/contract-billing-schedule.d.ts.map +1 -0
  64. package/dist/contract-money.d.ts +2 -0
  65. package/dist/contract-money.d.ts.map +1 -1
  66. package/dist/customer-funds-internal.d.ts +6 -0
  67. package/dist/customer-funds-internal.d.ts.map +1 -0
  68. package/dist/customer-funds.d.ts +108 -0
  69. package/dist/customer-funds.d.ts.map +1 -0
  70. package/dist/document-money.d.ts +11 -0
  71. package/dist/document-money.d.ts.map +1 -1
  72. package/dist/historical-commerce-internal.d.ts +3 -0
  73. package/dist/historical-commerce-internal.d.ts.map +1 -0
  74. package/dist/historical-customer-funds-types.d.ts +113 -0
  75. package/dist/historical-customer-funds-types.d.ts.map +1 -0
  76. package/dist/historical-customer-funds.d.ts +23 -0
  77. package/dist/historical-customer-funds.d.ts.map +1 -0
  78. package/dist/historical-funds-internal.d.ts +5 -0
  79. package/dist/historical-funds-internal.d.ts.map +1 -0
  80. package/dist/historical-sales-money.d.ts +22 -0
  81. package/dist/historical-sales-money.d.ts.map +1 -0
  82. package/dist/historical-sales-reports.d.ts +31 -0
  83. package/dist/historical-sales-reports.d.ts.map +1 -0
  84. package/dist/historical-sales-types.d.ts +106 -0
  85. package/dist/historical-sales-types.d.ts.map +1 -0
  86. package/dist/historical-sales-validation.d.ts +12 -0
  87. package/dist/historical-sales-validation.d.ts.map +1 -0
  88. package/dist/index.d.ts +22 -0
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +1668 -1643
  91. package/dist/index.js.map +1 -1
  92. package/dist/invoice-accounting-sync.d.ts +0 -1
  93. package/dist/invoice-accounting-sync.d.ts.map +1 -1
  94. package/dist/invoice-mcp.d.ts.map +1 -1
  95. package/dist/invoice-posting.d.ts.map +1 -1
  96. package/dist/invoice-retention.d.ts +16 -0
  97. package/dist/invoice-retention.d.ts.map +1 -0
  98. package/dist/invoices.d.ts +3 -0
  99. package/dist/invoices.d.ts.map +1 -1
  100. package/dist/invoices.js +2 -2
  101. package/dist/manifest.json +7626 -4446
  102. package/dist/models/Contract.d.ts +14 -0
  103. package/dist/models/Contract.d.ts.map +1 -1
  104. package/dist/models/DocumentTax.d.ts +2 -0
  105. package/dist/models/DocumentTax.d.ts.map +1 -1
  106. package/dist/models/HistoricalCustomerFunds.d.ts +36 -0
  107. package/dist/models/HistoricalCustomerFunds.d.ts.map +1 -0
  108. package/dist/models/HistoricalCustomerFundsAllocation.d.ts +32 -0
  109. package/dist/models/HistoricalCustomerFundsAllocation.d.ts.map +1 -0
  110. package/dist/models/HistoricalSalesLine.d.ts +29 -0
  111. package/dist/models/HistoricalSalesLine.d.ts.map +1 -0
  112. package/dist/models/Invoice.d.ts +12 -1
  113. package/dist/models/Invoice.d.ts.map +1 -1
  114. package/dist/models/Payment.d.ts +9 -1
  115. package/dist/models/Payment.d.ts.map +1 -1
  116. package/dist/models/PaymentAllocation.d.ts +3 -1
  117. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  118. package/dist/models/PaymentIntent.d.ts.map +1 -1
  119. package/dist/models/Payout.d.ts.map +1 -1
  120. package/dist/models/PurchasePriceObservation.d.ts +15 -0
  121. package/dist/models/PurchasePriceObservation.d.ts.map +1 -0
  122. package/dist/models/TaxCode.d.ts +1 -1
  123. package/dist/models/TaxCode.d.ts.map +1 -1
  124. package/dist/models/index.d.ts +1 -0
  125. package/dist/models/index.d.ts.map +1 -1
  126. package/dist/money.d.ts +5 -1
  127. package/dist/money.d.ts.map +1 -1
  128. package/dist/money.js +62 -15
  129. package/dist/money.js.map +1 -1
  130. package/dist/observed-purchase-price-internal.d.ts +3 -0
  131. package/dist/observed-purchase-price-internal.d.ts.map +1 -0
  132. package/dist/observed-purchase-price.d.ts +71 -0
  133. package/dist/observed-purchase-price.d.ts.map +1 -0
  134. package/dist/ordinary-accounting-internal.d.ts +3 -0
  135. package/dist/ordinary-accounting-internal.d.ts.map +1 -0
  136. package/dist/ordinary-accounting.d.ts +104 -0
  137. package/dist/ordinary-accounting.d.ts.map +1 -0
  138. package/dist/received-purchase-money.d.ts +87 -0
  139. package/dist/received-purchase-money.d.ts.map +1 -0
  140. package/dist/recipes.d.ts +4 -1
  141. package/dist/recipes.d.ts.map +1 -1
  142. package/dist/retention-accounting-internal.d.ts +3 -0
  143. package/dist/retention-accounting-internal.d.ts.map +1 -0
  144. package/dist/retention-accounting.d.ts +143 -0
  145. package/dist/retention-accounting.d.ts.map +1 -0
  146. package/dist/retention-money.d.ts +43 -0
  147. package/dist/retention-money.d.ts.map +1 -0
  148. package/dist/smrt-knowledge.json +1872 -113
  149. package/dist/tax/cookbook.d.ts.map +1 -1
  150. package/dist/tax-summary.d.ts +1 -0
  151. package/dist/tax-summary.d.ts.map +1 -1
  152. package/dist/types/index.d.ts +20 -1
  153. package/dist/types/index.d.ts.map +1 -1
  154. package/package.json +16 -16
  155. package/dist/chunks/CustomerCollection-BIPH2ZUx.js +0 -99
  156. package/dist/chunks/CustomerCollection-BIPH2ZUx.js.map +0 -1
  157. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js +0 -1666
  158. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +0 -1
  159. package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +0 -1
  160. package/dist/chunks/TaxCodeCollection-DaBOPG9c.js.map +0 -1
  161. package/dist/chunks/VendorCollection-D6ZcUYy8.js +0 -100
  162. package/dist/chunks/VendorCollection-D6ZcUYy8.js.map +0 -1
  163. package/dist/chunks/calculations-C9daQLsh.js +0 -82
  164. package/dist/chunks/calculations-C9daQLsh.js.map +0 -1
  165. package/dist/chunks/contract-money-EuToOghh.js.map +0 -1
  166. package/dist/chunks/document-money-DV5t4vlK.js +0 -1186
  167. package/dist/chunks/document-money-DV5t4vlK.js.map +0 -1
  168. package/dist/chunks/invoice-posting-C2i5dOVZ.js.map +0 -1
@@ -0,0 +1,2738 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { Y as InvoiceLineItemCollection, t as DocumentTaxCollection, x as lockAndReloadInvoice } from "./DocumentTaxCollection-C6cI5U89.js";
3
+ import { d as PaymentStatus, s as InvoiceStatus, u as PaymentMethod } from "./types-Bbm0Xflc.js";
4
+ import { o as minorToMajorUnits } from "./units-8ElbjVZK.js";
5
+ import { i as withOrdinaryAccountingAuthority, n as withRetentionAccountingAuthority, r as hasOrdinaryAccountingAuthority, t as hasRetentionAccountingAuthority } from "./retention-accounting-internal-DNxLLVvK.js";
6
+ import { i as claimAllocationParent, n as allocationTotal, r as assertAllocationParent, t as allocationOwner } from "./allocation-integrity-CH-i-hsY.js";
7
+ import { n as customerFundsState, t as assertCustomerFundsWrite } from "./customer-funds-internal-BQzoJXK5.js";
8
+ import { MoneyValidationError, assertIntegerMinorUnits, calculateMoneyDocument } from "../money.js";
9
+ import { t as HistoricalCustomerFundsAllocationCollection } from "./HistoricalCustomerFundsCollections-B5x8gTW3.js";
10
+ import { n as calculateAvailableRefund } from "./credit-allocation-yspz0Lab.js";
11
+ import { _ as withDocumentMoneyTransaction, m as isMoneyTransaction, p as DOCUMENT_MONEY_WRITE } from "./detect-DAE-_NAk.js";
12
+ import { c as validateRetentionEvidence, o as calculateInvoicePayable, s as parseInvoiceRetention, t as InvoiceCollection } from "./InvoiceCollection-BolwughC.js";
13
+ import { t as lockTenantMoneyRuntime } from "./tenant-money-barrier-DEVLrpaB.js";
14
+ import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, isPostgresDatabase, smrt } from "@happyvertical/smrt-core";
15
+ import { TenantScoped, getTenantId, queryGlobal, queryWithGlobals, tenantId } from "@happyvertical/smrt-tenancy";
16
+ import { createHash, randomUUID } from "node:crypto";
17
+ import { prepareQuickBooksCreditMemoRequest, prepareQuickBooksInvoiceRequest, prepareQuickBooksReceivableRequest, requireCreditMemoOperations } from "@happyvertical/accounting";
18
+ //#region src/models/Payment.ts
19
+ var __defProp$1 = Object.defineProperty;
20
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
21
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, {
22
+ enumerable: true,
23
+ configurable: true,
24
+ writable: true,
25
+ value
26
+ }) : obj[key] = value;
27
+ var __decorateClass$1 = (decorators, target, key, kind) => {
28
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
29
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
30
+ if (kind && result) __defProp$1(target, key, result);
31
+ return result;
32
+ };
33
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
34
+ var PAYMENT_STATUS_TRANSITIONS = {
35
+ [PaymentStatus.PENDING]: [
36
+ PaymentStatus.COMPLETED,
37
+ PaymentStatus.FAILED,
38
+ PaymentStatus.CANCELLED
39
+ ],
40
+ [PaymentStatus.COMPLETED]: [PaymentStatus.REFUNDED],
41
+ [PaymentStatus.FAILED]: [],
42
+ [PaymentStatus.REFUNDED]: [],
43
+ [PaymentStatus.CANCELLED]: []
44
+ };
45
+ var loadedPaymentStatus = /* @__PURE__ */ new WeakMap();
46
+ var settlementInProgress = /* @__PURE__ */ new WeakSet();
47
+ var recordingInProgress = /* @__PURE__ */ new WeakSet();
48
+ var Payment = class extends SmrtObject {
49
+ /** Incoming settlement or outgoing customer refund. Amount stays positive. */
50
+ kind = "payment";
51
+ customerFundsJson = "";
52
+ retentionAccountingBindingJson = "";
53
+ ordinaryAccountingBindingJson = "";
54
+ tenantId = null;
55
+ contractId = null;
56
+ customerId = null;
57
+ amount = 0;
58
+ currency = "USD";
59
+ method = PaymentMethod.BANK_TRANSFER;
60
+ status = PaymentStatus.PENDING;
61
+ /**
62
+ * External transaction ID (from payment processor)
63
+ */
64
+ transactionId = "";
65
+ reference = "";
66
+ journalId = "";
67
+ paidAt = null;
68
+ notes = "";
69
+ /**
70
+ * External ID in accounting provider (e.g., QBO payment ID)
71
+ */
72
+ externalId = "";
73
+ /**
74
+ * Accounting provider name ('quickbooks' | 'stripe' | 'paypal' | etc.)
75
+ */
76
+ externalProvider = "";
77
+ /**
78
+ * When payment was last synced to provider
79
+ */
80
+ syncedAt = null;
81
+ /**
82
+ * Stable identifier of the `PaymentBackend` adapter that served this
83
+ * payment — e.g. `base-usdc`, `solana-usdc`, `btc`, `stripe`, `paypal`.
84
+ *
85
+ * Distinct from {@link externalProvider}: `backendId` names the SMRT
86
+ * payment-rail adapter, while `externalProvider` names a downstream
87
+ * accounting destination (QuickBooks, Stripe-the-accounting-source).
88
+ * The same Stripe payment will have `backendId: 'stripe'` AND
89
+ * `externalProvider: 'stripe'`; a crypto payment synced to QBO will
90
+ * have `backendId: 'base-usdc'` and `externalProvider: 'quickbooks'`.
91
+ */
92
+ backendId = "";
93
+ /**
94
+ * Chain transaction hash or backend aggregator's reference id. For
95
+ * on-chain payments this is the tx hash (`0x...` on EVM, base58 sig on
96
+ * Solana, txid on Bitcoin); for fiat-rail backends it's the gateway's
97
+ * own reference. Kept distinct from {@link transactionId} so consumers
98
+ * that already populate `transactionId` with a provider-internal id
99
+ * don't have to overload it.
100
+ */
101
+ backendTxRef = "";
102
+ /**
103
+ * The amount the backend actually moved, in the native currency's own
104
+ * **integer minor units** — satoshis for BTC, cents for a fiat rail. For
105
+ * stablecoin rails this typically equals `amount`; for volatile-currency
106
+ * rails it's the satoshi/wei figure the chain recorded, independent of any
107
+ * USD valuation.
108
+ *
109
+ * Fresh PostgreSQL and DuckDB INTEGER columns materialize as BIGINT. SMRT
110
+ * rejects values outside JavaScript's safe-integer range during hydration;
111
+ * existing PostgreSQL `int4` columns require the explicit widening in #2424.
112
+ */
113
+ nativeAmount = 0;
114
+ /**
115
+ * Code identifying the native currency `nativeAmount` is denominated
116
+ * in. Mirrors the `backendId` namespacing convention — `USDC-base`,
117
+ * `BTC`, `ETH`, `USD-stripe`. Empty string means "use {@link currency}"
118
+ * (i.e. the payment was already quoted and settled in the same
119
+ * currency).
120
+ */
121
+ nativeCurrency = "";
122
+ /**
123
+ * USD valuation of the payment at the moment the price was quoted to
124
+ * the buyer (typically the moment a `PaymentIntent` was issued), in
125
+ * **integer minor units** (US cents). A default of `0` means no USD-quote
126
+ * snapshot was taken (the payment was already USD-denominated or the
127
+ * backend doesn't require drift accounting).
128
+ *
129
+ * This is money describing the same funds as {@link Payment.amount}, so it
130
+ * converts with it — a dollars-here / cents-there split would make
131
+ * {@link Payment.usdDrift} meaningless (#2401).
132
+ */
133
+ usdAtQuote = 0;
134
+ /**
135
+ * USD valuation of the payment at the moment it was confirmed on the
136
+ * backend (chain confirmation, gateway settlement, etc.), in integer minor
137
+ * units (US cents). The delta between {@link usdAtQuote} and
138
+ * `usdAtConfirmation` is the USD drift the operator absorbs (positive or
139
+ * negative) when accepting payment in a volatile native currency (#2401).
140
+ */
141
+ usdAtConfirmation = 0;
142
+ constructor(options = {}) {
143
+ super(options);
144
+ if (options.kind !== void 0) this.kind = options.kind;
145
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
146
+ if (options.contractId !== void 0) this.contractId = options.contractId;
147
+ if (options.customerId !== void 0) this.customerId = options.customerId;
148
+ if (options.amount !== void 0) this.amount = options.amount;
149
+ if (options.currency !== void 0) this.currency = options.currency;
150
+ if (options.method !== void 0) this.method = options.method;
151
+ if (options.status !== void 0) this.status = options.status;
152
+ if (options.transactionId !== void 0) this.transactionId = options.transactionId;
153
+ if (options.reference !== void 0) this.reference = options.reference;
154
+ if (options.customerFundsJson !== void 0) this.customerFundsJson = options.customerFundsJson;
155
+ if (options.journalId !== void 0) this.journalId = options.journalId;
156
+ if (options.paidAt !== void 0) this.paidAt = options.paidAt;
157
+ if (options.notes !== void 0) this.notes = options.notes;
158
+ if (options.externalId !== void 0) this.externalId = options.externalId;
159
+ if (options.externalProvider !== void 0) this.externalProvider = options.externalProvider;
160
+ if (options.syncedAt !== void 0) this.syncedAt = options.syncedAt;
161
+ if (options.backendId !== void 0) this.backendId = options.backendId;
162
+ if (options.backendTxRef !== void 0) this.backendTxRef = options.backendTxRef;
163
+ if (options.nativeAmount !== void 0) this.nativeAmount = options.nativeAmount;
164
+ if (options.nativeCurrency !== void 0) this.nativeCurrency = options.nativeCurrency;
165
+ if (options.usdAtQuote !== void 0) this.usdAtQuote = options.usdAtQuote;
166
+ if (options.usdAtConfirmation !== void 0) this.usdAtConfirmation = options.usdAtConfirmation;
167
+ }
168
+ /**
169
+ * Capture the persisted status the row was loaded with, so the save-time
170
+ * transition guard can reject illegal status flips made via raw field
171
+ * assignment (mass-assignment on the generated update route, a stale caller,
172
+ * etc.). Freshly-constructed (not-yet-saved) payments have no prior status.
173
+ */
174
+ async initialize() {
175
+ await super.initialize();
176
+ if (await this.isSaved()) loadedPaymentStatus.set(this, this.status);
177
+ return this;
178
+ }
179
+ /**
180
+ * Save-time state-machine guard (S5 audit #1390).
181
+ *
182
+ * `status` is mass-assignable on the generated update/create routes, and a
183
+ * COMPLETED Payment is treated as settlement proof downstream — e.g.
184
+ * {@link PaymentIntent}'s PAID verification trusts a COMPLETED Payment row.
185
+ * A forged `status: 'completed'` (with arbitrary amounts and no journal)
186
+ * would therefore satisfy that check without any money having moved.
187
+ *
188
+ * This guard enforces two things:
189
+ * - **Transitions must be legal** per {@link PAYMENT_STATUS_TRANSITIONS}.
190
+ * - **Reaching COMPLETED requires the verified settlement path — on
191
+ * creation AND on update.** Only `recordPayment()` (which posts a
192
+ * balanced journal and links `journalId`) may write a Payment into
193
+ * COMPLETED; it announces itself via {@link settlementInProgress}. A raw
194
+ * `status: 'completed'` is rejected whether the row is brand-new (a
195
+ * GENESIS `create({ status: 'completed' })`) or already persisted — that
196
+ * is the exact path a forged COMPLETED would take to satisfy
197
+ * PaymentIntent's PAID verification without any money having moved (codex
198
+ * HIGH#1, #1390 round 5). There is deliberately NO import/fixture
199
+ * carve-out: a COMPLETED Payment is settlement proof downstream, so it is
200
+ * only ever reachable through `recordPayment()`, on every surface
201
+ * (REST/MCP/CLI/direct). Fixtures/migrations that need a completed payment
202
+ * must drive it through `recordPayment()` (or start non-COMPLETED).
203
+ */
204
+ async save() {
205
+ return this.withTransaction(async () => {
206
+ if (this.kind !== "payment" && this.kind !== "refund") throw new Error("Payment kind must be payment or refund");
207
+ await claimAllocationParent(this, "payment");
208
+ assertIntegerMinorUnits("Payment", this.id || "<new>", [
209
+ ["amount", this.amount],
210
+ ["nativeAmount", this.nativeAmount],
211
+ ["usdAtQuote", this.usdAtQuote],
212
+ ["usdAtConfirmation", this.usdAtConfirmation]
213
+ ]);
214
+ const priorRow = await this.loadPersistedRow();
215
+ if (this.customerFundsJson || priorRow?.customer_funds_json) assertCustomerFundsWrite(this);
216
+ const ordinaryBinding = this.ordinaryAccountingBindingJson || String(priorRow?.ordinary_accounting_binding_json ?? "");
217
+ if (ordinaryBinding && !hasOrdinaryAccountingAuthority(this.tenantId, JSON.parse(ordinaryBinding).invoiceId)) throw new Error("Provider-recorded refund is immutable; use an explicit reversal workflow");
218
+ const retainedBinding = this.retentionAccountingBindingJson || String(priorRow?.retention_accounting_binding_json ?? "");
219
+ if (retainedBinding && !hasRetentionAccountingAuthority(this.tenantId, JSON.parse(retainedBinding).invoiceId)) throw new Error("Provider-recorded refund is immutable; use an explicit reversal workflow");
220
+ const prior = priorRow && priorRow.status != null ? priorRow.status : loadedPaymentStatus.get(this);
221
+ this.assertStatusTransition(prior);
222
+ if (this.status === PaymentStatus.COMPLETED && prior !== PaymentStatus.COMPLETED && !settlementInProgress.has(this)) throw new Error(`Payment ${this.id || "<new>"}: cannot set status to COMPLETED via raw assignment \u2014 a Payment is only completed through recordPayment(), which posts a balanced settlement journal. Use recordPayment() instead of setting status directly.`);
223
+ if (priorRow && (priorRow.status === PaymentStatus.COMPLETED || priorRow.status === PaymentStatus.REFUNDED)) this.assertSettledAmountsUnchanged(priorRow);
224
+ try {
225
+ await assertAllocationParent(this, "payment");
226
+ const result = await super.save();
227
+ loadedPaymentStatus.set(this, this.status);
228
+ return result;
229
+ } finally {
230
+ settlementInProgress.delete(this);
231
+ }
232
+ });
233
+ }
234
+ /** Managed receipts and refunds retain immutable cash provenance. */
235
+ async delete(options = {}) {
236
+ const persisted = await this.loadPersistedRow();
237
+ if (this.retentionAccountingBindingJson || persisted?.retention_accounting_binding_json || this.ordinaryAccountingBindingJson || persisted?.ordinary_accounting_binding_json) throw new Error("Provider-recorded refund history cannot be deleted");
238
+ if (this.customerFundsJson || persisted?.customer_funds_json) throw new Error("Customer funds cash history cannot be deleted");
239
+ return super.delete(options);
240
+ }
241
+ /**
242
+ * Reject an illegal status flip. Compares the about-to-be-written status
243
+ * against the status the row was loaded with. No-op transitions (status
244
+ * unchanged) and brand-new rows (no prior) are always allowed — the
245
+ * COMPLETED-specific settlement requirement is enforced separately in
246
+ * {@link save}.
247
+ */
248
+ assertStatusTransition(prior) {
249
+ if (prior === void 0) return;
250
+ if (prior === this.status) return;
251
+ if (!(PAYMENT_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`Payment ${this.id}: illegal status transition '${prior}' \u2192 '${this.status}'. Use the guarded helpers (recordPayment / markFailed / cancel).`);
252
+ }
253
+ /**
254
+ * Load the AUTHORITATIVE persisted row (S5 audit #1390 round 4, codex HIGH#1;
255
+ * extended round 6 to carry the full row, not just `status`). The WeakMap is
256
+ * only populated when {@link initialize} loaded the row from the DB; it is
257
+ * empty for an instance built via
258
+ * `collection.create({ id: <existing>, _skipLoad: true })` — the upsert path
259
+ * that lets a caller write onto an existing row without hydrating it. Trusting
260
+ * an empty WeakMap there would treat the write as a brand-new row and skip the
261
+ * transition guard entirely (a poisonable prior-state).
262
+ *
263
+ * So when this instance carries an `id`, read the persisted row straight from
264
+ * the database and use it as the prior — a create-onto-existing is an update.
265
+ * Returns `undefined` when no row exists (truly new), so callers fall back to
266
+ * the WeakMap (which is also empty then). Settled-amount columns are
267
+ * snake_case (`native_amount`, etc.); the `status` column is single-word.
268
+ */
269
+ async loadPersistedRow() {
270
+ if (!this.id) return void 0;
271
+ try {
272
+ return await this.db.get(this.tableName, { id: this.id }) ?? void 0;
273
+ } catch {
274
+ return;
275
+ }
276
+ }
277
+ /**
278
+ * Reject any change to the settled monetary fields of an already-COMPLETED
279
+ * Payment (S5 audit #1390 round 6 — codex ROOT INSIGHT). Mirrors the
280
+ * PaymentIntent PAID backing-field freeze. A COMPLETED Payment's economic
281
+ * identity is settled: the `amount` was posted into a balanced journal and is
282
+ * the figure PaymentIntent's PAID verification reconciles against, so it (and
283
+ * the native/USD valuation fields that describe the same money) must not
284
+ * drift. The only legal status move out of COMPLETED is REFUNDED, which
285
+ * reverses the funds without rewriting how much they were. Compared against
286
+ * the AUTHORITATIVE persisted row so the freeze holds on every surface, even
287
+ * for an un-hydrated create-onto-existing upsert.
288
+ */
289
+ assertSettledAmountsUnchanged(priorRow) {
290
+ const frozen = [
291
+ [
292
+ "journalId",
293
+ String(priorRow.journal_id ?? ""),
294
+ this.journalId ?? ""
295
+ ],
296
+ [
297
+ "paidAt",
298
+ priorRow.paid_at == null ? "" : new Date(priorRow.paid_at).getTime(),
299
+ this.paidAt == null ? "" : new Date(this.paidAt).getTime()
300
+ ],
301
+ [
302
+ "kind",
303
+ String(priorRow.kind ?? "payment"),
304
+ this.kind
305
+ ],
306
+ [
307
+ "amount",
308
+ Number(priorRow.amount ?? 0),
309
+ Number(this.amount ?? 0)
310
+ ],
311
+ [
312
+ "currency",
313
+ String(priorRow.currency ?? ""),
314
+ String(this.currency ?? "")
315
+ ],
316
+ [
317
+ "nativeAmount",
318
+ Number(priorRow.native_amount ?? 0),
319
+ Number(this.nativeAmount ?? 0)
320
+ ],
321
+ [
322
+ "nativeCurrency",
323
+ String(priorRow.native_currency ?? ""),
324
+ String(this.nativeCurrency ?? "")
325
+ ],
326
+ [
327
+ "usdAtQuote",
328
+ Number(priorRow.usd_at_quote ?? 0),
329
+ Number(this.usdAtQuote ?? 0)
330
+ ],
331
+ [
332
+ "usdAtConfirmation",
333
+ Number(priorRow.usd_at_confirmation ?? 0),
334
+ Number(this.usdAtConfirmation ?? 0)
335
+ ]
336
+ ];
337
+ for (const [field2, prior, next] of frozen) if (prior !== next) throw new Error(`Payment ${this.id}: '${field2}' is frozen once the payment is COMPLETED (was '${prior}', got '${next}'). A settled payment's monetary fields cannot be mutated \u2014 issue a refund instead.`);
338
+ }
339
+ /**
340
+ * USD drift between quote time and confirmation time — what the
341
+ * operator gained (positive) or lost (negative) by accepting a
342
+ * volatile-currency payment. Returns `0` when either side of the
343
+ * comparison is missing or zero, so callers don't have to special-case
344
+ * fiat-rail / stablecoin payments.
345
+ */
346
+ usdDrift() {
347
+ if (!this.usdAtQuote || !this.usdAtConfirmation) return 0;
348
+ return this.usdAtConfirmation - this.usdAtQuote;
349
+ }
350
+ /**
351
+ * Check if payment is pending
352
+ */
353
+ isPending() {
354
+ return this.status === PaymentStatus.PENDING;
355
+ }
356
+ /**
357
+ * Check if payment is completed
358
+ */
359
+ isCompleted() {
360
+ return this.status === PaymentStatus.COMPLETED;
361
+ }
362
+ /**
363
+ * Check if payment is refunded
364
+ */
365
+ isRefunded() {
366
+ return this.status === PaymentStatus.REFUNDED;
367
+ }
368
+ /**
369
+ * Record the payment and create a balanced journal entry.
370
+ *
371
+ * Creates a journal entry in smrt-ledgers:
372
+ * - Debit: Cash/Bank account (assets increase)
373
+ * - Credit: Accounts Receivable (receivables decrease)
374
+ *
375
+ * Both sides carry `this.amount`, so the journal posted from commerce is
376
+ * denominated in **integer minor units** like the rest of this package
377
+ * (#2401). smrt-ledgers is unit-agnostic — its `BALANCE_EPSILON` only checks
378
+ * that debits equal credits — and integer entries balance exactly, so this is
379
+ * strictly tighter than the major-unit journals commerce used to post.
380
+ *
381
+ * @param options - Ledger and account configuration
382
+ * @returns The created journal
383
+ *
384
+ * @example
385
+ * ```typescript
386
+ * const journal = await payment.recordPayment({
387
+ * ledgerId: ledger.id,
388
+ * receivablesAccountId: arAccount.id,
389
+ * cashAccountId: bankAccount.id
390
+ * });
391
+ * console.log(`Created journal: ${journal.number}`);
392
+ * ```
393
+ */
394
+ async recordPayment(options) {
395
+ if (recordingInProgress.has(this)) throw new Error("Payment recording is already in progress");
396
+ if (this.status === PaymentStatus.COMPLETED) throw new Error("Payment already recorded");
397
+ const previous = {
398
+ status: this.status,
399
+ journalId: this.journalId,
400
+ paidAt: this.paidAt,
401
+ loadedStatus: loadedPaymentStatus.get(this)
402
+ };
403
+ recordingInProgress.add(this);
404
+ try {
405
+ await this.withTransaction(async () => {
406
+ if (!this.isPersisted) await this.save();
407
+ if (!this.updated_at) throw new Error("Payment revision is missing; reload before recording");
408
+ await this.claimRevision(this.updated_at);
409
+ const persisted = await this.db.get(this.tableName, { id: this.id });
410
+ if (!persisted) throw new Error("Claimed payment could not be loaded");
411
+ if (persisted.status === PaymentStatus.COMPLETED) throw new Error("Payment already recorded");
412
+ await this.postPaymentInTransaction(options);
413
+ });
414
+ } catch (error) {
415
+ this.status = previous.status;
416
+ this.journalId = previous.journalId;
417
+ this.paidAt = previous.paidAt;
418
+ if (previous.loadedStatus === void 0) loadedPaymentStatus.delete(this);
419
+ else loadedPaymentStatus.set(this, previous.loadedStatus);
420
+ throw error;
421
+ } finally {
422
+ recordingInProgress.delete(this);
423
+ settlementInProgress.delete(this);
424
+ }
425
+ const { JournalCollection } = await import("@happyvertical/smrt-ledgers");
426
+ const journal = await (await JournalCollection.create({
427
+ ...this.options,
428
+ db: this.db
429
+ })).get({ id: this.journalId });
430
+ if (!journal) throw new Error("Recorded payment journal could not be loaded");
431
+ return journal;
432
+ }
433
+ /** Settle an outgoing refund through the same guarded journal transaction. */
434
+ async recordRefund(options) {
435
+ if (this.kind !== "refund") throw new Error("recordRefund requires Payment.kind=refund");
436
+ return this.recordPayment(options);
437
+ }
438
+ /** Post and complete only inside recordPayment's claimed transaction. */
439
+ async postPaymentInTransaction(options) {
440
+ if (this.status === PaymentStatus.COMPLETED) throw new Error("Payment already recorded");
441
+ if (this.amount <= 0) throw new Error("Payment amount must be positive");
442
+ const funds = customerFundsState(this.customerFundsJson);
443
+ const explicitDate = options.effectiveOn;
444
+ if (explicitDate !== void 0 && (typeof explicitDate !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(explicitDate) || !Number.isFinite(Date.parse(explicitDate)) || new Date(explicitDate).toISOString().slice(0, 10) !== explicitDate)) throw new Error("Settlement effectiveOn must be a calendar date");
445
+ if (funds) {
446
+ if (explicitDate !== void 0 && explicitDate !== funds.effectiveOn) throw new Error("Settlement date differs from reviewed customer funds");
447
+ assertCustomerFundsWrite(this);
448
+ if (options.cashAccountId !== funds.policy.cashAccountId || options.receivablesAccountId !== funds.policy.receivablesAccountId) throw new Error("Customer funds settlement accounts differ from reviewed policy");
449
+ }
450
+ const creditAccountId = funds?.policy.mode === "deposit-liability" ? funds.policy.liabilityAccountId : options.receivablesAccountId;
451
+ const effectiveOn = funds?.effectiveOn ?? explicitDate;
452
+ const effectiveDate = effectiveOn ? /* @__PURE__ */ new Date(`${effectiveOn}T12:00:00Z`) : /* @__PURE__ */ new Date();
453
+ const { JournalCollection } = await import("@happyvertical/smrt-ledgers");
454
+ const journal = await (await JournalCollection.create(this.options)).create({
455
+ date: effectiveDate,
456
+ description: this.kind === "refund" ? `Customer refund for contract ${this.contractId}` : `Payment received for contract ${this.contractId}`,
457
+ sourceModule: "smrt-commerce",
458
+ sourceRef: this.id
459
+ });
460
+ await journal.save();
461
+ await journal.addEntry({
462
+ accountId: options.cashAccountId,
463
+ currency: this.currency,
464
+ debit: this.kind === "payment" ? this.amount : 0,
465
+ credit: this.kind === "refund" ? this.amount : 0,
466
+ memo: `${this.kind === "refund" ? "Refund" : "Payment"} ${this.reference || this.id}`
467
+ });
468
+ await journal.addEntry({
469
+ accountId: creditAccountId,
470
+ currency: this.currency,
471
+ debit: this.kind === "refund" ? this.amount : 0,
472
+ credit: this.kind === "payment" ? this.amount : 0,
473
+ memo: `${this.kind === "refund" ? "Refund" : "Payment"} ${this.reference || this.id}`
474
+ });
475
+ await journal.post();
476
+ if (!journal.id) throw new Error("Payment.recordPayment: journal was posted but has no id");
477
+ this.journalId = journal.id;
478
+ this.status = PaymentStatus.COMPLETED;
479
+ this.paidAt = effectiveDate;
480
+ settlementInProgress.add(this);
481
+ await this.save();
482
+ return journal;
483
+ }
484
+ /**
485
+ * Get the linked journal entry (if recorded)
486
+ */
487
+ async getJournal() {
488
+ if (!this.journalId) return null;
489
+ try {
490
+ const { JournalCollection } = await import("@happyvertical/smrt-ledgers");
491
+ return await (await JournalCollection.create(this.options)).get({ id: this.journalId });
492
+ } catch {
493
+ return null;
494
+ }
495
+ }
496
+ /**
497
+ * Mark payment as failed
498
+ */
499
+ markFailed(reason) {
500
+ this.status = PaymentStatus.FAILED;
501
+ if (reason) this.notes = `${this.notes ? `${this.notes}
502
+ ` : ""}Failed: ${reason}`;
503
+ }
504
+ /**
505
+ * Cancel the payment
506
+ */
507
+ cancel() {
508
+ if (this.status === PaymentStatus.COMPLETED) throw new Error("Cannot cancel a completed payment. Use refund instead.");
509
+ this.status = PaymentStatus.CANCELLED;
510
+ }
511
+ };
512
+ __publicField$1(Payment, "__smrtPackage__", "@happyvertical/smrt-commerce");
513
+ __decorateClass$1([field({ type: "text" })], Payment.prototype, "customerFundsJson", 2);
514
+ __decorateClass$1([field({ sensitive: true })], Payment.prototype, "retentionAccountingBindingJson", 2);
515
+ __decorateClass$1([field({ sensitive: true })], Payment.prototype, "ordinaryAccountingBindingJson", 2);
516
+ __decorateClass$1([tenantId({ nullable: true })], Payment.prototype, "tenantId", 2);
517
+ __decorateClass$1([field({ description: "The order or agreement this payment is for, if any." }), foreignKey("SalesContract", { validateSubtype: true })], Payment.prototype, "contractId", 2);
518
+ __decorateClass$1([field({ description: "The customer who paid." }), foreignKey("Customer")], Payment.prototype, "customerId", 2);
519
+ __decorateClass$1([field({
520
+ description: "How much was paid.",
521
+ ui: { format: "money" }
522
+ })], Payment.prototype, "amount", 2);
523
+ __decorateClass$1([field({
524
+ description: "The currency of the payment.",
525
+ ui: { widget: "currency" }
526
+ })], Payment.prototype, "currency", 2);
527
+ __decorateClass$1([field({ description: "How it was paid: cash, check, card, bank transfer or other." })], Payment.prototype, "method", 2);
528
+ __decorateClass$1([field({ description: "Whether the payment is pending, completed, failed or refunded." })], Payment.prototype, "status", 2);
529
+ __decorateClass$1([field({ description: "A reference note, such as a check number." })], Payment.prototype, "reference", 2);
530
+ __decorateClass$1([crossPackageRef("@happyvertical/smrt-ledgers:Journal")], Payment.prototype, "journalId", 2);
531
+ __decorateClass$1([field({ description: "When the money was received." })], Payment.prototype, "paidAt", 2);
532
+ __decorateClass$1([field({
533
+ description: "Anything worth remembering about this payment.",
534
+ ui: { widget: "textarea" }
535
+ })], Payment.prototype, "notes", 2);
536
+ Payment = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
537
+ api: {
538
+ include: [
539
+ "list",
540
+ "get",
541
+ "create",
542
+ "update"
543
+ ],
544
+ writable: [
545
+ "kind",
546
+ "contractId",
547
+ "customerId",
548
+ "amount",
549
+ "currency",
550
+ "method",
551
+ "transactionId",
552
+ "reference",
553
+ "notes",
554
+ "externalId",
555
+ "externalProvider",
556
+ "backendId",
557
+ "backendTxRef",
558
+ "nativeAmount",
559
+ "nativeCurrency",
560
+ "usdAtQuote",
561
+ "usdAtConfirmation"
562
+ ]
563
+ },
564
+ mcp: { include: ["list", "get"] },
565
+ cli: { skipApiCheck: true }
566
+ })], Payment);
567
+ //#endregion
568
+ //#region src/collections/PaymentCollection.ts
569
+ var PaymentCollection_exports = /* @__PURE__ */ __exportAll({ PaymentCollection: () => PaymentCollection });
570
+ var PaymentCollection = class extends SmrtCollection {
571
+ static _itemClass = Payment;
572
+ /**
573
+ * Find payments by contract
574
+ *
575
+ * @param contractId - Contract ID
576
+ * @returns Array of payments
577
+ */
578
+ async findByContract(contractId) {
579
+ return await this.list({
580
+ where: { contractId },
581
+ orderBy: "created_at DESC"
582
+ });
583
+ }
584
+ /**
585
+ * Find payments by customer
586
+ *
587
+ * @param customerId - Customer ID
588
+ * @returns Array of payments
589
+ */
590
+ async findByCustomer(customerId) {
591
+ return await this.list({
592
+ where: { customerId },
593
+ orderBy: "created_at DESC"
594
+ });
595
+ }
596
+ /**
597
+ * Find payments by status
598
+ *
599
+ * @param status - Payment status
600
+ * @returns Array of payments
601
+ */
602
+ async findByStatus(status) {
603
+ return await this.list({
604
+ where: { status },
605
+ orderBy: "created_at DESC"
606
+ });
607
+ }
608
+ /**
609
+ * Find payments by method
610
+ *
611
+ * @param method - Payment method
612
+ * @returns Array of payments
613
+ */
614
+ async findByMethod(method) {
615
+ return await this.list({
616
+ where: { method },
617
+ orderBy: "created_at DESC"
618
+ });
619
+ }
620
+ /**
621
+ * Find all pending payments
622
+ *
623
+ * @returns Array of pending payments
624
+ */
625
+ async findPending() {
626
+ return await this.findByStatus(PaymentStatus.PENDING);
627
+ }
628
+ /**
629
+ * Find all completed payments
630
+ *
631
+ * @returns Array of completed payments
632
+ */
633
+ async findCompleted() {
634
+ return await this.findByStatus(PaymentStatus.COMPLETED);
635
+ }
636
+ /**
637
+ * Find payment by transaction ID
638
+ *
639
+ * @param transactionId - External transaction ID
640
+ * @returns Payment or null
641
+ */
642
+ async findByTransactionId(transactionId) {
643
+ return (await this.list({
644
+ where: { transactionId },
645
+ limit: 1
646
+ }))[0] || null;
647
+ }
648
+ /**
649
+ * Find payments in date range
650
+ *
651
+ * @param startDate - Start date
652
+ * @param endDate - End date
653
+ * @returns Array of payments
654
+ */
655
+ async findByDateRange(startDate, endDate) {
656
+ return await this.list({
657
+ where: {
658
+ "paidAt >=": startDate.toISOString(),
659
+ "paidAt <=": endDate.toISOString()
660
+ },
661
+ orderBy: "paidAt DESC"
662
+ });
663
+ }
664
+ /**
665
+ * Calculate total payments for a contract
666
+ *
667
+ * @param contractId - Contract ID
668
+ * @returns Total amount paid
669
+ */
670
+ async getTotalForContract(contractId) {
671
+ return (await this.list({ where: {
672
+ contractId,
673
+ status: PaymentStatus.COMPLETED
674
+ } })).reduce((sum, payment) => sum + payment.amount, 0);
675
+ }
676
+ /**
677
+ * Find payments linked to a ledger journal
678
+ *
679
+ * @param journalId - Journal ID from smrt-ledgers
680
+ * @returns Payment or null
681
+ */
682
+ async findByJournal(journalId) {
683
+ return (await this.list({
684
+ where: { journalId },
685
+ limit: 1
686
+ }))[0] || null;
687
+ }
688
+ /**
689
+ * Find all payments belonging to a specific tenant
690
+ *
691
+ * @param tenantId - Tenant ID
692
+ * @returns Array of payments for the tenant
693
+ */
694
+ async findByTenant(tenantId) {
695
+ return this.list({ where: { tenantId } });
696
+ }
697
+ /**
698
+ * Find all global payments (not associated with any tenant).
699
+ *
700
+ * Routes through the shared tenant-global helper so it does not throw under
701
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
702
+ * flagged as an isolation violation). (#1600)
703
+ *
704
+ * @returns Array of global payments
705
+ */
706
+ async findGlobal() {
707
+ return queryGlobal(this);
708
+ }
709
+ /**
710
+ * Find payments for a tenant including global payments.
711
+ *
712
+ * Fails closed if an active tenant context requests a different tenant's
713
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
714
+ *
715
+ * @param tenantId - Tenant ID
716
+ * @returns Array of tenant-specific and global payments
717
+ */
718
+ async findWithGlobals(tenantId) {
719
+ return queryWithGlobals(this, tenantId, "Payment.findWithGlobals");
720
+ }
721
+ };
722
+ //#endregion
723
+ //#region src/historical-funds-internal.ts
724
+ async function exists(db) {
725
+ return isPostgresDatabase(db) ? (await db.query("SELECT to_regclass(?) AS relation", ["historical_customer_funds_allocations"])).rows[0]?.relation != null : db.tableExists("historical_customer_funds_allocations");
726
+ }
727
+ async function historicalInvoiceAllocationTotal(db, invoiceId, effect) {
728
+ if (effect === "refund") return historicalCreditRefundCommittedTotal(db, invoiceId);
729
+ if (!await exists(db)) return historicalCreditRefundCommittedTotal(db, invoiceId);
730
+ const tenantId = getTenantId();
731
+ if (!tenantId) {
732
+ if ((await db.query("SELECT id FROM historical_customer_funds_allocations WHERE invoice_id=? LIMIT 1", invoiceId)).rows.length) throw new Error("Historical allocation capacity requires the explicit active tenant");
733
+ return historicalCreditRefundCommittedTotal(db, invoiceId);
734
+ }
735
+ const rows = await (await HistoricalCustomerFundsAllocationCollection.create({ db })).list({
736
+ where: {
737
+ tenantId,
738
+ invoiceId,
739
+ effect: "payment"
740
+ },
741
+ limit: 100001
742
+ });
743
+ if (rows.length > 1e5) throw new Error("Historical allocation capacity exceeds the bounded source scope");
744
+ const total = rows.filter((row) => row.effect === "payment").reduce((sum, row) => sum + BigInt(row.amountMinor), 0n);
745
+ if (total > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Historical allocation capacity is unsafe");
746
+ const combined = total + BigInt(effect === "payment" ? 0 : await historicalCreditRefundCommittedTotal(db, invoiceId));
747
+ if (combined > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Historical combined capacity is unsafe");
748
+ return Number(combined);
749
+ }
750
+ async function historicalCreditRefundCommittedTotal(db, creditNoteId) {
751
+ if (!(isPostgresDatabase(db) ? (await db.query("SELECT to_regclass(?) AS relation", ["historical_customer_funds"])).rows[0]?.relation != null : await db.tableExists("historical_customer_funds"))) return 0;
752
+ const tenantId = getTenantId();
753
+ if (!tenantId) {
754
+ if ((await db.query("SELECT id FROM historical_customer_funds WHERE source_credit_note_id=? LIMIT 1", creditNoteId)).rows.length) throw new Error("Historical refund capacity requires the explicit active tenant");
755
+ return 0;
756
+ }
757
+ const { HistoricalCustomerFundsCollection } = await import("./HistoricalCustomerFundsCollections-B5x8gTW3.js").then((n) => n.r);
758
+ const rows = await (await HistoricalCustomerFundsCollection.create({ db })).list({
759
+ where: {
760
+ tenantId,
761
+ sourceCreditNoteId: creditNoteId,
762
+ kind: "refund"
763
+ },
764
+ limit: 100001
765
+ });
766
+ if (rows.length > 1e5) throw new Error("Historical credit refund source scope exceeds its bounded limit");
767
+ const total = rows.reduce((sum, row) => sum + BigInt(row.amountMinor), 0n);
768
+ if (total > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Historical refund source total is unsafe");
769
+ return Number(total);
770
+ }
771
+ //#endregion
772
+ //#region src/models/PaymentAllocation.ts
773
+ var __defProp = Object.defineProperty;
774
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
775
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
776
+ enumerable: true,
777
+ configurable: true,
778
+ writable: true,
779
+ value
780
+ }) : obj[key] = value;
781
+ var __decorateClass = (decorators, target, key, kind) => {
782
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
783
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
784
+ if (kind && result) __defProp(target, key, result);
785
+ return result;
786
+ };
787
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
788
+ var PaymentAllocation = class extends SmrtObject {
789
+ tenantId = null;
790
+ paymentId = "";
791
+ invoiceId = "";
792
+ amount = 0;
793
+ allocatedAt = /* @__PURE__ */ new Date();
794
+ /**
795
+ * User/agent who made the allocation
796
+ */
797
+ allocatedBy = "";
798
+ notes = "";
799
+ constructor(options = {}) {
800
+ super(options);
801
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
802
+ if (options.paymentId !== void 0) this.paymentId = options.paymentId;
803
+ if (options.invoiceId !== void 0) this.invoiceId = options.invoiceId;
804
+ if (options.amount !== void 0) this.amount = options.amount;
805
+ if (options.allocatedAt !== void 0) this.allocatedAt = options.allocatedAt;
806
+ if (options.allocatedBy !== void 0) this.allocatedBy = options.allocatedBy;
807
+ if (options.notes !== void 0) this.notes = options.notes;
808
+ }
809
+ /** Save and validate against both claimed parent rows in one transaction. */
810
+ async save() {
811
+ assertIntegerMinorUnits("PaymentAllocation", this.id ?? "<new>", [["amount", this.amount]]);
812
+ if (this.amount <= 0) throw new Error("PaymentAllocation amount must be a positive number");
813
+ if (!this.paymentId || !this.invoiceId) throw new Error("PaymentAllocation requires payment and invoice");
814
+ return this.withTransaction(async () => {
815
+ const { PaymentCollection } = await Promise.resolve().then(() => PaymentCollection_exports);
816
+ const { InvoiceCollection } = await import("./InvoiceCollection-BolwughC.js").then((n) => n.n);
817
+ const { PaymentAllocationCollection } = await Promise.resolve().then(() => PaymentAllocationCollection_exports);
818
+ const payments = await PaymentCollection.create({ db: this.db });
819
+ const invoices = await InvoiceCollection.create({ db: this.db });
820
+ const allocations = await PaymentAllocationCollection.create({ db: this.db });
821
+ const prior = this.id ? await allocations.get({ id: this.id }, { cache: false }) : null;
822
+ if (prior && (prior.paymentId !== this.paymentId || prior.invoiceId !== this.invoiceId || prior.tenantId !== allocationOwner(this.tenantId))) throw new Error("PaymentAllocation parent identity is immutable");
823
+ const payment = await payments.get({ id: this.paymentId }, { cache: false });
824
+ if (!payment) throw new Error("PaymentAllocation referenced Payment does not exist");
825
+ if (payment.retentionAccountingBindingJson || payment.ordinaryAccountingBindingJson) throw new Error("Provider-recorded refund allocation is immutable");
826
+ if (customerFundsState(payment.customerFundsJson)) assertCustomerFundsWrite(this);
827
+ if (!payment.updated_at) throw new Error("Allocation parent has no persisted revision");
828
+ await payment.claimRevision(payment.updated_at);
829
+ const invoice = await invoices.get({ id: this.invoiceId }, { cache: false });
830
+ if (!invoice) throw new Error("PaymentAllocation referenced Invoice does not exist");
831
+ if (!invoice.updated_at) throw new Error("Allocation parent has no persisted revision");
832
+ await invoice.claimRevision(invoice.updated_at);
833
+ const owner = allocationOwner(this.tenantId);
834
+ if (payment.tenantId !== owner || invoice.tenantId !== owner || (payment.customerId || null) !== (invoice.customerId || null) || payment.currency !== invoice.currency) throw new Error("PaymentAllocation requires matching tenant, customer and currency");
835
+ if (payment.kind === "refund" && invoice.kind !== "credit_note" || payment.kind === "payment" && invoice.kind !== "invoice") throw new Error("PaymentAllocation requires payments on invoices and refunds on credit notes");
836
+ const paymentTotal = allocationTotal(await allocations.findByPayment(this.paymentId), this.id) + BigInt(this.amount);
837
+ const invoiceTotal = allocationTotal(await allocations.findByInvoice(this.invoiceId), this.id) + BigInt(this.amount) + BigInt(await historicalInvoiceAllocationTotal(this.db, this.invoiceId));
838
+ if (paymentTotal > BigInt(payment.amount)) throw new Error("PaymentAllocation would over-apply payment");
839
+ const invoiceCap = invoice.kind === "invoice" ? invoice.getPayableFacts().allocationCapMinor : invoice.totalAmount;
840
+ if (invoiceTotal > BigInt(Math.max(0, invoiceCap))) throw new Error("PaymentAllocation would over-pay invoice");
841
+ if (payment.kind === "refund") {
842
+ if (payment.status !== "completed") throw new Error("A refund must be settled before it is allocated");
843
+ if (!invoice.creditedInvoiceId) throw new Error("Refund credit note has no credited invoice");
844
+ const source = await invoices.get({ id: invoice.creditedInvoiceId }, { cache: false });
845
+ if (!source || source.tenantId !== owner) throw new Error("Refund source invoice does not exist in its tenant");
846
+ if (!source.updated_at) throw new Error("Refund source invoice has no persisted revision");
847
+ await source.claimRevision(source.updated_at);
848
+ const creditRows = await invoices.list({
849
+ where: {
850
+ tenantId: owner,
851
+ creditedInvoiceId: source.id
852
+ },
853
+ limit: 100001
854
+ });
855
+ if (creditRows.length > 1e5) throw new Error("Refund credit scope exceeds its bounded source limit");
856
+ const credits = creditRows.filter((credit) => credit.kind === "credit_note" && credit.status !== "draft" && credit.status !== "cancelled");
857
+ const issuedCredits = credits.reduce((sum, credit) => sum + credit.totalAmount, 0);
858
+ let settledRefunds = 0;
859
+ for (const credit of credits) {
860
+ if (!credit.id) continue;
861
+ settledRefunds += await historicalCreditRefundCommittedTotal(this.db, credit.id);
862
+ for (const allocation of await allocations.findByInvoice(credit.id)) {
863
+ if (allocation.id === this.id) continue;
864
+ const refund = await payments.get({ id: allocation.paymentId }, { cache: false });
865
+ if (refund?.kind === "refund" && refund.status === "completed") settledRefunds += allocation.amount;
866
+ }
867
+ }
868
+ const availableRefund = calculateAvailableRefund({
869
+ sourceTotalMinor: source.totalAmount,
870
+ sourcePaidMinor: source.amountPaid,
871
+ issuedCreditMinor: issuedCredits,
872
+ settledRefundMinor: settledRefunds
873
+ });
874
+ if (this.amount > availableRefund) throw new Error(`Refund allocation exceeds available unapplied credit (${availableRefund})`);
875
+ }
876
+ return await super.save();
877
+ });
878
+ }
879
+ /** Managed application reversal must also post its inverse journal and retain evidence. */
880
+ async delete(options = {}) {
881
+ return this.withTransaction(async () => {
882
+ const persisted = this.id ? await this.db.get(this.tableName, { id: this.id }) : null;
883
+ const { PaymentCollection } = await Promise.resolve().then(() => PaymentCollection_exports);
884
+ const payment = await (await PaymentCollection.create({ db: this.db })).get({ id: String(persisted?.payment_id ?? this.paymentId) }, { cache: false });
885
+ if (payment) {
886
+ if (!payment.updated_at) throw new Error("Allocation parent has no persisted revision");
887
+ await payment.claimRevision(payment.updated_at);
888
+ }
889
+ if (payment?.retentionAccountingBindingJson || payment?.ordinaryAccountingBindingJson) throw new Error("Provider-recorded refund allocation cannot be deleted");
890
+ if (payment?.customerFundsJson) assertCustomerFundsWrite(this);
891
+ return super.delete(options);
892
+ });
893
+ }
894
+ };
895
+ __publicField(PaymentAllocation, "__smrtPackage__", "@happyvertical/smrt-commerce");
896
+ __decorateClass([tenantId({ nullable: true })], PaymentAllocation.prototype, "tenantId", 2);
897
+ __decorateClass([field({ description: "The payment being applied." }), foreignKey("Payment")], PaymentAllocation.prototype, "paymentId", 2);
898
+ __decorateClass([field({ description: "The invoice it settles." }), foreignKey("Invoice")], PaymentAllocation.prototype, "invoiceId", 2);
899
+ __decorateClass([field({
900
+ description: "How much of the payment goes to this invoice.",
901
+ ui: { format: "money" }
902
+ })], PaymentAllocation.prototype, "amount", 2);
903
+ __decorateClass([field({ description: "When it was applied." })], PaymentAllocation.prototype, "allocatedAt", 2);
904
+ __decorateClass([field({ description: "Anything worth remembering about this allocation." })], PaymentAllocation.prototype, "notes", 2);
905
+ PaymentAllocation = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
906
+ api: { include: ["list", "get"] },
907
+ mcp: { include: ["list", "get"] },
908
+ cli: { include: ["list", "get"] }
909
+ })], PaymentAllocation);
910
+ //#endregion
911
+ //#region src/collections/PaymentAllocationCollection.ts
912
+ var PaymentAllocationCollection_exports = /* @__PURE__ */ __exportAll({ PaymentAllocationCollection: () => PaymentAllocationCollection });
913
+ var PaymentAllocationCollection = class PaymentAllocationCollection extends SmrtCollection {
914
+ static _itemClass = PaymentAllocation;
915
+ /**
916
+ * Apply a payment to several invoices atomically. Callers supply stable UUID
917
+ * allocation ids: exact replays reuse rows; reusing an id with other money
918
+ * or parents fails. Actor authorization remains the application's job.
919
+ */
920
+ async allocatePayment(input) {
921
+ if (!input.allocations.length || new Set(input.allocations.map((item) => item.id)).size !== input.allocations.length) throw new Error("Allocation batch requires distinct stable allocation ids");
922
+ const { PaymentCollection } = await Promise.resolve().then(() => PaymentCollection_exports);
923
+ const { InvoiceCollection } = await import("./InvoiceCollection-BolwughC.js").then((n) => n.n);
924
+ const { InvoiceStatus } = await import("./types-Bbm0Xflc.js").then((n) => n.m);
925
+ const payment = await (await PaymentCollection.create({ db: this.db })).get({ id: input.paymentId }, { cache: false });
926
+ if (!payment) throw new Error("Payment does not exist");
927
+ const committed = await payment.withTransaction(async () => {
928
+ if (!payment.updated_at) throw new Error("Allocation parent has no persisted revision");
929
+ await payment.claimRevision(payment.updated_at);
930
+ const allocations = await PaymentAllocationCollection.create({ db: payment.db });
931
+ const invoices = await InvoiceCollection.create({ db: payment.db });
932
+ const result = [];
933
+ for (const item of [...input.allocations].sort((a, b) => a.invoiceId.localeCompare(b.invoiceId) || a.id.localeCompare(b.id))) {
934
+ if (!item.id) throw new Error("Allocation requires a stable id");
935
+ const existing = await allocations.get({ id: item.id }, { cache: false });
936
+ if (existing) {
937
+ if (existing.paymentId !== input.paymentId || existing.invoiceId !== item.invoiceId || existing.amount !== item.amount || existing.tenantId !== payment.tenantId) throw new Error("Allocation replay does not match existing allocation");
938
+ result.push(existing);
939
+ } else result.push(await allocations.create({
940
+ ...item,
941
+ paymentId: input.paymentId,
942
+ tenantId: payment.tenantId,
943
+ allocatedBy: input.allocatedBy ?? "",
944
+ _insertOnly: true
945
+ }));
946
+ }
947
+ for (const invoiceId of [...new Set(input.allocations.map((item) => item.invoiceId))].sort()) {
948
+ const invoice = await invoices.get({ id: invoiceId }, { cache: false });
949
+ if (!invoice) throw new Error("Invoice does not exist");
950
+ if (invoice.status !== InvoiceStatus.WRITTEN_OFF) invoice.updatePaymentStatus(await allocations.getTotalAllocatedToInvoice(invoiceId));
951
+ await invoice.save();
952
+ }
953
+ return result;
954
+ });
955
+ return Promise.all(committed.map(async ({ id }) => {
956
+ const allocation = await this.get({ id: String(id) }, { cache: false });
957
+ if (!allocation) throw new Error("Committed allocation no longer exists");
958
+ return allocation;
959
+ }));
960
+ }
961
+ /** Remove one allocation atomically and rederive its invoice/credit status. */
962
+ async removeAllocation(id) {
963
+ const allocation = await this.get({ id }, { cache: false });
964
+ if (!allocation) return;
965
+ const { PaymentCollection } = await Promise.resolve().then(() => PaymentCollection_exports);
966
+ const { InvoiceCollection } = await import("./InvoiceCollection-BolwughC.js").then((n) => n.n);
967
+ const payment = await (await PaymentCollection.create({ db: this.db })).get({ id: allocation.paymentId }, { cache: false });
968
+ if (!payment) throw new Error("Allocation payment does not exist");
969
+ await payment.withTransaction(async () => {
970
+ const rows = await PaymentAllocationCollection.create({ db: payment.db });
971
+ const current = await rows.get({ id }, { cache: false });
972
+ if (!current) return;
973
+ const invoice = await (await InvoiceCollection.create({ db: payment.db })).get({ id: current.invoiceId }, { cache: false });
974
+ if (!invoice) throw new Error("Allocation invoice does not exist");
975
+ await current.delete();
976
+ invoice.updatePaymentStatus(await rows.getTotalAllocatedToInvoice(current.invoiceId));
977
+ await invoice.save();
978
+ });
979
+ }
980
+ /**
981
+ * Find allocations by payment
982
+ *
983
+ * @param paymentId - Payment ID
984
+ * @returns Array of allocations
985
+ */
986
+ async allFor(field, id) {
987
+ const rows = [];
988
+ for (let offset = 0;; offset += 1e3) {
989
+ const page = await this.list({
990
+ where: { [field]: id },
991
+ orderBy: ["allocatedAt DESC", "id ASC"],
992
+ offset,
993
+ limit: 1e3
994
+ });
995
+ rows.push(...page);
996
+ if (page.length < 1e3) return rows;
997
+ }
998
+ }
999
+ async findByPayment(paymentId) {
1000
+ return this.allFor("paymentId", paymentId);
1001
+ }
1002
+ /** Every committed row participates in payment and historical combined capacity. */
1003
+ async findByInvoice(invoiceId) {
1004
+ return this.allFor("invoiceId", invoiceId);
1005
+ }
1006
+ /**
1007
+ * Get total amount allocated to an invoice
1008
+ *
1009
+ * @param invoiceId - Invoice ID
1010
+ * @returns Total allocated amount
1011
+ */
1012
+ async getTotalAllocatedToInvoice(invoiceId) {
1013
+ const total = (await this.findByInvoice(invoiceId)).reduce((sum, alloc) => sum + BigInt(alloc.amount), BigInt(await historicalInvoiceAllocationTotal(this.db, invoiceId)));
1014
+ if (total > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Combined invoice allocation total is unsafe");
1015
+ return Number(total);
1016
+ }
1017
+ /**
1018
+ * Get total amount allocated from a payment
1019
+ *
1020
+ * @param paymentId - Payment ID
1021
+ * @returns Total allocated amount
1022
+ */
1023
+ async getTotalAllocatedFromPayment(paymentId) {
1024
+ return (await this.findByPayment(paymentId)).reduce((sum, alloc) => sum + alloc.amount, 0);
1025
+ }
1026
+ /**
1027
+ * Get remaining unallocated amount from a payment.
1028
+ *
1029
+ * @param paymentId - Payment ID
1030
+ * @param paymentAmount - Total payment amount
1031
+ * @returns Unallocated amount
1032
+ * @throws Error if payment is over-allocated (indicates data integrity issue)
1033
+ */
1034
+ async getUnallocatedFromPayment(paymentId, paymentAmount) {
1035
+ const allocated = await this.getTotalAllocatedFromPayment(paymentId);
1036
+ const { PaymentCollection } = await Promise.resolve().then(() => PaymentCollection_exports);
1037
+ const payment = await (await PaymentCollection.create({ db: this.db })).get({ id: paymentId }, { cache: false });
1038
+ const managed = customerFundsState(payment?.customerFundsJson);
1039
+ if (managed && (managed.kind !== "receipt" || payment?.amount !== paymentAmount)) throw new Error("Customer funds require their authoritative receipt amount");
1040
+ const refunded = managed?.operations.filter((op) => op.kind === "refund").reduce((sum, op) => sum + BigInt(op.amountMinor), 0n) ?? 0n;
1041
+ const remaining = Number(BigInt(paymentAmount) - BigInt(allocated) - refunded);
1042
+ if (remaining < 0) throw new Error(`Over-allocated payment detected for paymentId=${paymentId}: allocated amount (${allocated}) exceeds payment amount (${paymentAmount})`);
1043
+ return remaining;
1044
+ }
1045
+ /**
1046
+ * Find allocation by payment and invoice
1047
+ *
1048
+ * @param paymentId - Payment ID
1049
+ * @param invoiceId - Invoice ID
1050
+ * @returns Allocation or null
1051
+ */
1052
+ async findByPaymentAndInvoice(paymentId, invoiceId) {
1053
+ return (await this.list({
1054
+ where: {
1055
+ paymentId,
1056
+ invoiceId
1057
+ },
1058
+ limit: 1
1059
+ }))[0] || null;
1060
+ }
1061
+ /**
1062
+ * Find allocations by allocator
1063
+ *
1064
+ * @param allocatedBy - User/agent ID who made the allocation
1065
+ * @returns Array of allocations
1066
+ */
1067
+ async findByAllocator(allocatedBy) {
1068
+ return await this.list({
1069
+ where: { allocatedBy },
1070
+ orderBy: "allocatedAt DESC"
1071
+ });
1072
+ }
1073
+ /**
1074
+ * Find allocations in date range
1075
+ *
1076
+ * @param startDate - Start date
1077
+ * @param endDate - End date
1078
+ * @returns Array of allocations
1079
+ */
1080
+ async findByDateRange(startDate, endDate) {
1081
+ return await this.list({
1082
+ where: {
1083
+ "allocatedAt >=": startDate.toISOString(),
1084
+ "allocatedAt <=": endDate.toISOString()
1085
+ },
1086
+ orderBy: "allocatedAt DESC"
1087
+ });
1088
+ }
1089
+ /**
1090
+ * Find all payment allocations belonging to a specific tenant
1091
+ *
1092
+ * @param tenantId - Tenant ID
1093
+ * @returns Array of payment allocations for the tenant
1094
+ */
1095
+ async findByTenant(tenantId) {
1096
+ return this.list({ where: { tenantId } });
1097
+ }
1098
+ /**
1099
+ * Find all global payment allocations (not associated with any tenant).
1100
+ *
1101
+ * Routes through the shared tenant-global helper so it does not throw under
1102
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
1103
+ * flagged as an isolation violation). (#1600)
1104
+ *
1105
+ * @returns Array of global payment allocations
1106
+ */
1107
+ async findGlobal() {
1108
+ return queryGlobal(this);
1109
+ }
1110
+ /**
1111
+ * Find payment allocations for a tenant including global allocations.
1112
+ *
1113
+ * Fails closed if an active tenant context requests a different tenant's
1114
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
1115
+ *
1116
+ * @param tenantId - Tenant ID
1117
+ * @returns Array of tenant-specific and global payment allocations
1118
+ */
1119
+ async findWithGlobals(tenantId) {
1120
+ return queryWithGlobals(this, tenantId, "PaymentAllocation.findWithGlobals");
1121
+ }
1122
+ };
1123
+ //#endregion
1124
+ //#region src/accounting-snapshot.ts
1125
+ function requireText(value, label) {
1126
+ if (!value || value.trim() !== value) throw new Error(`${label} must be a nonempty trimmed string`);
1127
+ return value;
1128
+ }
1129
+ function requireSafeMinor(value, label) {
1130
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a nonnegative safe integer minor amount`);
1131
+ return value;
1132
+ }
1133
+ function requirePositiveQuantity(quantity, lineId) {
1134
+ if (!Number.isFinite(quantity) || quantity <= 0) throw new Error(`Line '${lineId}' quantity must be a positive finite number`);
1135
+ return quantity;
1136
+ }
1137
+ function percentToFraction(percent, componentId) {
1138
+ if (!/^(?:0|[1-9]\d{0,2})(?:\.\d{1,6})?$/.test(percent)) throw new Error(`Tax component '${componentId}' percent must be exact decimal text from 0 through 100`);
1139
+ const [whole, fraction = ""] = percent.split(".");
1140
+ const scale = 10n ** BigInt(fraction.length);
1141
+ const numerator = BigInt(whole) * scale + BigInt(fraction || "0");
1142
+ const denominator = 100n * scale;
1143
+ if (numerator > denominator) throw new Error(`Tax component '${componentId}' percent exceeds 100`);
1144
+ if (numerator === 0n) return "0";
1145
+ if (numerator === denominator) return "1";
1146
+ return `0.${numerator.toString().padStart(denominator.toString().length - 1, "0")}`.replace(/0+$/, "");
1147
+ }
1148
+ function globalTaxCalculation(lineAmountType) {
1149
+ switch (lineAmountType) {
1150
+ case "exclusive": return "TaxExcluded";
1151
+ case "inclusive": return "TaxInclusive";
1152
+ case "none": return "NotApplicable";
1153
+ }
1154
+ }
1155
+ function mapLine(line, currency, lineAmountType, mapping) {
1156
+ requireText(line.id, "Line ID");
1157
+ const lineMapping = mapping.lines[line.id];
1158
+ if (!lineMapping) throw new Error(`Missing explicit QuickBooks mapping for line '${line.id}'`);
1159
+ requireText(lineMapping.itemRef, `QuickBooks item reference for line '${line.id}'`);
1160
+ requireText(lineMapping.taxCodeRef, `QuickBooks tax-code reference for line '${line.id}'`);
1161
+ const amountMinor = requireSafeMinor(line.amountMinor, `Line '${line.id}' amount`);
1162
+ const taxMinor = requireSafeMinor(line.taxMinor, `Line '${line.id}' tax`);
1163
+ const componentIds = /* @__PURE__ */ new Set();
1164
+ let componentTotalMinor = 0;
1165
+ const taxComponents = line.taxComponents.map((component) => {
1166
+ if (component.compound !== void 0 && typeof component.compound !== "boolean") throw new MoneyValidationError("INVALID_MONEY_INPUT", "Frozen component compound flag must be boolean");
1167
+ if (component.compound && !lineMapping.taxComponents) throw new MoneyValidationError("ACCOUNTING_SYNC_UNSUPPORTED", "Compound accounting sync requires caller-verified ordered realm tax-code mapping");
1168
+ if (component.compound && component.taxableMinor === void 0) throw new MoneyValidationError("ACCOUNTING_SYNC_UNSUPPORTED", "Compound accounting sync requires the frozen component taxable basis");
1169
+ const basisMinor = component.taxableMinor === void 0 ? void 0 : requireSafeMinor(component.taxableMinor, `Line '${line.id}' component taxable basis`);
1170
+ if (basisMinor !== void 0 && basisMinor !== amountMinor + (component.compound ? componentTotalMinor : 0)) throw new MoneyValidationError("ACCOUNTING_SYNC_UNSUPPORTED", "Frozen component taxable basis does not match ordered source facts");
1171
+ requireText(component.taxComponentId, `Line '${line.id}' tax component ID`);
1172
+ if (componentIds.has(component.taxComponentId)) throw new Error(`Line '${line.id}' repeats tax component '${component.taxComponentId}'`);
1173
+ componentIds.add(component.taxComponentId);
1174
+ const taxAmountMinor = requireSafeMinor(component.taxMinor, `Line '${line.id}' component '${component.taxComponentId}' tax`);
1175
+ componentTotalMinor += taxAmountMinor;
1176
+ const componentMapping = mapping.components[component.taxComponentId];
1177
+ if (!componentMapping) throw new Error(`Missing explicit QuickBooks tax-rate reference for component '${component.taxComponentId}'`);
1178
+ requireText(componentMapping.quickbooksTaxRateRef, `QuickBooks tax-rate reference for component '${component.taxComponentId}'`);
1179
+ return {
1180
+ taxCode: component.taxComponentId,
1181
+ rate: percentToFraction(component.percent, component.taxComponentId),
1182
+ quickbooksTaxRateRef: componentMapping.quickbooksTaxRateRef,
1183
+ taxAmount: minorToMajorUnits(taxAmountMinor, currency),
1184
+ ...component.compound ? { compound: true } : {},
1185
+ ...basisMinor !== void 0 ? { taxableAmount: minorToMajorUnits(basisMinor, currency) } : {}
1186
+ };
1187
+ });
1188
+ if (lineMapping.taxComponents && (lineMapping.taxComponents.length !== taxComponents.length || lineMapping.taxComponents.some((part, index) => part.taxRateRef !== taxComponents[index].quickbooksTaxRateRef || typeof part.compound !== "boolean" || part.compound !== (taxComponents[index].compound ?? false)))) throw new MoneyValidationError("ACCOUNTING_SYNC_UNSUPPORTED", "Ordered realm tax-code mapping does not match frozen components");
1189
+ if (componentTotalMinor !== taxMinor) throw new Error(`Line '${line.id}' frozen component tax ${componentTotalMinor} does not equal line tax ${taxMinor}`);
1190
+ if (taxMinor > 0 && taxComponents.length === 0) throw new Error(`Line '${line.id}' tax requires frozen tax components`);
1191
+ const discountMinor = requireSafeMinor(line.discountMinor, `Line '${line.id}' discount`);
1192
+ const unitPriceMinor = requireSafeMinor(line.unitPriceMinor, `Line '${line.id}' unit price`);
1193
+ const result = {
1194
+ description: requireText(line.description, `Line '${line.id}' description`),
1195
+ sku: line.sku || void 0,
1196
+ quantity: requirePositiveQuantity(line.quantity, line.id),
1197
+ unitPrice: minorToMajorUnits(unitPriceMinor, currency),
1198
+ ...discountMinor === 0 ? {} : { discount: minorToMajorUnits(discountMinor, currency) },
1199
+ amount: minorToMajorUnits(amountMinor, currency),
1200
+ taxAmount: minorToMajorUnits(taxMinor, currency),
1201
+ taxComponents,
1202
+ quickbooksMapping: {
1203
+ itemRef: lineMapping.itemRef,
1204
+ taxCodeRef: lineMapping.taxCodeRef,
1205
+ ...lineMapping.taxComponents ? { taxComponents: lineMapping.taxComponents.map((part) => ({ ...part })) } : {}
1206
+ }
1207
+ };
1208
+ if (lineAmountType === "inclusive") return {
1209
+ ...result,
1210
+ taxInclusiveAmount: minorToMajorUnits(amountMinor + taxMinor, currency)
1211
+ };
1212
+ return result;
1213
+ }
1214
+ function mapFrozenAccountingSnapshot(snapshot, mapping) {
1215
+ const currency = requireText(snapshot.currency, "Currency");
1216
+ requireText(snapshot.id, "Document ID");
1217
+ requireText(snapshot.number, "Document number");
1218
+ requireText(snapshot.customerId, "Customer ID");
1219
+ if (!(snapshot.issueDate instanceof Date) || Number.isNaN(snapshot.issueDate.valueOf())) throw new Error("Issue date must be a valid Date");
1220
+ if (!Array.isArray(snapshot.lineItems) || snapshot.lineItems.length === 0) throw new Error("Frozen document requires at least one line item");
1221
+ const subtotalMinor = requireSafeMinor(snapshot.subtotalMinor, "Subtotal");
1222
+ const taxAmountMinor = requireSafeMinor(snapshot.taxAmountMinor, "Tax amount");
1223
+ const providerTaxMinor = requireSafeMinor(snapshot.providerTaxMinor, "Provider tax");
1224
+ const totalAmountMinor = requireSafeMinor(snapshot.totalAmountMinor, "Total");
1225
+ if (providerTaxMinor !== 0) throw new Error("Frozen provider tax cannot be mapped to QuickBooks without a stored component snapshot and explicit tax-rate reference");
1226
+ if (subtotalMinor + taxAmountMinor !== totalAmountMinor) throw new Error("Frozen subtotal plus tax must equal frozen total");
1227
+ const lineItems = snapshot.lineItems.map((line) => mapLine(line, currency, snapshot.lineAmountType, mapping));
1228
+ const linesSubtotalMinor = snapshot.lineItems.reduce((sum, line) => sum + line.amountMinor, 0);
1229
+ const linesTaxMinor = snapshot.lineItems.reduce((sum, line) => sum + line.taxMinor, 0);
1230
+ if (linesSubtotalMinor !== subtotalMinor) throw new Error("Frozen line net amounts do not equal frozen subtotal");
1231
+ if (linesTaxMinor !== taxAmountMinor) throw new Error("Frozen line tax amounts do not equal frozen tax total");
1232
+ const base = {
1233
+ id: snapshot.id,
1234
+ externalId: snapshot.externalId,
1235
+ customerId: snapshot.customerId,
1236
+ customerExternalId: snapshot.customerExternalId,
1237
+ issueDate: snapshot.issueDate,
1238
+ currency,
1239
+ subtotal: minorToMajorUnits(subtotalMinor, currency),
1240
+ taxAmount: minorToMajorUnits(taxAmountMinor, currency),
1241
+ totalAmount: minorToMajorUnits(totalAmountMinor, currency),
1242
+ lineItems,
1243
+ quickbooksMapping: { globalTaxCalculation: globalTaxCalculation(snapshot.lineAmountType) },
1244
+ reference: snapshot.reference,
1245
+ memo: snapshot.memo
1246
+ };
1247
+ if (snapshot.kind === "invoice") {
1248
+ if (!(snapshot.dueDate instanceof Date) || Number.isNaN(snapshot.dueDate.valueOf())) throw new Error("Invoice due date must be a valid Date");
1249
+ return {
1250
+ ...base,
1251
+ invoiceNumber: snapshot.number,
1252
+ dueDate: snapshot.dueDate
1253
+ };
1254
+ }
1255
+ const creditedInvoiceExternalId = requireText(snapshot.creditedInvoiceExternalId, "Credited invoice external ID");
1256
+ if (subtotalMinor === 0 || totalAmountMinor === 0) throw new Error("Credit memo amounts must be positive");
1257
+ return {
1258
+ ...base,
1259
+ creditMemoNumber: snapshot.number,
1260
+ creditedInvoiceExternalId
1261
+ };
1262
+ }
1263
+ //#endregion
1264
+ //#region src/invoice-accounting-sync.ts
1265
+ var invoice_accounting_sync_exports = /* @__PURE__ */ __exportAll({
1266
+ loadFrozenInvoiceAccountingSnapshot: () => loadFrozenInvoiceAccountingSnapshot,
1267
+ readFrozenAccountingSyncState: () => readFrozenAccountingSyncState,
1268
+ reconcileFrozenInvoiceAccountingSync: () => reconcileFrozenInvoiceAccountingSync,
1269
+ syncFrozenInvoiceToAccounting: () => syncFrozenInvoiceToAccounting,
1270
+ toFrozenInvoiceAccountingInput: () => toFrozenInvoiceAccountingInput
1271
+ });
1272
+ function requiredText(value, label) {
1273
+ if (!value || value.trim() !== value) throw new MoneyValidationError("INVALID_MONEY_INPUT", `${label} must be a nonempty trimmed string`);
1274
+ return value;
1275
+ }
1276
+ function assertPrincipalTenant(tenantId) {
1277
+ requiredText(tenantId, "Accounting tenant");
1278
+ if (getTenantId() !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Accounting sync requires its active principal tenant");
1279
+ }
1280
+ function unsupported(message) {
1281
+ throw new MoneyValidationError("ACCOUNTING_SYNC_UNSUPPORTED", message);
1282
+ }
1283
+ function parseSyncState(value) {
1284
+ if (!value) return null;
1285
+ try {
1286
+ const parsed = JSON.parse(value);
1287
+ if (!parsed || typeof parsed !== "object") throw new Error("not object");
1288
+ const state = parsed;
1289
+ const identity = state.identity;
1290
+ if (state.version !== 1 || ![
1291
+ "in_flight",
1292
+ "unknown",
1293
+ "rejected",
1294
+ "synced"
1295
+ ].includes(String(state.status)) || !identity || identity.provider !== "quickbooks" || typeof identity.realmId !== "string" || identity.environment !== "sandbox" && identity.environment !== "production" || typeof identity.invoiceId !== "string" || typeof identity.moneySnapshotId !== "string" || typeof identity.requestId !== "string" || typeof identity.payloadHash !== "string" || identity.documentKind !== void 0 && identity.documentKind !== "invoice" && identity.documentKind !== "creditMemo" || identity.creditedInvoiceExternalId !== void 0 && typeof identity.creditedInvoiceExternalId !== "string" || typeof state.attemptId !== "string" || typeof state.startedAt !== "string") throw new Error("invalid shape");
1296
+ return {
1297
+ ...state,
1298
+ identity: {
1299
+ ...identity,
1300
+ documentKind: identity.documentKind ?? "invoice"
1301
+ }
1302
+ };
1303
+ } catch {
1304
+ throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Stored accounting sync state is malformed and requires reconciliation");
1305
+ }
1306
+ }
1307
+ function sameIdentity(left, right) {
1308
+ return left.provider === right.provider && left.documentKind === right.documentKind && left.realmId === right.realmId && left.environment === right.environment && left.invoiceId === right.invoiceId && left.moneySnapshotId === right.moneySnapshotId && left.requestId === right.requestId && left.payloadHash === right.payloadHash && left.creditedInvoiceExternalId === right.creditedInvoiceExternalId;
1309
+ }
1310
+ function sameSyncScope(identity, snapshot, mapping) {
1311
+ return identity.provider === "quickbooks" && identity.documentKind === snapshot.document.kind && identity.realmId === mapping.realmId && identity.environment === mapping.environment && identity.invoiceId === snapshot.document.id && identity.moneySnapshotId === snapshot.moneySnapshotId;
1312
+ }
1313
+ function sum(values) {
1314
+ let result = 0n;
1315
+ for (const value of values) {
1316
+ if (!Number.isSafeInteger(value)) throw new MoneyValidationError("UNSAFE_MONEY", "Frozen accounting facts require safe integer minor units");
1317
+ result += BigInt(value);
1318
+ }
1319
+ if (result > BigInt(Number.MAX_SAFE_INTEGER) || result < BigInt(Number.MIN_SAFE_INTEGER)) throw new MoneyValidationError("UNSAFE_MONEY", "Frozen accounting totals exceed safe integer minor units");
1320
+ return Number(result);
1321
+ }
1322
+ function componentTotals(lines) {
1323
+ const totals = /* @__PURE__ */ new Map();
1324
+ for (const line of lines) for (const component of line.taxComponents) totals.set(component.taxComponentId, sum([totals.get(component.taxComponentId) ?? 0, component.taxMinor]));
1325
+ return totals;
1326
+ }
1327
+ async function loadSnapshot(db, tenantId, invoice) {
1328
+ if (invoice.moneyAuthority === "historical") throw new MoneyValidationError("ACCOUNTING_SYNC_UNSUPPORTED", "Historical sales source money requires an explicit reviewed provider representation; no quantities or tax breakdown will be invented");
1329
+ if (invoice.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Accounting invoice cannot cross tenant boundaries");
1330
+ if (invoice.status === InvoiceStatus.DRAFT || !invoice.moneySnapshotId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Accounting sync requires an issued frozen invoice");
1331
+ if ((invoice.getPayableFacts().retainedRemainingMinor > 0 || invoice.retentionAccountingStateJson) && !hasRetentionAccountingAuthority(invoice.tenantId, invoice.id)) unsupported("Unreleased retention requires a provider payable representation");
1332
+ if (invoice.status === InvoiceStatus.CANCELLED) unsupported("Cancelled documents cannot be created by accounting sync");
1333
+ const lines = await (await InvoiceLineItemCollection.create({ db })).findByInvoice(requiredText(invoice.id ?? "", "Accounting invoice ID"));
1334
+ if (lines.length === 0) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Frozen accounting invoice requires at least one line");
1335
+ for (const line of lines) if (line.tenantId !== tenantId || line.moneySnapshotId !== invoice.moneySnapshotId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Accounting line snapshot does not match its frozen invoice");
1336
+ const taxes = await (await DocumentTaxCollection.create({ db })).findByDocument(tenantId, invoice.kind, invoice.id ?? "");
1337
+ const lineTax = sum(lines.map((line) => line.taxAmount));
1338
+ const storedTax = sum(taxes.map((row) => row.taxMinor));
1339
+ if (lineTax + invoice.providerTaxAmount !== invoice.taxAmount || storedTax !== lineTax) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Frozen line, DocumentTax, and invoice tax totals do not reconcile");
1340
+ const lineComponents = componentTotals(lines);
1341
+ const storedComponents = /* @__PURE__ */ new Map();
1342
+ for (const row of taxes) {
1343
+ if (!row.taxComponentId) continue;
1344
+ storedComponents.set(row.taxComponentId, sum([storedComponents.get(row.taxComponentId) ?? 0, row.taxMinor]));
1345
+ }
1346
+ if ([.../* @__PURE__ */ new Set([...lineComponents.keys(), ...storedComponents.keys()])].some((id) => (lineComponents.get(id) ?? 0) !== (storedComponents.get(id) ?? 0))) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Frozen line components do not reconcile to DocumentTax");
1347
+ let creditedInvoiceExternalId;
1348
+ let creditedInvoiceScope;
1349
+ if (invoice.kind === "credit_note") {
1350
+ if (!invoice.creditedInvoiceId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Frozen credit note requires its credited invoice");
1351
+ const source = await (await InvoiceCollection.create({ db })).get({ id: invoice.creditedInvoiceId }, { cache: false });
1352
+ if (!source || source.tenantId !== tenantId || !source.externalId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Frozen credit note requires the source invoice external ID");
1353
+ if (source.retentionAccountingStateJson && !hasRetentionAccountingAuthority(source.tenantId, source.id)) unsupported("Provider-retained credit requires the retained-first accounting adjustment command");
1354
+ if (source.ordinaryAccountingStateJson && !hasOrdinaryAccountingAuthority(source.tenantId, source.id)) unsupported("Provider ordinary credit requires its owning accounting adjustment command");
1355
+ const sourceState = parseSyncState(source.accountingSyncStateJson);
1356
+ if (sourceState?.status !== "synced" || sourceState.externalId !== source.externalId) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Frozen credit note requires a reconciled source invoice accounting identity");
1357
+ creditedInvoiceExternalId = source.externalId;
1358
+ creditedInvoiceScope = {
1359
+ provider: sourceState.identity.provider,
1360
+ realmId: sourceState.identity.realmId,
1361
+ environment: sourceState.identity.environment
1362
+ };
1363
+ }
1364
+ return {
1365
+ moneySnapshotId: invoice.moneySnapshotId,
1366
+ creditedInvoiceScope,
1367
+ document: {
1368
+ kind: invoice.kind === "invoice" ? "invoice" : "creditMemo",
1369
+ id: invoice.id ?? "",
1370
+ number: invoice.invoiceNumber,
1371
+ customerId: invoice.customerId,
1372
+ customerExternalId: invoice.customerExternalId || void 0,
1373
+ issueDate: invoice.issueDate,
1374
+ dueDate: invoice.dueDate ?? void 0,
1375
+ currency: invoice.currency,
1376
+ lineAmountType: invoice.lineAmountType,
1377
+ taxRoundingScope: invoice.taxRoundingScope,
1378
+ subtotalMinor: invoice.subtotal,
1379
+ taxAmountMinor: invoice.taxAmount,
1380
+ providerTaxMinor: invoice.providerTaxAmount,
1381
+ totalAmountMinor: invoice.totalAmount,
1382
+ lineItems: lines.map((line) => ({
1383
+ id: requiredText(line.id ?? "", "Accounting line ID"),
1384
+ description: line.description,
1385
+ sku: line.sku || void 0,
1386
+ quantity: line.quantity,
1387
+ unitPriceMinor: line.unitPrice,
1388
+ discountMinor: line.discount,
1389
+ amountMinor: invoice.lineAmountType === "inclusive" ? line.amount - line.taxAmount : line.amount,
1390
+ taxMinor: line.taxAmount,
1391
+ taxComponents: structuredClone(line.taxComponents)
1392
+ })),
1393
+ creditedInvoiceExternalId,
1394
+ externalId: invoice.externalId || void 0,
1395
+ reference: invoice.reference || void 0,
1396
+ memo: invoice.customerNotes || void 0
1397
+ }
1398
+ };
1399
+ }
1400
+ function validateMapping(mapping) {
1401
+ requiredText(mapping.realmId, "QuickBooks realm ID");
1402
+ if (mapping.environment !== "sandbox" && mapping.environment !== "production") throw new MoneyValidationError("INVALID_MONEY_INPUT", "QuickBooks environment must be sandbox or production");
1403
+ if (!mapping.lines || typeof mapping.lines !== "object") throw new MoneyValidationError("INVALID_MONEY_INPUT", "QuickBooks line mapping is required");
1404
+ if (!mapping.components || typeof mapping.components !== "object") throw new MoneyValidationError("INVALID_MONEY_INPUT", "QuickBooks component mapping is required");
1405
+ }
1406
+ function toSdkInput(snapshot, mapping) {
1407
+ validateMapping(mapping);
1408
+ if (snapshot.document.kind === "creditMemo" && (snapshot.creditedInvoiceScope?.provider !== "quickbooks" || snapshot.creditedInvoiceScope.realmId !== mapping.realmId || snapshot.creditedInvoiceScope.environment !== mapping.environment)) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Credit note and credited invoice must use the same accounting provider, realm, and environment");
1409
+ if (snapshot.document.lineItems.some((line) => line.quantity < 0 || line.amountMinor < 0 || line.unitPriceMinor < 0)) unsupported("Signed-line accounting sync requires a public accounting SDK signed-line representation");
1410
+ return mapFrozenAccountingSnapshot(snapshot.document, mapping);
1411
+ }
1412
+ async function loadFrozenInvoiceAccountingSnapshot(input) {
1413
+ assertPrincipalTenant(input.tenantId);
1414
+ return withDocumentMoneyTransaction(input.db, async (tx) => {
1415
+ await lockTenantMoneyRuntime(tx, input.tenantId);
1416
+ const { invoice } = await lockAndReloadInvoice(tx, input.invoiceId, input.tenantId);
1417
+ return loadSnapshot(tx, input.tenantId, invoice);
1418
+ });
1419
+ }
1420
+ async function toFrozenInvoiceAccountingInput(input) {
1421
+ return toSdkInput(await loadFrozenInvoiceAccountingSnapshot(input), input.mapping);
1422
+ }
1423
+ function prepareRequest(snapshot, mapping, sdkInput) {
1424
+ const identity = {
1425
+ requestId: requestIdFor(snapshot, mapping),
1426
+ realmId: mapping.realmId,
1427
+ environment: mapping.environment
1428
+ };
1429
+ if (snapshot.document.kind === "invoice") {
1430
+ if (!("invoiceNumber" in sdkInput)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Frozen invoice mapping produced the wrong accounting document kind");
1431
+ const request2 = prepareQuickBooksInvoiceRequest(sdkInput, identity);
1432
+ return {
1433
+ request: request2,
1434
+ accountingInput: {
1435
+ ...sdkInput,
1436
+ idempotencyKey: request2.requestId,
1437
+ quickbooksRequest: request2
1438
+ }
1439
+ };
1440
+ }
1441
+ if (!("creditMemoNumber" in sdkInput)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Frozen credit-note mapping produced the wrong accounting document kind");
1442
+ const request = prepareQuickBooksCreditMemoRequest(sdkInput, identity);
1443
+ return {
1444
+ request,
1445
+ accountingInput: {
1446
+ ...sdkInput,
1447
+ quickbooksCreditMemoRequest: request
1448
+ }
1449
+ };
1450
+ }
1451
+ function requestIdFor(snapshot, mapping) {
1452
+ return `smrt_${createHash("sha256").update(JSON.stringify([
1453
+ "quickbooks",
1454
+ mapping.realmId,
1455
+ mapping.environment,
1456
+ snapshot.document.id,
1457
+ snapshot.moneySnapshotId
1458
+ ])).digest("hex").slice(0, 40)}`;
1459
+ }
1460
+ async function prepareSync(input) {
1461
+ if (input.provider.type !== "quickbooks") unsupported(`Frozen accounting sync does not support provider '${input.provider.type}'`);
1462
+ return withDocumentMoneyTransaction(input.db, async (tx) => {
1463
+ await lockTenantMoneyRuntime(tx, input.tenantId);
1464
+ const { invoice } = await lockAndReloadInvoice(tx, input.invoiceId, input.tenantId);
1465
+ if (invoice.ordinaryAccountingStateJson && !hasOrdinaryAccountingAuthority(invoice.tenantId, invoice.id)) unsupported("Provider ordinary invoice requires its owning accounting command");
1466
+ const snapshot = await loadSnapshot(tx, input.tenantId, invoice);
1467
+ const sdkInput = toSdkInput(snapshot, input.mapping);
1468
+ const prior = parseSyncState(invoice.accountingSyncStateJson);
1469
+ if (prior?.status === "synced" && sameSyncScope(prior.identity, snapshot, input.mapping)) {
1470
+ if (!prior.externalId || !prior.syncedAt) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Completed accounting sync state is incomplete");
1471
+ return {
1472
+ kind: "unchanged",
1473
+ result: {
1474
+ status: "unchanged",
1475
+ externalId: prior.externalId,
1476
+ syncedAt: new Date(prior.syncedAt),
1477
+ moneySnapshotId: prior.identity.moneySnapshotId
1478
+ }
1479
+ };
1480
+ }
1481
+ if (prior?.status === "in_flight") throw new MoneyValidationError("ACCOUNTING_SYNC_IN_PROGRESS", "Another accounting sync owns this invoice");
1482
+ if (prior?.status === "unknown") throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "The prior accounting write outcome is unknown and must be reconciled");
1483
+ if (prior?.status === "synced" || invoice.externalId) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Invoice accounting identity changed across provider, realm, environment, or snapshot");
1484
+ if (snapshot.document.kind === "creditMemo") try {
1485
+ requireCreditMemoOperations(input.provider);
1486
+ } catch (error) {
1487
+ unsupported(error instanceof Error ? error.message : String(error));
1488
+ }
1489
+ const { accountingInput, request } = prepareRequest(snapshot, input.mapping, sdkInput);
1490
+ const identity = {
1491
+ provider: "quickbooks",
1492
+ documentKind: snapshot.document.kind,
1493
+ realmId: request.realmId,
1494
+ environment: request.environment,
1495
+ invoiceId: snapshot.document.id,
1496
+ moneySnapshotId: snapshot.moneySnapshotId,
1497
+ requestId: request.requestId,
1498
+ payloadHash: request.payloadHash,
1499
+ creditedInvoiceExternalId: "creditedInvoiceExternalId" in request ? request.creditedInvoiceExternalId : void 0
1500
+ };
1501
+ if (prior && !sameIdentity(prior.identity, identity)) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Invoice accounting identity changed across provider, realm, environment, or snapshot");
1502
+ const attemptId = randomUUID();
1503
+ invoice.accountingSyncStateJson = JSON.stringify({
1504
+ version: 1,
1505
+ status: "in_flight",
1506
+ identity,
1507
+ attemptId,
1508
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
1509
+ });
1510
+ await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1511
+ return {
1512
+ kind: "push",
1513
+ documentKind: snapshot.document.kind,
1514
+ accountingInput,
1515
+ identity,
1516
+ attemptId
1517
+ };
1518
+ });
1519
+ }
1520
+ async function recordFailure(input, prepared, error) {
1521
+ const rejected = !!error && typeof error === "object" && error.code === "QBO_WRITE_ERROR" && error.outcome === "rejected";
1522
+ await withDocumentMoneyTransaction(input.db, async (tx) => {
1523
+ await lockTenantMoneyRuntime(tx, input.tenantId);
1524
+ const { invoice } = await lockAndReloadInvoice(tx, input.invoiceId, input.tenantId);
1525
+ const state = parseSyncState(invoice.accountingSyncStateJson);
1526
+ if (!state || state.attemptId !== prepared.attemptId || !sameIdentity(state.identity, prepared.identity)) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Accounting sync ownership changed after provider failure");
1527
+ invoice.accountingSyncStateJson = JSON.stringify({
1528
+ ...state,
1529
+ status: rejected ? "rejected" : "unknown"
1530
+ });
1531
+ await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1532
+ });
1533
+ }
1534
+ async function recordSuccess(input, prepared, result) {
1535
+ requiredText(result.externalId, "Accounting external ID");
1536
+ if (!(result.syncedAt instanceof Date) || Number.isNaN(result.syncedAt.valueOf())) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Accounting provider returned an invalid sync timestamp");
1537
+ return withDocumentMoneyTransaction(input.db, async (tx) => {
1538
+ await lockTenantMoneyRuntime(tx, input.tenantId);
1539
+ const { invoice } = await lockAndReloadInvoice(tx, input.invoiceId, input.tenantId);
1540
+ const state = parseSyncState(invoice.accountingSyncStateJson);
1541
+ if (state?.status !== "in_flight" || state.attemptId !== prepared.attemptId || !sameIdentity(state.identity, prepared.identity) || invoice.moneySnapshotId !== prepared.identity.moneySnapshotId) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Accounting sync ownership or frozen snapshot changed after provider write");
1542
+ const syncedAt = result.syncedAt.toISOString();
1543
+ invoice.externalId = result.externalId;
1544
+ invoice.externalProvider = prepared.identity.provider;
1545
+ invoice.syncedAt = result.syncedAt;
1546
+ invoice.accountingSyncStateJson = JSON.stringify({
1547
+ ...state,
1548
+ status: "synced",
1549
+ externalId: result.externalId,
1550
+ syncedAt
1551
+ });
1552
+ await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1553
+ return {
1554
+ status: "synced",
1555
+ externalId: result.externalId,
1556
+ syncedAt: result.syncedAt,
1557
+ moneySnapshotId: prepared.identity.moneySnapshotId
1558
+ };
1559
+ });
1560
+ }
1561
+ async function syncFrozenInvoiceToAccounting(input) {
1562
+ assertPrincipalTenant(input.tenantId);
1563
+ const prepared = await prepareSync(input);
1564
+ if (prepared.kind === "unchanged") return prepared.result;
1565
+ let result;
1566
+ try {
1567
+ if (prepared.documentKind === "invoice") {
1568
+ if (!("invoiceNumber" in prepared.accountingInput)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Prepared invoice has the wrong accounting document kind");
1569
+ result = await input.provider.invoices.push(prepared.accountingInput);
1570
+ } else {
1571
+ if (!("creditMemoNumber" in prepared.accountingInput)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Prepared credit note has the wrong accounting document kind");
1572
+ result = await requireCreditMemoOperations(input.provider).push(prepared.accountingInput);
1573
+ }
1574
+ } catch (error) {
1575
+ await recordFailure(input, prepared, error);
1576
+ throw error;
1577
+ }
1578
+ return recordSuccess(input, prepared, result);
1579
+ }
1580
+ async function reconcileFrozenInvoiceAccountingSync(input) {
1581
+ assertPrincipalTenant(input.tenantId);
1582
+ requiredText(input.requestId, "Accounting request ID");
1583
+ requiredText(input.attemptId, "Accounting attempt ID");
1584
+ return withDocumentMoneyTransaction(input.db, async (tx) => {
1585
+ await lockTenantMoneyRuntime(tx, input.tenantId);
1586
+ const { invoice } = await lockAndReloadInvoice(tx, input.invoiceId, input.tenantId);
1587
+ const snapshot = await loadSnapshot(tx, input.tenantId, invoice);
1588
+ const sdkInput = toSdkInput(snapshot, input.mapping);
1589
+ const { request } = prepareRequest(snapshot, input.mapping, sdkInput);
1590
+ const identity = {
1591
+ provider: "quickbooks",
1592
+ documentKind: snapshot.document.kind,
1593
+ realmId: request.realmId,
1594
+ environment: request.environment,
1595
+ invoiceId: snapshot.document.id,
1596
+ moneySnapshotId: snapshot.moneySnapshotId,
1597
+ requestId: request.requestId,
1598
+ payloadHash: request.payloadHash,
1599
+ creditedInvoiceExternalId: "creditedInvoiceExternalId" in request ? request.creditedInvoiceExternalId : void 0
1600
+ };
1601
+ const state = parseSyncState(invoice.accountingSyncStateJson);
1602
+ if (!state || state.status !== "unknown" && state.status !== "in_flight" || state.identity.requestId !== input.requestId || state.attemptId !== input.attemptId || !sameIdentity(state.identity, identity)) throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", "Accounting reconciliation does not match the unresolved frozen request");
1603
+ if (state.status === "in_flight" && input.confirmInterruptedOwnerStopped !== true) throw new MoneyValidationError("ACCOUNTING_SYNC_IN_PROGRESS", "Accounting reconciliation cannot release a live sync owner");
1604
+ if (input.outcome.status === "absent") {
1605
+ invoice.accountingSyncStateJson = JSON.stringify({
1606
+ ...state,
1607
+ status: "rejected"
1608
+ });
1609
+ await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1610
+ return {
1611
+ status: "ready",
1612
+ moneySnapshotId: identity.moneySnapshotId,
1613
+ requestId: identity.requestId
1614
+ };
1615
+ }
1616
+ const externalId = requiredText(input.outcome.externalId, "Accounting external ID");
1617
+ if (!(input.outcome.syncedAt instanceof Date) || Number.isNaN(input.outcome.syncedAt.valueOf())) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Accounting reconciliation requires a valid sync timestamp");
1618
+ const syncedAt = input.outcome.syncedAt.toISOString();
1619
+ invoice.externalId = externalId;
1620
+ invoice.externalProvider = identity.provider;
1621
+ invoice.syncedAt = input.outcome.syncedAt;
1622
+ invoice.accountingSyncStateJson = JSON.stringify({
1623
+ ...state,
1624
+ status: "synced",
1625
+ externalId,
1626
+ syncedAt
1627
+ });
1628
+ await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1629
+ return {
1630
+ status: "synced",
1631
+ externalId,
1632
+ syncedAt: input.outcome.syncedAt,
1633
+ moneySnapshotId: identity.moneySnapshotId
1634
+ };
1635
+ });
1636
+ }
1637
+ function readFrozenAccountingSyncState(invoice) {
1638
+ const state = parseSyncState(invoice.accountingSyncStateJson);
1639
+ if (!state) return null;
1640
+ return {
1641
+ status: state.status,
1642
+ provider: state.identity.provider,
1643
+ realmId: state.identity.realmId,
1644
+ environment: state.identity.environment,
1645
+ documentKind: state.identity.documentKind,
1646
+ moneySnapshotId: state.identity.moneySnapshotId,
1647
+ requestId: state.identity.requestId,
1648
+ attemptId: state.attemptId,
1649
+ startedAt: state.startedAt,
1650
+ request: {
1651
+ requestId: state.identity.requestId,
1652
+ realmId: state.identity.realmId,
1653
+ environment: state.identity.environment,
1654
+ payloadHash: state.identity.payloadHash,
1655
+ ...state.identity.documentKind === "creditMemo" ? { creditedInvoiceExternalId: requiredText(state.identity.creditedInvoiceExternalId ?? "", "Credited invoice external ID") } : {}
1656
+ }
1657
+ };
1658
+ }
1659
+ //#endregion
1660
+ //#region src/svelte/invoices/calculations.ts
1661
+ function numberDecimal(value, precision) {
1662
+ if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) throw new RangeError("Invoice rate or quantity is outside the supported range.");
1663
+ const [mantissa, exponent = "0"] = value.toString().split("e");
1664
+ const [whole, fraction = ""] = mantissa.split(".");
1665
+ const places = fraction.length - Number(exponent);
1666
+ if (places > precision) throw new RangeError("Invoice rate or quantity exceeds the supported decimal precision.");
1667
+ const coefficient = BigInt(whole + fraction);
1668
+ return places < 0 ? {
1669
+ coefficient: coefficient * 10n ** BigInt(-places),
1670
+ scale: 1n
1671
+ } : {
1672
+ coefficient,
1673
+ scale: 10n ** BigInt(places)
1674
+ };
1675
+ }
1676
+ function decimalText(value) {
1677
+ if (value.scale === 1n) return value.coefficient.toString();
1678
+ const places = value.scale.toString().length - 1;
1679
+ const digits = value.coefficient.toString().padStart(places + 1, "0");
1680
+ const split = digits.length - places;
1681
+ return `${digits.slice(0, split)}.${digits.slice(split)}`.replace(/0+$/, "").replace(/\.$/, "");
1682
+ }
1683
+ function rateTaxCode(percent) {
1684
+ const normalized = percent.replace(/^0+(?=\d)/, "").replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "") || "0";
1685
+ return normalized === "0" ? {
1686
+ id: "rate-zero",
1687
+ kind: "zero-rated",
1688
+ components: []
1689
+ } : {
1690
+ id: `rate-${normalized}`,
1691
+ kind: "taxable",
1692
+ components: [{
1693
+ taxComponentId: `rate-${normalized}`,
1694
+ percent: normalized,
1695
+ compound: false
1696
+ }]
1697
+ };
1698
+ }
1699
+ function calculateInvoiceMinorLine(input) {
1700
+ for (const amount of [input.unitPrice, input.discount]) if (!Number.isSafeInteger(amount)) throw new RangeError("Invoice money must be safe integer minor units.");
1701
+ if (input.taxRate > 1) throw new RangeError("Invoice tax rate is outside the supported range.");
1702
+ numberDecimal(input.quantity, 6);
1703
+ const tax = numberDecimal(input.taxRate, 8);
1704
+ const percent = decimalText({
1705
+ coefficient: tax.coefficient * 100n,
1706
+ scale: tax.scale
1707
+ });
1708
+ let result;
1709
+ try {
1710
+ result = calculateMoneyDocument({
1711
+ currency: "USD",
1712
+ lineAmountType: "exclusive",
1713
+ roundingScope: "line",
1714
+ lines: [{
1715
+ id: "line",
1716
+ quantity: input.quantity,
1717
+ unitPrice: input.unitPrice,
1718
+ discount: input.discount === 0 ? { kind: "none" } : {
1719
+ kind: "amount",
1720
+ minor: input.discount
1721
+ },
1722
+ taxCode: rateTaxCode(percent)
1723
+ }]
1724
+ }).lines[0];
1725
+ } catch (cause) {
1726
+ if (cause instanceof MoneyValidationError) throw new RangeError(cause.message, { cause });
1727
+ throw cause;
1728
+ }
1729
+ return {
1730
+ grossMinor: result.grossMinor,
1731
+ discountMinor: result.discountMinor,
1732
+ subtotalMinor: result.netMinor,
1733
+ taxMinor: result.taxMinor,
1734
+ totalMinor: result.netMinor + result.taxMinor
1735
+ };
1736
+ }
1737
+ //#endregion
1738
+ //#region src/accounting-steps-internal.ts
1739
+ function reject$2(message) {
1740
+ throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", message);
1741
+ }
1742
+ function text$2(value, label) {
1743
+ if (typeof value !== "string" || !value || value.trim() !== value) reject$2(`${label} is required`);
1744
+ }
1745
+ async function executeAccountingStep(engine, key, make) {
1746
+ const prepared = await engine.locked(async (invoice, state) => {
1747
+ if (!state) reject$2("Retention provider contract is missing");
1748
+ const expected = make(invoice, state), prior = state.steps.find((s) => s.key === key);
1749
+ if (prior && prior.request.payloadHash !== expected.payloadHash) reject$2("Retention operation identity reused with changed facts");
1750
+ if (prior?.status === "confirmed") {
1751
+ if (!prior.result) reject$2("Confirmed retention step has no readback");
1752
+ return { complete: prior.result };
1753
+ }
1754
+ if (prior && prior.status !== "rejected") reject$2("Retention operation has an unresolved remote owner");
1755
+ if (state.steps.some((s) => s.key !== key && s.status !== "confirmed")) reject$2("Another retention operation requires reconciliation");
1756
+ const step2 = {
1757
+ key,
1758
+ status: "in_flight",
1759
+ attemptId: randomUUID(),
1760
+ request: expected,
1761
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1762
+ priorAttempts: prior ? [...prior.priorAttempts ?? [], {
1763
+ attemptId: prior.attemptId,
1764
+ status: prior.status,
1765
+ startedAt: prior.startedAt,
1766
+ reconciliation: prior.reconciliation
1767
+ }] : []
1768
+ };
1769
+ if (prior) state.steps[state.steps.indexOf(prior)] = step2;
1770
+ else state.steps.push(step2);
1771
+ await engine.save(invoice, state);
1772
+ return { step: step2 };
1773
+ });
1774
+ if ("complete" in prepared && prepared.complete) return prepared.complete;
1775
+ if (!prepared.step) reject$2("Prepared retention step is missing");
1776
+ const step = prepared.step;
1777
+ let result;
1778
+ try {
1779
+ result = await engine.provider.execute(step.request);
1780
+ } catch (error) {
1781
+ await engine.locked(async (invoice, state) => {
1782
+ const current = state?.steps.find((s) => s.key === key);
1783
+ if (!state || current?.attemptId !== step.attemptId || current.status !== "in_flight") reject$2("Retention step ownership changed after remote failure");
1784
+ current.status = error?.outcome === "rejected" ? "rejected" : "unknown";
1785
+ await engine.save(invoice, state);
1786
+ });
1787
+ throw error;
1788
+ }
1789
+ return engine.locked(async (invoice, state) => {
1790
+ const current = state?.steps.find((s) => s.key === key);
1791
+ if (!state || current?.attemptId !== step.attemptId || current.status !== "in_flight" || result.payloadHash !== step.request.payloadHash) reject$2("Retention step ownership or readback changed after write");
1792
+ current.status = "confirmed";
1793
+ current.result = structuredClone(result);
1794
+ await engine.save(invoice, state);
1795
+ return result;
1796
+ });
1797
+ }
1798
+ async function reconcileAccountingStep(engine, input) {
1799
+ const step = await engine.locked(async (_invoice, state) => {
1800
+ const found = state?.steps.find((s) => s.key === input.stepKey);
1801
+ if (!found || found.request.requestId !== input.requestId || found.attemptId !== input.attemptId || !["in_flight", "unknown"].includes(found.status)) reject$2("Observed retention reconciliation owner differs");
1802
+ if (found.status === "in_flight" && !input.confirmInterruptedOwnerStopped) reject$2("Interrupted retention owner must be proven stopped");
1803
+ return structuredClone(found);
1804
+ });
1805
+ const result = input.outcome.status === "found" ? await engine.provider.read(step.request, input.outcome.externalId) : null;
1806
+ if (input.outcome.status === "absent") text$2(input.outcome.evidenceRef, "Authoritative provider absence evidence");
1807
+ return engine.locked(async (invoice, state) => {
1808
+ const current = state?.steps.find((s) => s.key === input.stepKey);
1809
+ if (!state || current?.attemptId !== step.attemptId || current.status !== step.status) reject$2("Retention reconciliation owner changed");
1810
+ current.reconciliation = {
1811
+ outcome: input.outcome.status,
1812
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
1813
+ interruptedOwnerStopped: input.confirmInterruptedOwnerStopped === true,
1814
+ ...input.outcome.status === "found" ? { externalId: input.outcome.externalId } : { evidenceRef: input.outcome.evidenceRef }
1815
+ };
1816
+ if (result) {
1817
+ if (result.payloadHash !== step.request.payloadHash) reject$2("Retention reconciliation readback differs");
1818
+ current.result = result;
1819
+ current.status = "confirmed";
1820
+ } else current.status = "rejected";
1821
+ await engine.save(invoice, state);
1822
+ return state;
1823
+ });
1824
+ }
1825
+ //#endregion
1826
+ //#region src/ordinary-accounting.ts
1827
+ var hash$1 = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
1828
+ function reject$1(message) {
1829
+ throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", message);
1830
+ }
1831
+ function text$1(value, label) {
1832
+ if (typeof value !== "string" || !value || value.trim() !== value) reject$1(`${label} is required`);
1833
+ }
1834
+ function date$1(value, label) {
1835
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) reject$1(`${label} is invalid`);
1836
+ }
1837
+ function principal$1(input) {
1838
+ if (!input.tenantId || getTenantId() !== input.tenantId) reject$1("Ordinary accounting requires its active tenant");
1839
+ if (isMoneyTransaction(input.db)) reject$1("Provider writes cannot run inside a caller-owned money transaction");
1840
+ if (input.provider.type !== "quickbooks" || !input.provider.receivableTransfers) reject$1("Provider lacks reviewed receivable operations");
1841
+ }
1842
+ function syncedIdentity(invoice, mapping) {
1843
+ const sync = readFrozenAccountingSyncState(invoice);
1844
+ if (!sync || sync.status !== "synced" || sync.documentKind !== "invoice" || sync.provider !== "quickbooks" || sync.moneySnapshotId !== invoice.moneySnapshotId || sync.realmId !== mapping.realmId || sync.environment !== mapping.environment || invoice.externalProvider !== "quickbooks" || !invoice.externalId) reject$1("Ordinary accounting requires its reconciled frozen invoice export identity");
1845
+ }
1846
+ function readOrdinaryInvoiceAccountingState(invoice) {
1847
+ if (!invoice.ordinaryAccountingStateJson) return null;
1848
+ let state;
1849
+ try {
1850
+ state = JSON.parse(invoice.ordinaryAccountingStateJson);
1851
+ } catch {
1852
+ return reject$1("Malformed ordinary accounting state");
1853
+ }
1854
+ if (!state || !state.mapping || state.version !== 1 || state.kind !== "ordinary" || state.mapping.kind !== "ordinary" || state.invoiceId !== invoice.id || state.moneySnapshotId !== invoice.moneySnapshotId || state.externalInvoiceId !== null && state.externalInvoiceId !== invoice.externalId || state.currency !== invoice.currency || state.grossMinor !== invoice.totalAmount || state.mapping.customerRef !== invoice.customerExternalId || !Array.isArray(state.steps) || !Array.isArray(state.credits) || !Array.isArray(state.refunds) || invoice.retentionStateJson || invoice.retentionAccountingStateJson || invoice.moneyAuthority !== "calculated") reject$1("Ordinary provider source identity changed");
1855
+ if (state.externalInvoiceId !== null) syncedIdentity(invoice, state.mapping);
1856
+ return state;
1857
+ }
1858
+ async function locked$1(input, operation) {
1859
+ return withDocumentMoneyTransaction(input.db, async (tx) => {
1860
+ await lockTenantMoneyRuntime(tx, input.tenantId);
1861
+ const { invoice } = await lockAndReloadInvoice(tx, input.invoiceId, input.tenantId);
1862
+ return operation(invoice, readOrdinaryInvoiceAccountingState(invoice));
1863
+ });
1864
+ }
1865
+ async function save$1(invoice, state) {
1866
+ invoice.ordinaryAccountingStateJson = JSON.stringify(state);
1867
+ await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
1868
+ }
1869
+ function refreshStatus$1(state) {
1870
+ state.status = state.steps.every((s) => s.status === "confirmed") && state.credits.every((c) => c.committed) && state.refunds.every((r) => r.committed) ? "ready" : "pending";
1871
+ }
1872
+ function requireReady$1(state) {
1873
+ if (!state || state.status !== "ready" || state.steps.some((s) => s.status !== "confirmed")) reject$1("Ordinary provider operations require reconciliation");
1874
+ return state;
1875
+ }
1876
+ function requestId$1(state, key) {
1877
+ return `smrt_ord_${hash$1([
1878
+ state.invoiceId,
1879
+ state.moneySnapshotId,
1880
+ state.mapping.realmId,
1881
+ state.mapping.environment,
1882
+ key
1883
+ ]).slice(0, 40)}`;
1884
+ }
1885
+ function executeStep$1(input, key, make) {
1886
+ return executeAccountingStep({
1887
+ locked: (operation) => locked$1(input, operation),
1888
+ save: save$1,
1889
+ provider: input.provider.receivableTransfers
1890
+ }, key, make);
1891
+ }
1892
+ function validateOrdinarySource(invoice, input) {
1893
+ if (invoice.kind !== "invoice" || !invoice.moneySnapshotId || invoice.retentionStateJson || invoice.retentionAccountingStateJson || invoice.moneyAuthority !== "calculated" || [
1894
+ InvoiceStatus.DRAFT,
1895
+ InvoiceStatus.CANCELLED,
1896
+ InvoiceStatus.WRITTEN_OFF
1897
+ ].includes(invoice.status) || invoice.collectionMethod || invoice.providerTaxAmount || invoice.currency !== input.mapping.currency || invoice.customerExternalId !== input.mapping.customerRef || input.invoiceMapping.realmId !== input.mapping.realmId || input.invoiceMapping.environment !== input.mapping.environment) reject$1("Ordinary accounting requires its issued, nonretained canonical frozen source");
1898
+ }
1899
+ async function syncOrdinaryInvoiceToAccounting(input) {
1900
+ principal$1(input);
1901
+ text$1(input.requestId, "Ordinary request");
1902
+ text$1(input.evidenceRef, "Ordinary evidence");
1903
+ if (input.mapping.kind !== "ordinary") reject$1("Ordinary mapping is required");
1904
+ await locked$1(input, async (invoice) => {
1905
+ validateOrdinarySource(invoice, input);
1906
+ await toFrozenInvoiceAccountingInput({
1907
+ db: invoice.db,
1908
+ tenantId: input.tenantId,
1909
+ invoiceId: input.invoiceId,
1910
+ mapping: input.invoiceMapping
1911
+ });
1912
+ });
1913
+ await input.provider.receivableTransfers.verifyMapping(input.mapping);
1914
+ await locked$1(input, async (invoice, state) => {
1915
+ validateOrdinarySource(invoice, input);
1916
+ const prior = readFrozenAccountingSyncState(invoice);
1917
+ if (prior) {
1918
+ const request = prepareQuickBooksInvoiceRequest({
1919
+ ...await toFrozenInvoiceAccountingInput({
1920
+ db: invoice.db,
1921
+ tenantId: input.tenantId,
1922
+ invoiceId: input.invoiceId,
1923
+ mapping: input.invoiceMapping
1924
+ }),
1925
+ externalId: void 0
1926
+ }, {
1927
+ requestId: prior.requestId,
1928
+ realmId: input.invoiceMapping.realmId,
1929
+ environment: input.invoiceMapping.environment
1930
+ });
1931
+ if (prior.provider !== "quickbooks" || prior.documentKind !== "invoice" || prior.moneySnapshotId !== invoice.moneySnapshotId || prior.realmId !== request.realmId || prior.environment !== request.environment || prior.request.payloadHash !== request.payloadHash) reject$1("Ordinary invoice mapping differs from its frozen export request");
1932
+ }
1933
+ if (state) {
1934
+ if (hash$1([
1935
+ state.requestId,
1936
+ state.evidenceRef,
1937
+ state.mapping,
1938
+ state.invoiceMapping
1939
+ ]) !== hash$1([
1940
+ input.requestId,
1941
+ input.evidenceRef,
1942
+ input.mapping,
1943
+ input.invoiceMapping
1944
+ ])) reject$1("Ordinary accounting initialization changed");
1945
+ return;
1946
+ }
1947
+ await save$1(invoice, {
1948
+ version: 1,
1949
+ kind: "ordinary",
1950
+ invoiceId: input.invoiceId,
1951
+ moneySnapshotId: invoice.moneySnapshotId,
1952
+ externalInvoiceId: null,
1953
+ invoiceMapping: structuredClone(input.invoiceMapping),
1954
+ requestId: input.requestId,
1955
+ evidenceRef: input.evidenceRef,
1956
+ currency: invoice.currency,
1957
+ grossMinor: invoice.totalAmount,
1958
+ mapping: structuredClone(input.mapping),
1959
+ status: "pending",
1960
+ steps: [],
1961
+ credits: [],
1962
+ refunds: []
1963
+ });
1964
+ });
1965
+ const result = await withOrdinaryAccountingAuthority(input.tenantId, input.invoiceId, () => syncFrozenInvoiceToAccounting({
1966
+ ...input,
1967
+ mapping: input.invoiceMapping
1968
+ }));
1969
+ return locked$1(input, async (invoice, state) => {
1970
+ if (!state) reject$1("Ordinary provider reservation disappeared");
1971
+ syncedIdentity(invoice, state.mapping);
1972
+ state.externalInvoiceId = result.externalId;
1973
+ refreshStatus$1(state);
1974
+ await save$1(invoice, state);
1975
+ return state;
1976
+ });
1977
+ }
1978
+ async function reconcileOrdinaryBaseInvoice(input) {
1979
+ principal$1(input);
1980
+ await locked$1(input, async (_invoice, state) => {
1981
+ if (!state || hash$1(state.invoiceMapping) !== hash$1(input.mapping)) reject$1("Ordinary base mapping differs");
1982
+ });
1983
+ return reconcileFrozenInvoiceAccountingSync(input);
1984
+ }
1985
+ async function reconcileOrdinaryAccountingStep(input) {
1986
+ principal$1(input);
1987
+ return reconcileAccountingStep({
1988
+ locked: (operation) => locked$1(input, operation),
1989
+ save: save$1,
1990
+ provider: input.provider.receivableTransfers
1991
+ }, input);
1992
+ }
1993
+ async function syncOrdinaryCreditToAccounting(input) {
1994
+ principal$1(input);
1995
+ text$1(input.requestId, "Credit adjustment request");
1996
+ text$1(input.evidenceRef, "Credit adjustment evidence");
1997
+ date$1(input.effectiveOn, "Credit adjustment date");
1998
+ const reserved = await locked$1(input, async (invoice, state) => {
1999
+ if (!state) reject$1("Ordinary provider contract is missing");
2000
+ const credit = await (await InvoiceCollection.create({ db: invoice.db })).get({ id: input.creditInvoiceId }, { cache: false });
2001
+ if (!credit || credit.tenantId !== input.tenantId || credit.customerId !== invoice.customerId || credit.creditedInvoiceId !== invoice.id || credit.kind !== "credit_note" || !credit.moneySnapshotId || [InvoiceStatus.DRAFT, InvoiceStatus.CANCELLED].includes(credit.status) || credit.currency !== state.currency || credit.customerExternalId !== state.mapping.customerRef) reject$1("Credit adjustment requires its issued same-customer source credit");
2002
+ if (input.creditMapping.realmId !== state.mapping.realmId || input.creditMapping.environment !== state.mapping.environment) reject$1("Credit adjustment provider scope differs");
2003
+ const prior = state.credits.find((c) => c.creditId === input.creditInvoiceId || c.requestId === input.requestId);
2004
+ if (prior) {
2005
+ if (prior.creditId !== input.creditInvoiceId || prior.requestId !== input.requestId || prior.moneySnapshotId !== credit.moneySnapshotId || prior.grossMinor !== credit.totalAmount || prior.evidenceRef !== input.evidenceRef || prior.effectiveOn !== input.effectiveOn || hash$1(prior.mapping) !== hash$1(input.creditMapping)) reject$1("Credit adjustment identity reused with changed facts");
2006
+ return structuredClone(prior);
2007
+ }
2008
+ requireReady$1(state);
2009
+ if (state.credits.reduce((sum, c) => sum + c.grossMinor, 0) + credit.totalAmount > invoice.creditedAmount) reject$1("Provider credit exceeds issued source credit capacity");
2010
+ const next = {
2011
+ creditId: input.creditInvoiceId,
2012
+ moneySnapshotId: credit.moneySnapshotId,
2013
+ requestId: input.requestId,
2014
+ evidenceRef: input.evidenceRef,
2015
+ effectiveOn: input.effectiveOn,
2016
+ grossMinor: credit.totalAmount,
2017
+ appliedMinor: credit.creditedAmount,
2018
+ mapping: structuredClone(input.creditMapping),
2019
+ committed: false
2020
+ };
2021
+ state.credits.push(next);
2022
+ state.status = "pending";
2023
+ await save$1(invoice, state);
2024
+ return next;
2025
+ });
2026
+ const creditResult = await withOrdinaryAccountingAuthority(input.tenantId, input.invoiceId, () => syncFrozenInvoiceToAccounting({
2027
+ db: input.db,
2028
+ tenantId: input.tenantId,
2029
+ invoiceId: input.creditInvoiceId,
2030
+ provider: input.provider,
2031
+ mapping: input.creditMapping
2032
+ }));
2033
+ const prefix = `credit:${input.requestId}`;
2034
+ const creditSource = (state) => ({
2035
+ type: "CreditMemo",
2036
+ id: creditResult.externalId,
2037
+ customerRef: state.mapping.customerRef,
2038
+ evidenceRef: reserved.moneySnapshotId
2039
+ });
2040
+ if (reserved.appliedMinor > 0) await executeStep$1(input, `${prefix}:due-application`, (invoice, state) => prepareQuickBooksReceivableRequest({
2041
+ kind: "application",
2042
+ mapping: state.mapping,
2043
+ source: creditSource(state),
2044
+ requestId: requestId$1(state, `${prefix}:due-application`),
2045
+ effectiveOn: reserved.effectiveOn,
2046
+ evidenceRef: reserved.evidenceRef,
2047
+ amountMinor: reserved.appliedMinor,
2048
+ debit: {
2049
+ type: "Invoice",
2050
+ id: invoice.externalId
2051
+ },
2052
+ credit: {
2053
+ type: "CreditMemo",
2054
+ id: creditResult.externalId
2055
+ }
2056
+ }));
2057
+ return locked$1(input, async (invoice, state) => {
2058
+ if (!state) reject$1("Ordinary provider state is missing");
2059
+ const credit = state.credits.find((c) => c.creditId === input.creditInvoiceId);
2060
+ if (!credit) reject$1("Credit reservation disappeared");
2061
+ credit.committed = true;
2062
+ refreshStatus$1(state);
2063
+ await save$1(invoice, state);
2064
+ return state;
2065
+ });
2066
+ }
2067
+ async function syncOrdinaryRefundToAccounting(input) {
2068
+ principal$1(input);
2069
+ text$1(input.requestId, "Refund recording request");
2070
+ text$1(input.evidenceRef, "Refund settlement evidence");
2071
+ date$1(input.effectiveOn, "Refund effective date");
2072
+ const reservation = await locked$1(input, async (invoice, state) => {
2073
+ if (!state) reject$1("Ordinary provider contract is missing");
2074
+ const credit = state.credits.find((c) => c.creditId === input.creditInvoiceId && c.committed);
2075
+ if (!credit) reject$1("Refund requires its reconciled provider credit");
2076
+ const payment = await (await PaymentCollection.create({ db: invoice.db })).get({ id: input.paymentId }, { cache: false });
2077
+ const creditInvoice = await (await InvoiceCollection.create({ db: invoice.db })).get({ id: input.creditInvoiceId }, { cache: false });
2078
+ if (!payment || !creditInvoice || payment.tenantId !== input.tenantId || creditInvoice.tenantId !== input.tenantId || creditInvoice.customerId !== invoice.customerId || creditInvoice.currency !== state.currency || creditInvoice.creditedInvoiceId !== invoice.id || payment.customerId !== invoice.customerId || payment.currency !== state.currency || payment.kind !== "refund" || payment.status !== "completed" || !payment.journalId || !payment.paidAt || new Date(payment.paidAt).toISOString().slice(0, 10) !== input.effectiveOn || !creditInvoice.externalId || creditInvoice.moneySnapshotId !== credit.moneySnapshotId) reject$1("Provider refund requires its completed same-customer local cash journal and source credit");
2079
+ const allocations = await (await PaymentAllocationCollection.create({ db: invoice.db })).findByPayment(input.paymentId);
2080
+ if (allocations.length !== 1 || allocations[0].invoiceId !== input.creditInvoiceId || allocations[0].amount !== payment.amount) reject$1("Refund recording requires one exact full source-credit allocation");
2081
+ const fingerprint = hash$1([
2082
+ payment.id,
2083
+ payment.customerId,
2084
+ payment.currency,
2085
+ payment.amount,
2086
+ payment.journalId,
2087
+ new Date(payment.paidAt).toISOString(),
2088
+ allocations[0].id,
2089
+ allocations[0].amount,
2090
+ credit.moneySnapshotId
2091
+ ]);
2092
+ const next = {
2093
+ paymentId: input.paymentId,
2094
+ creditId: input.creditInvoiceId,
2095
+ requestId: input.requestId,
2096
+ evidenceRef: input.evidenceRef,
2097
+ effectiveOn: input.effectiveOn,
2098
+ amountMinor: payment.amount,
2099
+ paymentFingerprint: fingerprint,
2100
+ committed: false
2101
+ };
2102
+ const prior = state.refunds.find((r) => r.paymentId === input.paymentId || r.requestId === input.requestId);
2103
+ if (prior) {
2104
+ if (hash$1({
2105
+ ...prior,
2106
+ committed: false
2107
+ }) !== hash$1(next)) reject$1("Refund recording identity reused with changed facts");
2108
+ return {
2109
+ ...prior,
2110
+ externalCreditId: creditInvoice.externalId
2111
+ };
2112
+ }
2113
+ requireReady$1(state);
2114
+ if (!state.mapping.cashAccountRef) reject$1("Refund provider bank account must be explicitly reviewed");
2115
+ if (state.refunds.filter((r) => r.creditId === input.creditInvoiceId).reduce((sum, r) => sum + r.amountMinor, 0) + payment.amount > credit.grossMinor - credit.appliedMinor) reject$1("Refund exceeds unapplied provider credit capacity");
2116
+ if (payment.ordinaryAccountingBindingJson || payment.retentionAccountingBindingJson) reject$1("Refund is already bound to a provider operation");
2117
+ payment.ordinaryAccountingBindingJson = JSON.stringify({
2118
+ invoiceId: input.invoiceId,
2119
+ tenantId: input.tenantId,
2120
+ requestId: input.requestId,
2121
+ paymentFingerprint: fingerprint
2122
+ });
2123
+ await withOrdinaryAccountingAuthority(input.tenantId, input.invoiceId, () => payment.save());
2124
+ state.refunds.push(next);
2125
+ state.status = "pending";
2126
+ await save$1(invoice, state);
2127
+ return {
2128
+ ...next,
2129
+ externalCreditId: creditInvoice.externalId
2130
+ };
2131
+ });
2132
+ const prefix = `refund:${input.requestId}`;
2133
+ const creditSource = (state) => ({
2134
+ type: "CreditMemo",
2135
+ id: reservation.externalCreditId,
2136
+ customerRef: state.mapping.customerRef,
2137
+ evidenceRef: reservation.paymentFingerprint
2138
+ });
2139
+ const journal = await executeStep$1(input, prefix, (_invoice, state) => prepareQuickBooksReceivableRequest({
2140
+ kind: "cash-refund",
2141
+ mapping: state.mapping,
2142
+ source: creditSource(state),
2143
+ requestId: requestId$1(state, prefix),
2144
+ effectiveOn: reservation.effectiveOn,
2145
+ evidenceRef: reservation.evidenceRef,
2146
+ amountMinor: reservation.amountMinor
2147
+ }));
2148
+ await executeStep$1(input, `${prefix}:application`, (_invoice, state) => prepareQuickBooksReceivableRequest({
2149
+ kind: "application",
2150
+ mapping: state.mapping,
2151
+ source: creditSource(state),
2152
+ requestId: requestId$1(state, `${prefix}:application`),
2153
+ effectiveOn: reservation.effectiveOn,
2154
+ evidenceRef: reservation.evidenceRef,
2155
+ amountMinor: reservation.amountMinor,
2156
+ debit: {
2157
+ type: "JournalEntry",
2158
+ id: journal.externalId
2159
+ },
2160
+ credit: {
2161
+ type: "CreditMemo",
2162
+ id: reservation.externalCreditId
2163
+ }
2164
+ }));
2165
+ return locked$1(input, async (invoice, state) => {
2166
+ if (!state) reject$1("Ordinary provider state disappeared");
2167
+ const refund = state.refunds.find((r) => r.requestId === input.requestId);
2168
+ if (!refund) reject$1("Refund reservation disappeared");
2169
+ refund.committed = true;
2170
+ refreshStatus$1(state);
2171
+ await save$1(invoice, state);
2172
+ return state;
2173
+ });
2174
+ }
2175
+ async function reconcileOrdinaryCreditAccounting(input) {
2176
+ principal$1(input);
2177
+ const mapping = await locked$1(input, async (_invoice, state) => {
2178
+ const reserved = state?.credits.find((credit) => credit.creditId === input.creditInvoiceId);
2179
+ if (!reserved) reject$1("Ordinary credit has no approved provider reservation");
2180
+ return structuredClone(reserved.mapping);
2181
+ });
2182
+ return withOrdinaryAccountingAuthority(input.tenantId, input.invoiceId, () => reconcileFrozenInvoiceAccountingSync({
2183
+ ...input,
2184
+ invoiceId: input.creditInvoiceId,
2185
+ mapping
2186
+ }));
2187
+ }
2188
+ async function reviewOrdinaryCredit(input, read) {
2189
+ if (getTenantId() !== input.tenantId) reject$1("Ordinary review requires its active tenant");
2190
+ return withDocumentMoneyTransaction(input.db, async (tx) => {
2191
+ await lockTenantMoneyRuntime(tx, input.tenantId);
2192
+ const { invoice } = await lockAndReloadInvoice(tx, input.invoiceId, input.tenantId);
2193
+ if (!readOrdinaryInvoiceAccountingState(invoice)) reject$1("Ordinary provider contract is missing");
2194
+ const credit = await (await InvoiceCollection.create({ db: tx })).get({ id: input.creditInvoiceId }, { cache: false });
2195
+ if (!credit || credit.tenantId !== input.tenantId || credit.creditedInvoiceId !== invoice.id || credit.kind !== "credit_note" || credit.customerId !== invoice.customerId || credit.currency !== invoice.currency || credit.moneyAuthority !== "calculated") reject$1("Ordinary credit review requires exact source lineage");
2196
+ return withOrdinaryAccountingAuthority(input.tenantId, input.invoiceId, () => read({
2197
+ db: tx,
2198
+ tenantId: input.tenantId,
2199
+ invoiceId: input.creditInvoiceId
2200
+ }));
2201
+ });
2202
+ }
2203
+ function loadOrdinaryCreditAccountingSnapshot(input) {
2204
+ return reviewOrdinaryCredit(input, loadFrozenInvoiceAccountingSnapshot);
2205
+ }
2206
+ function toOrdinaryCreditAccountingInput(input) {
2207
+ return reviewOrdinaryCredit(input, async (scope) => await toFrozenInvoiceAccountingInput({
2208
+ ...scope,
2209
+ mapping: input.mapping
2210
+ }));
2211
+ }
2212
+ //#endregion
2213
+ //#region src/retention-accounting.ts
2214
+ var hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
2215
+ function reject(message) {
2216
+ throw new MoneyValidationError("ACCOUNTING_SYNC_RECONCILIATION_REQUIRED", message);
2217
+ }
2218
+ function text(value, label) {
2219
+ if (typeof value !== "string" || !value || value.trim() !== value) reject(`${label} is required`);
2220
+ }
2221
+ function date(value, label) {
2222
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) reject(`${label} is invalid`);
2223
+ }
2224
+ function principal(input) {
2225
+ if (!input.tenantId || getTenantId() !== input.tenantId) reject("Retention accounting requires its active tenant");
2226
+ if (isMoneyTransaction(input.db)) reject("Provider writes cannot run inside a caller-owned money transaction");
2227
+ if (input.provider.type !== "quickbooks" || !input.provider.receivableTransfers) reject("Provider lacks the reviewed receivable transfer capability");
2228
+ }
2229
+ function readRetainedInvoiceAccountingState(invoice) {
2230
+ if (!invoice.retentionAccountingStateJson) return null;
2231
+ let state;
2232
+ try {
2233
+ state = JSON.parse(invoice.retentionAccountingStateJson);
2234
+ } catch {
2235
+ return reject("Malformed retained accounting state");
2236
+ }
2237
+ if (state.version !== 1 || state.invoiceId !== invoice.id || state.moneySnapshotId !== invoice.moneySnapshotId || state.currency !== invoice.currency || state.grossMinor !== invoice.totalAmount || state.mapping.customerRef !== invoice.customerExternalId || !Array.isArray(state.steps) || !Array.isArray(state.releases) || !Array.isArray(state.credits) || !Array.isArray(state.refunds)) reject("Retention provider source identity changed");
2238
+ const retention = parseInvoiceRetention(invoice.retentionStateJson);
2239
+ if (!retention) reject("Provider retention agreement is missing");
2240
+ const { releases: _releases, ...agreement } = retention;
2241
+ if (state.retentionAgreementHash !== hash(agreement)) reject("Provider retention agreement changed");
2242
+ return state;
2243
+ }
2244
+ async function loadRetainedInvoiceAccountingSnapshot(input) {
2245
+ if (!input.tenantId || getTenantId() !== input.tenantId) reject("Retained accounting review requires its active tenant");
2246
+ return withDocumentMoneyTransaction(input.db, async (tx) => {
2247
+ await lockTenantMoneyRuntime(tx, input.tenantId);
2248
+ const { invoice } = await lockAndReloadInvoice(tx, input.invoiceId, input.tenantId);
2249
+ const retention = parseInvoiceRetention(invoice.retentionStateJson);
2250
+ if (!retention || retention.retainedMinor <= 0 || invoice.kind !== "invoice" || invoice.moneyAuthority === "historical") reject("Retained accounting review requires a calculated retained invoice");
2251
+ readRetainedInvoiceAccountingState(invoice);
2252
+ return withRetentionAccountingAuthority(input.tenantId, input.invoiceId, () => loadFrozenInvoiceAccountingSnapshot({
2253
+ ...input,
2254
+ db: tx
2255
+ }));
2256
+ });
2257
+ }
2258
+ async function toRetainedInvoiceAccountingInput(input) {
2259
+ return mapFrozenAccountingSnapshot((await loadRetainedInvoiceAccountingSnapshot(input)).document, input.mapping);
2260
+ }
2261
+ async function reviewRetainedCredit(input, read) {
2262
+ if (!input.tenantId || getTenantId() !== input.tenantId) reject("Retained credit review requires its active tenant");
2263
+ return withDocumentMoneyTransaction(input.db, async (tx) => {
2264
+ const original = await loadRetainedInvoiceAccountingSnapshot({
2265
+ ...input,
2266
+ db: tx
2267
+ });
2268
+ const { invoice: credit } = await lockAndReloadInvoice(tx, input.creditInvoiceId, input.tenantId);
2269
+ if (credit.kind !== "credit_note" || credit.creditedInvoiceId !== input.invoiceId || credit.customerId !== original.document.customerId || credit.currency !== original.document.currency || credit.moneyAuthority === "historical") reject("Retained credit review requires its exact calculated source lineage");
2270
+ return withRetentionAccountingAuthority(input.tenantId, input.invoiceId, () => read({
2271
+ db: tx,
2272
+ tenantId: input.tenantId,
2273
+ invoiceId: input.creditInvoiceId
2274
+ }));
2275
+ });
2276
+ }
2277
+ function loadRetainedCreditAccountingSnapshot(input) {
2278
+ return reviewRetainedCredit(input, loadFrozenInvoiceAccountingSnapshot);
2279
+ }
2280
+ function toRetainedCreditAccountingInput(input) {
2281
+ return reviewRetainedCredit(input, async (scope) => {
2282
+ return await toFrozenInvoiceAccountingInput({
2283
+ ...scope,
2284
+ mapping: input.mapping
2285
+ });
2286
+ });
2287
+ }
2288
+ async function locked(input, operation) {
2289
+ return withDocumentMoneyTransaction(input.db, async (tx) => {
2290
+ await lockTenantMoneyRuntime(tx, input.tenantId);
2291
+ const { invoice } = await lockAndReloadInvoice(tx, input.invoiceId, input.tenantId);
2292
+ return operation(invoice, readRetainedInvoiceAccountingState(invoice));
2293
+ });
2294
+ }
2295
+ async function save(invoice, state) {
2296
+ invoice.retentionAccountingStateJson = JSON.stringify(state);
2297
+ await invoice.saveDerived(DOCUMENT_MONEY_WRITE);
2298
+ }
2299
+ function refreshStatus(state) {
2300
+ state.status = state.steps.every((step) => step.status === "confirmed") && state.releases.every((event) => event.committed) && state.credits.every((event) => event.committed) && state.refunds.every((event) => event.committed) ? "ready" : "pending";
2301
+ }
2302
+ function requireReady(state) {
2303
+ if (!state || state.status !== "ready" || state.steps.some((s) => s.status !== "confirmed")) reject("Retention provider bundle requires reconciliation before a new operation");
2304
+ return state;
2305
+ }
2306
+ function requestId(state, key) {
2307
+ return `smrt_ret_${hash([
2308
+ state.invoiceId,
2309
+ state.moneySnapshotId,
2310
+ state.mapping.realmId,
2311
+ state.mapping.environment,
2312
+ key
2313
+ ]).slice(0, 40)}`;
2314
+ }
2315
+ function source(invoice, state) {
2316
+ if (!invoice.externalId || invoice.externalProvider !== "quickbooks") reject("Retention needs a reconciled provider invoice");
2317
+ return {
2318
+ type: "Invoice",
2319
+ id: invoice.externalId,
2320
+ customerRef: state.mapping.customerRef,
2321
+ evidenceRef: state.moneySnapshotId
2322
+ };
2323
+ }
2324
+ async function executeStep(input, key, make) {
2325
+ return executeAccountingStep({
2326
+ locked: (operation) => locked(input, operation),
2327
+ save,
2328
+ provider: input.provider.receivableTransfers
2329
+ }, key, make);
2330
+ }
2331
+ async function syncRetainedInvoiceToAccounting(input) {
2332
+ principal(input);
2333
+ text(input.requestId, "Retention request");
2334
+ text(input.evidenceRef, "Retention evidence");
2335
+ date(input.effectiveOn, "Retention effective date");
2336
+ await input.provider.receivableTransfers.verifyMapping(input.mapping);
2337
+ await locked(input, async (invoice, state) => {
2338
+ if (input.mapping.realmId !== input.invoiceMapping.realmId || input.mapping.environment !== input.invoiceMapping.environment || input.mapping.currency !== invoice.currency || input.mapping.customerRef !== invoice.customerExternalId) reject("Retention mapping differs from frozen invoice provider authority");
2339
+ if (state) {
2340
+ if (hash([
2341
+ state.requestId,
2342
+ state.evidenceRef,
2343
+ state.effectiveOn,
2344
+ state.mapping,
2345
+ state.invoiceMapping
2346
+ ]) !== hash([
2347
+ input.requestId,
2348
+ input.evidenceRef,
2349
+ input.effectiveOn,
2350
+ input.mapping,
2351
+ input.invoiceMapping
2352
+ ])) reject("Retention provider contract is immutable");
2353
+ return;
2354
+ }
2355
+ if ("moneyAuthority" in invoice && invoice.moneyAuthority === "historical") reject("Historical source-owned invoices require an explicit reconciled provider authority contract; original journals cannot be duplicated");
2356
+ const retention = parseInvoiceRetention(invoice.retentionStateJson);
2357
+ if (!retention || retention.retainedMinor <= 0 || retention.releases.length || invoice.creditedAmount || invoice.amountPaid || !invoice.moneySnapshotId || invoice.kind !== "invoice" || [
2358
+ InvoiceStatus.DRAFT,
2359
+ InvoiceStatus.CANCELLED,
2360
+ InvoiceStatus.WRITTEN_OFF
2361
+ ].includes(invoice.status) || invoice.externalId || invoice.externalProvider || invoice.accountingSyncStateJson || invoice.collectionMethod || invoice.providerTaxAmount) reject("Initial provider retention requires an issued, uncredited, unpaid, unreleased host-owned retained invoice");
2362
+ const { releases: _releases, ...agreement } = retention;
2363
+ state = {
2364
+ version: 1,
2365
+ invoiceId: input.invoiceId,
2366
+ moneySnapshotId: invoice.moneySnapshotId,
2367
+ requestId: input.requestId,
2368
+ evidenceRef: input.evidenceRef,
2369
+ effectiveOn: input.effectiveOn,
2370
+ currency: invoice.currency,
2371
+ grossMinor: invoice.totalAmount,
2372
+ retainedMinor: retention.retainedMinor,
2373
+ retentionAgreementHash: hash(agreement),
2374
+ mapping: structuredClone(input.mapping),
2375
+ invoiceMapping: structuredClone(input.invoiceMapping),
2376
+ status: "pending",
2377
+ steps: [],
2378
+ releases: [],
2379
+ credits: [],
2380
+ refunds: []
2381
+ };
2382
+ await save(invoice, state);
2383
+ });
2384
+ await withRetentionAccountingAuthority(input.tenantId, input.invoiceId, () => syncFrozenInvoiceToAccounting({
2385
+ ...input,
2386
+ mapping: input.invoiceMapping
2387
+ }));
2388
+ const withheld = await executeStep(input, "withhold", (invoice, state) => prepareQuickBooksReceivableRequest({
2389
+ kind: "withhold",
2390
+ mapping: state.mapping,
2391
+ source: source(invoice, state),
2392
+ requestId: requestId(state, "withhold"),
2393
+ effectiveOn: state.effectiveOn,
2394
+ evidenceRef: state.evidenceRef,
2395
+ amountMinor: state.retainedMinor
2396
+ }));
2397
+ await executeStep(input, "withhold-application", (invoice, state) => prepareQuickBooksReceivableRequest({
2398
+ kind: "application",
2399
+ mapping: state.mapping,
2400
+ source: source(invoice, state),
2401
+ requestId: requestId(state, "withhold-application"),
2402
+ effectiveOn: state.effectiveOn,
2403
+ evidenceRef: state.evidenceRef,
2404
+ amountMinor: state.retainedMinor,
2405
+ debit: {
2406
+ type: "Invoice",
2407
+ id: invoice.externalId
2408
+ },
2409
+ credit: {
2410
+ type: "JournalEntry",
2411
+ id: withheld.externalId
2412
+ }
2413
+ }));
2414
+ return locked(input, async (invoice, state) => {
2415
+ if (!state) reject("Missing retained provider state");
2416
+ if (state.steps.some((step) => step.status !== "confirmed") || state.releases.some((release) => !release.committed)) reject("Another retention operation is unfinished");
2417
+ refreshStatus(state);
2418
+ await save(invoice, state);
2419
+ return state;
2420
+ });
2421
+ }
2422
+ async function releaseRetainedInvoiceToAccounting(input) {
2423
+ principal(input);
2424
+ validateRetentionEvidence(input);
2425
+ date(input.effectiveOn, "Release effective date");
2426
+ date(input.dueOn, "Release due date");
2427
+ if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor <= 0) reject("Release amount or due date is invalid");
2428
+ const key = `release:${input.requestId}`;
2429
+ await locked(input, async (invoice, state) => {
2430
+ if (!state) reject("Retention provider contract is missing");
2431
+ const prior = state.releases.find((r) => r.requestId === input.requestId);
2432
+ const release = {
2433
+ requestId: input.requestId,
2434
+ policyVersion: input.policyVersion,
2435
+ evidenceRef: input.evidenceRef,
2436
+ actorId: input.actorId,
2437
+ reason: input.reason,
2438
+ amountMinor: input.amountMinor,
2439
+ effectiveOn: input.effectiveOn,
2440
+ dueOn: input.dueOn,
2441
+ stepKey: key,
2442
+ committed: false
2443
+ };
2444
+ if (prior) {
2445
+ if (hash({
2446
+ ...prior,
2447
+ committed: false
2448
+ }) !== hash(release)) reject("Release request identity reused with changed facts");
2449
+ return;
2450
+ }
2451
+ requireReady(state);
2452
+ if (!invoice.updated_at || new Date(invoice.updated_at).toISOString() !== new Date(input.expectedUpdatedAt).toISOString()) reject("Stale retention release revision");
2453
+ if (invoice.creditedAmount !== state.credits.filter((c) => c.committed).reduce((sum, c) => sum + c.grossMinor, 0)) reject("Provider retention credit adjustment must reconcile before release");
2454
+ if (input.amountMinor > calculateInvoicePayable(invoice).retainedRemainingMinor) reject("Release exceeds retained capacity");
2455
+ state.releases.push(release);
2456
+ state.status = "pending";
2457
+ await save(invoice, state);
2458
+ });
2459
+ await executeStep(input, key, (invoice, state) => prepareQuickBooksReceivableRequest({
2460
+ kind: "release",
2461
+ mapping: state.mapping,
2462
+ source: source(invoice, state),
2463
+ requestId: requestId(state, key),
2464
+ effectiveOn: input.effectiveOn,
2465
+ evidenceRef: input.evidenceRef,
2466
+ amountMinor: input.amountMinor
2467
+ }));
2468
+ return locked(input, async (invoice, state) => {
2469
+ if (!state) reject("Retention provider contract is missing");
2470
+ const release = state.releases.find((r) => r.requestId === input.requestId);
2471
+ if (!release) reject("Release reservation disappeared");
2472
+ if (!release.committed) {
2473
+ if (invoice.creditedAmount !== state.credits.filter((c) => c.committed).reduce((sum, c) => sum + c.grossMinor, 0) || input.amountMinor > calculateInvoicePayable(invoice).retainedRemainingMinor) reject("Source capacity changed after provider release; reconciliation required");
2474
+ const retention = parseInvoiceRetention(invoice.retentionStateJson);
2475
+ retention.releases.push({
2476
+ requestId: release.requestId,
2477
+ policyVersion: release.policyVersion,
2478
+ evidenceRef: release.evidenceRef,
2479
+ actorId: release.actorId,
2480
+ reason: release.reason,
2481
+ amountMinor: release.amountMinor,
2482
+ effectiveOn: release.effectiveOn,
2483
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
2484
+ });
2485
+ invoice.retentionStateJson = JSON.stringify(retention);
2486
+ release.committed = true;
2487
+ }
2488
+ refreshStatus(state);
2489
+ await save(invoice, state);
2490
+ return state;
2491
+ });
2492
+ }
2493
+ async function reconcileRetainedAccountingStep(input) {
2494
+ principal(input);
2495
+ return reconcileAccountingStep({
2496
+ locked: (operation) => locked(input, operation),
2497
+ save,
2498
+ provider: input.provider.receivableTransfers
2499
+ }, input);
2500
+ }
2501
+ async function reconcileRetainedBaseInvoice(input) {
2502
+ principal(input);
2503
+ await locked(input, async (_invoice, state) => {
2504
+ if (!state || hash(state.invoiceMapping) !== hash(input.mapping)) reject("Retention base mapping differs");
2505
+ });
2506
+ return withRetentionAccountingAuthority(input.tenantId, input.invoiceId, () => reconcileFrozenInvoiceAccountingSync(input));
2507
+ }
2508
+ async function syncRetainedCreditToAccounting(input) {
2509
+ principal(input);
2510
+ text(input.requestId, "Credit adjustment request");
2511
+ text(input.evidenceRef, "Credit adjustment evidence");
2512
+ date(input.effectiveOn, "Credit adjustment date");
2513
+ const reserved = await locked(input, async (invoice, state) => {
2514
+ if (!state) reject("Retention provider contract is missing");
2515
+ const credit = await (await InvoiceCollection.create({ db: invoice.db })).get({ id: input.creditInvoiceId }, { cache: false });
2516
+ if (!credit || credit.tenantId !== input.tenantId || credit.creditedInvoiceId !== invoice.id || credit.kind !== "credit_note" || !credit.moneySnapshotId || [InvoiceStatus.DRAFT, InvoiceStatus.CANCELLED].includes(credit.status) || credit.currency !== state.currency || credit.customerExternalId !== state.mapping.customerRef) reject("Credit adjustment requires its issued same-customer source credit");
2517
+ if (input.creditMapping.realmId !== state.mapping.realmId || input.creditMapping.environment !== state.mapping.environment) reject("Credit adjustment provider scope differs");
2518
+ const prior = state.credits.find((c) => c.creditId === input.creditInvoiceId || c.requestId === input.requestId);
2519
+ if (prior) {
2520
+ if (prior.creditId !== input.creditInvoiceId || prior.requestId !== input.requestId || prior.moneySnapshotId !== credit.moneySnapshotId || prior.grossMinor !== credit.totalAmount || prior.evidenceRef !== input.evidenceRef || prior.effectiveOn !== input.effectiveOn || hash(prior.mapping) !== hash(input.creditMapping)) reject("Credit adjustment identity reused with changed facts");
2521
+ return structuredClone(prior);
2522
+ }
2523
+ requireReady(state);
2524
+ if (state.credits.reduce((sum, c) => sum + c.grossMinor, 0) + credit.totalAmount > invoice.creditedAmount) reject("Provider credit exceeds issued source credit capacity");
2525
+ const remainingRetained = state.retainedMinor - state.releases.filter((r) => r.committed).reduce((sum, r) => sum + r.amountMinor, 0) - state.credits.reduce((sum, c) => sum + c.retainedMinor, 0);
2526
+ const retainedMinor = Math.min(remainingRetained, credit.totalAmount);
2527
+ if (retainedMinor < 0 || retainedMinor > credit.creditedAmount) reject("Retained-first credit exceeds its approved source allocation");
2528
+ const next = {
2529
+ creditId: input.creditInvoiceId,
2530
+ moneySnapshotId: credit.moneySnapshotId,
2531
+ requestId: input.requestId,
2532
+ evidenceRef: input.evidenceRef,
2533
+ effectiveOn: input.effectiveOn,
2534
+ grossMinor: credit.totalAmount,
2535
+ retainedMinor,
2536
+ appliedMinor: credit.creditedAmount - retainedMinor,
2537
+ mapping: structuredClone(input.creditMapping),
2538
+ committed: false
2539
+ };
2540
+ state.credits.push(next);
2541
+ state.status = "pending";
2542
+ await save(invoice, state);
2543
+ return next;
2544
+ });
2545
+ const creditResult = await withRetentionAccountingAuthority(input.tenantId, input.invoiceId, () => syncFrozenInvoiceToAccounting({
2546
+ db: input.db,
2547
+ tenantId: input.tenantId,
2548
+ invoiceId: input.creditInvoiceId,
2549
+ provider: input.provider,
2550
+ mapping: input.creditMapping
2551
+ }));
2552
+ const prefix = `credit:${input.requestId}`;
2553
+ const creditSource = (state) => ({
2554
+ type: "CreditMemo",
2555
+ id: creditResult.externalId,
2556
+ customerRef: state.mapping.customerRef,
2557
+ evidenceRef: reserved.moneySnapshotId
2558
+ });
2559
+ if (reserved.retainedMinor > 0) {
2560
+ const cancelled = await executeStep(input, `${prefix}:retention`, (_invoice, state) => prepareQuickBooksReceivableRequest({
2561
+ kind: "release",
2562
+ mapping: state.mapping,
2563
+ source: creditSource(state),
2564
+ requestId: requestId(state, `${prefix}:retention`),
2565
+ effectiveOn: reserved.effectiveOn,
2566
+ evidenceRef: reserved.evidenceRef,
2567
+ amountMinor: reserved.retainedMinor
2568
+ }));
2569
+ await executeStep(input, `${prefix}:retention-application`, (_invoice, state) => prepareQuickBooksReceivableRequest({
2570
+ kind: "application",
2571
+ mapping: state.mapping,
2572
+ source: creditSource(state),
2573
+ requestId: requestId(state, `${prefix}:retention-application`),
2574
+ effectiveOn: reserved.effectiveOn,
2575
+ evidenceRef: reserved.evidenceRef,
2576
+ amountMinor: reserved.retainedMinor,
2577
+ debit: {
2578
+ type: "JournalEntry",
2579
+ id: cancelled.externalId
2580
+ },
2581
+ credit: {
2582
+ type: "CreditMemo",
2583
+ id: creditResult.externalId
2584
+ }
2585
+ }));
2586
+ }
2587
+ if (reserved.appliedMinor > 0) await executeStep(input, `${prefix}:due-application`, (invoice, state) => prepareQuickBooksReceivableRequest({
2588
+ kind: "application",
2589
+ mapping: state.mapping,
2590
+ source: creditSource(state),
2591
+ requestId: requestId(state, `${prefix}:due-application`),
2592
+ effectiveOn: reserved.effectiveOn,
2593
+ evidenceRef: reserved.evidenceRef,
2594
+ amountMinor: reserved.appliedMinor,
2595
+ debit: {
2596
+ type: "Invoice",
2597
+ id: invoice.externalId
2598
+ },
2599
+ credit: {
2600
+ type: "CreditMemo",
2601
+ id: creditResult.externalId
2602
+ }
2603
+ }));
2604
+ return locked(input, async (invoice, state) => {
2605
+ if (!state) reject("Retention provider state is missing");
2606
+ const credit = state.credits.find((c) => c.creditId === input.creditInvoiceId);
2607
+ if (!credit) reject("Credit reservation disappeared");
2608
+ credit.committed = true;
2609
+ refreshStatus(state);
2610
+ await save(invoice, state);
2611
+ return state;
2612
+ });
2613
+ }
2614
+ async function syncRetainedRefundToAccounting(input) {
2615
+ principal(input);
2616
+ text(input.requestId, "Refund recording request");
2617
+ text(input.evidenceRef, "Refund settlement evidence");
2618
+ date(input.effectiveOn, "Refund effective date");
2619
+ const reservation = await locked(input, async (invoice, state) => {
2620
+ if (!state) reject("Retention provider contract is missing");
2621
+ const credit = state.credits.find((c) => c.creditId === input.creditInvoiceId && c.committed);
2622
+ if (!credit) reject("Refund requires its reconciled provider credit");
2623
+ const payment = await (await PaymentCollection.create({ db: invoice.db })).get({ id: input.paymentId }, { cache: false });
2624
+ const creditInvoice = await (await InvoiceCollection.create({ db: invoice.db })).get({ id: input.creditInvoiceId }, { cache: false });
2625
+ if (!payment || !creditInvoice || payment.tenantId !== input.tenantId || payment.customerId !== invoice.customerId || payment.currency !== state.currency || payment.kind !== "refund" || payment.status !== "completed" || !payment.journalId || !payment.paidAt || !creditInvoice.externalId || creditInvoice.moneySnapshotId !== credit.moneySnapshotId) reject("Provider refund requires its completed same-customer local cash journal and source credit");
2626
+ const allocations = await (await PaymentAllocationCollection.create({ db: invoice.db })).findByPayment(input.paymentId);
2627
+ if (allocations.length !== 1 || allocations[0].invoiceId !== input.creditInvoiceId || allocations[0].amount !== payment.amount) reject("Refund recording requires one exact full source-credit allocation");
2628
+ const fingerprint = hash([
2629
+ payment.id,
2630
+ payment.customerId,
2631
+ payment.currency,
2632
+ payment.amount,
2633
+ payment.journalId,
2634
+ new Date(payment.paidAt).toISOString(),
2635
+ allocations[0].id,
2636
+ allocations[0].amount,
2637
+ credit.moneySnapshotId
2638
+ ]);
2639
+ const next = {
2640
+ paymentId: input.paymentId,
2641
+ creditId: input.creditInvoiceId,
2642
+ requestId: input.requestId,
2643
+ evidenceRef: input.evidenceRef,
2644
+ effectiveOn: input.effectiveOn,
2645
+ amountMinor: payment.amount,
2646
+ paymentFingerprint: fingerprint,
2647
+ committed: false
2648
+ };
2649
+ const prior = state.refunds.find((r) => r.paymentId === input.paymentId || r.requestId === input.requestId);
2650
+ if (prior) {
2651
+ if (hash({
2652
+ ...prior,
2653
+ committed: false
2654
+ }) !== hash(next)) reject("Refund recording identity reused with changed facts");
2655
+ return {
2656
+ ...prior,
2657
+ externalCreditId: creditInvoice.externalId
2658
+ };
2659
+ }
2660
+ requireReady(state);
2661
+ if (!state.mapping.cashAccountRef) reject("Refund provider bank account must be explicitly reviewed");
2662
+ if (state.refunds.filter((r) => r.creditId === input.creditInvoiceId).reduce((sum, r) => sum + r.amountMinor, 0) + payment.amount > credit.grossMinor - credit.retainedMinor - credit.appliedMinor) reject("Refund exceeds unapplied provider credit capacity");
2663
+ if (payment.retentionAccountingBindingJson) reject("Refund is already bound to a provider operation");
2664
+ payment.retentionAccountingBindingJson = JSON.stringify({
2665
+ invoiceId: input.invoiceId,
2666
+ tenantId: input.tenantId,
2667
+ requestId: input.requestId,
2668
+ paymentFingerprint: fingerprint
2669
+ });
2670
+ await withRetentionAccountingAuthority(input.tenantId, input.invoiceId, () => payment.save());
2671
+ state.refunds.push(next);
2672
+ state.status = "pending";
2673
+ await save(invoice, state);
2674
+ return {
2675
+ ...next,
2676
+ externalCreditId: creditInvoice.externalId
2677
+ };
2678
+ });
2679
+ const prefix = `refund:${input.requestId}`;
2680
+ const creditSource = (state) => ({
2681
+ type: "CreditMemo",
2682
+ id: reservation.externalCreditId,
2683
+ customerRef: state.mapping.customerRef,
2684
+ evidenceRef: reservation.paymentFingerprint
2685
+ });
2686
+ const journal = await executeStep(input, prefix, (_invoice, state) => prepareQuickBooksReceivableRequest({
2687
+ kind: "cash-refund",
2688
+ mapping: state.mapping,
2689
+ source: creditSource(state),
2690
+ requestId: requestId(state, prefix),
2691
+ effectiveOn: reservation.effectiveOn,
2692
+ evidenceRef: reservation.evidenceRef,
2693
+ amountMinor: reservation.amountMinor
2694
+ }));
2695
+ await executeStep(input, `${prefix}:application`, (_invoice, state) => prepareQuickBooksReceivableRequest({
2696
+ kind: "application",
2697
+ mapping: state.mapping,
2698
+ source: creditSource(state),
2699
+ requestId: requestId(state, `${prefix}:application`),
2700
+ effectiveOn: reservation.effectiveOn,
2701
+ evidenceRef: reservation.evidenceRef,
2702
+ amountMinor: reservation.amountMinor,
2703
+ debit: {
2704
+ type: "JournalEntry",
2705
+ id: journal.externalId
2706
+ },
2707
+ credit: {
2708
+ type: "CreditMemo",
2709
+ id: reservation.externalCreditId
2710
+ }
2711
+ }));
2712
+ return locked(input, async (invoice, state) => {
2713
+ if (!state) reject("Retention provider state disappeared");
2714
+ const refund = state.refunds.find((r) => r.requestId === input.requestId);
2715
+ if (!refund) reject("Refund reservation disappeared");
2716
+ refund.committed = true;
2717
+ refreshStatus(state);
2718
+ await save(invoice, state);
2719
+ return state;
2720
+ });
2721
+ }
2722
+ async function reconcileRetainedCreditAccounting(input) {
2723
+ principal(input);
2724
+ const mapping = await locked(input, async (_invoice, state) => {
2725
+ const reserved = state?.credits.find((credit) => credit.creditId === input.creditInvoiceId);
2726
+ if (!reserved) reject("Retained credit has no approved provider reservation");
2727
+ return structuredClone(reserved.mapping);
2728
+ });
2729
+ return withRetentionAccountingAuthority(input.tenantId, input.invoiceId, () => reconcileFrozenInvoiceAccountingSync({
2730
+ ...input,
2731
+ invoiceId: input.creditInvoiceId,
2732
+ mapping
2733
+ }));
2734
+ }
2735
+ //#endregion
2736
+ export { PaymentAllocationCollection_exports as A, invoice_accounting_sync_exports as C, syncFrozenInvoiceToAccounting as D, reconcileFrozenInvoiceAccountingSync as E, Payment as F, historicalCreditRefundCommittedTotal as M, PaymentCollection as N, toFrozenInvoiceAccountingInput as O, PaymentCollection_exports as P, calculateInvoiceMinorLine as S, readFrozenAccountingSyncState as T, reconcileOrdinaryCreditAccounting as _, reconcileRetainedBaseInvoice as a, syncOrdinaryRefundToAccounting as b, syncRetainedCreditToAccounting as c, toRetainedCreditAccountingInput as d, toRetainedInvoiceAccountingInput as f, reconcileOrdinaryBaseInvoice as g, reconcileOrdinaryAccountingStep as h, reconcileRetainedAccountingStep as i, PaymentAllocation as j, PaymentAllocationCollection as k, syncRetainedInvoiceToAccounting as l, readOrdinaryInvoiceAccountingState as m, loadRetainedInvoiceAccountingSnapshot as n, reconcileRetainedCreditAccounting as o, loadOrdinaryCreditAccountingSnapshot as p, readRetainedInvoiceAccountingState as r, releaseRetainedInvoiceToAccounting as s, loadRetainedCreditAccountingSnapshot as t, syncRetainedRefundToAccounting as u, syncOrdinaryCreditToAccounting as v, loadFrozenInvoiceAccountingSnapshot as w, toOrdinaryCreditAccountingInput as x, syncOrdinaryInvoiceToAccounting as y };
2737
+
2738
+ //# sourceMappingURL=retention-accounting-c7kY7gFU.js.map