@happyvertical/smrt-commerce 0.56.2 → 0.57.1

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 (181) hide show
  1. package/AGENTS.md +24 -11
  2. package/README.md +137 -89
  3. package/agents/accounting-snapshot-mapper.md +3 -3
  4. package/agents/commercial-ui.md +2 -2
  5. package/agents/contract-money.md +9 -12
  6. package/agents/cost-billing.md +3 -6
  7. package/agents/customer-vendor-ui.md +1 -2
  8. package/agents/document-money.md +58 -31
  9. package/agents/invoice-calculations.md +31 -56
  10. package/agents/invoice-preparation.md +62 -73
  11. package/agents/invoice-quantity.md +9 -21
  12. package/agents/minor-unit-migration.md +4 -4
  13. package/agents/money-calculator.md +7 -8
  14. package/agents/money-model.md +18 -22
  15. package/agents/money-upgrade.md +92 -0
  16. package/agents/revenue-journal.md +2 -2
  17. package/agents/tax-codes.md +17 -14
  18. package/agents/tax-summary.md +8 -6
  19. package/dist/billing/period-close.d.ts.map +1 -1
  20. package/dist/billing/runtime.d.ts +5 -2
  21. package/dist/billing/runtime.d.ts.map +1 -1
  22. package/dist/chunks/{ContractLineItemCollection--BrGMtNz.js → ContractLineItemCollection-BZcrlrrY.js} +19 -24
  23. package/dist/chunks/ContractLineItemCollection-BZcrlrrY.js.map +1 -0
  24. package/dist/chunks/{CustomerCollection-CjGaZSpf.js → CustomerCollection-BIPH2ZUx.js} +2 -2
  25. package/dist/chunks/{CustomerCollection-CjGaZSpf.js.map → CustomerCollection-BIPH2ZUx.js.map} +1 -1
  26. package/dist/chunks/{DocumentTaxCollection-KOkaQT2M.js → DocumentTaxCollection-DDzlywMW.js} +14 -7
  27. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +1 -0
  28. package/dist/chunks/{InvoiceCollection-BUe_8FT1.js → InvoiceCollection-cinzAqt6.js} +50 -161
  29. package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +1 -0
  30. package/dist/chunks/{TaxCodeCollection-DF2eaJed.js → TaxCodeCollection-DaBOPG9c.js} +11 -4
  31. package/dist/chunks/{TaxCodeCollection-DF2eaJed.js.map → TaxCodeCollection-DaBOPG9c.js.map} +1 -1
  32. package/dist/chunks/{TaxComponentCollection-3x24xSNh.js → TaxComponentCollection-C4zxnyVu.js} +3 -3
  33. package/dist/chunks/{TaxComponentCollection-3x24xSNh.js.map → TaxComponentCollection-C4zxnyVu.js.map} +1 -1
  34. package/dist/chunks/VendorCollection-D6ZcUYy8.js +100 -0
  35. package/dist/chunks/VendorCollection-D6ZcUYy8.js.map +1 -0
  36. package/dist/chunks/{allocation-integrity-Cq0g6CfO.js → allocation-integrity-WmWyXhtG.js} +6 -6
  37. package/dist/chunks/{allocation-integrity-Cq0g6CfO.js.map → allocation-integrity-WmWyXhtG.js.map} +1 -1
  38. package/dist/chunks/calculations-C9daQLsh.js +82 -0
  39. package/dist/chunks/calculations-C9daQLsh.js.map +1 -0
  40. package/dist/chunks/contract-money-EuToOghh.js +301 -0
  41. package/dist/chunks/contract-money-EuToOghh.js.map +1 -0
  42. package/dist/chunks/detect-CY9ufaFb.js +328 -0
  43. package/dist/chunks/detect-CY9ufaFb.js.map +1 -0
  44. package/dist/chunks/{document-money-oUDvdjOf.js → document-money-DV5t4vlK.js} +388 -62
  45. package/dist/chunks/document-money-DV5t4vlK.js.map +1 -0
  46. package/dist/chunks/{invoice-posting-CF3fE_Ec.js → invoice-posting-C2i5dOVZ.js} +20 -22
  47. package/dist/chunks/invoice-posting-C2i5dOVZ.js.map +1 -0
  48. package/dist/chunks/{revenue-journal-DY9fro8E.js → revenue-journal-LLA9Kh_i.js} +3 -14
  49. package/dist/chunks/revenue-journal-LLA9Kh_i.js.map +1 -0
  50. package/dist/chunks/tenant-money-barrier-6TlqUeVj.js +255 -0
  51. package/dist/chunks/tenant-money-barrier-6TlqUeVj.js.map +1 -0
  52. package/dist/chunks/types-Bbm0Xflc.js.map +1 -1
  53. package/dist/chunks/{units-CNcjH_eE.js → units-8ElbjVZK.js} +18 -3
  54. package/dist/chunks/units-8ElbjVZK.js.map +1 -0
  55. package/dist/collections/ContractCollection.d.ts.map +1 -1
  56. package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
  57. package/dist/collections/InvoiceCollection.d.ts.map +1 -1
  58. package/dist/collections/InvoiceLineItemCollection.d.ts +0 -30
  59. package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
  60. package/dist/collections/TaxCodeCollection.d.ts +5 -1
  61. package/dist/collections/TaxCodeCollection.d.ts.map +1 -1
  62. package/dist/collections/TaxSettingsCollection.d.ts +15 -8
  63. package/dist/collections/TaxSettingsCollection.d.ts.map +1 -1
  64. package/dist/collections/TaxSummaryReportCollection.d.ts +3 -3
  65. package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -1
  66. package/dist/collections/index.d.ts +0 -1
  67. package/dist/collections/index.d.ts.map +1 -1
  68. package/dist/commerce-money-tools.d.ts.map +1 -1
  69. package/dist/contract-money.d.ts +1 -13
  70. package/dist/contract-money.d.ts.map +1 -1
  71. package/dist/document-money-internal.d.ts +7 -3
  72. package/dist/document-money-internal.d.ts.map +1 -1
  73. package/dist/document-money.d.ts +1 -11
  74. package/dist/document-money.d.ts.map +1 -1
  75. package/dist/index.d.ts +6 -5
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +667 -1650
  78. package/dist/index.js.map +1 -1
  79. package/dist/invoice-accounting-sync.d.ts.map +1 -1
  80. package/dist/invoice-mcp.d.ts +3 -3
  81. package/dist/invoice-mcp.d.ts.map +1 -1
  82. package/dist/invoice-posting.d.ts +11 -5
  83. package/dist/invoice-posting.d.ts.map +1 -1
  84. package/dist/invoice-record-screen.d.ts +1 -1
  85. package/dist/invoice-record-screen.d.ts.map +1 -1
  86. package/dist/invoices.d.ts +6 -1
  87. package/dist/invoices.d.ts.map +1 -1
  88. package/dist/invoices.js +2 -2
  89. package/dist/manifest.json +37 -765
  90. package/dist/migrations/invoiceEditorStorage.d.ts +2 -4
  91. package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -1
  92. package/dist/models/Contract.d.ts.map +1 -1
  93. package/dist/models/ContractLineItem.d.ts +6 -22
  94. package/dist/models/ContractLineItem.d.ts.map +1 -1
  95. package/dist/models/Invoice.d.ts +18 -48
  96. package/dist/models/Invoice.d.ts.map +1 -1
  97. package/dist/models/InvoiceLineItem.d.ts +8 -37
  98. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  99. package/dist/models/TaxSettings.d.ts +0 -10
  100. package/dist/models/TaxSettings.d.ts.map +1 -1
  101. package/dist/models/index.d.ts +0 -1
  102. package/dist/models/index.d.ts.map +1 -1
  103. package/dist/playground.js +1 -1
  104. package/dist/playground.js.map +1 -1
  105. package/dist/recipes.d.ts +8 -11
  106. package/dist/recipes.d.ts.map +1 -1
  107. package/dist/revenue-journal.d.ts +0 -8
  108. package/dist/revenue-journal.d.ts.map +1 -1
  109. package/dist/smrt-knowledge.json +79 -661
  110. package/dist/svelte/components/InvoiceEditor.svelte +7 -148
  111. package/dist/svelte/components/InvoiceEditor.svelte.d.ts +4 -1
  112. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  113. package/dist/svelte/components/InvoiceTotals.svelte +14 -118
  114. package/dist/svelte/components/InvoiceTotals.svelte.d.ts +4 -27
  115. package/dist/svelte/components/InvoiceTotals.svelte.d.ts.map +1 -1
  116. package/dist/svelte/components/MoneyInvoiceEditor.svelte +3 -23
  117. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +3 -26
  118. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -1
  119. package/dist/svelte/index.d.ts +2 -3
  120. package/dist/svelte/index.d.ts.map +1 -1
  121. package/dist/svelte/index.js +1 -2
  122. package/dist/svelte/invoices/calculations.d.ts +3 -108
  123. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  124. package/dist/svelte/invoices/calculations.js +12 -309
  125. package/dist/svelte/invoices/messages.d.ts +1 -37
  126. package/dist/svelte/invoices/messages.d.ts.map +1 -1
  127. package/dist/svelte/invoices/messages.js +1 -37
  128. package/dist/svelte/invoices/money-editor.d.ts +1 -1
  129. package/dist/svelte/invoices/money-editor.d.ts.map +1 -1
  130. package/dist/svelte/invoices/types.d.ts +14 -114
  131. package/dist/svelte/invoices/types.d.ts.map +1 -1
  132. package/dist/svelte/playground/InvoicePreparationPreview.svelte +85 -94
  133. package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -1
  134. package/dist/svelte/playground.js +1 -1
  135. package/dist/tax/index.d.ts +2 -1
  136. package/dist/tax/index.d.ts.map +1 -1
  137. package/dist/tax/standard-codes.d.ts +12 -0
  138. package/dist/tax/standard-codes.d.ts.map +1 -0
  139. package/dist/tax/tenant-bootstrap.d.ts +29 -0
  140. package/dist/tax/tenant-bootstrap.d.ts.map +1 -0
  141. package/dist/tenant-money-barrier.d.ts +12 -44
  142. package/dist/tenant-money-barrier.d.ts.map +1 -1
  143. package/dist/types/index.d.ts +10 -23
  144. package/dist/types/index.d.ts.map +1 -1
  145. package/dist/ui.js +1 -1
  146. package/dist/ui.js.map +1 -1
  147. package/dist/upgrade/detect.d.ts +129 -0
  148. package/dist/upgrade/detect.d.ts.map +1 -0
  149. package/dist/upgrade/money-v2.d.ts +81 -0
  150. package/dist/upgrade/money-v2.d.ts.map +1 -0
  151. package/package.json +14 -13
  152. package/agents/line-amount-migration.md +0 -50
  153. package/dist/chunks/ContractLineItemCollection--BrGMtNz.js.map +0 -1
  154. package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js.map +0 -1
  155. package/dist/chunks/InvoiceCollection-BUe_8FT1.js.map +0 -1
  156. package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js +0 -468
  157. package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js.map +0 -1
  158. package/dist/chunks/TaxSettingsCollection-DYYF-In-.js +0 -319
  159. package/dist/chunks/TaxSettingsCollection-DYYF-In-.js.map +0 -1
  160. package/dist/chunks/calculations-DaHR17il.js +0 -333
  161. package/dist/chunks/calculations-DaHR17il.js.map +0 -1
  162. package/dist/chunks/document-money-oUDvdjOf.js.map +0 -1
  163. package/dist/chunks/invoice-posting-CF3fE_Ec.js.map +0 -1
  164. package/dist/chunks/revenue-journal-DY9fro8E.js.map +0 -1
  165. package/dist/chunks/tenant-money-barrier-y6EAa6vY.js +0 -135
  166. package/dist/chunks/tenant-money-barrier-y6EAa6vY.js.map +0 -1
  167. package/dist/chunks/units-CNcjH_eE.js.map +0 -1
  168. package/dist/collections/MoneyMigrationDocumentCollection.d.ts +0 -12
  169. package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +0 -1
  170. package/dist/migrations/commerceLineAmounts.d.ts +0 -84
  171. package/dist/migrations/commerceLineAmounts.d.ts.map +0 -1
  172. package/dist/models/MoneyMigrationDocument.d.ts +0 -28
  173. package/dist/models/MoneyMigrationDocument.d.ts.map +0 -1
  174. package/dist/svelte/components/InvoiceLineEditor.svelte +0 -92
  175. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +0 -8
  176. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +0 -1
  177. package/dist/svelte/invoices/fields.d.ts +0 -6
  178. package/dist/svelte/invoices/fields.d.ts.map +0 -1
  179. package/dist/svelte/invoices/fields.js +0 -22
  180. package/dist/tax/legacy.d.ts +0 -48
  181. package/dist/tax/legacy.d.ts.map +0 -1
