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