@happyvertical/smrt-commerce 0.51.32 → 0.51.34

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 (79) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +88 -0
  3. package/dist/__tests__/foreign-key-fixtures.d.ts +13 -0
  4. package/dist/__tests__/foreign-key-fixtures.d.ts.map +1 -0
  5. package/dist/__tests__/helpers/anchor-fixture.d.ts +36 -0
  6. package/dist/__tests__/helpers/anchor-fixture.d.ts.map +1 -0
  7. package/dist/__tests__/helpers/billing-fixture.d.ts +51 -0
  8. package/dist/__tests__/helpers/billing-fixture.d.ts.map +1 -0
  9. package/dist/__tests__/helpers/consumer-license-sale.d.ts +13 -0
  10. package/dist/__tests__/helpers/consumer-license-sale.d.ts.map +1 -0
  11. package/dist/__tests__/helpers/crypto-rail.d.ts +69 -0
  12. package/dist/__tests__/helpers/crypto-rail.d.ts.map +1 -0
  13. package/dist/__tests__/helpers/fake-stripe.d.ts +147 -0
  14. package/dist/__tests__/helpers/fake-stripe.d.ts.map +1 -0
  15. package/dist/billing/btcpay.d.ts +40 -0
  16. package/dist/billing/btcpay.d.ts.map +1 -0
  17. package/dist/billing/cards.d.ts +14 -4
  18. package/dist/billing/credits.d.ts +7 -2
  19. package/dist/billing/credits.d.ts.map +1 -1
  20. package/dist/billing/crypto.d.ts +32 -0
  21. package/dist/billing/crypto.d.ts.map +1 -0
  22. package/dist/billing/cycles.d.ts +21 -1
  23. package/dist/billing/events.d.ts +23 -4
  24. package/dist/billing/events.d.ts.map +1 -1
  25. package/dist/billing/index.d.ts +5 -1
  26. package/dist/billing/index.d.ts.map +1 -1
  27. package/dist/billing/invoice-payments.d.ts +29 -0
  28. package/dist/billing/invoice-payments.d.ts.map +1 -0
  29. package/dist/billing/jobs.d.ts +3 -3
  30. package/dist/billing/payment-attempts.d.ts +143 -0
  31. package/dist/billing/payment-attempts.d.ts.map +1 -0
  32. package/dist/billing/period-close.d.ts +1 -1
  33. package/dist/billing/provider.d.ts +109 -3
  34. package/dist/billing/provider.d.ts.map +1 -1
  35. package/dist/billing/runtime.d.ts +122 -14
  36. package/dist/billing/runtime.d.ts.map +1 -1
  37. package/dist/billing/stripe.d.ts +16 -2
  38. package/dist/billing/stripe.d.ts.map +1 -1
  39. package/dist/billing/top-up.d.ts +4 -4
  40. package/dist/collections/ContractCollection.d.ts +4 -0
  41. package/dist/collections/ContractLineItemCollection.d.ts +4 -0
  42. package/dist/collections/CustomerCollection.d.ts +4 -0
  43. package/dist/collections/FulfillmentCollection.d.ts +5 -1
  44. package/dist/collections/FulfillmentLineItemCollection.d.ts +4 -0
  45. package/dist/collections/InvoiceCollection.d.ts +4 -0
  46. package/dist/collections/InvoiceLineItemCollection.d.ts +5 -1
  47. package/dist/collections/PaymentAllocationCollection.d.ts +4 -0
  48. package/dist/collections/PaymentCollection.d.ts +5 -1
  49. package/dist/collections/PaymentInstrumentCollection.d.ts +4 -0
  50. package/dist/collections/PaymentIntentCollection.d.ts +12 -1
  51. package/dist/collections/PayoutCollection.d.ts +12 -1
  52. package/dist/collections/VendorCollection.d.ts +4 -0
  53. package/dist/index.d.ts +64 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +4055 -2859
  56. package/dist/index.js.map +1 -1
  57. package/dist/manifest.json +920 -91
  58. package/dist/migrations/moneyMinorUnits.d.ts +61 -1
  59. package/dist/models/Contract.d.ts +6 -2
  60. package/dist/models/ContractLineItem.d.ts +5 -1
  61. package/dist/models/Customer.d.ts +5 -1
  62. package/dist/models/Fulfillment.d.ts +5 -1
  63. package/dist/models/FulfillmentLineItem.d.ts +5 -1
  64. package/dist/models/Invoice.d.ts +6 -2
  65. package/dist/models/InvoiceLineItem.d.ts +5 -1
  66. package/dist/models/Payment.d.ts +6 -2
  67. package/dist/models/PaymentAllocation.d.ts +5 -1
  68. package/dist/models/PaymentInstrument.d.ts +5 -1
  69. package/dist/models/PaymentIntent.d.ts +24 -1
  70. package/dist/models/Payout.d.ts +24 -1
  71. package/dist/models/Vendor.d.ts +5 -1
  72. package/dist/models/billing.d.ts +111 -1
  73. package/dist/models/billing.d.ts.map +1 -1
  74. package/dist/models/index.d.ts +1 -1
  75. package/dist/models/index.d.ts.map +1 -1
  76. package/dist/smrt-knowledge.json +320 -9
  77. package/dist/types/index.d.ts +5 -1
  78. package/dist/ui.d.ts +14 -1
  79. package/package.json +11 -10
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-commerce",
6
- "packageVersion": "0.51.32",
6
+ "packageVersion": "0.51.34",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "0fe6828db1ed296b3d837aab3927e51b9780803b12baddc0e6f0a041f0f5a02a",
11
- "packageJson": "8b1aba26328edb4d90f98471ba4de88afd834efc84e45a39cf5d12ee34f22375",
12
- "agents": "05a98db8f3cc2137b030cbcbaedb112880f42613574cea68f6aebe03b7adcc3b"
10
+ "manifest": "cee37a9162573cbdbbadd50ef38df605c5d52ddfaeba993de44ba162fc596eb2",
11
+ "packageJson": "4513b37b442f80b0a425007c97e2a0026fbe8da8c555172d6f1d2865014637e0",
12
+ "agents": "73b483024defc068aa338ec735187f71e99ebc33b3ed21123c80fd686e2501d4"
13
13
  },