package/AGENTS.md CHANGED
@@ -12,7 +12,7 @@ E-commerce with Contract STI hierarchy, invoice lifecycle, payment tracking, pay
12
12
  - **LicenseSale**: industry-neutral licensing primitive. Carries an *immutable* rights snapshot (`rightsMedium`, `rightsDistributionScope`, `rightsExclusivity`, `rightsDuration`, `rightsTerritory`, `rightsSublicensing`, `rightsDerivatives` — typed `Meta<T>` fields), licensee identity (`licenseeEmail`, optional `licenseeLegalEntity` / `licenseeJurisdiction`), and a signed-PDF reference (`pdfUrl`, `pdfHash`, optional `onChainHashRegistryRef`). Once saved at `ContractStatus.ACCEPTED`, the rights snapshot is frozen — mutating any of the seven rights fields and re-saving throws. The only legal transition out of ACCEPTED is `revoke()` (moves to CANCELLED without touching rights). Useful for stock media, music licensing, code-asset marketplaces, license keys, anywhere rights are sold for a fee.
13
13
  - **ContractLineItem**: items on contracts.
14
14
  - **Invoice**: status machine `DRAFT → SENT → VIEWED → PARTIAL → PAID` (also OVERDUE, CANCELLED, WRITTEN_OFF). `recognizeRevenue()` creates balanced AR journal entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable).
