@happyvertical/smrt-commerce 0.56.0 → 0.56.2

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 (213) hide show
  1. package/AGENTS.md +9 -8
  2. package/README.md +98 -4
  3. package/agents/accounting-snapshot-mapper.md +111 -0
  4. package/agents/contract-money.md +24 -0
  5. package/agents/cost-billing.md +7 -2
  6. package/agents/customer-vendor-ui.md +7 -1
  7. package/agents/document-money.md +127 -0
  8. package/agents/invoice-calculations.md +14 -2
  9. package/agents/line-amount-migration.md +50 -0
  10. package/agents/minor-unit-migration.md +20 -0
  11. package/agents/money-calculator.md +45 -0
  12. package/agents/money-model.md +113 -0
  13. package/agents/revenue-journal.md +23 -0
  14. package/agents/tax-codes.md +48 -0
  15. package/agents/tax-summary.md +128 -0
  16. package/dist/accounting-snapshot.d.ts +79 -0
  17. package/dist/accounting-snapshot.d.ts.map +1 -0
  18. package/dist/billing/runtime.d.ts +2 -0
  19. package/dist/billing/runtime.d.ts.map +1 -1
  20. package/dist/chunks/ContractLineItemCollection--BrGMtNz.js +265 -0
  21. package/dist/chunks/ContractLineItemCollection--BrGMtNz.js.map +1 -0
  22. package/dist/chunks/CustomerCollection-CjGaZSpf.js +99 -0
  23. package/dist/chunks/CustomerCollection-CjGaZSpf.js.map +1 -0
  24. package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js +1659 -0
  25. package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js.map +1 -0
  26. package/dist/chunks/InvoiceCollection-BUe_8FT1.js +1047 -0
  27. package/dist/chunks/InvoiceCollection-BUe_8FT1.js.map +1 -0
  28. package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js +468 -0
  29. package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js.map +1 -0
  30. package/dist/chunks/TaxCodeCollection-DF2eaJed.js +149 -0
  31. package/dist/chunks/TaxCodeCollection-DF2eaJed.js.map +1 -0
  32. package/dist/chunks/TaxComponentCollection-3x24xSNh.js +129 -0
  33. package/dist/chunks/TaxComponentCollection-3x24xSNh.js.map +1 -0
  34. package/dist/chunks/TaxSettingsCollection-DYYF-In-.js +319 -0
  35. package/dist/chunks/TaxSettingsCollection-DYYF-In-.js.map +1 -0
  36. package/dist/chunks/{money-DfrRxrel.js → allocation-integrity-Cq0g6CfO.js} +7 -12
  37. package/dist/chunks/allocation-integrity-Cq0g6CfO.js.map +1 -0
  38. package/dist/chunks/{calculations-Bz7RB3qK.js → calculations-DaHR17il.js} +134 -54
  39. package/dist/chunks/calculations-DaHR17il.js.map +1 -0
  40. package/dist/chunks/credit-allocation-yspz0Lab.js +121 -0
  41. package/dist/chunks/credit-allocation-yspz0Lab.js.map +1 -0
  42. package/dist/chunks/document-money-oUDvdjOf.js +860 -0
  43. package/dist/chunks/document-money-oUDvdjOf.js.map +1 -0
  44. package/dist/chunks/invoice-posting-CF3fE_Ec.js +229 -0
  45. package/dist/chunks/invoice-posting-CF3fE_Ec.js.map +1 -0
  46. package/dist/chunks/money-rational-CcW7gzG5.js +64 -0
  47. package/dist/chunks/money-rational-CcW7gzG5.js.map +1 -0
  48. package/dist/chunks/revenue-journal-DY9fro8E.js +140 -0
  49. package/dist/chunks/revenue-journal-DY9fro8E.js.map +1 -0
  50. package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  51. package/dist/chunks/tenant-money-barrier-y6EAa6vY.js +135 -0
  52. package/dist/chunks/tenant-money-barrier-y6EAa6vY.js.map +1 -0
  53. package/dist/chunks/{types-zVnIjSOk.js → types-Bbm0Xflc.js} +4 -14
  54. package/dist/chunks/types-Bbm0Xflc.js.map +1 -0
  55. package/dist/chunks/{units-BpZ0_Dpk.js → units-CNcjH_eE.js} +2 -2
  56. package/dist/chunks/{units-BpZ0_Dpk.js.map → units-CNcjH_eE.js.map} +1 -1
  57. package/dist/collections/ContractCollection.d.ts +35 -1
  58. package/dist/collections/ContractCollection.d.ts.map +1 -1
  59. package/dist/collections/ContractLineItemCollection.d.ts +26 -1
  60. package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
  61. package/dist/collections/DocumentTaxCollection.d.ts +75 -0
  62. package/dist/collections/DocumentTaxCollection.d.ts.map +1 -0
  63. package/dist/collections/InvoiceCollection.d.ts +26 -1
  64. package/dist/collections/InvoiceCollection.d.ts.map +1 -1
  65. package/dist/collections/InvoiceLineItemCollection.d.ts +26 -1
  66. package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
  67. package/dist/collections/MoneyMigrationDocumentCollection.d.ts +12 -0
  68. package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +1 -0
  69. package/dist/collections/PaymentAllocationCollection.d.ts +2 -0
  70. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
  71. package/dist/collections/TaxCodeCollection.d.ts +7 -0
  72. package/dist/collections/TaxCodeCollection.d.ts.map +1 -0
  73. package/dist/collections/TaxComponentCollection.d.ts +6 -0
  74. package/dist/collections/TaxComponentCollection.d.ts.map +1 -0
  75. package/dist/collections/TaxSettingsCollection.d.ts +16 -0
  76. package/dist/collections/TaxSettingsCollection.d.ts.map +1 -0
  77. package/dist/collections/TaxSummaryReportCollection.d.ts +151 -0
  78. package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -0
  79. package/dist/collections/index.d.ts +6 -0
  80. package/dist/collections/index.d.ts.map +1 -1
  81. package/dist/commerce-money-tools.d.ts +9 -0
  82. package/dist/commerce-money-tools.d.ts.map +1 -0
  83. package/dist/contract-money.d.ts +27 -0
  84. package/dist/contract-money.d.ts.map +1 -0
  85. package/dist/cost-billing.d.ts +0 -1
  86. package/dist/cost-billing.d.ts.map +1 -1
  87. package/dist/cost-billing.js +10 -15
  88. package/dist/cost-billing.js.map +1 -1
  89. package/dist/credit-allocation.d.ts +48 -0
  90. package/dist/credit-allocation.d.ts.map +1 -0
  91. package/dist/document-money-internal.d.ts +44 -0
  92. package/dist/document-money-internal.d.ts.map +1 -0
  93. package/dist/document-money.d.ts +85 -0
  94. package/dist/document-money.d.ts.map +1 -0
  95. package/dist/index.d.ts +15 -4
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +5411 -4249
  98. package/dist/index.js.map +1 -1
  99. package/dist/invoice-accounting-sync.d.ts +96 -0
  100. package/dist/invoice-accounting-sync.d.ts.map +1 -0
  101. package/dist/invoice-mcp.d.ts +16 -0
  102. package/dist/invoice-mcp.d.ts.map +1 -0
  103. package/dist/invoice-posting.d.ts +9 -0
  104. package/dist/invoice-posting.d.ts.map +1 -0
  105. package/dist/invoice-record-screen.d.ts +60 -0
  106. package/dist/invoice-record-screen.d.ts.map +1 -0
  107. package/dist/invoices.d.ts +2 -0
  108. package/dist/invoices.d.ts.map +1 -1
  109. package/dist/invoices.js +3 -2
  110. package/dist/manifest.json +12804 -6095
  111. package/dist/migrations/commerceLineAmounts.d.ts +84 -0
  112. package/dist/migrations/commerceLineAmounts.d.ts.map +1 -0
  113. package/dist/migrations/partyLinks.d.ts.map +1 -1
  114. package/dist/models/Contract.d.ts +34 -4
  115. package/dist/models/Contract.d.ts.map +1 -1
  116. package/dist/models/ContractLineItem.d.ts +43 -6
  117. package/dist/models/ContractLineItem.d.ts.map +1 -1
  118. package/dist/models/Customer.d.ts +7 -0
  119. package/dist/models/Customer.d.ts.map +1 -1
  120. package/dist/models/DocumentTax.d.ts +50 -0
  121. package/dist/models/DocumentTax.d.ts.map +1 -0
  122. package/dist/models/Invoice.d.ts +46 -2
  123. package/dist/models/Invoice.d.ts.map +1 -1
  124. package/dist/models/InvoiceLineItem.d.ts +46 -5
  125. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  126. package/dist/models/MoneyMigrationDocument.d.ts +28 -0
  127. package/dist/models/MoneyMigrationDocument.d.ts.map +1 -0
  128. package/dist/models/Payment.d.ts +5 -1
  129. package/dist/models/Payment.d.ts.map +1 -1
  130. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  131. package/dist/models/TaxCode.d.ts +22 -0
  132. package/dist/models/TaxCode.d.ts.map +1 -0
  133. package/dist/models/TaxComponent.d.ts +21 -0
  134. package/dist/models/TaxComponent.d.ts.map +1 -0
  135. package/dist/models/TaxSettings.d.ts +33 -0
  136. package/dist/models/TaxSettings.d.ts.map +1 -0
  137. package/dist/models/TaxSummaryReport.d.ts +35 -0
  138. package/dist/models/TaxSummaryReport.d.ts.map +1 -0
  139. package/dist/models/Vendor.d.ts +9 -0
  140. package/dist/models/Vendor.d.ts.map +1 -1
  141. package/dist/models/index.d.ts +7 -1
  142. package/dist/models/index.d.ts.map +1 -1
  143. package/dist/money-rational.d.ts +15 -0
  144. package/dist/money-rational.d.ts.map +1 -0
  145. package/dist/money.d.ts +106 -25
  146. package/dist/money.d.ts.map +1 -1
  147. package/dist/money.js +274 -0
  148. package/dist/money.js.map +1 -0
  149. package/dist/recipes.d.ts +15 -15
  150. package/dist/revenue-journal.d.ts +61 -0
  151. package/dist/revenue-journal.d.ts.map +1 -0
  152. package/dist/smrt-knowledge.json +9621 -7592
  153. package/dist/svelte/components/InvoiceActions.svelte +12 -0
  154. package/dist/svelte/components/InvoiceActions.svelte.d.ts +5 -0
  155. package/dist/svelte/components/InvoiceActions.svelte.d.ts.map +1 -1
  156. package/dist/svelte/components/InvoiceEditor.svelte +15 -1
  157. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  158. package/dist/svelte/components/InvoiceLineItems.svelte +3 -3
  159. package/dist/svelte/components/InvoiceRecordScreen.svelte +94 -0
  160. package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts +21 -0
  161. package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts.map +1 -0
  162. package/dist/svelte/components/InvoiceTotals.svelte +2 -2
  163. package/dist/svelte/components/MoneyInvoiceEditor.svelte +282 -0
  164. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +31 -0
  165. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -0
  166. package/dist/svelte/components/PurchaseOrderEditor.svelte +53 -1
  167. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +5 -0
  168. package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -1
  169. package/dist/svelte/components/QuoteEditor.svelte +54 -1
  170. package/dist/svelte/components/QuoteEditor.svelte.d.ts +5 -0
  171. package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -1
  172. package/dist/svelte/i18n.d.ts +16 -0
  173. package/dist/svelte/i18n.d.ts.map +1 -1
  174. package/dist/svelte/i18n.js +16 -0
  175. package/dist/svelte/index.d.ts +7 -2
  176. package/dist/svelte/index.d.ts.map +1 -1
  177. package/dist/svelte/index.js +4 -1
  178. package/dist/svelte/invoices/calculations.d.ts +7 -2
  179. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  180. package/dist/svelte/invoices/calculations.js +141 -47
  181. package/dist/svelte/invoices/money-editor.d.ts +123 -0
  182. package/dist/svelte/invoices/money-editor.d.ts.map +1 -0
  183. package/dist/svelte/invoices/money-editor.js +101 -0
  184. package/dist/svelte/invoices/types.d.ts +5 -1
  185. package/dist/svelte/invoices/types.d.ts.map +1 -1
  186. package/dist/svelte/purchasing-i18n.d.ts +5 -0
  187. package/dist/svelte/purchasing-i18n.d.ts.map +1 -1
  188. package/dist/svelte/purchasing-i18n.js +5 -0
  189. package/dist/svelte/quote-i18n.d.ts +5 -0
  190. package/dist/svelte/quote-i18n.d.ts.map +1 -1
  191. package/dist/svelte/quote-i18n.js +5 -0
  192. package/dist/tax/cookbook.d.ts +22 -0
  193. package/dist/tax/cookbook.d.ts.map +1 -0
  194. package/dist/tax/example-pack.d.ts +25 -0
  195. package/dist/tax/example-pack.d.ts.map +1 -0
  196. package/dist/tax/identity.d.ts +5 -0
  197. package/dist/tax/identity.d.ts.map +1 -0
  198. package/dist/tax/index.d.ts +4 -0
  199. package/dist/tax/index.d.ts.map +1 -0
  200. package/dist/tax/legacy.d.ts +48 -0
  201. package/dist/tax/legacy.d.ts.map +1 -0
  202. package/dist/tax-summary-report-adapter.d.ts +37 -0
  203. package/dist/tax-summary-report-adapter.d.ts.map +1 -0
  204. package/dist/tax-summary.d.ts +129 -0
  205. package/dist/tax-summary.d.ts.map +1 -0
  206. package/dist/tenant-money-barrier.d.ts +66 -0
  207. package/dist/tenant-money-barrier.d.ts.map +1 -0
  208. package/dist/types/index.d.ts +138 -1
  209. package/dist/types/index.d.ts.map +1 -1
  210. package/package.json +19 -11
  211. package/dist/chunks/calculations-Bz7RB3qK.js.map +0 -1
  212. package/dist/chunks/money-DfrRxrel.js.map +0 -1
  213. package/dist/chunks/types-zVnIjSOk.js.map +0 -1