14
14
  "exports": [
15
15
  ".",
@@ -20,6 +20,7 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "@happyvertical/accounting": "catalog:",
23
+ "@happyvertical/payments": "catalog:",
23
24
  "@happyvertical/smrt-core": "workspace:*",
24
25
  "@happyvertical/smrt-jobs": "workspace:*",
25
26
  "@happyvertical/smrt-ledgers": "workspace:*",
@@ -34,7 +35,7 @@
34
35
  "fast-glob": "^3.3.3",
35
36
  "svelte": "^5.57.1",
36
37
  "svelte-check": "^4.7.6",
37
- "typescript": "5.9.3",
38
+ "typescript": "6.0.3",
38
39
  "vite": "8.1.4",
39
40
  "vitest": "4.1.10"
40
41
  },
@@ -60,7 +61,7 @@
60
61
  "smrt"
61
62
  ],
62
63
  "risks": [
63
- "cross-package-refs:12",
64
+ "cross-package-refs:14",
64
65
  "sensitive-fields-excluded",
65
66
  "sti-inheritance"
66
67
  ],
@@ -13093,6 +13094,299 @@
13093
13094
  "tags": [],
13094
13095
  "risks": []
13095
13096
  },
13097
+ {
13098
+ "name": "BillingPaymentAttempt",
13099
+ "qualifiedName": "@happyvertical/smrt-commerce:BillingPaymentAttempt",
13100
+ "collection": "billingpaymentattempts",
13101
+ "tableName": "_smrt_billing_payment_attempts",
13102
+ "packageName": "@happyvertical/smrt-commerce",
13103
+ "extends": "SmrtObject",
13104
+ "fields": [
13105
+ {
13106
+ "name": "sellerTenantId",
13107
+ "type": "crossPackageRef",
13108
+ "required": true,
13109
+ "related": "@happyvertical/smrt-users:Tenant",
13110
+ "columnType": "UUID"
13111
+ },
13112
+ {
13113
+ "name": "payerTenantId",
13114
+ "type": "crossPackageRef",
13115
+ "required": true,
13116
+ "related": "@happyvertical/smrt-users:Tenant",
13117
+ "columnType": "UUID"
13118
+ },
13119
+ {
13120
+ "name": "billingAccountId",
13121
+ "type": "text",
13122
+ "required": false,
13123
+ "columnType": "TEXT",
13124
+ "default": ""
13125
+ },
13126
+ {
13127
+ "name": "purpose",
13128
+ "type": "text",
13129
+ "required": false,
13130
+ "columnType": "TEXT",
13131
+ "default": "credit_purchase"
13132
+ },
13133
+ {
13134
+ "name": "invoiceId",
13135
+ "type": "text",
13136
+ "required": false,
13137
+ "columnType": "TEXT",
13138
+ "default": ""
13139
+ },
13140
+ {
13141
+ "name": "spendingPolicyId",
13142
+ "type": "text",
13143
+ "required": false,
13144
+ "columnType": "TEXT",
13145
+ "default": ""
13146
+ },
13147
+ {
13148
+ "name": "provider",
13149
+ "type": "text",
13150
+ "required": false,
13151
+ "columnType": "TEXT",
13152
+ "default": ""
13153
+ },
13154
+ {
13155
+ "name": "orderId",
13156
+ "type": "text",
13157
+ "required": false,
13158
+ "columnType": "TEXT",
13159
+ "default": ""
13160
+ },
13161
+ {
13162
+ "name": "checkoutId",
13163
+ "type": "text",
13164
+ "required": false,
13165
+ "columnType": "TEXT",
13166
+ "default": ""
13167
+ },
13168
+ {
13169
+ "name": "status",
13170
+ "type": "text",
13171
+ "required": false,
13172
+ "columnType": "TEXT",
13173
+ "default": "open"
13174
+ },
13175
+ {
13176
+ "name": "exception",
13177
+ "type": "text",
13178
+ "required": false,
13179
+ "columnType": "TEXT",
13180
+ "default": "none"
13181
+ },
13182
+ {
13183
+ "name": "flag",
13184
+ "type": "text",
13185
+ "required": false,
13186
+ "columnType": "TEXT",
13187
+ "default": ""
13188
+ },
13189
+ {
13190
+ "name": "amount",
13191
+ "type": "integer",
13192
+ "required": false,
13193
+ "columnType": "INTEGER",
13194
+ "default": 0
13195
+ },
13196
+ {
13197
+ "name": "currency",
13198
+ "type": "text",
13199
+ "required": false,
13200
+ "columnType": "TEXT",
13201
+ "default": "USD"
13202
+ },
13203
+ {
13204
+ "name": "amountPaid",
13205
+ "type": "integer",
13206
+ "required": false,
13207
+ "columnType": "INTEGER",
13208
+ "default": 0
13209
+ },
13210
+ {
13211
+ "name": "nativeCurrency",
13212
+ "type": "text",
13213
+ "required": false,
13214
+ "columnType": "TEXT",
13215
+ "default": ""
13216
+ },
13217
+ {
13218
+ "name": "nativeAmountDue",
13219
+ "type": "integer",
13220
+ "required": false,
13221
+ "columnType": "INTEGER",
13222
+ "default": 0
13223
+ },
13224
+ {
13225
+ "name": "nativeAmountPaid",
13226
+ "type": "integer",
13227
+ "required": false,
13228
+ "columnType": "INTEGER",
13229
+ "default": 0
13230
+ },
13231
+ {
13232
+ "name": "rate",
13233
+ "type": "text",
13234
+ "required": false,
13235
+ "columnType": "TEXT",
13236
+ "default": ""
13237
+ },
13238
+ {
13239
+ "name": "rateSource",
13240
+ "type": "text",
13241
+ "required": false,
13242
+ "columnType": "TEXT",
13243
+ "default": ""
13244
+ },
13245
+ {
13246
+ "name": "checkoutUrl",
13247
+ "type": "text",
13248
+ "required": false,
13249
+ "columnType": "TEXT",
13250
+ "default": ""
13251
+ },
13252
+ {
13253
+ "name": "expiresAt",
13254
+ "type": "datetime",
13255
+ "required": false,
13256
+ "columnType": "TIMESTAMP"
13257
+ },
13258
+ {
13259
+ "name": "payments",
13260
+ "type": "json",
13261
+ "required": false,
13262
+ "columnType": "JSON",
13263
+ "default": []
13264
+ },
13265
+ {
13266
+ "name": "timeline",
13267
+ "type": "json",
13268
+ "required": false,
13269
+ "columnType": "JSON",
13270
+ "default": []
13271
+ },
13272
+ {
13273
+ "name": "settledAt",
13274
+ "type": "datetime",
13275
+ "required": false,
13276
+ "columnType": "TIMESTAMP"
13277
+ },
13278
+ {
13279
+ "name": "paymentId",
13280
+ "type": "text",
13281
+ "required": false,
13282
+ "columnType": "TEXT",
13283
+ "default": ""
13284
+ },
13285
+ {
13286
+ "name": "creditGrantId",
13287
+ "type": "text",
13288
+ "required": false,
13289
+ "columnType": "TEXT",
13290
+ "default": ""
13291
+ },
13292
+ {
13293
+ "name": "resolution",
13294
+ "type": "text",
13295
+ "required": false,
13296
+ "columnType": "TEXT",
13297
+ "default": ""
13298
+ },
13299
+ {
13300
+ "name": "resolvedAt",
13301
+ "type": "datetime",
13302
+ "required": false,
13303
+ "columnType": "TIMESTAMP"
13304
+ },
13305
+ {
13306
+ "name": "outOfBandRequestedAt",
13307
+ "type": "datetime",
13308
+ "required": false,
13309
+ "columnType": "TIMESTAMP"
13310
+ },
13311
+ {
13312
+ "name": "pausedStanding",
13313
+ "type": "text",
13314
+ "required": false,
13315
+ "columnType": "TEXT",
13316
+ "default": ""
13317
+ },
13318
+ {
13319
+ "name": "excessAmount",
13320
+ "type": "integer",
13321
+ "required": false,
13322
+ "columnType": "INTEGER",
13323
+ "default": 0
13324
+ },
13325
+ {
13326
+ "name": "excessRevision",
13327
+ "type": "integer",
13328
+ "required": false,
13329
+ "columnType": "INTEGER",
13330
+ "default": 0
13331
+ },
13332
+ {
13333
+ "name": "settlementOutcome",
13334
+ "type": "text",
13335
+ "required": false,
13336
+ "columnType": "TEXT",
13337
+ "default": ""
13338
+ },
13339
+ {
13340
+ "name": "refundedAmount",
13341
+ "type": "integer",
13342
+ "required": false,
13343
+ "columnType": "INTEGER",
13344
+ "default": 0
13345
+ },
13346
+ {
13347
+ "name": "refundedPrincipal",
13348
+ "type": "integer",
13349
+ "required": false,
13350
+ "columnType": "INTEGER",
13351
+ "default": 0
13352
+ },
13353
+ {
13354
+ "name": "refunds",
13355
+ "type": "json",
13356
+ "required": false,
13357
+ "columnType": "JSON",
13358
+ "default": []
13359
+ }
13360
+ ],
13361
+ "relationships": [
13362
+ {
13363
+ "name": "sellerTenantId",
13364
+ "type": "crossPackageRef",
13365
+ "required": true,
13366
+ "related": "@happyvertical/smrt-users:Tenant",
13367
+ "columnType": "UUID"
13368
+ },
13369
+ {
13370
+ "name": "payerTenantId",
13371
+ "type": "crossPackageRef",
13372
+ "required": true,
13373
+ "related": "@happyvertical/smrt-users:Tenant",
13374
+ "columnType": "UUID"
13375
+ }
13376
+ ],
13377
+ "methods": [],
13378
+ "conflictColumns": [
13379
+ "provider",
13380
+ "checkout_id"
13381
+ ],
13382
+ "surfaces": [],
13383
+ "relationshipFeatures": [
13384
+ "crossPackageRef",
13385
+ "uuidColumns"
13386
+ ],
13387
+ "tags": [],
13388
+ "risks": []
13389
+ },
13096
13390
  {
13097
13391
  "name": "BillingAccountCollection",
13098
13392
  "qualifiedName": "@happyvertical/smrt-commerce:BillingAccountCollection",
@@ -13143,6 +13437,23 @@
13143
13437
  ],
