@happyvertical/smrt-commerce 0.56.1 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/AGENTS.md +24 -11
  2. package/README.md +159 -93
  3. package/agents/accounting-snapshot-mapper.md +3 -3
  4. package/agents/commercial-ui.md +2 -2
  5. package/agents/contract-money.md +9 -6
  6. package/agents/cost-billing.md +3 -6
  7. package/agents/customer-vendor-ui.md +6 -1
  8. package/agents/document-money.md +59 -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 +12 -8
  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-DYjhj_pU.js → ContractLineItemCollection-BZcrlrrY.js} +22 -27
  23. package/dist/chunks/ContractLineItemCollection-BZcrlrrY.js.map +1 -0
  24. package/dist/chunks/{CustomerCollection-DQIujn4f.js → CustomerCollection-BIPH2ZUx.js} +3 -3
  25. package/dist/chunks/{CustomerCollection-DQIujn4f.js.map → CustomerCollection-BIPH2ZUx.js.map} +1 -1
  26. package/dist/chunks/{DocumentTaxCollection-BEpoqXAQ.js → DocumentTaxCollection-DDzlywMW.js} +41 -14
  27. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +1 -0
  28. package/dist/chunks/{InvoiceCollection-CKQflFjc.js → InvoiceCollection-cinzAqt6.js} +53 -164
  29. package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +1 -0
  30. package/dist/chunks/{TaxCodeCollection-DOpzW4y8.js → TaxCodeCollection-DaBOPG9c.js} +11 -4
  31. package/dist/chunks/{TaxCodeCollection-DOpzW4y8.js.map → TaxCodeCollection-DaBOPG9c.js.map} +1 -1
  32. package/dist/chunks/{TaxComponentCollection-Cs2uXwEl.js → TaxComponentCollection-C4zxnyVu.js} +3 -3
  33. package/dist/chunks/{TaxComponentCollection-Cs2uXwEl.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-DI3G16BN.js → allocation-integrity-WmWyXhtG.js} +6 -6
  37. package/dist/chunks/{allocation-integrity-DI3G16BN.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-DNVhDJyC.js → document-money-DV5t4vlK.js} +389 -63
  45. package/dist/chunks/document-money-DV5t4vlK.js.map +1 -0
  46. package/dist/chunks/{invoice-posting-BwV8g7z0.js → invoice-posting-C2i5dOVZ.js} +21 -23
  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-LDETvQ1C.js → types-Bbm0Xflc.js} +2 -1
  53. package/dist/chunks/types-Bbm0Xflc.js.map +1 -0
  54. package/dist/chunks/{units-CNcjH_eE.js → units-8ElbjVZK.js} +18 -3
  55. package/dist/chunks/units-8ElbjVZK.js.map +1 -0
  56. package/dist/collections/ContractCollection.d.ts.map +1 -1
  57. package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
  58. package/dist/collections/InvoiceCollection.d.ts.map +1 -1
  59. package/dist/collections/InvoiceLineItemCollection.d.ts +0 -30
  60. package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
  61. package/dist/collections/TaxCodeCollection.d.ts +5 -1
  62. package/dist/collections/TaxCodeCollection.d.ts.map +1 -1
  63. package/dist/collections/TaxSettingsCollection.d.ts +15 -8
  64. package/dist/collections/TaxSettingsCollection.d.ts.map +1 -1
  65. package/dist/collections/TaxSummaryReportCollection.d.ts +3 -3
  66. package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -1
  67. package/dist/collections/index.d.ts +0 -1
  68. package/dist/collections/index.d.ts.map +1 -1
  69. package/dist/commerce-money-tools.d.ts.map +1 -1
  70. package/dist/contract-money.d.ts +1 -13
  71. package/dist/contract-money.d.ts.map +1 -1
  72. package/dist/document-money-internal.d.ts +7 -3
  73. package/dist/document-money-internal.d.ts.map +1 -1
  74. package/dist/document-money.d.ts +1 -11
  75. package/dist/document-money.d.ts.map +1 -1
  76. package/dist/index.d.ts +6 -5
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +686 -1656
  79. package/dist/index.js.map +1 -1
  80. package/dist/invoice-accounting-sync.d.ts.map +1 -1
  81. package/dist/invoice-mcp.d.ts +3 -3
  82. package/dist/invoice-mcp.d.ts.map +1 -1
  83. package/dist/invoice-posting.d.ts +11 -5
  84. package/dist/invoice-posting.d.ts.map +1 -1
  85. package/dist/invoice-record-screen.d.ts +1 -1
  86. package/dist/invoice-record-screen.d.ts.map +1 -1
  87. package/dist/invoices.d.ts +6 -1
  88. package/dist/invoices.d.ts.map +1 -1
  89. package/dist/invoices.js +2 -2
  90. package/dist/manifest.json +1041 -863
  91. package/dist/migrations/invoiceEditorStorage.d.ts +2 -4
  92. package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -1
  93. package/dist/migrations/partyLinks.d.ts.map +1 -1
  94. package/dist/models/Contract.d.ts +10 -4
  95. package/dist/models/Contract.d.ts.map +1 -1
  96. package/dist/models/ContractLineItem.d.ts +9 -17
  97. package/dist/models/ContractLineItem.d.ts.map +1 -1
  98. package/dist/models/DocumentTax.d.ts.map +1 -1
  99. package/dist/models/Invoice.d.ts +18 -48
  100. package/dist/models/Invoice.d.ts.map +1 -1
  101. package/dist/models/InvoiceLineItem.d.ts +13 -32
  102. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  103. package/dist/models/TaxSettings.d.ts +0 -10
  104. package/dist/models/TaxSettings.d.ts.map +1 -1
  105. package/dist/models/index.d.ts +1 -2
  106. package/dist/models/index.d.ts.map +1 -1
  107. package/dist/playground.js +1 -1
  108. package/dist/playground.js.map +1 -1
  109. package/dist/recipes.d.ts +8 -11
  110. package/dist/recipes.d.ts.map +1 -1
  111. package/dist/revenue-journal.d.ts +0 -8
  112. package/dist/revenue-journal.d.ts.map +1 -1
  113. package/dist/smrt-knowledge.json +2886 -6121
  114. package/dist/svelte/components/InvoiceEditor.svelte +7 -148
  115. package/dist/svelte/components/InvoiceEditor.svelte.d.ts +4 -1
  116. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  117. package/dist/svelte/components/InvoiceTotals.svelte +14 -118
  118. package/dist/svelte/components/InvoiceTotals.svelte.d.ts +4 -27
  119. package/dist/svelte/components/InvoiceTotals.svelte.d.ts.map +1 -1
  120. package/dist/svelte/components/MoneyInvoiceEditor.svelte +3 -23
  121. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +3 -26
  122. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -1
  123. package/dist/svelte/index.d.ts +2 -3
  124. package/dist/svelte/index.d.ts.map +1 -1
  125. package/dist/svelte/index.js +1 -2
  126. package/dist/svelte/invoices/calculations.d.ts +3 -108
  127. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  128. package/dist/svelte/invoices/calculations.js +12 -309
  129. package/dist/svelte/invoices/messages.d.ts +1 -37
  130. package/dist/svelte/invoices/messages.d.ts.map +1 -1
  131. package/dist/svelte/invoices/messages.js +1 -37
  132. package/dist/svelte/invoices/money-editor.d.ts +1 -1
  133. package/dist/svelte/invoices/money-editor.d.ts.map +1 -1
  134. package/dist/svelte/invoices/types.d.ts +14 -114
  135. package/dist/svelte/invoices/types.d.ts.map +1 -1
  136. package/dist/svelte/playground/InvoicePreparationPreview.svelte +85 -94
  137. package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -1
  138. package/dist/svelte/playground.js +1 -1
  139. package/dist/tax/index.d.ts +2 -1
  140. package/dist/tax/index.d.ts.map +1 -1
  141. package/dist/tax/standard-codes.d.ts +12 -0
  142. package/dist/tax/standard-codes.d.ts.map +1 -0
  143. package/dist/tax/tenant-bootstrap.d.ts +29 -0
  144. package/dist/tax/tenant-bootstrap.d.ts.map +1 -0
  145. package/dist/tenant-money-barrier.d.ts +12 -44
  146. package/dist/tenant-money-barrier.d.ts.map +1 -1
  147. package/dist/types/index.d.ts +11 -23
  148. package/dist/types/index.d.ts.map +1 -1
  149. package/dist/ui.js +1 -1
  150. package/dist/ui.js.map +1 -1
  151. package/dist/upgrade/detect.d.ts +129 -0
  152. package/dist/upgrade/detect.d.ts.map +1 -0
  153. package/dist/upgrade/money-v2.d.ts +81 -0
  154. package/dist/upgrade/money-v2.d.ts.map +1 -0
  155. package/package.json +14 -13
  156. package/agents/line-amount-migration.md +0 -50
  157. package/dist/chunks/ContractLineItemCollection-DYjhj_pU.js.map +0 -1
  158. package/dist/chunks/DocumentTaxCollection-BEpoqXAQ.js.map +0 -1
  159. package/dist/chunks/InvoiceCollection-CKQflFjc.js.map +0 -1
  160. package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js +0 -467
  161. package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js.map +0 -1
  162. package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js +0 -319
  163. package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js.map +0 -1
  164. package/dist/chunks/calculations-DaHR17il.js +0 -333
  165. package/dist/chunks/calculations-DaHR17il.js.map +0 -1
  166. package/dist/chunks/document-money-DNVhDJyC.js.map +0 -1
  167. package/dist/chunks/invoice-posting-BwV8g7z0.js.map +0 -1
  168. package/dist/chunks/revenue-journal-DY9fro8E.js.map +0 -1
  169. package/dist/chunks/tenant-money-barrier-DjRXgMnX.js +0 -135
  170. package/dist/chunks/tenant-money-barrier-DjRXgMnX.js.map +0 -1
  171. package/dist/chunks/types-LDETvQ1C.js.map +0 -1
  172. package/dist/chunks/units-CNcjH_eE.js.map +0 -1
  173. package/dist/collections/MoneyMigrationDocumentCollection.d.ts +0 -12
  174. package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +0 -1
  175. package/dist/migrations/commerceLineAmounts.d.ts +0 -84
  176. package/dist/migrations/commerceLineAmounts.d.ts.map +0 -1
  177. package/dist/models/MoneyMigrationDocument.d.ts +0 -28
  178. package/dist/models/MoneyMigrationDocument.d.ts.map +0 -1
  179. package/dist/svelte/components/InvoiceLineEditor.svelte +0 -92
  180. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +0 -8
  181. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +0 -1
  182. package/dist/svelte/invoices/fields.d.ts +0 -6
  183. package/dist/svelte/invoices/fields.d.ts.map +0 -1
  184. package/dist/svelte/invoices/fields.js +0 -22
  185. package/dist/tax/legacy.d.ts +0 -48
  186. 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