15
- - **InvoiceLineItem**: fractional-quantity line items with safe minor-unit validation/recalculation on save; editor-mode recovery and existing-column migration are documented in [invoice quantity persistence](agents/invoice-quantity.md).
15
+ - **InvoiceLineItem**: fractional-quantity line items with safe minor-unit validation/recalculation on save; the existing-column migration is documented in [invoice quantity persistence](agents/invoice-quantity.md).
16
16
  - **Payment** / **PaymentAllocation**: tracks payments against invoices (status: see Gotchas). Carries optional backend-adapter fields for PaymentBackend-routed flows: `backendId` (rail adapter id — `base-usdc`, `btc`, `stripe`, distinct from `externalProvider` which names an accounting sync destination), `backendTxRef` (chain tx hash or gateway settlement id), `nativeAmount` / `nativeCurrency` (what actually arrived), `usdAtQuote` / `usdAtConfirmation` (drift accounting for volatile-currency rails). `Payment.usdDrift()` returns the confirmation - quote delta, or `0` when either side is unset.
17
17
  - **PaymentInstrument**: reusable saved payment method ("card on file"). Stores only provider references and non-sensitive display metadata (`providerCustomerId`, `providerPaymentMethodId`, brand / last4 / expiry), never raw card data. `isDefault` is domain-managed, not API-writable: `PaymentInstrumentCollection.setDefaultForCustomer(customerId, instrumentId)` validates the target and clears the customer's other defaults (one per customer).
18
18
  - **PaymentIntent**: short-lived pre-payment commitment with multi-option semantics. Locks a USD price for a fixed window (default 15 minutes) and lists one or more `PaymentOption`s describing different rails (`backendId`, `currency`, `payTo`, `nativeAmount`, optional `chain` / `memo` / `x402Capable` / `expiresAt`). First option to receive payment wins; the others are implicitly retired. State machine `awaiting_payment → paid → (issued | retired)` plus `expired` and `cancelled`. Mutate via dedicated `markPaid` / `markIssued` / `expire` / `cancel` / `retire` helpers — direct status assignment bypasses the invariant checks. Idempotency: see the natural-key Gotcha.
@@ -34,11 +34,16 @@ language for end users and update them when a recipe's fields change. See
34
34
  See [payment allocation integrity](agents/payment-allocations.md) for atomic splits,
35
35
  retry identity, parent corrections and supported dialects.
36
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.
37
+ and the standard codes every tenant gets.
38
+ See [document money persistence](agents/document-money.md) for the one money
39
+ model: tax-code resolution, atomic aggregate writes, issued snapshots, and
40
+ frozen fields. Every Invoice and Contract money write needs a tenant. Host
41
+ bootstrap is `ensureCommerceTenant(db, tenantId, { cookbook })`, which returns
42
+ `{ tenantId }`. Money data written by an earlier version is refused with
43
+ `COMMERCE_DATA_NEEDS_UPGRADE` until `smrt db:upgrade-commerce-money` converts
44
+ it; see [money data upgrade](agents/money-upgrade.md).
40
45
  See [Contract money persistence](agents/contract-money.md) for the Contract