13144
13438
  "tags": [],
13145
13439
  "risks": []
13440
+ },
13441
+ {
13442
+ "name": "BillingPaymentAttemptCollection",
13443
+ "qualifiedName": "@happyvertical/smrt-commerce:BillingPaymentAttemptCollection",
13444
+ "collection": "billingpaymentattempts",
13445
+ "tableName": "_smrt_billing_payment_attempts",
13446
+ "packageName": "@happyvertical/smrt-commerce",
13447
+ "extends": "SmrtCollection",
13448
+ "fields": [],
13449
+ "relationships": [],
13450
+ "methods": [],
13451
+ "surfaces": [],
13452
+ "relationshipFeatures": [
13453
+ "uuidColumns"
13454
+ ],
13455
+ "tags": [],
13456
+ "risks": []
13146
13457
  }
13147
13458
  ],
13148
13459
  "surfaces": [
@@ -19112,11 +19423,11 @@
19112
19423
  "prompts": [],
19113
19424
  "relationshipsV2": {
19114
19425
  "foreignKeyFields": 37,
19115
- "crossPackageRefFields": 12,
19426
+ "crossPackageRefFields": 14,
19116
19427
  "junctionCollections": 0,
19117
19428
  "hierarchicalObjects": 0,
19118
19429
  "polymorphicAssociations": 0,
19119
- "uuidColumns": 156
19430
+ "uuidColumns": 162
19120
19431
  },
19121
- "agentDoc": "# @happyvertical/smrt-commerce\n\nE-commerce with Contract STI hierarchy, invoice lifecycle, payment tracking, payout remittance, and optional ledger integration.\n\n## Models\n\n- **Customer** / **Vendor**: linked to Profile via string ID (not FK). 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).\n- **Contract** (STI base → Estimate, Order, Lease, Agreement, PurchaseOrder, WholesaleOrder, ProductionOrder, Cart, LicenseSale): 9 contract types sharing one table. Carries `channelId` (open-ended string — `dtc-web`, `wholesale-b2b`, `pos-store-N`, etc.) so the same model serves DTC checkout, B2B portals, and POS.\n- **WholesaleOrder**: B2B order. Conventional pairing — customer has `customerType: 'wholesale'`, NET-30/60 terms, delivered via wholesale-portal channel.\n- **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`).\n- **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.\n- **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.\n- **ContractLineItem**: items on contracts.\n- **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).\n- **InvoiceLineItem**: line items on invoices.\n- **Payment** / **PaymentAllocation**: tracks payments against invoices. Status controlled by `Invoice.updatePaymentStatus()`, not Payment model. 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.\n- **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. Use `PaymentInstrumentCollection.setDefaultForCustomer()` to change the default so the single-default-per-customer invariant is enforced; generated create/update routes cannot write `isDefault` directly.\n- **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 via natural key `(tenant_id, offering_ref, licensee_email, idempotency_key)` and `PaymentIntentCollection.getOrCreateByIdempotencyKey()`.\n- **Payout**: operator-to-supplier remittance. Distinct from Payment because direction, status machine, and chain semantics differ. Status machine `pending → sent → confirmed → failed` (failed is terminal but resettable via `resetFromFailed()` after fixing the underlying problem). References source `paymentId` (plain string) and destination `vendorId` (foreign key). Amount invariant `supplierNet === grossAmount - operatorFee` enforced exactly on save (integer minor units, no tolerance — #2401). `PayoutCollection.createFromPayment()` is the typical entry point; it pulls native amount / currency from the source Payment.\n- **Fulfillment** / **FulfillmentLineItem**: shipment/delivery tracking.\n\n## Ledger Integration\n\n`@happyvertical/smrt-ledgers` is a regular dependency, loaded lazily via dynamic `import()` so the coupling stays runtime-only (no hard static import; the package graph stays a DAG — see #1582). Invoice stores `arJournalId` and `revenueJournalId` as string references. `recognizeRevenue()` creates a balanced AR journal entry; `getArJournal()` returns null when no journal has been recognized yet.\n\n## Billing-period close (#3060)\n\n`src/billing/` closes periods from smrt-subscriptions charges; see the README\nfor usage.\n\n- **Port, not provider.** Period close, events, and credit checkout call only\n the `BillingProvider` port (integer minor units). `stripe.ts` adapts\n `@happyvertical/accounting`, which takes currency *major* units; convert with\n `units.ts` and never hand-roll a Stripe call here. `units.ts` exponents are\n an explicit ISO 4217 table matching the SDK's, never `Intl` display digits\n (CLDR differs for IQD, MGA, … and drifts by ICU release). The SDK's\n `*Minor` fields are already ISO minor units. Capabilities a rail may lack\n (`chargeSavedPaymentMethod`, `createSetupCheckout`, `getCheckout`,\n `setDefaultPaymentMethod`, `markInvoiceUncollectible`) are optional port\n methods: detect them by presence, and keep additions optional (#3139).\n `sendAndReconcile` refuses a provider invoice whose subtotal or total\n disagrees with the local invoice.\n- **Replay safety is identity.** Close, invoice, line, payment, allocation,\n and claim ids are `deterministicId()`s; `BillingLineSource` (conflict key\n `source_type, source_id`) is the only double-billing guard, so never bill a\n source without claiming it, and never widen that key. Each close step is\n persisted before the next; `acquireCloseLease()` serializes workers. A\n close that nets to a credit keeps its claims and ends `carried_forward`;\n its subtotal is a `credit_carry_forward` source for the payer's next billed\n close, which marks the carried close `completed`.\n- **Cycles and flat-plan coverage (#3116).** `cycles.ts` is the schedule\n (pure; `BillingAccount.billingAnchorAt`, null = calendar months). A close\n with no period closes each payer's `lastEndedBillingPeriod()`. Flat-plan\n claims are the exception to \"id from the source\": their `sourceId` is\n `<subscriptionId>:seq:<chainSequence>` and their `periodStart/End` is the\n exact time they bill. `claimFlatWindow()` reads the last number, then the\n coverage (claims ending after the window start), claims one gap as number\n n + 1, and retries on conflict; reading in that order is what makes it\n safe. That numbering, not the period, stops overlaps when schedules or\n payers change — never claim a flat plan outside it, and keep both reads\n bounded (no whole-history scans). Each numbered claim is its own invoice\n line keyed `lineKey|periodStart` (a function of the claim alone, so a\n resumed close rebuilds the same line ids). Proration is\n `prorateMinorUnits()` (half up, BigInt) against the period's full cycle.\n- **System tables.** `BillingAccount`, `BillingPeriodClose`, and\n `BillingLineSource` are not tenant-scoped and have no generated surface.\n Collections stay unexported; models are root exports (#3082). Invoices,\n payments, and journals are written in the seller's tenant; payer-owned\n subscriptions and credit grants in a system context, only after\n verification.\n- **Events** use smrt-jobs' delivery inbox under provider\n `<name>-billing:<sellerTenantId>` and a provider-filtered\n `ForgeProjectionRuntime`, so a runtime never claims another seller's\n events; any other runtime sharing the inbox must pass `providers` too. Only\n events this package acts on are stored: foreign checkouts are `ignored`,\n and only `smrt_*` checkout metadata is kept; it is\n HMAC-signed with the webhook secret at creation (empty values are omitted:\n Stripe drops them), so another integration on the same account cannot forge\n a credit purchase or a card setup; credit is granted only when the\n collected total equals it plus provider tax. Checkout amounts come from the\n re-read session (`getCheckout()`), never the Stripe event, whose amounts are\n in Stripe's unit. Rotating the webhook secret strands in-flight checkouts\n (up to 24 h). `observe()` re-reads provider state, so ordering never depends\n on delivery order; the only provider writes in `observe()` are idempotent\n (making a saved card the default). The standing hook runs inside the event\n transaction and must be idempotent.\n- **Card on file and top-ups (#3139).** A saved card is the payer customer's\n default `PaymentInstrument` (seller tenant, id from the provider method).\n `autoChargeInvoices` bills such payers `charge_automatically`, decided at\n first push; activation follows `paid`, never send. An auto top-up attempt\n is a `PENDING` `Payment` (`reference` `auto-top-up:<policyId>`, id from the\n policy and `count()` of its attempts) inserted before the charge, so racing\n evaluations derive one id and one charge; the charge key is derived from\n that id. `applyAutoTopUpOutcome()` must run in a transaction and starts with\n a conditional `UPDATE … WHERE status = 'pending'` row lock, so the hook and\n the `payment` webhook settle once; the grant is keyed by the charge key.\n Never settle an attempt outside that function. Taxed accounts are skipped\n unless `taxedAccounts: 'charge_untaxed'` (no tax on off-session charges,\n happyvertical/sdk#1283). A provider write-off moves the local invoice to\n `WRITTEN_OFF` (a later payment is recorded; the status stays).\n- **`Invoice.providerTaxAmount`** is added to line-item tax; it is\n server-managed (not API-writable). `toAccountingInput()` emits major units.\n- **Known limits**: write-offs post no bad-debt journal (AR stays until paid\n or journaled by an operator); discounts still reach Stripe as negative lines (the SDK's\n line `discount` would change the lines a replayed push reconciles, so it\n needs its own migration); `RetailCharge` has no adjustment ledger. Flat\n plans are monthly, in arrears; plan changes are not prorated (#3119).\n\n## Cross-Package References\n\n- `customerId` → `@foreignKey('Customer')` (hard reference within package)\n- `profileId` → plain string to smrt-profiles\n- `arJournalId`, `revenueJournalId` → plain string to smrt-ledgers\n- `skuId` (on `PaymentIntent`, `LicenseSale`) → plain string to smrt-products\n- `paymentId` (on `PaymentIntent`, `Payout`, `LicenseSale`) → plain string to `Payment` (same package, but kept as plain string for cross-model consistency)\n- `vendorId` on `Payout` → `@foreignKey(Vendor)` (hard reference within package)\n- Billing records reference tenants with `@crossPackageRef('@happyvertical/smrt-users:Tenant')` and charges, plan periods, and adjustments by plain `sourceId`\n\n## Gotchas\n\n- **Optional tenancy**: all models `@TenantScoped({ mode: 'optional' })` + nullable tenantId\n- **Currency is integer minor units** (cents, satoshis) — `$19.99` is `1999`, like affiliates. Money is exact, so money fields initialize `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER columns (BIGINT on fresh PostgreSQL/DuckDB databases). Writing a fractional major-unit value is the bug — the model rejects it via `assertIntegerMinorUnits` on `Invoice`/`Payment`/`PaymentAllocation`/`Payout`/`Contract` before the database sees it, because PostgreSQL would reject it with `22P02` while SQLite's affinity silently stores it. `pnpm --filter @happyvertical/smrt-commerce test:postgres` is the lane that holds the line (#2361, #2401, #2373).\n- **Rates are the opposite**: `InvoiceLineItem.taxRate` and `ContractLineItem.taxRate` are inherently fractional and must initialize `= 0.0`. INTEGER would truncate every rate to 0. `ContractLineItem.quantity` is decimal for the same reason (contracts price fractional hours/weight/bandwidth); `InvoiceLineItem.quantity` is an integer count.\n- **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem.getTaxAmount()` and `ContractLineItem.calculateAmount()` `Math.round()` the fractional product to whole minor units. Everything downstream is exact integer arithmetic, which is why **there are no `EPSILON` tolerances left on any money path** (#2401). `Invoice`, `PaymentAllocation`, `Payout` and `PaymentIntent` compare with `===` / `>` / `>=`. Do not reintroduce a tolerance: against integers it forgives a real one-cent discrepancy rather than float fuzz. (`FulfillmentLineItem`'s quantity epsilon is unrelated — quantities are genuinely decimal there.)\n- **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).\n- **`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.\n- **`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.\n- **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.\n- **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.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax rate is external**: no tax rate field on Invoice — rate must be calculated externally\n- **Profile linking**: separate `ProfileCollection.create()` needed to fetch actual Profile object\n- **PaymentIntent natural key**: `conflictColumns: ['tenant_id', 'offering_ref', 'licensee_email', 'idempotency_key']` — a retried `create` with the same tuple upserts the existing row. Use `getOrCreateByIdempotencyKey()` to branch on `{ intent, created }`. Empty natural-key inputs (e.g. blank `idempotencyKey`) disable dedup by design.\n- **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.\n- **PaymentInstrument defaults**: `isDefault` is domain-managed, not API-writable. Call `setDefaultForCustomer(customerId, instrumentId)` so the target is validated and the customer's other instruments are cleared.\n- **LicenseSale immutability**: rights snapshot freezes on save-with-status-ACCEPTED. The captured snapshot lives in a module-scoped `WeakMap<LicenseSale, string>` so it doesn't interact with the schema or round-trip through `_meta_data`. Drafts (status != ACCEPTED) remain mutable. To \"change\" an issued license: `revoke()` it, then issue a new `LicenseSale` row.\n- **Vendor.payoutAddresses normalization**: the constructor accepts either a `Record<string, string>` or a pre-serialized JSON string. `initialize()` re-normalizes after the framework's option-override pass; `save()` re-normalizes defensively against direct field assignment. Non-string values inside the input map are silently dropped to preserve the typed invariant downstream.\n"
19432
+ "agentDoc": "# @happyvertical/smrt-commerce\n\nE-commerce with Contract STI hierarchy, invoice lifecycle, payment tracking, payout remittance, and optional ledger integration.\n\n## Models\n\n- **Customer** / **Vendor**: linked to Profile via string ID (not FK). 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).\n- **Contract** (STI base → Estimate, Order, Lease, Agreement, PurchaseOrder, WholesaleOrder, ProductionOrder, Cart, LicenseSale): 9 contract types sharing one table. Carries `channelId` (open-ended string — `dtc-web`, `wholesale-b2b`, `pos-store-N`, etc.) so the same model serves DTC checkout, B2B portals, and POS.\n- **WholesaleOrder**: B2B order. Conventional pairing — customer has `customerType: 'wholesale'`, NET-30/60 terms, delivered via wholesale-portal channel.\n- **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`).\n- **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.\n- **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.\n- **ContractLineItem**: items on contracts.\n- **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).\n- **InvoiceLineItem**: line items on invoices.\n- **Payment** / **PaymentAllocation**: tracks payments against invoices. Status controlled by `Invoice.updatePaymentStatus()`, not Payment model. 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.\n- **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. Use `PaymentInstrumentCollection.setDefaultForCustomer()` to change the default so the single-default-per-customer invariant is enforced; generated create/update routes cannot write `isDefault` directly.\n- **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 via natural key `(tenant_id, offering_ref, licensee_email, idempotency_key)` and `PaymentIntentCollection.getOrCreateByIdempotencyKey()`.\n- **Payout**: operator-to-supplier remittance. Distinct from Payment because direction, status machine, and chain semantics differ. Status machine `pending → sent → confirmed → failed` (failed is terminal but resettable via `resetFromFailed()` after fixing the underlying problem). References source `paymentId` (plain string) and destination `vendorId` (foreign key). Amount invariant `supplierNet === grossAmount - operatorFee` enforced exactly on save (integer minor units, no tolerance — #2401). `PayoutCollection.createFromPayment()` is the typical entry point; it pulls native amount / currency from the source Payment.\n- **Fulfillment** / **FulfillmentLineItem**: shipment/delivery tracking.\n\n## Ledger Integration\n\n`@happyvertical/smrt-ledgers` is a regular dependency, loaded lazily via dynamic `import()` so the coupling stays runtime-only (no hard static import; the package graph stays a DAG — see #1582). Invoice stores `arJournalId` and `revenueJournalId` as string references. `recognizeRevenue()` creates a balanced AR journal entry; `getArJournal()` returns null when no journal has been recognized yet.\n\n## Billing-period close (#3060)\n\n`src/billing/` closes periods from smrt-subscriptions charges; see the README\nfor usage.\n\n- **Port, not provider.** Period close, events, and credit checkout call only\n the `BillingProvider` port (integer minor units). `stripe.ts` adapts\n `@happyvertical/accounting`, which takes currency *major* units; convert with\n `units.ts` and never hand-roll a Stripe call here. `units.ts` exponents are\n an explicit ISO 4217 table matching the SDK's, never `Intl` display digits\n (CLDR differs for IQD, MGA, … and drifts by ICU release). The SDK's\n `*Minor` fields are already ISO minor units. Capabilities a rail may lack\n (`chargeSavedPaymentMethod`, `createSetupCheckout`, `getCheckout`,\n `setDefaultPaymentMethod`, `markInvoiceUncollectible`) are optional port\n methods: detect them by presence, and keep additions optional (#3139).\n `sendAndReconcile` refuses a provider invoice whose subtotal or total\n disagrees with the local invoice.\n- **Replay safety is identity.** Close, invoice, line, payment, allocation,\n and claim ids are `deterministicId()`s; `BillingLineSource` (conflict key\n `source_type, source_id`) is the only double-billing guard, so never bill a\n source without claiming it, and never widen that key. Each close step is\n persisted before the next; `acquireCloseLease()` serializes workers. A\n close that nets to a credit keeps its claims and ends `carried_forward`;\n its subtotal is a `credit_carry_forward` source for the payer's next billed\n close, which marks the carried close `completed`.\n- **Cycles and flat-plan coverage (#3116).** `cycles.ts` is the schedule\n (pure; `BillingAccount.billingAnchorAt`, null = calendar months). A close\n with no period closes each payer's `lastEndedBillingPeriod()`. Flat-plan\n claims are the exception to \"id from the source\": their `sourceId` is\n `<subscriptionId>:seq:<chainSequence>` and their `periodStart/End` is the\n exact time they bill. `claimFlatWindow()` reads the last number, then the\n coverage (claims ending after the window start), claims one gap as number\n n + 1, and retries on conflict; reading in that order is what makes it\n safe. That numbering, not the period, stops overlaps when schedules or\n payers change — never claim a flat plan outside it, and keep both reads\n bounded (no whole-history scans). Each numbered claim is its own invoice\n line keyed `lineKey|periodStart` (a function of the claim alone, so a\n resumed close rebuilds the same line ids). Proration is\n `prorateMinorUnits()` (half up, BigInt) against the period's full cycle.\n- **System tables.** `BillingAccount`, `BillingPeriodClose`, and\n `BillingLineSource` are not tenant-scoped and have no generated surface.\n Collections stay unexported; models are root exports (#3082). Invoices,\n payments, and journals are written in the seller's tenant; payer-owned\n subscriptions and credit grants in a system context, only after\n verification.\n- **Events** use smrt-jobs' delivery inbox under provider\n `<name>-billing:<sellerTenantId>` and a provider-filtered\n `ForgeProjectionRuntime`, so a runtime never claims another seller's\n events; any other runtime sharing the inbox must pass `providers` too. Only\n events this package acts on are stored: foreign checkouts are `ignored`,\n and only `smrt_*` checkout metadata is kept; it is\n HMAC-signed with the webhook secret at creation (empty values are omitted:\n Stripe drops them), so another integration on the same account cannot forge\n a credit purchase or a card setup; credit is granted only when the\n collected total equals it plus provider tax. Checkout amounts come from the\n re-read session (`getCheckout()`), never the Stripe event, whose amounts are\n in Stripe's unit. Rotating the webhook secret strands in-flight checkouts\n (up to 24 h). `observe()` re-reads provider state, so ordering never depends\n on delivery order; the only provider writes in `observe()` are idempotent\n (making a saved card the default). The standing hook runs inside the event\n transaction and must be idempotent.\n- **Card on file and top-ups (#3139).** A saved card is the payer customer's\n default `PaymentInstrument` (seller tenant, id from the provider method).\n `autoChargeInvoices` bills such payers `charge_automatically`, decided at\n first push; activation follows `paid`, never send. An auto top-up attempt\n is a `PENDING` `Payment` (`reference` `auto-top-up:<policyId>`, id from the\n policy and `count()` of its attempts) inserted before the charge, so racing\n evaluations derive one id and one charge; the charge key is derived from\n that id. `applyAutoTopUpOutcome()` must run in a transaction and starts with\n a conditional `UPDATE … WHERE status = 'pending'` row lock, so the hook and\n the `payment` webhook settle once; the grant is keyed by the charge key.\n Never settle an attempt outside that function. Taxed accounts are skipped\n unless `taxedAccounts: 'charge_untaxed'` (no tax on off-session charges,\n happyvertical/sdk#1283). A provider write-off moves the local invoice to\n `WRITTEN_OFF` (a later payment is recorded; the status stays).\n- **`Invoice.providerTaxAmount`** is added to line-item tax; it is\n server-managed (not API-writable). `toAccountingInput()` emits major units.\n- **Payment rails (#3138).** A runtime has one issuing `provider` and any\n number of `paymentProviders` (unique names, one inbox namespace each; a\n provider with `capabilities.issuesInvoices === false` can never issue).\n `observe()` resolves the provider from the delivery namespace, never the\n payload. Crypto rails implement the port over `@happyvertical/payments`'\n `CryptoCheckoutGateway`; never call BTCPay (or any gateway) from here.\n Settlement is decided by the pure `decideAttempt()` from gateway state and\n `paymentPolicy` — the gateway's `settled` is authoritative and nothing here\n counts confirmations. A `BillingPaymentAttempt` (`(provider, checkout_id)`\n deterministic id) records every state and settles once; grants and payments\n are keyed by checkout id. Invoice payments close the issuer's invoice out of\n band in `observe()` (idempotent) before `project()` records the payment, and\n `settlePaidInvoice` skips (records nothing for) a `paidOutOfBand` invoice\n the rail latched with `outOfBandRequestedAt`; other out-of-band closes are\n recorded as `OTHER` payments. Rails never save cards or charge tax: a taxed\n credit purchase on a rail needs an explicit `automaticTax: false`. Exceptions are flags for operators;\n nothing reverses or refunds automatically.\n- **Known limits**: write-offs post no bad-debt journal (AR stays until paid\n or journaled by an operator); discounts still reach Stripe as negative lines (the SDK's\n line `discount` would change the lines a replayed push reconciles, so it\n needs its own migration); `RetailCharge` has no adjustment ledger. Flat\n plans are monthly, in arrears; plan changes are not prorated (#3119).\n\n## Cross-Package References\n\n- `customerId` → `@foreignKey('Customer')` (hard reference within package)\n- `profileId` → plain string to smrt-profiles\n- `arJournalId`, `revenueJournalId` → plain string to smrt-ledgers\n- `skuId` (on `PaymentIntent`, `LicenseSale`) → plain string to smrt-products\n- `paymentId` (on `PaymentIntent`, `Payout`, `LicenseSale`) → plain string to `Payment` (same package, but kept as plain string for cross-model consistency)\n- `vendorId` on `Payout` → `@foreignKey(Vendor)` (hard reference within package)\n- Billing records reference tenants with `@crossPackageRef('@happyvertical/smrt-users:Tenant')` and charges, plan periods, and adjustments by plain `sourceId`\n\n## Gotchas\n\n- **Optional tenancy**: all models `@TenantScoped({ mode: 'optional' })` + nullable tenantId\n- **Currency is integer minor units** (cents, satoshis) — `$19.99` is `1999`, like affiliates. Money is exact, so money fields initialize `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER columns (BIGINT on fresh PostgreSQL/DuckDB databases). Writing a fractional major-unit value is the bug — the model rejects it via `assertIntegerMinorUnits` on `Invoice`/`Payment`/`PaymentAllocation`/`Payout`/`Contract` before the database sees it, because PostgreSQL would reject it with `22P02` while SQLite's affinity silently stores it. `pnpm --filter @happyvertical/smrt-commerce test:postgres` is the lane that holds the line (#2361, #2401, #2373).\n- **Rates are the opposite**: `InvoiceLineItem.taxRate` and `ContractLineItem.taxRate` are inherently fractional and must initialize `= 0.0`. INTEGER would truncate every rate to 0. `ContractLineItem.quantity` is decimal for the same reason (contracts price fractional hours/weight/bandwidth); `InvoiceLineItem.quantity` is an integer count.\n- **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem.getTaxAmount()` and `ContractLineItem.calculateAmount()` `Math.round()` the fractional product to whole minor units. Everything downstream is exact integer arithmetic, which is why **there are no `EPSILON` tolerances left on any money path** (#2401). `Invoice`, `PaymentAllocation`, `Payout` and `PaymentIntent` compare with `===` / `>` / `>=`. Do not reintroduce a tolerance: against integers it forgives a real one-cent discrepancy rather than float fuzz. (`FulfillmentLineItem`'s quantity epsilon is unrelated — quantities are genuinely decimal there.)\n- **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).\n- **`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.\n- **`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.\n- **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.\n- **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.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax rate is external**: no tax rate field on Invoice — rate must be calculated externally\n- **Profile linking**: separate `ProfileCollection.create()` needed to fetch actual Profile object\n- **PaymentIntent natural key**: `conflictColumns: ['tenant_id', 'offering_ref', 'licensee_email', 'idempotency_key']` — a retried `create` with the same tuple upserts the existing row. Use `getOrCreateByIdempotencyKey()` to branch on `{ intent, created }`. Empty natural-key inputs (e.g. blank `idempotencyKey`) disable dedup by design.\n- **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.\n- **PaymentInstrument defaults**: `isDefault` is domain-managed, not API-writable. Call `setDefaultForCustomer(customerId, instrumentId)` so the target is validated and the customer's other instruments are cleared.\n- **LicenseSale immutability**: rights snapshot freezes on save-with-status-ACCEPTED. The captured snapshot lives in a module-scoped `WeakMap<LicenseSale, string>` so it doesn't interact with the schema or round-trip through `_meta_data`. Drafts (status != ACCEPTED) remain mutable. To \"change\" an issued license: `revoke()` it, then issue a new `LicenseSale` row.\n- **Vendor.payoutAddresses normalization**: the constructor accepts either a `Record<string, string>` or a pre-serialized JSON string. `initialize()` re-normalizes after the framework's option-override pass; `save()` re-normalizes defensively against direct field assignment. Non-string values inside the input map are silently dropped to preserve the typed invariant downstream.\n"
19122
19433
  }