@@ -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
 
@@ -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,54 +129,99 @@ 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
 
159
197
  ### Models
160
198
 
199
+ #### Supplier quotations
200
+
201
+ `SupplierQuote` extends `PurchaseContract`, requires `vendorId`, and has
202
+ `ContractType.SUPPLIER_QUOTE`. Create it through `PurchaseContractCollection`
203
+ (or `ContractCollection`) with `_meta_type:
204
+ '@happyvertical/smrt-commerce:SupplierQuote'`. It inherits `expiryDate`,
205
+ `reference`, `notes`, and `terms`; it has no customer relationship. Purchase
206
+ branch reads include quotes and sales branch reads exclude them.
207
+
208
+ Keep received quote revisions and shared quote identity in your application.
209
+ Awarding a quote creates a separate `PurchaseOrder`; accepting or saving a
210
+ quote never converts its identity into an order. Quotes use purchase-side
211
+ money calculation and frozen snapshots, but never contribute incurred purchase
212
+ tax to reports, regardless of status. Existing Estimate and PurchaseOrder
213
+ report eligibility is unchanged.
214
+
161
215
  #### Upgrading contract parties (#3898)
162
216
 
163
217
  `Contract` no longer declares `customerId` or `vendorId`. Narrow a contract to
164
218
  `SalesContract` to read `customerId`, or to `PurchaseContract` to read `vendorId`.