41
- aggregate, purchase recoverability, and migration-only materialization boundary.
46
+ aggregate and purchase recoverability.
42
47
 
43
48
  ## Ledger Integration
44
49
 
@@ -128,8 +133,14 @@ for usage.
128
133
  `WRITTEN_OFF` (a later payment, by card or on a rail (#3188), is recorded
129
134
  and reinstates the payer; the status stays, so "paid in full" for a
130
135
  written-off invoice is read from its allocations).
131
- - **`Invoice.providerTaxAmount`** is added to line-item tax; it is
132
- server-managed (not API-writable). `toAccountingInput()` emits major units.
136
+ - **Billing invoices are money-model invoices.** The provider taxes them
137
+ (`providerTaxAmount`, server-managed, not API-writable, added to line tax),
138
+ so every billing line is an explicit `taxMode: 'override'` to the seller's
139
+ `out-of-scope` code. Charges are written before credits so the running
140
+ subtotal never goes negative, and provider tax is written in the save that
141
+ issues the invoice. Revenue posting is idempotent. `ledger.bookCurrency` is
142
+ required (ISO 4217, validated by `BillingRuntime`) and must equal the
143
+ billing currency. `toAccountingInput({ mapping })` emits major units.
133
144
  - **Payment rails (#3138).** A runtime has one issuing `provider` and any
134
145
  number of `paymentProviders` (unique names, one inbox namespace each; a
135
146
  provider with `capabilities.issuesInvoices === false` can never issue).
@@ -167,11 +178,13 @@ for usage.
167
178
  ## Gotchas
168
179
 
169
180
  - **Optional tenancy**: all models `@TenantScoped({ mode: 'optional' })` + nullable tenantId
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.
181
+ - **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, 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.
182
+ - **Money documents need a tenant**: models are optionally tenant-scoped, but every Invoice and Contract money write requires one (`MoneyValidationError` `INVALID_MONEY_INPUT`, `Commerce money documents require a tenant`).
183
+ - **Earlier-version money data is refused**: never add a read or conversion path for it. Detection and the one-shot upgrade are in [money data upgrade](agents/money-upgrade.md).
184
+ - **Book currency is required**: `recognizeRevenue()` and `cancelWithRevenueReversal()` take a required `bookCurrency`, which must equal the invoice currency.
185
+ - **Asset-native amounts stay outside line rescales**: `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.
173
186
  - **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`
174
- - **Tax rate is external**: no tax rate field on Invoice — rate must be calculated externally
187
+ - **Tax comes from tax codes**: the server resolves each line's code and computes line tax and header totals; client totals and a line's stored `taxRate` are never inputs
175
188
  - **Profile linking**: separate `ProfileCollection.create()` needed to fetch actual Profile object
176
189
  - **PaymentIntent natural key**: `conflictColumns: ['tenant_id', 'offering_ref', 'licensee_email', 'idempotency_key']` — a retried `create` with the same tuple upserts the existing row. Use `PaymentIntentCollection.getOrCreateByIdempotencyKey()` to branch on `{ intent, created }`. Empty natural-key inputs (e.g. blank `idempotencyKey`) disable dedup by design.
177
190
  - **Payout state-machine guards**: `markSent` requires a non-empty `backendTxRef`; `markConfirmed` only valid from `SENT`; `markFailed` only valid from `PENDING` / `SENT` (a confirmed payout can't fail — that path is a refund). `resetFromFailed()` is the dedicated escape hatch from `FAILED` back to `PENDING` after an operator fixes the underlying problem; it clears `backendTxRef` so the next attempt picks up a fresh one.
package/README.md CHANGED
@@ -11,70 +11,80 @@ pnpm add @happyvertical/smrt-commerce
11
11
  ## Usage
12
12
 
13
13
  ```typescript
14
+ import { withTenant } from '@happyvertical/smrt-tenancy';
14
15
  import {
15
- Customer, CustomerCollection,
16
- Order, ContractCollection,
17
- Invoice, InvoiceCollection,
18
- Payment, PaymentCollection,
19
- ContractStatus, InvoiceStatus, PaymentMethod
16
+ CustomerCollection,
17
+ ContractCollection,
18
+ InvoiceCollection,
19
+ InvoiceLineItemCollection,
20
+ PaymentCollection,
21
+ PaymentMethod,
22
+ ensureCommerceTenant,
20
23
  } from '@happyvertical/smrt-commerce';
21
24
 
22
- // Create a customer linked to a profile
23
- const customers = await CustomerCollection.create({ db });
24
- const customer = await customers.create({
25
- profileId: 'profile-uuid',
26
- creditLimit: 1000000, // $10,000.00 — money is integer minor units
27
- paymentTerms: 'Net 30',
28
- });
29
- await customer.save();
30
-
31
- // Create an order (STI contract type)
32
- const contracts = await ContractCollection.create({ db });
33
- const order = await contracts.create({
34
- _meta_type: 'Order',
35
- customerId: customer.id,
36
- subtotal: 100000, // $1,000.00
37
- taxAmount: 5000, // $50.00
38
- totalAmount: 105000, // $1,050.00
39
- currency: 'CAD',
40
- });
41
- await order.save();
42
-
43
- // Create an invoice for the order
44
- const invoices = await InvoiceCollection.create({ db });
45
- const invoiceNumber = await invoices.generateInvoiceNumber();
46
- const invoice = await invoices.create({
47
- customerId: customer.id,
48
- contractId: order.id,
49
- invoiceNumber,
50
- subtotal: 100000, // $1,000.00
51
- taxAmount: 5000, // $50.00
52
- totalAmount: 105000, // $1,050.00
53
- });
54
- await invoice.save();
55
-
56
- // Recognize revenue (creates balanced journal in smrt-ledgers)
57
- await invoice.recognizeRevenue({
58
- arAccountId: 'ar-account-id',
59
- revenueAccountId: 'revenue-account-id',
60
- taxAccountId: 'tax-account-id',
61
- });
62
-
63
- // Record a payment
64
- const payments = await PaymentCollection.create({ db });
65
- const payment = await payments.create({
66
- contractId: order.id,
67
- customerId: customer.id,
68
- amount: 105000,
69
- method: PaymentMethod.CREDIT_CARD,
70
- });
71
- await payment.save();
72
-
73
- // Record payment with ledger integration
74
- await payment.recordPayment({
75
- ledgerId: 'ledger-id',
76
- receivablesAccountId: 'ar-account-id',
77
- cashAccountId: 'bank-account-id',
25
+ // Once per tenant, after the host creates it: tax settings and standard codes.
26
+ await ensureCommerceTenant(db, tenantId);
27
+
28
+ // Money documents (invoices, contracts, and their lines) belong to a tenant.
29
+ await withTenant({ tenantId }, async () => {
30
+ // Create a customer linked to a profile
31
+ const customers = await CustomerCollection.create({ db });
32
+ const customer = await customers.create({
33
+ profileId: 'profile-uuid',
34
+ creditLimit: 1000000, // $10,000.00 — money is integer minor units
35
+ paymentTerms: 'Net 30',
36
+ });
37
+
38
+ // Create an order (STI contract type)
39
+ const contracts = await ContractCollection.create({ db });
40
+ const order = await contracts.create({
41
+ _meta_type: 'Order',
42
+ customerId: customer.id,
43
+ currency: 'CAD',
44
+ });
45
+
46
+ // Create an invoice. The server resolves each line's tax code and computes
47
+ // line tax and the header totals; header amounts are never inputs.
48
+ const invoices = await InvoiceCollection.create({ db });
49
+ const invoice = await invoices.create({
50
+ customerId: customer.id,
51
+ contractId: order.id,
52
+ invoiceNumber: await invoices.generateInvoiceNumber(),
53
+ currency: 'CAD',
54
+ });
55
+ const lines = await InvoiceLineItemCollection.create({ db });
56
+ await lines.create({
57
+ invoiceId: invoice.id,
58
+ description: 'Consulting',
59
+ quantity: 10,
60
+ unitPrice: 10000, // $100.00
61
+ });
62
+
63
+ // Issuing freezes the money snapshot; then recognize revenue (creates a
64
+ // balanced journal in smrt-ledgers from the frozen facts).
65
+ const issued = await invoices.get({ id: invoice.id }, { cache: false });
66
+ issued.markSent();
67
+ await issued.save();
68
+ await issued.recognizeRevenue({
69
+ arAccountId: 'ar-account-id',
70
+ revenueAccountId: 'revenue-account-id',
71
+ taxAccountId: 'tax-account-id',
72
+ bookCurrency: 'CAD', // must equal the invoice currency
73
+ });
74
+
75
+ // Record a payment with ledger integration
76
+ const payments = await PaymentCollection.create({ db });
77
+ const payment = await payments.create({
78
+ contractId: order.id,
79
+ customerId: customer.id,
80
+ amount: issued.totalAmount,
81
+ method: PaymentMethod.CREDIT_CARD,
82
+ });
83
+ await payment.recordPayment({
84
+ ledgerId: 'ledger-id',
85
+ receivablesAccountId: 'ar-account-id',
86
+ cashAccountId: 'bank-account-id',
87
+ });
78
88
  });
79
89
  ```
80
90
 
@@ -104,7 +114,7 @@ const totals = calculateMoneyDocument({
104
114
  ```
105
115
 
106
116
  `resolveLineTaxCode()` resolves caller-loaded codes without I/O, and the
107
- Svelte `InvoiceEditor` opts into the v2 money controls through its `money`
117
+ Svelte `InvoiceEditor` renders the money controls from its required `money`
108
118
  prop. Frozen editor documents render a supplied stored snapshot without
109
119
  recalculation. See the [commerce money calculator guide](../../docs/content/commerce-money.md)
110
120
  for pricing modes, rounding, input and result types, the native-form editor
@@ -119,40 +129,68 @@ one settings row per tenant supplies sales/purchase defaults, amount mode,
119
129
  rounding scope, and labelled registration numbers. A referenced component rate
120
130
  is immutable: archive it and create a replacement for a rate change.
121
131
 
122
- `ensureTenantTaxCodes(db, tenantId)` idempotently creates the baseline
123
- non-taxable codes. `applyTaxCodeExamplePack(db, tenantId)` installs illustrative
132
+ `ensureTenantTaxCodes(db, tenantId)` idempotently creates the standard
133
+ non-taxable codes, `exempt`, `zero-rated`, and `out-of-scope`
134
+ (`STANDARD_TAX_CODES`). `applyTaxCodeExamplePack(db, tenantId)` installs illustrative
124
135
  regional examples for review, and `applyCommerceCookbookSettings(db, tenantId,
125
136
  cookbook)` explicitly materializes a validated cookbook's defaults after schema
126
137
  setup and authentication establish the tenant. See the [commerce money
127
138
  calculator guide](../../docs/content/commerce-money.md#tenant-tax-catalogue) for
128
139
  the supported kinds, seed behavior, and cookbook rate units.
129
140
 
130
- ### Persisted Invoice v2
141
+ ### Persisted Invoice money
131
142
 
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.
143
+ Commerce has one money model. Tax codes resolve each line's tax: the line's
144
+ override, else `exempt` for an exempt customer, else the product's code, the
145
+ customer's code, then the tenant default. The server computes line
146
+ `taxAmount` and the header totals and ignores any client total. Every Invoice
147
+ and Contract money write needs a tenant; without one it fails with
148
+ `MoneyValidationError` `INVALID_MONEY_INPUT`.
149
+ `ensureCommerceTenant(db, tenantId, { cookbook })` is the one bootstrap call a
150
+ host makes after creating a tenant. It seeds the tenant's tax settings, the
151
+ standard codes, and the cookbook's defaults, and returns `{ tenantId }`.
138
152
 
139
153
  Issuing an Invoice (leaving `DRAFT`) or assigning `arJournalId` freezes its
140
154
  money snapshot. Header and line money changes then fail with `FROZEN_DOCUMENT`,
141
155
  while later lifecycle and payment updates retain the snapshot. Provider tax
142
156
  stays separate in `providerTaxAmount` and contributes to the Invoice headers
143
- without creating an application tax code. In v2,
157
+ without creating an application tax code.
144
158
  `InvoiceLineItemCollection.getTotalForInvoice()` sums line amounts for
145
- compatibility; use persisted `Invoice.totalAmount` for the amount due. The
159
+ compatibility; use persisted `Invoice.totalAmount` for the amount due.
160
+ `recognizeRevenue()` and `cancelWithRevenueReversal()` require a `bookCurrency`
161
+ equal to the invoice currency, and `toAccountingInput({ mapping })` requires
162
+ explicit QuickBooks realm mapping. The
146
163
  [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
164
+ and [document-money contract](agents/document-money.md) cover persisted Invoice
148
165
  semantics, credits, refunds, and posting. The materialized Invoice tax-report
149
166
  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.
167
+ [tax-summary contract](agents/tax-summary.md). Contract money persistence,
168
+ PurchaseContract tax contributors, and the materialized tax-summary host and
169
+ drilldown are available. Applications opt in by registering
170
+ `createTaxSummaryMaterializedReportHost()` in their server-only report-host
171
+ registry; the route binds the authenticated tenant and read permission. See the
172
+ tax-summary contract for registration details.
173
+
174
+ ### Upgrading money data from an earlier version
175
+
176
+ Commerce refuses money data written by an earlier version with
177
+ `COMMERCE_DATA_NEEDS_UPGRADE` instead of reading it. That is a schema that
178
+ still has the earlier money columns or the `commerce_money_migration_documents` table,
179
+ an invoice or contract without a tenant, or a tenant with invoices or contracts but no tax settings row or with
180
+ documents in the earlier format. `smrt db:status`
181
+ reports it, so `smrt app doctor` reports `migration-required`. Run the normal
182
+ schema migration, then upgrade once:
183
+
184
+ ```sh
185
+ smrt db:upgrade-commerce-money --check # report only
186
+ smrt db:upgrade-commerce-money # every tenant
187
+ ```
188
+
189
+ Each tenant converts in its own transaction. Stored rates become tax codes,
190
+ issued documents keep every stored total, and drafts are recomputed. The
191
+ earlier columns are dropped once no tenant needs the upgrade. See
192
+ [money data upgrade](agents/money-upgrade.md) and the
193
+ [CLI guide](../cli/agents/commerce-money-upgrade.md).
156
194
 
157
195
  ## API
158
196
 
@@ -318,6 +356,7 @@ const billing = await BillingRuntime.create({
318
356
  provider: createStripeBillingProvider({ stripe, webhookSecret }),
319
357
  billingRelationships, // smrt-tenancy BillingRelationshipService
320
358
  ledger: {
359
+ bookCurrency: 'USD', // ISO 4217; must equal the billing currency
321
360
  arAccountId,
322
361
  revenueAccountId,
323
362
  taxAccountId,
@@ -465,7 +504,13 @@ const evaluator = await SpendingPolicyEvaluator.create({
465
504
  upgrade (`<subscriptionId>:<periodStart>`, number 0) count as billed
466
505
  coverage, so nothing is billed again after it.
467
506
  - **Tax** comes from the provider (Stripe Tax) using the account customer's
468
- `defaultBillingAddress`; the invoice records it as `providerTaxAmount`.
507
+ `defaultBillingAddress`; the invoice records it as `providerTaxAmount` in the
508
+ save that issues it. Billing invoices are ordinary money-model invoices in
509
+ the seller's tenant; period close seeds its standard codes if needed. Every
510
+ billing line is an explicit override to the seller's `out-of-scope` code, so
511
+ the seller's default code never taxes it a second time. Charges are written
512
+ before credits, so the running subtotal never goes negative. Revenue posting
513
+ uses `ledger.bookCurrency` and is idempotent.
469
514
  - **Events** are verified, stored in smrt-jobs' durable delivery inbox, and
470
515
  applied against current provider state: paid invoices record a payment and
471
516
  allocation; failures and overdue notices mark the payer `past_due` and its
@@ -751,14 +796,17 @@ The same models carry `@field({ ui: { widget } })` hints in the manifest:
751
796
 
752
797
  ## Invoice preparation UI
753
798
 
754
- `InvoiceLineEditor` and `InvoiceEditor` from `@happyvertical/smrt-commerce/svelte`
755
- provide editable descriptions/SKUs, fractional quantities, unit prices, flat or
756
- percentage discounts, explicit or inherited tax, customer, dates, currency and
757
- terms. Calculated previews use the same exact minor-unit arithmetic exported
758
- for server handlers from `@happyvertical/smrt-commerce/invoices`.
759
- `InvoiceAllocationFields` and `InvoiceReview` extend this foundation with
760
- caller-owned sources and approvals. See [the editor contract](agents/invoice-preparation.md),
761
- [calculation rules](agents/invoice-calculations.md), and the required
799
+ `InvoiceEditor` from `@happyvertical/smrt-commerce/svelte` is the invoice form,
800
+ built on the commercial money model, v2, which is the only invoice money model.
801
+ It requires a `money` prop (`MoneyEditorProps`): a lossless draft with
802
+ descriptions, fractional quantities, unit prices, percentage or amount
803
+ discounts and a tax code per line (inherited or overridden), plus the
804
+ authorized tax codes. Its previews run the same exact calculator the server
805
+ uses, and the server re-resolves tax codes and recomputes every stored amount.
806
+ An issued document renders only its frozen stored snapshot.
807
+ `InvoiceAllocationFields` and `InvoiceReview` add caller-owned sources and
808
+ approvals. See [the editor contract](agents/invoice-preparation.md), the
809
+ [line calculation helper](agents/invoice-calculations.md), and the
762
810
  [existing-database storage migration](agents/invoice-quantity.md).
763
811
 
764
812
  ## Exact cost billing
@@ -4,9 +4,9 @@
4
4
  snapshot. `src/invoice-accounting-sync.ts` is the owning persistence and
5
5
  provider boundary. It re-reads the issued Invoice, InvoiceLineItem and
6
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.
7
+ pure mapper. `Invoice.toAccountingInput({ mapping })` always requires explicit
8
+ QuickBooks realm mapping and is routed through the frozen boundary; calling it
9
+ without options throws `INVALID_MONEY_INPUT`.
10
10
 
11
11
  The released `@happyvertical/accounting` 0.103.7 path accepts frozen invoices
12
12
  and positive credit memos. Commerce sends persisted pre-discount unit price,
@@ -36,8 +36,8 @@ Do not mix currencies in totals or infer an unknown allocation as zero.
36
36
 
37
37
  The original invoice display DTOs in `src/svelte/types.ts` and their components
38
38
  accept decimal major-unit amounts. Preserve that published behavior. Applications
39
- adapting model amounts into those legacy components must explicitly convert units;
40
- do not pass a model's integer amount directly or silently change a legacy prop's
39
+ adapting model amounts into those major-unit components must explicitly convert
40
+ units; do not pass a model's integer amount directly or silently change a prop's
41
41
  meaning. New editor DTOs are separate contracts.
42
42
 
43
43
  Approved or otherwise retained invoice facts use the explicit `retained` display
@@ -2,9 +2,10 @@
2
2
 
3
3
  Contract money runs through `contract-money.ts`. A tenant operation enters
4
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.
5
+ lines on the scoped executor, resolves tax codes, and replaces the Contract
6
+ aggregate plus `DocumentTax` rows in the same transaction. Every Contract money
7
+ write needs a tenant, and a tenant whose data needs the upgrade is refused (see
8
+ [money data upgrade](money-upgrade.md)).
8
9
 
9
10
  Draft Contract line writes recompute every sibling. `subtotal` is tax-exclusive
10
11
  net after discount, `taxAmount` is the component total, and `totalAmount` is
@@ -13,12 +14,8 @@ component says so. A Contract leaving draft snapshots registrations, party tax
13
14
  identity, per-line component amounts, and `moneySnapshotId`; later money edits
14
15
  are refused with `FROZEN_DOCUMENT`.
15
16
 
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.
17
+ `SupplierQuote` uses purchase defaults and recoverability. Non-draft quotes
18
+ freeze just like other Contracts, but quote snapshots (including custom
19
+ descendants) are excluded by their hydrated STI subtype, not the mutable
20
+ `contractType` scalar, from report candidates and purchase tax facts. Awarding
21
+ requires a separate PurchaseOrder.
@@ -3,13 +3,10 @@
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
- Legacy Invoice calculations and editor defaults are unchanged by this entry
7
- point. In particular,
8
- `calculateInvoiceMinorLine` retains negative ties toward positive infinity; the
9
- new opt-in helpers use symmetric half-away-from-zero ties. Positive-only invoice
10
- editor validation remains distinct from these generic signed calculations. The
6
+ It does not change Invoice calculations or the invoice editor; both use the
7
+ commerce money calculator, whose ties also round half away from zero. The
11
8
  `roundingScope: 'invoice'` behavior in this guide is a standalone cost-billing
12
- convention; v2 document arithmetic uses `roundingScope: 'document'` and stores
9
+ convention; document arithmetic uses `roundingScope: 'document'` and stores
13
10
  allocated tax facts. See the [commercial money model](money-model.md) and
14
11
  [money calculator](money-calculator.md).
15
12
 
@@ -179,8 +179,7 @@ money-v2 fields (#4011): line `taxAmount`, `discountTotal` on Invoice and
179
179
  Contract, `Invoice.creditedAmount`, and `DocumentTax` `taxableMinor` /
180
180
  `taxMinor` / `recoverableMinor`; the test asserts every integer named
181
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
182
+ STI subtypes) is hinted. Payout and Payment's
184
183
  native/USD drift amounts are not hinted: their currency is a payout rail or
185
184
  fixed USD, not the record's `currency`. The line-item FKs
186
185
  (`ContractLineItem.contractId`, `InvoiceLineItem.invoiceId`,
@@ -1,28 +1,51 @@
1
1
  # Document money persistence
2
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.
3
+ Commerce has one money model. Tax codes resolve each line's tax, the server
4
+ computes line `taxAmount` and the header totals, and issuing a document freezes
5
+ it with a money snapshot. Every Invoice and Contract money write needs a
6
+ tenant; a tenantless write fails with `MoneyValidationError`
7
+ `INVALID_MONEY_INPUT` (`Commerce money documents require a tenant`).
8
+
9
+ ### Tenant settings and bootstrap (#4009)
10
+
11
+ Each tenant has one `TaxSettings` row. It is created once, wherever the first
12
+ caller reaches it: `getOrCreateForTenant`, the cookbook path, a generated
13
+ create, or the runtime barrier ahead of a tenant's first document. Creation
14
+ runs in one transaction under a tenant advisory lock on PostgreSQL (the
15
+ embedded write queue elsewhere), re-reads before inserting, and inserts the
16
+ deterministic settings id, so racing first writers converge on one row.
17
+
18
+ `ensureCommerceTenant(db, tenantId, { cookbook? })` is the one host bootstrap
19
+ call after tenant creation. It seeds the settings row, the standard tax codes
20
+ (`exempt`, `zero-rated`, `out-of-scope`), and the cookbook's commerce tax
21
+ settings when one is given. It returns `{ tenantId }` and is idempotent;
22
+ re-running it with a cookbook re-applies the cookbook defaults.
23
+
24
+ Money data written by an earlier version is refused, never read. A schema with
25
+ the earlier version's money columns or table, or an Invoice or Contract row
26
+ without a tenant, refuses the whole database. A tenant with Invoice or Contract
27
+ rows but no settings row, or with old-format documents (an issued document
28
+ without a money snapshot, or a line with neither a tax code nor a component
29
+ snapshot), is refused on its own. All fail with `COMMERCE_DATA_NEEDS_UPGRADE`
30
+ and name `smrt db:upgrade-commerce-money`.
31
+ See [money data upgrade](money-upgrade.md). Rows written with raw SQL bypass
32
+ the model and are not recalculated.
8
33
 
9
34
  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
35
+ accounting-sync phase checks for old-format data, then acquires the tenant
36
+ settings row before document rows. The lock is held through the owning
37
+ transaction. Nested coordinators reuse an explicitly registered transaction
38
+ executor; adapter shape is never used as transaction proof. Runtime code never
39
+ creates schema.
40
+
41
+ Saving an invoice line locks and reloads its parent on the caller's
20
42
  transaction, reloads every sibling, resolves current tax defaults, calculates
21
43
  the document once, and writes line snapshots, `DocumentTax`, and header totals
22
44
  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.
45
+ the line overrides), product, customer code, then the tenant default; `none`
46
+ mode uses the tenant's `out-of-scope` code. Product reads use the products
47
+ package's collection on that same transaction executor; tenant-owned and
48
+ catalog-global products are accepted.
26
49
 
27
50
  The first transition away from `DRAFT`, or setting `arJournalId`, freezes the
28
51
  money document. The invoice and every line receive the same stable
@@ -69,18 +92,17 @@ draft overlay through the owning resolver/calculator without writes and rejects
69
92
  frozen documents. A supplied line id must name a stored line exactly once; omit
70
93
  the id for a new preview-only line. `getInvoiceMoneyFacts({ db, tenantId,
71
94
  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.
95
+ and `DocumentTax` rows without recalculation. Both facades take the tenant
96
+ settings lock and refuse a tenant whose data needs the upgrade.
75
97
 
76
98
  ### Record-screen composition
77
99
 
78
100
  `loadInvoiceRecordScreenData({ db, tenantId, invoiceId })` is the commerce
79
101
  server adapter for a generic invoice record page. A caller must already have
80
102
  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
103
+ argument. It holds the tenant money lock while it reads the stored header,
104
+ lines, `DocumentTax`, and related credit-note summaries, so it never mixes a
105
+ stale header with a different snapshot of those rows. Its totals
84
106
  are a direct projection of persisted minor-unit values and component snapshots;
85
107
  it does not resolve a code or recalculate tax.
86
108
 
@@ -91,9 +113,10 @@ may expose `onCreateCreditNote` only with a server-derived
91
113
  `canCreateCreditNote` authorization result. The wrapper hides the action for
92
114
  credit-note records and never treats a client callback as authorization.
93
115
 
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
116
+ `InvoiceLineItem.amount` is the post-discount amount in the document's
117
+ `lineAmountType`: tax-exclusive net for `exclusive`, tax-inclusive for
118
+ `inclusive`. `taxAmount` is separate. `taxRate` is derived by the server from
119
+ the resolved components (`0.05` means 5%); it is never an input. `taxCodeId` and component identity may be null for
97
120
  a genuine unassigned zero-tax line. Optional `productId` and
98
121
  `revenueAccountId` are nullable UUID references so PostgreSQL never receives an
99
122
  empty-string sentinel.
@@ -103,7 +126,7 @@ labels, percentage, liability account, taxable minor units, and tax minor
103
126
  units. Provider tax remains the invoice header's `providerTaxAmount`; commerce
104
127
  does not invent a tax code or component for provider-owned tax.
105
128
 
106
- Revenue recognition for `v2` reads only the frozen invoice, lines, and
129
+ Revenue recognition reads only the frozen invoice, lines, and
107
130
  `DocumentTax` rows. It groups each line's stored net by its snapshotted revenue
108
131
  account (falling back to the caller's revenue account), groups component tax by
109
132
  the snapshotted liability account (falling back to the caller's tax account),
@@ -118,10 +141,14 @@ the invoice parent lock and transaction, so retries converge. Account IDs from
118
141
  frozen snapshots are still authorization checked: every used account must be
119
142
  active and owned by the invoice tenant.
120
143
 
121
- A posted `v2` invoice cannot move directly to `CANCELLED`. Call
144
+ A posted invoice cannot move directly to `CANCELLED`. Call
122
145
  `cancelWithRevenueReversal({ bookCurrency })`; it reads the original posted
123
146
  journal, posts one deterministic journal with every debit and credit reversed,
124
147
  and stores `arReversalJournalId` with the status transition atomically. The
125
148
  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.
149
+ entire operation, and retry returns the same reversal.
150
+
151
+ Billing period close writes ordinary money-model invoices. The payment provider
152
+ taxes them, so each billing line is an explicit override to the seller's
153
+ `out-of-scope` code, and the provider's tax is stored in `providerTaxAmount`
154
+ in the save that issues the invoice.