@@ -1,4 +1,8 @@
1
- import { SmrtObjectOptions } from '@happyvertical/smrt-core';
1
+ /**
2
+ * Commerce package type definitions
3
+ * @packageDocumentation
4
+ */
5
+ import type { SmrtObjectOptions } from '@happyvertical/smrt-core';
2
6
  export declare enum CustomerStatus {
3
7
  ACTIVE = "active",
4
8
  INACTIVE = "inactive",
package/dist/ui.d.ts CHANGED
@@ -1,4 +1,17 @@
1
- import { ModuleUISlot, SmrtModuleMeta } from '@happyvertical/smrt-types';
1
+ /**
2
+ * Commerce Module UI Slot Declarations
3
+ *
4
+ * This file defines the UI extension points for the commerce module.
5
+ * UI components are implemented in the ./svelte subpath.
6
+ *
7
+ * @example Usage
8
+ * ```typescript
9
+ * // In application code
10
+ * import { COMMERCE_MODULE_META } from '@happyvertical/smrt-commerce';
11
+ * console.log(COMMERCE_MODULE_META.uiSlots);
12
+ * ```
13
+ */
14
+ import type { ModuleUISlot, SmrtModuleMeta } from '@happyvertical/smrt-types';
2
15
  /**
3
16
  * Commerce module UI slots
4
17
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-commerce",
3
- "version": "0.51.32",
3
+ "version": "0.51.34",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Commerce models for the SMRT framework - contracts, fulfillments, payments",
6
6
  "type": "module",
@@ -51,13 +51,14 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@happyvertical/accounting": "^0.94.1",
54
- "@happyvertical/smrt-core": "0.51.32",
55
- "@happyvertical/smrt-jobs": "0.51.32",
56
- "@happyvertical/smrt-ledgers": "0.51.32",
57
- "@happyvertical/smrt-subscriptions": "0.51.32",
58
- "@happyvertical/smrt-tenancy": "0.51.32",
59
- "@happyvertical/smrt-types": "0.51.32",
60
- "@happyvertical/smrt-ui": "0.51.32"
54
+ "@happyvertical/payments": "^0.94.1",
55
+ "@happyvertical/smrt-core": "0.51.34",
56
+ "@happyvertical/smrt-jobs": "0.51.34",
57
+ "@happyvertical/smrt-ledgers": "0.51.34",
58
+ "@happyvertical/smrt-subscriptions": "0.51.34",
59
+ "@happyvertical/smrt-tenancy": "0.51.34",
60
+ "@happyvertical/smrt-types": "0.51.34",
61
+ "@happyvertical/smrt-ui": "0.51.34"
61
62
  },
62
63
  "peerDependencies": {
63
64
  "svelte": "^5.57.1"
@@ -68,14 +69,14 @@
68
69
  }
69
70
  },
70
71
  "devDependencies": {
71
- "@happyvertical/smrt-vitest": "0.51.32",
72
+ "@happyvertical/smrt-vitest": "0.51.34",
72
73
  "@sveltejs/package": "^2.5.8",
73
74
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
74
75
  "@types/node": "24.13.6",
75
76
  "fast-glob": "^3.3.3",
76
77
  "svelte": "^5.57.1",
77
78
  "svelte-check": "^4.7.6",
78
- "typescript": "5.9.3",
79
+ "typescript": "6.0.3",
79
80
  "vite": "8.1.4",
80
81
  "vitest": "4.1.10"
81
82
  },