165
- The nine leaf qualified names and the `contracts` table stay the same; a
219
+ The existing leaf qualified names and the `contracts` table stay the same; a
166
220
  `db:migrate --dry-run` against a v0.55.19 schema reports no DDL changes on
167
221
  SQLite or PostgreSQL. Sales leaves no longer accept `vendorId`; purchase leaves
168
222
  no longer accept `customerId`. `Order`, `Estimate`, `Agreement`, `Lease`, and
169
- `WholesaleOrder` require a customer on save. `PurchaseOrder` requires a vendor.
223
+ `WholesaleOrder` require a customer on save. `PurchaseOrder` and `SupplierQuote`
224
+ require a vendor.
170
225
  `Cart`, `LicenseSale`, and `ProductionOrder` retain optional parties on their side.
171
226
 
172
227
  Before upgrading existing data, run the read-only
@@ -203,6 +258,7 @@ This preflight never edits rows; an operator chooses the data correction.
203
258
  | `Order` | Customer purchase order |
204
259
  | `Lease` | Rental or lease agreement |
205
260
  | `Agreement` | Service or maintenance agreement |
261
+ | `SupplierQuote` | Quotation received from a vendor; distinct from an awarded purchase order |
206
262
  | `PurchaseOrder` | Order sent to a vendor/supplier |