package/AGENTS.md CHANGED
@@ -33,6 +33,12 @@ language for end users and update them when a recipe's fields change. See
33
33
 
34
34
  See [payment allocation integrity](agents/payment-allocations.md) for atomic splits,
35
35
  retry identity, parent corrections and supported dialects.
36
+ See [tax codes](agents/tax-codes.md) for tenant-owned rate components, default codes,
37
+ and legacy-rate migration.
38
+ See [document money persistence](agents/document-money.md) for the per-tenant
39
+ legacy/v2 gate, atomic aggregate writes, issued snapshots, and frozen fields.
40
+ See [Contract money persistence](agents/contract-money.md) for the Contract
41
+ aggregate, purchase recoverability, and migration-only materialization boundary.
36
42
 
37
43
  ## Ledger Integration
38
44
 
@@ -161,14 +167,9 @@ for usage.
161
167
  ## Gotchas
162
168
 
163
169
  - **Optional tenancy**: all models `@TenantScoped({ mode: 'optional' })` + nullable tenantId
164
- - **Currency is integer minor units** (cents, satoshis) — `$19.99` is `1999`, like affiliates. Money is exact, so money fields initialize `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER columns (BIGINT on fresh PostgreSQL/DuckDB databases). Writing a fractional major-unit value is the bug — the model rejects it via `assertIntegerMinorUnits` on `Invoice`/`Payment`/`PaymentAllocation`/`Payout`/`Contract` before the database sees it, because PostgreSQL would reject it with `22P02` while SQLite's affinity silently stores it. `pnpm --filter @happyvertical/smrt-commerce test:postgres` is the lane that holds the line (#2361, #2401, #2373).
165
- - **Rates are the opposite**: `InvoiceLineItem.taxRate` and `ContractLineItem.taxRate` are inherently fractional and must initialize `= 0.0`. INTEGER would truncate every rate to 0. `ContractLineItem.quantity` is decimal for the same reason (contracts price fractional hours/weight/bandwidth); `InvoiceLineItem.quantity` is also decimal (six fractional places); existing integer columns require the explicit [quantity migration](agents/invoice-quantity.md).
166
- - **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem` uses exact shared invoice arithmetic to round fractional quantity × price and then tax to minor units; `ContractLineItem.calculateAmount()` uses `Math.round()` for tax. Everything downstream is exact integer arithmetic, which is why **there are no `EPSILON` tolerances left on any money path** (#2401). `Invoice`, `PaymentAllocation`, `Payout` and `PaymentIntent` compare with `===` / `>` / `>=`. Do not reintroduce a tolerance: against integers it forgives a real one-cent discrepancy rather than float fuzz. (`FulfillmentLineItem`'s quantity epsilon is unrelated — quantities are genuinely decimal there.)
167
- - **Migrating an existing database to minor units**: `preflightCommerceMoneyMinorUnits(db)` reports, per column, whether it still holds major units and which rows would be rounded or exceed JavaScript's safe-integer range; `migrateCommerceMoneyToMinorUnits(db)` converts them. Both are exported from the package root. The rescale is idempotent via a `_smrt_backfills` marker, changes the column type in place on PostgreSQL/DuckDB (`ALTER … TYPE BIGINT USING round(col * 100)`), and on SQLite rescales values only — SQLite cannot alter a declared type in place, so those columns come back in `declaredTypeChangePending` and need the table-rebuild path (#2370).
168
- - **`Payment.nativeAmount` is NOT in that migration, and must not be.** Its minor unit is a property of the *row's asset*: satoshis on a BTC rail (×1e8), cents on a fiat or stablecoin rail (×1e2), in one column discriminated only by `nativeCurrency`. A blanket ×100 turns 0.00713 BTC into `1` instead of `713000` sats, and a round 0.01 BTC even passes the preflight's integrality check while being wrong by six orders of magnitude. Convert it in two deliberate steps — normalise each row to its own asset's minor units while the column is still floating-point, then `rescaleMoneyColumnsToMinorUnits(db, COMMERCE_NATIVE_UNIT_COLUMNS, { scale: 1, … })` for the type change. `COMMERCE_NATIVE_UNIT_COLUMNS` is exported with a worked example; step one is deliberately the deployment's to write, because only it knows which `nativeCurrency` values it has used.
169
- - **`PaymentIntent.paymentOptions[].nativeAmount` has the same per-asset problem inside a JSON column and is likewise not migrated.** An intent's price lock defaults to 15 minutes, so the supported answer is to let open intents expire and be re-quoted: an unconverted option fails the exact `!==` reconciliation against the payment that arrives, which is a loud failure rather than a silently mis-scaled quote.
170
- - **Range and existing deployments**: fresh PostgreSQL/DuckDB INTEGER columns are BIGINT, and the runtime rejects values outside JavaScript's safe-integer range rather than rounding them. Existing PostgreSQL `int4` columns do not self-repair; widen them through the explicit migration tracked in #2424.
171
- - **Journals posted from commerce carry minor units**: `Payment.recordPayment()` and `Invoice.recognizeRevenue()` write `amount` / `totalAmount` straight into smrt-ledgers entries. smrt-ledgers is unit-agnostic (its `BALANCE_EPSILON` only checks debits === credits), and integer entries balance exactly, so this is strictly tighter than the major-unit journals commerce used to post.
170
+ - **Money units and arithmetic**: money fields use signed integer minor units; rates and quantities remain decimal where their contracts require them. Exact rounding, tax-code resolution, legacy adapters, safe-integer validation, and journal units are defined in the [commercial money model](agents/money-model.md) and [money calculator](agents/money-calculator.md). Do not reintroduce floating-point tolerances on money paths.
171
+ - **Tenant money migration**: the [tenant line-amount migration](agents/line-amount-migration.md) is authoritative for preflight, the durable journal, tenant barrier, marker activation, issued snapshots, verification, and checksum-guarded rollback. It is distinct from the compatibility behavior described in [document money](agents/document-money.md).
172
+ - **Asset-native amounts stay outside line migration**: `Payment.nativeAmount` and `PaymentIntent.paymentOptions[].nativeAmount` are per-asset values identified by `nativeCurrency`; normalize each asset to its own minor unit before any type conversion, and never apply the commerce ×100 line-money rescale to them.
172
173
  - **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`
173
174
  - **Tax rate is external**: no tax rate field on Invoice — rate must be calculated externally
174
175
  - **Profile linking**: separate `ProfileCollection.create()` needed to fetch actual Profile object
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @happyvertical/smrt-commerce
2
2
 
3
- Commerce models for the s-m-r-t framework. Covers customers, vendors, contracts (9 STI types), invoices with ledger integration, payments, and fulfillment tracking.
3
+ Commerce models for the s-m-r-t framework. Covers customers, vendors, contracts (10 STI types), invoices with ledger integration, payments, and fulfillment tracking.
4
4
 
5
5
  ## Installation
6
6
 
@@ -78,19 +78,112 @@ await payment.recordPayment({
78
78
  });
79
79
  ```
80
80
 
81
+ ## Commercial money calculator
82
+
83
+ The public `/money` entry is a pure calculator for line amounts, discounts,
84
+ component tax, document rounding, and totals. It consumes and returns integer
85
+ minor-unit money values; tax and discount percentages are exact text (`'5'`
86
+ means 5%). It does not persist or load documents.
87
+
88
+ ```ts
89
+ import { calculateMoneyDocument } from '@happyvertical/smrt-commerce/money';
90
+
91
+ const totals = calculateMoneyDocument({
92
+ currency: 'CAD',
93
+ lineAmountType: 'exclusive',
94
+ roundingScope: 'line',
95
+ lines: [{
96
+ id: 'consulting', quantity: 1, unitPrice: 10000,
97
+ discount: { kind: 'none' },
98
+ taxCode: {
99
+ id: 'gst', kind: 'taxable',
100
+ components: [{ taxComponentId: 'gst', percent: '5', compound: false }],
101
+ },
102
+ }],
103
+ });
104
+ ```
105
+
106
+ `resolveLineTaxCode()` resolves caller-loaded codes without I/O, and the
107
+ Svelte `InvoiceEditor` opts into the v2 money controls through its `money`
108
+ prop. Frozen editor documents render a supplied stored snapshot without
109
+ recalculation. See the [commerce money calculator guide](../../docs/content/commerce-money.md)
110
+ for pricing modes, rounding, input and result types, the native-form editor
111
+ contract, and the boundaries this package does not define.
112
+
113
+ ### Tenant tax catalogue and defaults
114
+
115
+ The package root also exports tenant-scoped `TaxComponent`, `TaxCode`, and
116
+ `TaxSettings` models and their collections. Components hold exact percentage
117
+ rates and optional liability accounts; codes combine compatible components; the
118
+ one settings row per tenant supplies sales/purchase defaults, amount mode,
119
+ rounding scope, and labelled registration numbers. A referenced component rate
120
+ is immutable: archive it and create a replacement for a rate change.
121
+
122
+ `ensureTenantTaxCodes(db, tenantId)` idempotently creates the baseline
123
+ non-taxable codes. `applyTaxCodeExamplePack(db, tenantId)` installs illustrative
124
+ regional examples for review, and `applyCommerceCookbookSettings(db, tenantId,
125
+ cookbook)` explicitly materializes a validated cookbook's defaults after schema
126
+ setup and authentication establish the tenant. See the [commerce money
127
+ calculator guide](../../docs/content/commerce-money.md#tenant-tax-catalogue) for
128
+ the supported kinds, seed behavior, and cookbook rate units.
129
+
130
+ ### Persisted Invoice v2
131
+
132
+ The public `Invoice` and `InvoiceLineItem` save paths use the server-owned
133
+ `TaxSettings.moneyModel` gate. Missing settings and `legacy` preserve the
134
+ existing behavior; `migrating` rejects Invoice money writes; `v2` atomically
135
+ recalculates the draft Invoice aggregate from its lines and current tax inputs.
136
+ The marker is not writable through generated surfaces or ordinary saves of
137
+ an existing settings row.
138
+
139
+ Issuing an Invoice (leaving `DRAFT`) or assigning `arJournalId` freezes its
140
+ money snapshot. Header and line money changes then fail with `FROZEN_DOCUMENT`,
141
+ while later lifecycle and payment updates retain the snapshot. Provider tax
142
+ stays separate in `providerTaxAmount` and contributes to the Invoice headers
143
+ without creating an application tax code. In v2,
144
+ `InvoiceLineItemCollection.getTotalForInvoice()` sums line amounts for
145
+ compatibility; use persisted `Invoice.totalAmount` for the amount due. The
146
+ [commerce money calculator guide](../../docs/content/commerce-money.md#persisted-invoice-money)
147
+ and [document-money contract](agents/document-money.md) cover persisted Invoice-v2
148
+ semantics, credits, refunds, and posting. The materialized Invoice tax-report
149
+ descriptor and tenant-safe drilldown are documented in
150
+ [tax-summary contract](agents/tax-summary.md). Tenant migration commands,
151
+ Contract money persistence, PurchaseContract tax contributors, and the
152
+ materialized tax-summary host and drilldown are available. Applications opt in
153
+ by registering `createTaxSummaryMaterializedReportHost()` in their server-only
154
+ report-host registry; the route binds the authenticated tenant and read
155
+ permission. See the tax-summary contract for registration and migration details.
156
+
81
157
  ## API
82
158
 
83
159
  ### Models
84
160
 
161
+ #### Supplier quotations
162
+
163
+ `SupplierQuote` extends `PurchaseContract`, requires `vendorId`, and has
164
+ `ContractType.SUPPLIER_QUOTE`. Create it through `PurchaseContractCollection`
165
+ (or `ContractCollection`) with `_meta_type:
166
+ '@happyvertical/smrt-commerce:SupplierQuote'`. It inherits `expiryDate`,
167
+ `reference`, `notes`, and `terms`; it has no customer relationship. Purchase
168
+ branch reads include quotes and sales branch reads exclude them.
169
+
170
+ Keep received quote revisions and shared quote identity in your application.
171
+ Awarding a quote creates a separate `PurchaseOrder`; accepting or saving a
172
+ quote never converts its identity into an order. Quotes use purchase-side
173
+ money calculation and frozen snapshots, but never contribute incurred purchase
174
+ tax to reports, regardless of status. Existing Estimate and PurchaseOrder
175
+ report eligibility is unchanged.
176
+
85
177
  #### Upgrading contract parties (#3898)
86
178
 
87
179
  `Contract` no longer declares `customerId` or `vendorId`. Narrow a contract to
88
180
  `SalesContract` to read `customerId`, or to `PurchaseContract` to read `vendorId`.
89
- The nine leaf qualified names and the `contracts` table stay the same; a
181
+ The existing leaf qualified names and the `contracts` table stay the same; a
90
182
  `db:migrate --dry-run` against a v0.55.19 schema reports no DDL changes on
91
183
  SQLite or PostgreSQL. Sales leaves no longer accept `vendorId`; purchase leaves
92
184
  no longer accept `customerId`. `Order`, `Estimate`, `Agreement`, `Lease`, and
93
- `WholesaleOrder` require a customer on save. `PurchaseOrder` requires a vendor.
185
+ `WholesaleOrder` require a customer on save. `PurchaseOrder` and `SupplierQuote`
186
+ require a vendor.
94
187
  `Cart`, `LicenseSale`, and `ProductionOrder` retain optional parties on their side.
95
188
 
96
189
  Before upgrading existing data, run the read-only
@@ -127,6 +220,7 @@ This preflight never edits rows; an operator chooses the data correction.
127
220
  | `Order` | Customer purchase order |
128
221
  | `Lease` | Rental or lease agreement |
129
222
  | `Agreement` | Service or maintenance agreement |
223
+ | `SupplierQuote` | Quotation received from a vendor; distinct from an awarded purchase order |
130
224
  | `PurchaseOrder` | Order sent to a vendor/supplier |
131
225
  | `ContractLineItem` | Line item within a contract |
132
226
  | `Invoice` | Billing document with status lifecycle and ledger integration |
@@ -154,7 +248,7 @@ replay, parent revision refresh and compatibility requirements.
154
248
 
155
249
  | Export | Values |
156
250
  |--------|--------|
157
- | `ContractType` | `estimate`, `order`, `lease`, `agreement`, `purchase_order`, `wholesale_order`, `production_order`, `cart`, `license_sale` |
251
+ | `ContractType` | `estimate`, `order`, `lease`, `agreement`, `supplier_quote`, `purchase_order`, `wholesale_order`, `production_order`, `cart`, `license_sale` |
158
252
  | `ContractStatus` | `draft`, `sent`, `accepted`, `declined`, `completed`, `cancelled` |
159
253
  | `CustomerStatus` | `active`, `inactive`, `suspended` |
160
254
  | `VendorStatus` | `active`, `inactive`, `suspended` |
@@ -0,0 +1,111 @@
1
+ # Frozen commerce snapshot → accounting SDK mapper
2
+
3
+ `src/accounting-snapshot.ts` is the pure adapter for an approved commerce money
4
+ snapshot. `src/invoice-accounting-sync.ts` is the owning persistence and
5
+ provider boundary. It re-reads the issued Invoice, InvoiceLineItem and
6
+ DocumentTax rows and verifies their shared `moneySnapshotId` before calling the
7
+ pure mapper. `Invoice.toAccountingInput()` keeps its legacy behavior for legacy
8
+ tenants; a money-v2 tenant must supply explicit QuickBooks realm mapping and is
9
+ routed through the frozen boundary.
10
+
11
+ The released `@happyvertical/accounting` 0.103.7 path accepts frozen invoices
12
+ and positive credit memos. Commerce sends persisted pre-discount unit price,
13
+ line discount, net amount, allocated tax, and inclusive gross amount through
14
+ the SDK's public input types. It never sends a numeric `taxRate`; every taxable
15
+ line has explicit Item, TaxCode, and component TaxRate references. Exempt and
16
+ out-of-scope lines carry no fabricated components. No dependency override or
17
+ private SDK import is used.
18
+
19
+ The caller supplies persisted document and line facts: document mode and
20
+ rounding scope, integer-minor header totals, line pre-discount unit price and
21
+ discount, post-discount net amount, and each component's ID, exact percent text,
22
+ and allocated minor tax amount. The mapper converts only ISO 4217 minor units
23
+ to major units. It never calculates a discount, tax, rate, or allocation. For
24
+ an inclusive line, `taxInclusiveAmount` is the exact sum of the persisted net
25
+ and persisted tax facts, so the SDK can serialize QBO `TaxInclusiveAmt`.
26
+
27
+ `FrozenQuickBooksTaxMapping` is tenant/realm configuration and must supply an
28
+ Item.Id and TaxCode.Id for every line plus a TaxRate.Id for every stored
29
+ component. The mapper refuses missing mappings and never derives a provider ID
30
+ from a tax-code name, percent, or component identifier. It also refuses a
31
+ nonzero `providerTaxMinor`: header-only provider tax has no frozen component or
32
+ rate reference and cannot safely become a QBO tax line.
33
+
34
+ The mapper checks only snapshot identities before any provider I/O: each line's
35
+ component sum equals its stored tax, lines sum to header net/tax, and header net
36
+ plus tax equals total. `taxRoundingScope` is retained as frozen provenance; no
37
+ rounding is repeated. Currency exponents come from the shared ISO map, so JPY
38
+ and KWD retain their exact stored values.
39
+
40
+ Credit memos use positive input values and require `creditedInvoiceExternalId`.
41
+ Their provider direction is the SDK's `CreditMemo` operation. The owning sync
42
+ boundary calls the public `requireCreditMemoOperations()` capability guard
43
+ before persisting an in-flight owner, prepares and persists the SDK's
44
+ `quickbooksCreditMemoRequest`, then records the returned external ID. The
45
+ source invoice must have a reconciled QuickBooks identity in the same realm and
46
+ environment. The adapter never auto-applies a credit memo to its invoice.
47
+
48
+ ## Sync identity and recovery
49
+
50
+ `syncFrozenInvoiceToAccounting()` derives a QuickBooks request identity from
51
+ the provider, realm, environment, document id, and immutable
52
+ `moneySnapshotId`. The SDK forwards the persisted `requestId` as QuickBooks'
53
+ `requestid` parameter and validates the payload hash. Commerce persists an
54
+ `in_flight` owner before provider I/O, so a competing call is refused instead
55
+ of issuing a second create. A successful call persists the external id and a
56
+ `synced` state; another call for the same identity returns `unchanged` without
57
+ provider I/O. A different realm, environment, provider, or snapshot never
58
+ shares that no-op.
59
+
60
+ The database transaction and provider request are separate phases. Commerce
61
+ does not claim they are atomic. If the provider outcome or the post-write local
62
+ commit is uncertain, the stored state requires reconciliation and refuses a
63
+ blind retry. A provider rejection known not to have committed may be retried
64
+ with the same prepared request. `readFrozenAccountingSyncState()` exposes the
65
+ request descriptor needed by an operator-owned reconciliation workflow.
66
+
67
+ Recovery is an explicit application flow rather than a database repair:
68
+
69
+ 1. Load the invoice and call `readFrozenAccountingSyncState()`.
70
+ 2. Use its realm, environment, `requestId`, and payload hash to query the
71
+ configured QuickBooks realm. An absence result does not establish that an
72
+ active request has stopped. For `in_flight`, first terminate or otherwise
73
+ positively establish that the original process/request owner cannot finish.
74
+ 3. Call `reconcileFrozenInvoiceAccountingSync()` with that same request ID,
75
+ attempt ID, and mapping. Report `found` with the provider document ID and
76
+ observed timestamp, or report `absent` only after QuickBooks authoritatively
77
+ confirms no create.
78
+ 4. A `found` result records the sync and makes later calls no-ops. An `absent`
79
+ result returns `ready`; the next sync reuses the identical prepared request.
80
+
81
+ The reconciliation API accepts both `unknown` and interrupted `in_flight`
82
+ records so a process crash after the remote write cannot wedge the invoice.
83
+ It rejects a changed snapshot, request, realm, environment, or mapping. It does
84
+ not perform the remote lookup itself. A normal caller cannot reconcile an
85
+ `in_flight` record; the trusted server caller must pass
86
+ `confirmInterruptedOwnerStopped: true` after establishing quiescence. That flag
87
+ is an explicit operational assertion, not a lease inferred from time or from a
88
+ provider lookup. `unknown` records already have no live local owner and do not
89
+ require it.
90
+
91
+ ## Deployment and capability boundary
92
+
93
+ `Invoice.accountingSyncStateJson` is a sensitive, server-owned persistence
94
+ field. It is excluded from public serialization and from the shared generated
95
+ REST, MCP/Chat, and CLI writable allowlist. Deployments must apply the normal
96
+ generated schema migration that adds `accounting_sync_state_json` before using
97
+ this sync API. Runtime code never creates or alters schema.
98
+
99
+ Signed adjustment lines remain a known acceptance gap. The commerce money model
100
+ can persist signed adjustments inside a positive document, but the accounting
101
+ SDK has no reviewed signed-line representation. The owning sync boundary
102
+ therefore rejects them before state or provider mutation. Header-only provider
103
+ tax is also refused because it has no persisted component identity or realm tax
104
+ rate mapping. Inclusive documents, discounted lines, exempt lines, and positive
105
+ credit memos use the released public SDK contract.
106
+
107
+ The mapper's A/B fixtures are the commerce calculator's actual CAD goldens:
108
+ example A is tax-exclusive and example B is tax-inclusive. Both cover line and
109
+ document allocation; example B's document scope preserves line 3's allocated
110
+ GST cent. Focused sync tests inspect the exact public SDK request inputs and
111
+ persisted create descriptors without performing a provider network write.
@@ -0,0 +1,24 @@
1
+ # Contract money persistence
2
+
3
+ Contract money runs through `contract-money.ts`. A tenant operation enters
4
+ `withTenantMoneyRuntime` before it locks and reloads its Contract and sibling
5
+ lines on the scoped executor. The legacy barrier variants retain the old line
6
+ calculation; only active v2 tenants resolve tax codes and replace the Contract
7
+ aggregate plus `DocumentTax` rows in the same transaction.
8
+
9
+ Draft Contract line writes recompute every sibling. `subtotal` is tax-exclusive
10
+ net after discount, `taxAmount` is the component total, and `totalAmount` is
11
+ their exact sum. Purchase component tax is recoverable only when the frozen
12
+ component says so. A Contract leaving draft snapshots registrations, party tax
13
+ identity, per-line component amounts, and `moneySnapshotId`; later money edits
14
+ are refused with `FROZEN_DOCUMENT`.
15
+
16
+ `materializeContractMoneySnapshot` is migration-only. Its caller supplies the
17
+ already transaction-bound migration owner executor; it verifies that owner,
18
+ locks/reloads the Contract, and never starts a runtime transaction.
19
+
20
+ `SupplierQuote` uses purchase defaults and recoverability, including during
21
+ legacy-to-v2 migration. Non-draft quotes freeze just like other Contracts, but
22
+ quote snapshots (including custom descendants) are excluded by their hydrated
23
+ STI subtype, not the mutable `contractType` scalar, from report candidates and
24
+ purchase tax facts. Awarding requires a separate PurchaseOrder.
@@ -3,10 +3,15 @@
3
3
  `@happyvertical/smrt-commerce/cost-billing` is an independent, transport-neutral
4
4
  entry point without model, server or Svelte dependencies. It does not persist,
5
5
  post, authorize, select a currency/jurisdiction, or implement credit-note lifecycle.
6
- Existing Invoice calculations and editor defaults are unchanged. In particular,
6
+ Legacy Invoice calculations and editor defaults are unchanged by this entry
7
+ point. In particular,
7
8
  `calculateInvoiceMinorLine` retains negative ties toward positive infinity; the
8
9
  new opt-in helpers use symmetric half-away-from-zero ties. Positive-only invoice
9
- editor validation remains distinct from these generic signed calculations.
10
+ editor validation remains distinct from these generic signed calculations. The
11
+ `roundingScope: 'invoice'` behavior in this guide is a standalone cost-billing
12
+ convention; v2 document arithmetic uses `roundingScope: 'document'` and stores
13
+ allocated tax facts. See the [commercial money model](money-model.md) and
14
+ [money calculator](money-calculator.md).
10
15
 
11
16
  Call `resolveCostMarkup(overrides)` with caller-approved precedence, most specific
12
17
  first. Each item has `source`, optional `policy` and `version`, and optional
@@ -174,7 +174,13 @@ vitest.selects.config.ts` (also part of `pnpm test:ui`).
174
174
  Minor-unit amounts on Contract, Invoice, their line items, Payment,
175
175
  PaymentAllocation, `Customer.creditLimit` and `Vendor.minimumOrderAmount`
176
176
  carry `ui: { format: 'money' }` (#3718; `widget: 'currency'` stays the
177
- currency-code picker on the text `currency` field). Payout and Payment's
177
+ currency-code picker on the text `currency` field). That includes the
178
+ money-v2 fields (#4011): line `taxAmount`, `discountTotal` on Invoice and
179
+ Contract, `Invoice.creditedAmount`, and `DocumentTax` `taxableMinor` /
180
+ `taxMinor` / `recoverableMinor`; the test asserts every integer named
181
+ `*Amount`, `*Total`, `*Minor` or `subtotal` on those models (and Contract's
182
+ STI subtypes) is hinted. `legacyAmount` is `@internal` migration-rollback
183
+ state, not a user-facing amount, and stays unhinted. Payout and Payment's
178
184
  native/USD drift amounts are not hinted: their currency is a payout rail or
179
185
  fixed USD, not the record's `currency`. The line-item FKs
180
186
  (`ContractLineItem.contractId`, `InvoiceLineItem.invoiceId`,
@@ -0,0 +1,127 @@
1
+ # Document money persistence
2
+
3
+ Tenant money behavior is selected by the server-owned `TaxSettings.moneyModel`
4
+ marker. Missing settings and `legacy` preserve the existing line-save behavior.
5
+ `migrating` is a persisted maintenance guard: invoice money writes fail until
6
+ the owning migration restores `legacy` or activates `v2`. Generated surfaces
7
+ cannot write either the marker or its owner token.
8
+
9
+ Every Invoice/InvoiceLineItem get, list, preview, mutation, posting, and
10
+ accounting-sync phase acquires the same tenant settings row before document
11
+ rows. The barrier is held through the owning transaction, so migration cannot
12
+ start between the marker check and a money read/write. Nested coordinators
13
+ reuse an explicitly registered transaction executor; adapter shape is never
14
+ used as transaction proof. When the settings table exists, the first legacy
15
+ runtime boundary materializes the deterministic legacy row solely as this
16
+ durable serialization point. Older schemas without the table remain legacy
17
+ and runtime never creates schema.
18
+
19
+ In `v2`, saving an invoice line locks and reloads its parent on the caller's
20
+ transaction, reloads every sibling, resolves current tax defaults, calculates
21
+ the document once, and writes line snapshots, `DocumentTax`, and header totals
22
+ atomically. Tax resolution order is line override, customer exemption (unless
23
+ the line overrides), product, customer, then app default. Product reads use
24
+ the products package's collection on that same transaction executor;
25
+ tenant-owned and catalog-global products are accepted.
26
+
27
+ The first transition away from `DRAFT`, or setting `arJournalId`, freezes the
28
+ money document. The invoice and every line receive the same stable
29
+ `moneySnapshotId`. Later payment and lifecycle saves keep that identity.
30
+ That persisted snapshot identity remains the freeze authority if payment
31
+ allocation changes move the lifecycle status back to `DRAFT`; it does not turn
32
+ an issued document back into an editable money draft.
33
+ Frozen fields include party/customer identity, currency, issue and due dates,
34
+ document amount/rounding settings, seller and party tax snapshots, line
35
+ description/SKU/product, quantity/pricing/discount modes, tax code and component
36
+ snapshots, source period, revenue account, and sort order. Header and child
37
+ money edits, child moves, additions, and deletions then fail with
38
+ `FROZEN_DOCUMENT`; status, payment, reminder, and delivery metadata continue
39
+ through their existing lifecycle guards.
40
+
41
+ Credit notes reuse `Invoice` with `kind: 'credit_note'` and a frozen
42
+ `creditedInvoiceId`; their `DocumentTax.documentType` is `credit_note`. Stored
43
+ header amounts remain positive and posting/report direction comes from `kind`.
44
+ A targeted credit line carries `creditedLineId` plus a server-derived frozen
45
+ calculation basis. The credit document inherits and re-enforces its source
46
+ `lineAmountType` and `taxRoundingScope`; later tenant defaults cannot reinterpret
47
+ the frozen unit price or tax allocation. Quantity credits reuse the source price
48
+ and allocate its frozen flat or percentage discount from cumulative credited
49
+ quantity; this prevents repeated rounding from exceeding the source discount,
50
+ and the final quantity receives its exact remaining discount. Amount credits use
51
+ an explicit tax-exclusive magnitude and restore the source line sign. A quantity
52
+ request is also bounded by tax-exclusive value already consumed through amount
53
+ credits, so mixing the two bases cannot exceed the source line. Tax components,
54
+ labels, rates, and accounts come from the issued source snapshot. When a request
55
+ exhausts every remaining original line, the final line also receives the exact
56
+ remaining document/component residual.
57
+ Free-form goodwill lines require an explicit tax code and do not consume source
58
+ line capacity.
59
+
60
+ Issued credit notes reduce the source `getBalance()` and update its
61
+ server-managed `creditedAmount` under the same locks. Refunds are positive
62
+ `Payment` rows with `kind: 'refund'`, allocated only to credit notes; their
63
+ available amount is the unapplied issued credit after source settlement and
64
+ prior settled refunds. Incoming payments debit cash and credit receivables;
65
+ refund journals reverse those sides.
66
+
67
+ `previewInvoiceMoney({ db, tenantId, invoiceId, draft })` applies a proposed
68
+ draft overlay through the owning resolver/calculator without writes and rejects
69
+ frozen documents. A supplied line id must name a stored line exactly once; omit
70
+ the id for a new preview-only line. `getInvoiceMoneyFacts({ db, tenantId,
71
+ invoiceId })` reads the authoritative stored header, lines, `moneySnapshotId`,
72
+ and `DocumentTax` rows without recalculation. Both facades require an active
73
+ `v2` tenant and fail closed while migration is in progress, so legacy or
74
+ partially migrated amounts are never presented as money-v2 facts.
75
+
76
+ ### Record-screen composition
77
+
78
+ `loadInvoiceRecordScreenData({ db, tenantId, invoiceId })` is the commerce
79
+ server adapter for a generic invoice record page. A caller must already have
80
+ that exact active tenant; the adapter never switches principals based on its
81
+ argument. It holds the shared v2 money read barrier while it reads the stored
82
+ header, lines, `DocumentTax`, and related credit-note summaries, so a migration
83
+ cannot mix a stale header with a different snapshot of those rows. Its totals
84
+ are a direct projection of persisted minor-unit values and component snapshots;
85
+ it does not resolve a code or recalculate tax.
86
+
87
+ `InvoiceRecordScreen` from `@happyvertical/smrt-commerce/svelte` composes that
88
+ projection with `RecordScreen.lineItemStoredTotals`, the released generic
89
+ line-items widget contract. A host passes its own generic screen wiring and
90
+ may expose `onCreateCreditNote` only with a server-derived
91
+ `canCreateCreditNote` authorization result. The wrapper hides the action for
92
+ credit-note records and never treats a client callback as authorization.
93
+
94
+ `InvoiceLineItem.amount` is tax-exclusive net money in `v2`.
95
+ `taxAmount` is separate, while `legacyAmount` is reserved for rollback during
96
+ the compatibility migration. `taxCodeId` and component identity may be null for
97
+ a genuine unassigned zero-tax line. Optional `productId` and
98
+ `revenueAccountId` are nullable UUID references so PostgreSQL never receives an
99
+ empty-string sentinel.
100
+
101
+ `DocumentTax` is derived and read-only. Its rows snapshot the code/component
102
+ labels, percentage, liability account, taxable minor units, and tax minor
103
+ units. Provider tax remains the invoice header's `providerTaxAmount`; commerce
104
+ does not invent a tax code or component for provider-owned tax.
105
+
106
+ Revenue recognition for `v2` reads only the frozen invoice, lines, and
107
+ `DocumentTax` rows. It groups each line's stored net by its snapshotted revenue
108
+ account (falling back to the caller's revenue account), groups component tax by
109
+ the snapshotted liability account (falling back to the caller's tax account),
110
+ and posts provider tax to the fallback once. Non-taxable zero rows do not post.
111
+ Every entry is an exact non-negative debit or credit in the invoice currency.
112
+ The caller must supply its authoritative `bookCurrency`; commerce rejects a
113
+ missing, unsupported, or different currency because ledgers has no book model.
114
+
115
+ The revenue journal identity derives from the tenant, invoice, and stable
116
+ `moneySnapshotId`. Creation, posting, and the invoice's `arJournalId` link share
117
+ the invoice parent lock and transaction, so retries converge. Account IDs from
118
+ frozen snapshots are still authorization checked: every used account must be
119
+ active and owned by the invoice tenant.
120
+
121
+ A posted `v2` invoice cannot move directly to `CANCELLED`. Call
122
+ `cancelWithRevenueReversal({ bookCurrency })`; it reads the original posted
123
+ journal, posts one deterministic journal with every debit and credit reversed,
124
+ and stores `arReversalJournalId` with the status transition atomically. The
125
+ original journal remains posted. Failed link or reversal writes roll back the
126
+ entire operation, and retry returns the same reversal. Legacy tenants keep the
127
+ existing recognition and cancellation behavior.
@@ -2,6 +2,11 @@
2
2
 
3
3
  `@happyvertical/smrt-commerce/invoices` is a Svelte-free entry usable by server handlers and browsers. `calculateInvoiceLine`, `calculateInvoiceDraft`, and `resolveInvoiceLineDraft` share exact decimal arithmetic. The UI consumes these helpers for previews; preview results confer no authority.
4
4
 
5
+ This guide describes the standalone, legacy-compatible invoice editor helpers.
6
+ For tenant `moneyModel: 'v2'` document arithmetic, persisted tax components, and
7
+ issued snapshots, use the [commercial money model](money-model.md) and [money
8
+ calculator](money-calculator.md) instead.
9
+
5
10
  Drafts retain unparsed text. Quantity accepts nonnegative decimals with at most six fractional digits and must round-trip through the model numeric representation. Prices and flat discounts accept only whole currency minor units expressed in major-unit text. Currency uses the shared explicit ISO minor-unit table; unknown codes and codes without a minor unit are rejected. Servers must authorize their supported currency set. No exchange conversion occurs. Rates are percentages from 0 through 100 with at most six fractional digits. `discountType` is `flat` or `percent`; `taxMode` is `inherit` or `override`. An override of `0` is zero tax; an empty override is invalid. Inherited lines ignore their dormant override string.
6
11
 
7
12
  Calculation order is quantity × price rounded half up to minor units, discount rounded half up and subtracted, then tax on the discounted subtotal rounded half up. Flat discounts cannot exceed gross. Numeric model quantities and rates use their canonical decimal representation (including supported exponent notation), never a `toFixed` expansion of binary approximation. No floating-point multiplication or epsilon comparison determines money. Unsafe individual or aggregate results are rejected; invalid lines produce errors without monetary totals. Header validation requires customer, real issue/due dates with due date not before issue date, valid currency shape/default rate, at least one line, and unique nonblank row keys.
@@ -12,13 +17,18 @@ Trailing zeroes do not make an exact amount invalid: `100.00` JPY is 100 minor u
12
17
 
13
18
  ## Server responsibilities
14
19
 
15
- Parse native form values into the typed draft (never pass unvalidated arbitrary JSON as the typed object), enforce request/row limits, authorize customer and source references in the active tenant, and validate edits against invoice lifecycle state. Replace inherited rate/currency policy with server-authorized context, call the shared validator/resolver, and save resolved rows and invoice within the application's transaction/retry contract. Ignore any client-supplied calculated amount. Default tax remains externally determined; this module is not a tax determination service. Caller-owned extension allocations, budget availability and approval evidence require their own server validation and are not part of line pricing.
20
+ Parse native form values into the typed draft (never pass unvalidated arbitrary JSON as the typed object), enforce request/row limits, authorize customer and source references in the active tenant, and validate edits against invoice lifecycle state. Replace inherited rate/currency policy with server-authorized context, call the shared validator/resolver, and save resolved rows and invoice within the application's transaction/retry contract. Ignore any client-supplied calculated amount. For these legacy-compatible helpers, default tax remains externally determined; this module is not a tax determination service. V2 tax-code resolution and stored tax facts are defined by the [commercial money model](money-model.md). Caller-owned extension allocations, budget availability and approval evidence require their own server validation and are not part of line pricing.
16
21
 
17
22
  Changing currency changes the precision and denomination of the entered values, not their numeric text. The UI must not silently convert entered amounts or rewrite invalid drafts. Reopening or changing authorized tax context requires fresh server validation.
18
23
 
19
24
 
20
25
  ## Explicit tax rounding scope
21
26
 
27
+ The `roundingScope: 'invoice'` option below is the standalone editor's
28
+ legacy-compatible aggregate calculation. It is distinct from v2's
29
+ `roundingScope: 'document'`, which allocates rounded tax into persisted
30
+ document tax facts; see the [money calculator](money-calculator.md).
31
+
22
32
  `calculateInvoiceDraft(values, { roundingScope: 'invoice' })` and the shared
23
33
  `InvoiceEditor` / `InvoiceLineEditor` `roundingScope` prop accept `line` or
24
34
  `invoice`. Omission preserves per-line rounding. The server chooses this option
@@ -41,4 +51,6 @@ sum to the aggregate. In invoice mode the UI displays each line's subtotal and
41
51
  an explanation, with tax and total shown only at invoice level. Persist the
42
52
  aggregate with the authorized scope and retained draft when using invoice
43
53
  rounding; do not reconstruct it by summing standalone resolved line totals.
44
- No persistence schema or `InvoiceLineItem` tax behavior is changed by this API.
54
+ No persistence schema or `InvoiceLineItem` tax behavior is changed by this
55
+ legacy-compatible adapter API. V2 persistence and issued tax snapshots are
56
+ defined by the [commercial money model](money-model.md).
@@ -0,0 +1,50 @@
1
+ # Tenant line-amount migration
2
+
3
+ The operational CLI entrypoints are documented in
4
+ `@happyvertical/smrt-cli/agents/commerce-money-migration.md`. A CLI migration
5
+ chooses and prints its owner UUID before the tenant is claimed, so a failure
6
+ after the claim can be resumed with the exact same `--migration-id`; a
7
+ different owner is rejected.
8
+
9
+ `preflightCommerceLineAmounts(db, { tenantId?, out? })` is read-only. It does
10
+ not create `TaxSettings`. Review its structured report (and optional explicit
11
+ `jsonPath`/`csvPath`) before migration. `UNRECONCILED`, orphan or cross-tenant
12
+ lines, missing tenancy, an ambiguous base Contract direction, and an
13
+ `in_flight`/`unknown` accounting sync block the tenant before any marker or
14
+ document mutation. Draft total changes also require `acceptDraftChanges`.
15
+
16
+ `migrateCommerceLineAmounts(db, { tenantId, acceptDraftChanges?, migrationId? })`
17
+ claims the tenant marker, maps legacy rates, and processes each document under
18
+ the shared tenant barrier. A sensitive internal
19
+ `MoneyMigrationDocument` record commits its checksummed pre-image before the
20
+ document transaction. The applied transaction stores a checksummed post-image;
21
+ an exact retry skips it, while any mismatch fails closed. Runtime reads and
22
+ writes remain blocked until every issued document verifies and the marker is
23
+ written `v2` last.
24
+
25
+ Issued migration preserves every previously stored header field, timestamp,
26
+ revision and journal link. The only header writes are the additive v2 snapshot
27
+ columns `discount_total`, `line_amount_type`, `tax_rounding_scope`,
28
+ `seller_tax_registrations_json`, `party_tax_id`, and `money_snapshot_id`.
29
+ Issued legacy snapshots always record `line_amount_type=exclusive` and
30
+ `tax_rounding_scope=line`, which describe the historical storage contract;
31
+ current tenant defaults apply only when calculating new v2 drafts.
32
+ Drafts are recomputed through the owning aggregate after their legacy rate is
33
+ promoted to an explicit code override. When a tenant has both exempt and
34
+ ordinary zero-rate history, migration selects the durable code from each
35
+ document's linked customer: exempt customers remain `exempt`; other zero-rate
36
+ documents remain `out-of-scope`.
37
+
38
+ `rollbackCommerceLineAmounts(db, { tenantId, migrationId })` claims the tenant
39
+ barrier and validates journal existence plus every current post-image in one
40
+ transaction. Every current tenant invoice and contract must have a matching
41
+ journal entry; an unjournaled header was created under v2 and makes rollback
42
+ unsafe, so rollback refuses before changing the marker or document. Any other
43
+ refusal before restoration begins also rolls the claim back, so the tenant
44
+ remains on v2 and ordinary runtime reads stay available. Once restoration
45
+ begins, each document commits separately under the same owner; an interruption
46
+ leaves the marker in `migrating`, and retrying that owner skips durable
47
+ `rolled_back` journal entries. Exact pre-images restore the header, lines,
48
+ taxes, timestamps and revisions, then the marker returns to `legacy`.
49
+ The journal is installed by normal schema migration and retained through the
50
+ rollback window. Runtime code never creates its table.
@@ -0,0 +1,20 @@
1
+ # Legacy major-unit to minor-unit migration
2
+
3
+ This guide covers the existing numeric-column rescale only. It is not the
4
+ Invoice-v2 tenant money-model migration: it does not change `TaxSettings`,
5
+ calculate tax, create snapshots, or activate v2.
6
+
7
+ `preflightCommerceMoneyMinorUnits(db)` reports whether known commerce columns
8
+ still contain major units, rows that would round, and values outside JavaScript
9
+ safe-integer range. After reviewing its report, run
10
+ `migrateCommerceMoneyToMinorUnits(db)`. The migration is idempotent through its
11
+ `_smrt_backfills` marker. PostgreSQL and DuckDB change columns to `BIGINT` and
12
+ rescale values; SQLite rescales values but reports its required declared-type
13
+ table rebuild as `declaredTypeChangePending`.
14
+
15
+ `Payment.nativeAmount` and `PaymentIntent.paymentOptions[].nativeAmount` are
16
+ excluded. Their units depend on the row currency or asset (for example BTC
17
+ satoshis), so a blanket ×100 is wrong. First normalize each value to its own
18
+ asset's minor unit, then use `rescaleMoneyColumnsToMinorUnits()` with scale 1
19
+ for `Payment.nativeAmount`. Let open payment intents expire and be re-quoted;
20
+ their JSON options have no safe blanket conversion.