207
263
  | `ContractLineItem` | Line item within a contract |
208
264
  | `Invoice` | Billing document with status lifecycle and ledger integration |
@@ -230,7 +286,7 @@ replay, parent revision refresh and compatibility requirements.
230
286
 
231
287
  | Export | Values |
232
288
  |--------|--------|
233
- | `ContractType` | `estimate`, `order`, `lease`, `agreement`, `purchase_order`, `wholesale_order`, `production_order`, `cart`, `license_sale` |
289
+ | `ContractType` | `estimate`, `order`, `lease`, `agreement`, `supplier_quote`, `purchase_order`, `wholesale_order`, `production_order`, `cart`, `license_sale` |
234
290
  | `ContractStatus` | `draft`, `sent`, `accepted`, `declined`, `completed`, `cancelled` |
235
291
  | `CustomerStatus` | `active`, `inactive`, `suspended` |
236
292
  | `VendorStatus` | `active`, `inactive`, `suspended` |
@@ -300,6 +356,7 @@ const billing = await BillingRuntime.create({
300
356
  provider: createStripeBillingProvider({ stripe, webhookSecret }),
301
357
  billingRelationships, // smrt-tenancy BillingRelationshipService
302
358
  ledger: {
359
+ bookCurrency: 'USD', // ISO 4217; must equal the billing currency
303
360
  arAccountId,
304
361
  revenueAccountId,
305
362
  taxAccountId,
@@ -447,7 +504,13 @@ const evaluator = await SpendingPolicyEvaluator.create({
447
504
  upgrade (`<subscriptionId>:<periodStart>`, number 0) count as billed
448
505
  coverage, so nothing is billed again after it.
449
506
  - **Tax** comes from the provider (Stripe Tax) using the account customer's
450
- `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.
451
514
  - **Events** are verified, stored in smrt-jobs' durable delivery inbox, and
452
515
  applied against current provider state: paid invoices record a payment and
453
516
  allocation; failures and overdue notices mark the payer `past_due` and its
@@ -733,14 +796,17 @@ The same models carry `@field({ ui: { widget } })` hints in the manifest:
733
796
 
734
797
  ## Invoice preparation UI
735
798
 
736
- `InvoiceLineEditor` and `InvoiceEditor` from `@happyvertical/smrt-commerce/svelte`
737
- provide editable descriptions/SKUs, fractional quantities, unit prices, flat or
738
- percentage discounts, explicit or inherited tax, customer, dates, currency and
739
- terms. Calculated previews use the same exact minor-unit arithmetic exported
740
- for server handlers from `@happyvertical/smrt-commerce/invoices`.
741
- `InvoiceAllocationFields` and `InvoiceReview` extend this foundation with
742
- caller-owned sources and approvals. See [the editor contract](agents/invoice-preparation.md),
743
- [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
744
810
  [existing-database storage migration](agents/invoice-quantity.md).
745
811
 
746
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,6 +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.
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
 
@@ -174,7 +174,12 @@ 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. Payout and Payment's
178
183
  native/USD drift amounts are not hinted: their currency is a payout rail or
179
184
  fixed USD, not the record's `currency`. The line-item FKs
180
185
  (`ContractLineItem.contractId`, `InvoiceLineItem.invoiceId`,