@happyvertical/smrt-commerce 0.56.2 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/AGENTS.md +24 -11
  2. package/README.md +137 -89
  3. package/agents/accounting-snapshot-mapper.md +3 -3
  4. package/agents/commercial-ui.md +2 -2
  5. package/agents/contract-money.md +9 -12
  6. package/agents/cost-billing.md +3 -6
  7. package/agents/customer-vendor-ui.md +1 -2
  8. package/agents/document-money.md +58 -31
  9. package/agents/invoice-calculations.md +31 -56
  10. package/agents/invoice-preparation.md +62 -73
  11. package/agents/invoice-quantity.md +9 -21
  12. package/agents/minor-unit-migration.md +4 -4
  13. package/agents/money-calculator.md +7 -8
  14. package/agents/money-model.md +18 -22
  15. package/agents/money-upgrade.md +92 -0
  16. package/agents/revenue-journal.md +2 -2
  17. package/agents/tax-codes.md +17 -14
  18. package/agents/tax-summary.md +8 -6
  19. package/dist/billing/period-close.d.ts.map +1 -1
  20. package/dist/billing/runtime.d.ts +5 -2
  21. package/dist/billing/runtime.d.ts.map +1 -1
  22. package/dist/chunks/{ContractLineItemCollection--BrGMtNz.js → ContractLineItemCollection-BZcrlrrY.js} +19 -24
  23. package/dist/chunks/ContractLineItemCollection-BZcrlrrY.js.map +1 -0
  24. package/dist/chunks/{CustomerCollection-CjGaZSpf.js → CustomerCollection-BIPH2ZUx.js} +2 -2
  25. package/dist/chunks/{CustomerCollection-CjGaZSpf.js.map → CustomerCollection-BIPH2ZUx.js.map} +1 -1
  26. package/dist/chunks/{DocumentTaxCollection-KOkaQT2M.js → DocumentTaxCollection-DDzlywMW.js} +14 -7
  27. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +1 -0
  28. package/dist/chunks/{InvoiceCollection-BUe_8FT1.js → InvoiceCollection-cinzAqt6.js} +50 -161
  29. package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +1 -0
  30. package/dist/chunks/{TaxCodeCollection-DF2eaJed.js → TaxCodeCollection-DaBOPG9c.js} +11 -4
  31. package/dist/chunks/{TaxCodeCollection-DF2eaJed.js.map → TaxCodeCollection-DaBOPG9c.js.map} +1 -1
  32. package/dist/chunks/{TaxComponentCollection-3x24xSNh.js → TaxComponentCollection-C4zxnyVu.js} +3 -3
  33. package/dist/chunks/{TaxComponentCollection-3x24xSNh.js.map → TaxComponentCollection-C4zxnyVu.js.map} +1 -1
  34. package/dist/chunks/VendorCollection-D6ZcUYy8.js +100 -0
  35. package/dist/chunks/VendorCollection-D6ZcUYy8.js.map +1 -0
  36. package/dist/chunks/{allocation-integrity-Cq0g6CfO.js → allocation-integrity-WmWyXhtG.js} +6 -6
  37. package/dist/chunks/{allocation-integrity-Cq0g6CfO.js.map → allocation-integrity-WmWyXhtG.js.map} +1 -1
  38. package/dist/chunks/calculations-C9daQLsh.js +82 -0
  39. package/dist/chunks/calculations-C9daQLsh.js.map +1 -0
  40. package/dist/chunks/contract-money-EuToOghh.js +301 -0
  41. package/dist/chunks/contract-money-EuToOghh.js.map +1 -0
  42. package/dist/chunks/detect-CY9ufaFb.js +328 -0
  43. package/dist/chunks/detect-CY9ufaFb.js.map +1 -0
  44. package/dist/chunks/{document-money-oUDvdjOf.js → document-money-DV5t4vlK.js} +388 -62
  45. package/dist/chunks/document-money-DV5t4vlK.js.map +1 -0
  46. package/dist/chunks/{invoice-posting-CF3fE_Ec.js → invoice-posting-C2i5dOVZ.js} +20 -22
  47. package/dist/chunks/invoice-posting-C2i5dOVZ.js.map +1 -0
  48. package/dist/chunks/{revenue-journal-DY9fro8E.js → revenue-journal-LLA9Kh_i.js} +3 -14
  49. package/dist/chunks/revenue-journal-LLA9Kh_i.js.map +1 -0
  50. package/dist/chunks/tenant-money-barrier-6TlqUeVj.js +255 -0
  51. package/dist/chunks/tenant-money-barrier-6TlqUeVj.js.map +1 -0
  52. package/dist/chunks/types-Bbm0Xflc.js.map +1 -1
  53. package/dist/chunks/{units-CNcjH_eE.js → units-8ElbjVZK.js} +18 -3
  54. package/dist/chunks/units-8ElbjVZK.js.map +1 -0
  55. package/dist/collections/ContractCollection.d.ts.map +1 -1
  56. package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
  57. package/dist/collections/InvoiceCollection.d.ts.map +1 -1
  58. package/dist/collections/InvoiceLineItemCollection.d.ts +0 -30
  59. package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
  60. package/dist/collections/TaxCodeCollection.d.ts +5 -1
  61. package/dist/collections/TaxCodeCollection.d.ts.map +1 -1
  62. package/dist/collections/TaxSettingsCollection.d.ts +15 -8
  63. package/dist/collections/TaxSettingsCollection.d.ts.map +1 -1
  64. package/dist/collections/TaxSummaryReportCollection.d.ts +3 -3
  65. package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -1
  66. package/dist/collections/index.d.ts +0 -1
  67. package/dist/collections/index.d.ts.map +1 -1
  68. package/dist/commerce-money-tools.d.ts.map +1 -1
  69. package/dist/contract-money.d.ts +1 -13
  70. package/dist/contract-money.d.ts.map +1 -1
  71. package/dist/document-money-internal.d.ts +7 -3
  72. package/dist/document-money-internal.d.ts.map +1 -1
  73. package/dist/document-money.d.ts +1 -11
  74. package/dist/document-money.d.ts.map +1 -1
  75. package/dist/index.d.ts +6 -5
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +667 -1650
  78. package/dist/index.js.map +1 -1
  79. package/dist/invoice-accounting-sync.d.ts.map +1 -1
  80. package/dist/invoice-mcp.d.ts +3 -3
  81. package/dist/invoice-mcp.d.ts.map +1 -1
  82. package/dist/invoice-posting.d.ts +11 -5
  83. package/dist/invoice-posting.d.ts.map +1 -1
  84. package/dist/invoice-record-screen.d.ts +1 -1
  85. package/dist/invoice-record-screen.d.ts.map +1 -1
  86. package/dist/invoices.d.ts +6 -1
  87. package/dist/invoices.d.ts.map +1 -1
  88. package/dist/invoices.js +2 -2
  89. package/dist/manifest.json +37 -765
  90. package/dist/migrations/invoiceEditorStorage.d.ts +2 -4
  91. package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -1
  92. package/dist/models/Contract.d.ts.map +1 -1
  93. package/dist/models/ContractLineItem.d.ts +6 -22
  94. package/dist/models/ContractLineItem.d.ts.map +1 -1
  95. package/dist/models/Invoice.d.ts +18 -48
  96. package/dist/models/Invoice.d.ts.map +1 -1
  97. package/dist/models/InvoiceLineItem.d.ts +8 -37
  98. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  99. package/dist/models/TaxSettings.d.ts +0 -10
  100. package/dist/models/TaxSettings.d.ts.map +1 -1
  101. package/dist/models/index.d.ts +0 -1
  102. package/dist/models/index.d.ts.map +1 -1
  103. package/dist/playground.js +1 -1
  104. package/dist/playground.js.map +1 -1
  105. package/dist/recipes.d.ts +8 -11
  106. package/dist/recipes.d.ts.map +1 -1
  107. package/dist/revenue-journal.d.ts +0 -8
  108. package/dist/revenue-journal.d.ts.map +1 -1
  109. package/dist/smrt-knowledge.json +79 -661
  110. package/dist/svelte/components/InvoiceEditor.svelte +7 -148
  111. package/dist/svelte/components/InvoiceEditor.svelte.d.ts +4 -1
  112. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  113. package/dist/svelte/components/InvoiceTotals.svelte +14 -118
  114. package/dist/svelte/components/InvoiceTotals.svelte.d.ts +4 -27
  115. package/dist/svelte/components/InvoiceTotals.svelte.d.ts.map +1 -1
  116. package/dist/svelte/components/MoneyInvoiceEditor.svelte +3 -23
  117. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +3 -26
  118. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -1
  119. package/dist/svelte/index.d.ts +2 -3
  120. package/dist/svelte/index.d.ts.map +1 -1
  121. package/dist/svelte/index.js +1 -2
  122. package/dist/svelte/invoices/calculations.d.ts +3 -108
  123. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  124. package/dist/svelte/invoices/calculations.js +12 -309
  125. package/dist/svelte/invoices/messages.d.ts +1 -37
  126. package/dist/svelte/invoices/messages.d.ts.map +1 -1
  127. package/dist/svelte/invoices/messages.js +1 -37
  128. package/dist/svelte/invoices/money-editor.d.ts +1 -1
  129. package/dist/svelte/invoices/money-editor.d.ts.map +1 -1
  130. package/dist/svelte/invoices/types.d.ts +14 -114
  131. package/dist/svelte/invoices/types.d.ts.map +1 -1
  132. package/dist/svelte/playground/InvoicePreparationPreview.svelte +85 -94
  133. package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -1
  134. package/dist/svelte/playground.js +1 -1
  135. package/dist/tax/index.d.ts +2 -1
  136. package/dist/tax/index.d.ts.map +1 -1
  137. package/dist/tax/standard-codes.d.ts +12 -0
  138. package/dist/tax/standard-codes.d.ts.map +1 -0
  139. package/dist/tax/tenant-bootstrap.d.ts +29 -0
  140. package/dist/tax/tenant-bootstrap.d.ts.map +1 -0
  141. package/dist/tenant-money-barrier.d.ts +12 -44
  142. package/dist/tenant-money-barrier.d.ts.map +1 -1
  143. package/dist/types/index.d.ts +10 -23
  144. package/dist/types/index.d.ts.map +1 -1
  145. package/dist/ui.js +1 -1
  146. package/dist/ui.js.map +1 -1
  147. package/dist/upgrade/detect.d.ts +129 -0
  148. package/dist/upgrade/detect.d.ts.map +1 -0
  149. package/dist/upgrade/money-v2.d.ts +81 -0
  150. package/dist/upgrade/money-v2.d.ts.map +1 -0
  151. package/package.json +14 -13
  152. package/agents/line-amount-migration.md +0 -50
  153. package/dist/chunks/ContractLineItemCollection--BrGMtNz.js.map +0 -1
  154. package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js.map +0 -1
  155. package/dist/chunks/InvoiceCollection-BUe_8FT1.js.map +0 -1
  156. package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js +0 -468
  157. package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js.map +0 -1
  158. package/dist/chunks/TaxSettingsCollection-DYYF-In-.js +0 -319
  159. package/dist/chunks/TaxSettingsCollection-DYYF-In-.js.map +0 -1
  160. package/dist/chunks/calculations-DaHR17il.js +0 -333
  161. package/dist/chunks/calculations-DaHR17il.js.map +0 -1
  162. package/dist/chunks/document-money-oUDvdjOf.js.map +0 -1
  163. package/dist/chunks/invoice-posting-CF3fE_Ec.js.map +0 -1
  164. package/dist/chunks/revenue-journal-DY9fro8E.js.map +0 -1
  165. package/dist/chunks/tenant-money-barrier-y6EAa6vY.js +0 -135
  166. package/dist/chunks/tenant-money-barrier-y6EAa6vY.js.map +0 -1
  167. package/dist/chunks/units-CNcjH_eE.js.map +0 -1
  168. package/dist/collections/MoneyMigrationDocumentCollection.d.ts +0 -12
  169. package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +0 -1
  170. package/dist/migrations/commerceLineAmounts.d.ts +0 -84
  171. package/dist/migrations/commerceLineAmounts.d.ts.map +0 -1
  172. package/dist/models/MoneyMigrationDocument.d.ts +0 -28
  173. package/dist/models/MoneyMigrationDocument.d.ts.map +0 -1
  174. package/dist/svelte/components/InvoiceLineEditor.svelte +0 -92
  175. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +0 -8
  176. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +0 -1
  177. package/dist/svelte/invoices/fields.d.ts +0 -6
  178. package/dist/svelte/invoices/fields.d.ts.map +0 -1
  179. package/dist/svelte/invoices/fields.js +0 -22
  180. package/dist/tax/legacy.d.ts +0 -48
  181. package/dist/tax/legacy.d.ts.map +0 -1
@@ -3,27 +3,27 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-commerce",
6
- "packageVersion": "0.56.2",
6
+ "packageVersion": "0.57.0",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "1709d85cb8792ebd76ad2cad155869fee2a842e76bf9026de20db17b7cfdb970",
11
- "packageJson": "4bed4334e0555afdf01c9638e465444975b6910a0072058009536fd98410a400",
12
- "agents": "da15a65f8cedc749febdb6da45b6ff23d56d069c50c5050b7a031e99f805d261",
13
- "moduleDoc:agents/customer-vendor-ui.md": "9610530794a391407ebe1b4776d445051e60b2e05392ac027dcf993a6fd7e4d0",
14
- "moduleDoc:README.md": "8e2643ad99129702519fea8bae4da5f13728ac7e040f714682b8126f648a6be7",
15
- "moduleDoc:agents/invoice-quantity.md": "bc3b617d4fb8cc87511763b281283107557ba64795e64522193ef967a3715d32",
10
+ "manifest": "f0d9613eb446ae46723681439479b308b5783a3111d5ed9917dad8ae412ab5b7",
11
+ "packageJson": "c5c76488cb1a2603ade2818edea139ab54289f1282232fc1c39538d057ffe13a",
12
+ "agents": "db9d73082925a67f2045eb7d81f371020986426ec073b136f109f5c896190338",
13
+ "moduleDoc:agents/customer-vendor-ui.md": "2b22e519411a16d8c493496fc168da77b9acd25f6077ea8fbac00cfee3a3501c",
14
+ "moduleDoc:README.md": "cb679617ad121b62cdd24d973aeb95b001a52c80efe544219e0755304939c9e5",
15
+ "moduleDoc:agents/invoice-quantity.md": "3a5fbb464c8fe63acc021b2259ba36f052bb22c49b29e503639c5316814e2028",
16
16
  "moduleDoc:agents/payment-allocations.md": "46c0b921f0e40060cd1caec2dcefb01ed6612e31e4bf220a1a0b61e4e2a58212",
17
- "moduleDoc:agents/tax-codes.md": "92976babf2d5e90baaccea1704b9069987b578e1bc73dc51925780769c318dcc",
18
- "moduleDoc:agents/document-money.md": "64336ea251f6a0069f2426852ae21af77e75c55c0917de4fe62970f4a1f76f74",
19
- "moduleDoc:agents/contract-money.md": "3722667a0ad115393a4afaee0f010bf8d1227115c5f46ec8c22d0dc64f8313e7",
20
- "moduleDoc:agents/money-model.md": "3ecc5a59c39434d7a0bf96be1ca9a194280bf95a4efe3839708df5c98cfd7907",
21
- "moduleDoc:agents/money-calculator.md": "75a9d3e24056505113798e15c68248bef4ce41745685cab29546c12e37735b6d",
22
- "moduleDoc:agents/line-amount-migration.md": "0441e77c27dbdf40ad89b71769943249588dc90dca2bd4e442c93c8b3906ca11",
23
- "moduleDoc:agents/commercial-ui.md": "308729a481c82903d06837e7052c04509a92cf17d17345f71dce88d8d42d0431",
17
+ "moduleDoc:agents/tax-codes.md": "6b07282f0ac93db4fd42ddb4a9551d880e24ec2bd2fae9d06c06561313b3cb9f",
18
+ "moduleDoc:agents/document-money.md": "7f198737c66d190dbdc9cc9a3dbef460fb7c9fb8ee47dd0eaaadde2f2ba91d47",
19
+ "moduleDoc:agents/money-upgrade.md": "904e218a271d4a8d36a47a50f5f26802a9cf8ba7a69daf7d8857b6b2e4ca814d",
20
+ "moduleDoc:agents/contract-money.md": "21cbd647e47dda6c0e5c51123e89ed1317282ea6ad0f3a6679b320b73264e5d2",
21
+ "moduleDoc:agents/money-model.md": "1adb9ed97d688832274596d21dd60ea13df1cb07770b1619cdd03f8683eb1102",
22
+ "moduleDoc:agents/money-calculator.md": "415e5d499d2b81393c9b8720ead35a1c24d52db7e052964deb01449b52486b2d",
23
+ "moduleDoc:agents/commercial-ui.md": "37397f3e3d77c30ef40b0962d606e17fa61b5bb6f57d1cf13cdc26b4fc6a6cbc",
24
24
  "moduleDoc:agents/quote-ui.md": "9d1faaa12f39e330294693f5309d9766c593dc0f7ced9e8b1dcaf5c81b066205",
25
25
  "moduleDoc:agents/purchasing-ui.md": "d2b413009d466f3320934adf1e30ffbb7278138b859df69635e7c1f77efbbd1c",
26
- "moduleDoc:agents/cost-billing.md": "ba252155137123a092391147df2630fb9c4aeba745635651ac484fc1cf13dbdd"
26
+ "moduleDoc:agents/cost-billing.md": "eaedcc68bb5c2ad7994bf1fc873175b6b800a6ca87e04ba6a51e041d4b765561"
27
27
  },
28
28
  "exports": [
29
29
  ".",
@@ -43,12 +43,13 @@
43
43
  "@happyvertical/smrt-jobs": "workspace:*",
44
44
  "@happyvertical/smrt-ledgers": "workspace:*",
45
45
  "@happyvertical/smrt-products": "workspace:*",
46
+ "@happyvertical/smrt-reports": "workspace:*",
46
47
  "@happyvertical/smrt-subscriptions": "workspace:*",
48
+ "@happyvertical/smrt-svelte": "workspace:*",
47
49
  "@happyvertical/smrt-tenancy": "workspace:*",
48
50
  "@happyvertical/smrt-types": "workspace:*",
49
51
  "@happyvertical/smrt-ui": "workspace:*",
50
- "@happyvertical/smrt-reports": "workspace:*",
51
- "@happyvertical/smrt-svelte": "workspace:*",
52
+ "@happyvertical/sql": "catalog:",
52
53
  "@happyvertical/smrt-vitest": "workspace:*",
53
54
  "@sveltejs/package": "^2.5.8",
54
55
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
@@ -78,7 +79,9 @@
78
79
  "@happyvertical/smrt-ui",
79
80
  "@happyvertical/smrt-vitest"
80
81
  ],
81
- "sdkDependencies": [],
82
+ "sdkDependencies": [
83
+ "@happyvertical/sql"
84
+ ],
82
85
  "tags": [
83
86
  "accounting",
84
87
  "commerce",
@@ -1604,12 +1607,8 @@
1604
1607
  "findWithGlobals",
1605
1608
  "get",
1606
1609
  "getNextSortOrder",
1607
- "getSubtotalForInvoice",
1608
- "getTaxForInvoice",
1609
1610
  "getTotalForInvoice",
1610
- "list",
1611
- "recalculateAmounts",
1612
- "toAccountingLineItems"
1611
+ "list"
1613
1612
  ],
1614
1613
  "methodSignatures": [
1615
1614
  {
@@ -1675,22 +1674,6 @@
1675
1674
  ],
1676
1675
  "returns": "Promise<number>"
1677
1676
  },
1678
- {
1679
- "name": "getSubtotalForInvoice",
1680
- "async": true,
1681
- "params": [
1682
- "invoiceId: string"
1683
- ],
1684
- "returns": "Promise<number>"
1685
- },
1686
- {
1687
- "name": "getTaxForInvoice",
1688
- "async": true,
1689
- "params": [
1690
- "invoiceId: string"
1691
- ],
1692
- "returns": "Promise<number>"
1693
- },
1694
1677
  {
1695
1678
  "name": "getTotalForInvoice",
1696
1679
  "async": true,
@@ -1706,22 +1689,6 @@
1706
1689
  "options?: SmrtListOptions<InvoiceLineItem>"
1707
1690
  ],
1708
1691
  "returns": "Promise<SmrtObject[] | Record<string, unknown>[]>"
1709
- },
1710
- {
1711
- "name": "recalculateAmounts",
1712
- "async": true,
1713
- "params": [
1714
- "invoiceId: string"
1715
- ],
1716
- "returns": "Promise<InvoiceLineItem[]>"
1717
- },
1718
- {
1719
- "name": "toAccountingLineItems",
1720
- "async": true,
1721
- "params": [
1722
- "invoiceId: string"
1723
- ],
1724
- "returns": "Promise<AccountingLineItemInput[]>"
1725
1692
  }
1726
1693
  ],
1727
1694
  "surfaces": [
@@ -1749,36 +1716,12 @@
1749
1716
  "operation": "getTotalForInvoice",
1750
1717
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1751
1718
  },
1752
- {
1753
- "kind": "cli",
1754
- "name": "invoicelineitemcollection_getSubtotalForInvoice",
1755
- "operation": "getSubtotalForInvoice",
1756
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1757
- },
1758
- {
1759
- "kind": "cli",
1760
- "name": "invoicelineitemcollection_getTaxForInvoice",
1761
- "operation": "getTaxForInvoice",
1762
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1763
- },
1764
1719
  {
1765
1720
  "kind": "cli",
1766
1721
  "name": "invoicelineitemcollection_getNextSortOrder",
1767
1722
  "operation": "getNextSortOrder",
1768
1723
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1769
1724
  },
1770
- {
1771
- "kind": "cli",
1772
- "name": "invoicelineitemcollection_recalculateAmounts",
1773
- "operation": "recalculateAmounts",
1774
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1775
- },
1776
- {
1777
- "kind": "cli",
1778
- "name": "invoicelineitemcollection_toAccountingLineItems",
1779
- "operation": "toAccountingLineItems",
1780
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1781
- },
1782
1725
  {
1783
1726
  "kind": "cli",
1784
1727
  "name": "invoicelineitemcollection_findByTenant",
@@ -1848,78 +1791,12 @@
1848
1791
  "reason": "not listed in api.include",
1849
1792
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1850
1793
  },
1851
- {
1852
- "kind": "api",
1853
- "operation": "getSubtotalForInvoice",
1854
- "code": "not-included",
1855
- "reason": "not listed in api.include",
1856
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1857
- },
1858
- {
1859
- "kind": "api",
1860
- "operation": "getTaxForInvoice",
1861
- "code": "not-included",
1862
- "reason": "not listed in api.include",
1863
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1864
- },
1865
1794
  {
1866
1795
  "kind": "api",
1867
1796
  "operation": "getTotalForInvoice",
1868
1797
  "code": "not-included",
1869
1798
  "reason": "not listed in api.include",
1870
1799
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1871
- },
1872
- {
1873
- "kind": "api",
1874
- "operation": "recalculateAmounts",
1875
- "code": "not-included",
1876
- "reason": "not listed in api.include",
1877
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1878
- },
1879
- {
1880
- "kind": "api",
1881
- "operation": "toAccountingLineItems",
1882
- "code": "not-included",
1883
- "reason": "not listed in api.include",
1884
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
1885
- }
1886
- ],
1887
- "relationshipFeatures": [
1888
- "uuidColumns"
1889
- ],
1890
- "tags": [],
1891
- "risks": []
1892
- },
1893
- {
1894
- "name": "MoneyMigrationDocumentCollection",
1895
- "qualifiedName": "@happyvertical/smrt-commerce:MoneyMigrationDocumentCollection",
1896
- "collection": "moneymigrationdocuments",
1897
- "tableName": "commerce_money_migration_documents",
1898
- "packageName": "@happyvertical/smrt-commerce",
1899
- "extends": "SmrtCollection",
1900
- "fields": [],
1901
- "relationships": [],
1902
- "methods": [
1903
- "findDocument"
1904
- ],
1905
- "methodSignatures": [
1906
- {
1907
- "name": "findDocument",
1908
- "async": true,
1909
- "params": [
1910
- "input: object"
1911
- ],
1912
- "returns": "Promise<MoneyMigrationDocument | null>"
1913
- }
1914
- ],
1915
- "surfaces": [],
1916
- "withheldSurfaces": [
1917
- {
1918
- "kind": "api",
1919
- "operation": "findDocument",
1920
- "code": "api-disabled",
1921
- "reason": "api is disabled",
1922
- "objectName": "@happyvertical/smrt-commerce:MoneyMigrationDocumentCollection"
1923
1800
  }
1924
1801
  ],
1925
1802
  "relationshipFeatures": [
@@ -3174,7 +3051,8 @@
3174
3051
  "name": "findByCode",
3175
3052
  "async": true,
3176
3053
  "params": [
3177
- "code: string"
3054
+ "code: string",
3055
+ "tenantId?: string"
3178
3056
  ],
3179
3057
  "returns": "Promise<TaxCode | null>"
3180
3058
  }
@@ -3229,42 +3107,11 @@
3229
3107
  "fields": [],
3230
3108
  "relationships": [],
3231
3109
  "methods": [
3232
- "activateMoneyV2",
3233
- "beginMoneyMigration",
3234
- "beginMoneyRollback",
3235
3110
  "getForTenant",
3236
3111
  "getOrCreateForTenant",
3237
- "lockForTenant",
3238
- "restoreLegacyMoneyModel"
3112
+ "lockForTenant"
3239
3113
  ],
3240
3114
  "methodSignatures": [
3241
- {
3242
- "name": "activateMoneyV2",
3243
- "async": true,
3244
- "params": [
3245
- "tenantId: string",
3246
- "migrationId: string"
3247
- ],
3248
- "returns": "Promise<void>"
3249
- },
3250
- {
3251
- "name": "beginMoneyMigration",
3252
- "async": true,
3253
- "params": [
3254
- "tenantId: string",
3255
- "requestedMigrationId?: string"
3256
- ],
3257
- "returns": "Promise<string>"
3258
- },
3259
- {
3260
- "name": "beginMoneyRollback",
3261
- "async": true,
3262
- "params": [
3263
- "tenantId: string",
3264
- "migrationId: string"
3265
- ],
3266
- "returns": "Promise<void>"
3267
- },
3268
3115
  {
3269
3116
  "name": "getForTenant",
3270
3117
  "async": true,
@@ -3285,19 +3132,9 @@
3285
3132
  "name": "lockForTenant",
3286
3133
  "async": true,
3287
3134
  "params": [
3288
- "tenantId: string",
3289
- "create?: any"
3290
- ],
3291
- "returns": "Promise<TaxSettings | null>"
3292
- },
3293
- {
3294
- "name": "restoreLegacyMoneyModel",
3295
- "async": true,
3296
- "params": [
3297
- "tenantId: string",
3298
- "migrationId: string"
3135
+ "tenantId: string"
3299
3136
  ],
3300
- "returns": "Promise<void>"
3137
+ "returns": "Promise<TaxSettings>"
3301
3138
  }
3302
3139
  ],
3303
3140
  "surfaces": [
@@ -3318,54 +3155,9 @@
3318
3155
  "name": "taxsettingscollection_lockForTenant",
3319
3156
  "operation": "lockForTenant",
3320
3157
  "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3321
- },
3322
- {
3323
- "kind": "cli",
3324
- "name": "taxsettingscollection_beginMoneyMigration",
3325
- "operation": "beginMoneyMigration",
3326
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3327
- },
3328
- {
3329
- "kind": "cli",
3330
- "name": "taxsettingscollection_activateMoneyV2",
3331
- "operation": "activateMoneyV2",
3332
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3333
- },
3334
- {
3335
- "kind": "cli",
3336
- "name": "taxsettingscollection_restoreLegacyMoneyModel",
3337
- "operation": "restoreLegacyMoneyModel",
3338
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3339
- },
3340
- {
3341
- "kind": "cli",
3342
- "name": "taxsettingscollection_beginMoneyRollback",
3343
- "operation": "beginMoneyRollback",
3344
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3345
3158
  }
3346
3159
  ],
3347
3160
  "withheldSurfaces": [
3348
- {
3349
- "kind": "api",
3350
- "operation": "activateMoneyV2",
3351
- "code": "not-included",
3352
- "reason": "not listed in api.include",
3353
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3354
- },
3355
- {
3356
- "kind": "api",
3357
- "operation": "beginMoneyMigration",
3358
- "code": "not-included",
3359
- "reason": "not listed in api.include",
3360
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3361
- },
3362
- {
3363
- "kind": "api",
3364
- "operation": "beginMoneyRollback",
3365
- "code": "not-included",
3366
- "reason": "not listed in api.include",
3367
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3368
- },
3369
3161
  {
3370
3162
  "kind": "api",
3371
3163
  "operation": "getForTenant",
@@ -3386,13 +3178,6 @@
3386
3178
  "code": "not-included",
3387
3179
  "reason": "not listed in api.include",
3388
3180
  "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3389
- },
3390
- {
3391
- "kind": "api",
3392
- "operation": "restoreLegacyMoneyModel",
3393
- "code": "not-included",
3394
- "reason": "not listed in api.include",
3395
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
3396
3181
  }
3397
3182
  ],
3398
3183
  "relationshipFeatures": [
@@ -10545,12 +10330,6 @@
10545
10330
  "columnType": "INTEGER",
10546
10331
  "default": 0
10547
10332
  },
10548
- {
10549
- "name": "legacyAmount",
10550
- "type": "integer",
10551
- "required": false,
10552
- "columnType": "INTEGER"
10553
- },
10554
10333
  {
10555
10334
  "name": "moneySnapshotId",
10556
10335
  "type": "text",
@@ -10631,9 +10410,9 @@
10631
10410
  }
10632
10411
  ],
10633
10412
  "methods": [
10634
- "calculateAmount",
10635
10413
  "delete",
10636
10414
  "deleteDerived",
10415
+ "initialize",
10637
10416
  "isRecurring",
10638
10417
  "isSubscriptionActive",
10639
10418
  "rebindMoneyRead",
@@ -10641,10 +10420,6 @@
10641
10420
  "saveDerived"
10642
10421
  ],
10643
10422
  "methodSignatures": [
10644
- {
10645
- "name": "calculateAmount",
10646
- "returns": "number"
10647
- },
10648
10423
  {
10649
10424
  "name": "delete",
10650
10425
  "async": true,
@@ -10662,6 +10437,11 @@
10662
10437
  ],
10663
10438
  "returns": "Promise<void>"
10664
10439
  },
10440
+ {
10441
+ "name": "initialize",
10442
+ "async": true,
10443
+ "returns": "Promise<this>"
10444
+ },
10665
10445
  {
10666
10446
  "name": "isRecurring",
10667
10447
  "returns": "boolean"
@@ -10687,8 +10467,7 @@
10687
10467
  "name": "saveDerived",
10688
10468
  "async": true,
10689
10469
  "params": [
10690
- "capability: DocumentMoneyWriteCapability",
10691
- "legacy: boolean"
10470
+ "capability: DocumentMoneyWriteCapability"
10692
10471
  ],
10693
10472
  "returns": "Promise<this>"
10694
10473
  }
@@ -10782,12 +10561,6 @@
10782
10561
  "operation": "rebindMoneyRead",
10783
10562
  "objectName": "@happyvertical/smrt-commerce:ContractLineItem"
10784
10563
  },
10785
- {
10786
- "kind": "cli",
10787
- "name": "contractlineitem_calculateAmount",
10788
- "operation": "calculateAmount",
10789
- "objectName": "@happyvertical/smrt-commerce:ContractLineItem"
10790
- },
10791
10564
  {
10792
10565
  "kind": "cli",
10793
10566
  "name": "contractlineitem_saveDerived",
@@ -10828,16 +10601,16 @@
10828
10601
  "withheldSurfaces": [
10829
10602
  {
10830
10603
  "kind": "api",
10831
- "operation": "calculateAmount",
10604
+ "operation": "deleteDerived",
10832
10605
  "code": "not-included",
10833
10606
  "reason": "not listed in api.include",
10834
10607
  "objectName": "@happyvertical/smrt-commerce:ContractLineItem"
10835
10608
  },
10836
10609
  {
10837
10610
  "kind": "api",
10838
- "operation": "deleteDerived",
10839
- "code": "not-included",
10840
- "reason": "not listed in api.include",
10611
+ "operation": "initialize",
10612
+ "code": "lifecycle-method",
10613
+ "reason": "`initialize` is a framework lifecycle method, not a distinct operation",
10841
10614
  "objectName": "@happyvertical/smrt-commerce:ContractLineItem"
10842
10615
  },
10843
10616
  {
@@ -12305,7 +12078,7 @@
12305
12078
  "params": [
12306
12079
  "options: CancelRevenueOptions"
12307
12080
  ],
12308
- "returns": "Promise<Journal | null>"
12081
+ "returns": "Promise<Journal>"
12309
12082
  },
12310
12083
  {
12311
12084
  "name": "delete",
@@ -12395,8 +12168,7 @@
12395
12168
  "name": "saveDerived",
12396
12169
  "async": true,
12397
12170
  "params": [
12398
- "capability: DocumentMoneyWriteCapability",
12399
- "options: object"
12171
+ "capability: DocumentMoneyWriteCapability"
12400
12172
  ],
12401
12173
  "returns": "Promise<this>"
12402
12174
  },
@@ -12844,13 +12616,6 @@
12844
12616
  "default": 1,
12845
12617
  "description": "How many."
12846
12618
  },
12847
- {
12848
- "name": "invoiceEditorStateJson",
12849
- "type": "text",
12850
- "required": false,
12851
- "columnType": "TEXT",
12852
- "default": ""
12853
- },
12854
12619
  {
12855
12620
  "name": "unitPrice",
12856
12621
  "type": "integer",
@@ -12926,12 +12691,6 @@
12926
12691
  "columnType": "INTEGER",
12927
12692
  "default": 0
12928
12693
  },
12929
- {
12930
- "name": "legacyAmount",
12931
- "type": "integer",
12932
- "required": false,
12933
- "columnType": "INTEGER"
12934
- },
12935
12694
  {
12936
12695
  "name": "moneySnapshotId",
12937
12696
  "type": "text",
@@ -13025,33 +12784,16 @@
13025
12784
  }
13026
12785
  ],
13027
12786
  "methods": [
13028
- "applyEditorDraft",
13029
- "calculateAmount",
13030
12787
  "delete",
13031
12788
  "deleteDerived",
13032
- "getEditorDraft",
13033
- "getSubtotal",
13034
- "getTaxAmount",
13035
12789
  "hasPeriod",
13036
12790
  "hasSource",
12791
+ "initialize",
13037
12792
  "rebindMoneyRead",
13038
12793
  "save",
13039
- "saveDerived",
13040
- "toAccountingLineItem"
12794
+ "saveDerived"
13041
12795
  ],
13042
12796
  "methodSignatures": [
13043
- {
13044
- "name": "applyEditorDraft",
13045
- "params": [
13046
- "draft: InvoiceLineDraft",
13047
- "context: InvoiceCalculationContext"
13048
- ],
13049
- "returns": "void"
13050
- },
13051
- {
13052
- "name": "calculateAmount",
13053
- "returns": "number"
13054
- },
13055
12797
  {
13056
12798
  "name": "delete",
13057
12799
  "async": true,
@@ -13069,21 +12811,6 @@
13069
12811
  ],
13070
12812
  "returns": "Promise<void>"
13071
12813
  },
13072
- {
13073
- "name": "getEditorDraft",
13074
- "params": [
13075
- "authorizedContext: InvoiceCalculationContext"
13076
- ],
13077
- "returns": "InvoiceLineDraft | null"
13078
- },
13079
- {
13080
- "name": "getSubtotal",
13081
- "returns": "number"
13082
- },
13083
- {
13084
- "name": "getTaxAmount",
13085
- "returns": "number"
13086
- },
13087
12814
  {
13088
12815
  "name": "hasPeriod",
13089
12816
  "returns": "boolean"
@@ -13092,6 +12819,11 @@
13092
12819
  "name": "hasSource",
13093
12820
  "returns": "boolean"
13094
12821
  },
12822
+ {
12823
+ "name": "initialize",
12824
+ "async": true,
12825
+ "returns": "Promise<this>"
12826
+ },
13095
12827
  {
13096
12828
  "name": "rebindMoneyRead",
13097
12829
  "params": [
@@ -13109,17 +12841,9 @@
13109
12841
  "name": "saveDerived",
13110
12842
  "async": true,
13111
12843
  "params": [
13112
- "capability: DocumentMoneyWriteCapability",
13113
- "legacy: boolean"
12844
+ "capability: DocumentMoneyWriteCapability"
13114
12845
  ],
13115
12846
  "returns": "Promise<this>"
13116
- },
13117
- {
13118
- "name": "toAccountingLineItem",
13119
- "params": [
13120
- "currency?: string"
13121
- ],
13122
- "returns": "AccountingLineItemInput"
13123
12847
  }
13124
12848
  ],
13125
12849
  "tenant": {
@@ -13209,36 +12933,6 @@
13209
12933
  "operation": "rebindMoneyRead",
13210
12934
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13211
12935
  },
13212
- {
13213
- "kind": "cli",
13214
- "name": "invoicelineitem_calculateAmount",
13215
- "operation": "calculateAmount",
13216
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13217
- },
13218
- {
13219
- "kind": "cli",
13220
- "name": "invoicelineitem_getSubtotal",
13221
- "operation": "getSubtotal",
13222
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13223
- },
13224
- {
13225
- "kind": "cli",
13226
- "name": "invoicelineitem_getTaxAmount",
13227
- "operation": "getTaxAmount",
13228
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13229
- },
13230
- {
13231
- "kind": "cli",
13232
- "name": "invoicelineitem_applyEditorDraft",
13233
- "operation": "applyEditorDraft",
13234
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13235
- },
13236
- {
13237
- "kind": "cli",
13238
- "name": "invoicelineitem_getEditorDraft",
13239
- "operation": "getEditorDraft",
13240
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13241
- },
13242
12936
  {
13243
12937
  "kind": "cli",
13244
12938
  "name": "invoicelineitem_saveDerived",
@@ -13263,12 +12957,6 @@
13263
12957
  "operation": "hasPeriod",
13264
12958
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13265
12959
  },
13266
- {
13267
- "kind": "cli",
13268
- "name": "invoicelineitem_toAccountingLineItem",
13269
- "operation": "toAccountingLineItem",
13270
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13271
- },
13272
12960
  {
13273
12961
  "kind": "mcp",
13274
12962
  "name": "invoicelineitem_list",
@@ -13283,20 +12971,6 @@
13283
12971
  }
13284
12972
  ],
13285
12973
  "withheldSurfaces": [
13286
- {
13287
- "kind": "api",
13288
- "operation": "applyEditorDraft",
13289
- "code": "not-included",
13290
- "reason": "not listed in api.include",
13291
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13292
- },
13293
- {
13294
- "kind": "api",
13295
- "operation": "calculateAmount",
13296
- "code": "not-included",
13297
- "reason": "not listed in api.include",
13298
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13299
- },
13300
12974
  {
13301
12975
  "kind": "api",
13302
12976
  "operation": "deleteDerived",
@@ -13306,37 +12980,23 @@
13306
12980
  },
13307
12981
  {
13308
12982
  "kind": "api",
13309
- "operation": "getEditorDraft",
13310
- "code": "not-included",
13311
- "reason": "not listed in api.include",
13312
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13313
- },
13314
- {
13315
- "kind": "api",
13316
- "operation": "getSubtotal",
13317
- "code": "not-included",
13318
- "reason": "not listed in api.include",
13319
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13320
- },
13321
- {
13322
- "kind": "api",
13323
- "operation": "getTaxAmount",
12983
+ "operation": "hasPeriod",
13324
12984
  "code": "not-included",
13325
12985
  "reason": "not listed in api.include",
13326
12986
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13327
12987
  },
13328
12988
  {
13329
12989
  "kind": "api",
13330
- "operation": "hasPeriod",
12990
+ "operation": "hasSource",
13331
12991
  "code": "not-included",
13332
12992
  "reason": "not listed in api.include",
13333
12993
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13334
12994
  },
13335
12995
  {
13336
12996
  "kind": "api",
13337
- "operation": "hasSource",
13338
- "code": "not-included",
13339
- "reason": "not listed in api.include",
12997
+ "operation": "initialize",
12998
+ "code": "lifecycle-method",
12999
+ "reason": "`initialize` is a framework lifecycle method, not a distinct operation",
13340
13000
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13341
13001
  },
13342
13002
  {
@@ -13359,13 +13019,6 @@
13359
13019
  "code": "not-included",
13360
13020
  "reason": "not listed in api.include",
13361
13021
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13362
- },
13363
- {
13364
- "kind": "api",
13365
- "operation": "toAccountingLineItem",
13366
- "code": "not-included",
13367
- "reason": "not listed in api.include",
13368
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
13369
13022
  }
13370
13023
  ],
13371
13024
  "relationshipFeatures": [
@@ -13376,79 +13029,6 @@
13376
13029
  "tags": [],
13377
13030
  "risks": []
13378
13031
  },
13379
- {
13380
- "name": "MoneyMigrationDocument",
13381
- "qualifiedName": "@happyvertical/smrt-commerce:MoneyMigrationDocument",
13382
- "collection": "moneymigrationdocuments",
13383
- "tableName": "commerce_money_migration_documents",
13384
- "packageName": "@happyvertical/smrt-commerce",
13385
- "extends": "SmrtObject",
13386
- "fields": [
13387
- {
13388
- "name": "tenantId",
13389
- "type": "text",
13390
- "required": true,
13391
- "columnType": "UUID"
13392
- },
13393
- {
13394
- "name": "migrationId",
13395
- "type": "text",
13396
- "required": false,
13397
- "columnType": "TEXT"
13398
- },
13399
- {
13400
- "name": "documentType",
13401
- "type": "text",
13402
- "required": false,
13403
- "columnType": "TEXT",
13404
- "default": "invoice"
13405
- },
13406
- {
13407
- "name": "documentId",
13408
- "type": "text",
13409
- "required": false,
13410
- "columnType": "TEXT"
13411
- },
13412
- {
13413
- "name": "state",
13414
- "type": "text",
13415
- "required": false,
13416
- "columnType": "TEXT",
13417
- "default": "prepared"
13418
- },
13419
- {
13420
- "name": "preChecksum",
13421
- "type": "text",
13422
- "required": false,
13423
- "columnType": "TEXT"
13424
- },
13425
- {
13426
- "name": "postChecksum",
13427
- "type": "text",
13428
- "required": false,
13429
- "columnType": "TEXT"
13430
- }
13431
- ],
13432
- "relationships": [],
13433
- "methods": [],
13434
- "tenant": {
13435
- "scoped": true,
13436
- "mode": "required",
13437
- "field": "tenantId"
13438
- },
13439
- "conflictColumns": [
13440
- "tenant_id",
13441
- "migration_id",
13442
- "document_type",
13443
- "document_id"
13444
- ],
13445
- "surfaces": [],
13446
- "relationshipFeatures": [
13447
- "uuidColumns"
13448
- ],
13449
- "tags": [],
13450
- "risks": []
13451
- },
13452
13032
  {
13453
13033
  "name": "Payment",
13454
13034
  "qualifiedName": "@happyvertical/smrt-commerce:Payment",
@@ -15688,19 +15268,6 @@
15688
15268
  "type": "text",
15689
15269
  "required": false,
15690
15270
  "columnType": "TEXT"
15691
- },
15692
- {
15693
- "name": "moneyModel",
15694
- "type": "text",
15695
- "required": false,
15696
- "columnType": "TEXT",
15697
- "default": "legacy"
15698
- },
15699
- {
15700
- "name": "moneyMigrationId",
15701
- "type": "text",
15702
- "required": false,
15703
- "columnType": "TEXT"
15704
15271
  }
15705
15272
  ],
15706
15273
  "relationships": [
@@ -15720,31 +15287,13 @@
15720
15287
  }
15721
15288
  ],
15722
15289
  "methods": [
15723
- "save",
15724
- "saveMoneyModel",
15725
- "saveMoneyModelOnTransaction"
15290
+ "save"
15726
15291
  ],
15727
15292
  "methodSignatures": [
15728
15293
  {
15729
15294
  "name": "save",
15730
15295
  "async": true,
15731
15296
  "returns": "Promise<this>"
15732
- },
15733
- {
15734
- "name": "saveMoneyModel",
15735
- "async": true,
15736
- "params": [
15737
- "capability: MoneyMigrationWriteCapability"
15738
- ],
15739
- "returns": "Promise<this>"
15740
- },
15741
- {
15742
- "name": "saveMoneyModelOnTransaction",
15743
- "async": true,
15744
- "params": [
15745
- "capability: MoneyMigrationWriteCapability"
15746
- ],
15747
- "returns": "Promise<this>"
15748
15297
  }
15749
15298
  ],
15750
15299
  "tenant": {
@@ -15810,18 +15359,6 @@
15810
15359
  "operation": "delete",
15811
15360
  "objectName": "@happyvertical/smrt-commerce:TaxSettings"
15812
15361
  },
15813
- {
15814
- "kind": "cli",
15815
- "name": "taxsettings_saveMoneyModel",
15816
- "operation": "saveMoneyModel",
15817
- "objectName": "@happyvertical/smrt-commerce:TaxSettings"
15818
- },
15819
- {
15820
- "kind": "cli",
15821
- "name": "taxsettings_saveMoneyModelOnTransaction",
15822
- "operation": "saveMoneyModelOnTransaction",
15823
- "objectName": "@happyvertical/smrt-commerce:TaxSettings"
15824
- },
15825
15362
  {
15826
15363
  "kind": "mcp",
15827
15364
  "name": "taxsettings_get",
@@ -15836,20 +15373,6 @@
15836
15373
  "code": "lifecycle-method",
15837
15374
  "reason": "`save` is a framework lifecycle method, not a distinct operation",
15838
15375
  "objectName": "@happyvertical/smrt-commerce:TaxSettings"
15839
- },
15840
- {
15841
- "kind": "api",
15842
- "operation": "saveMoneyModel",
15843
- "code": "not-included",
15844
- "reason": "not listed in api.include",
15845
- "objectName": "@happyvertical/smrt-commerce:TaxSettings"
15846
- },
15847
- {
15848
- "kind": "api",
15849
- "operation": "saveMoneyModelOnTransaction",
15850
- "code": "not-included",
15851
- "reason": "not listed in api.include",
15852
- "objectName": "@happyvertical/smrt-commerce:TaxSettings"
15853
15376
  }
15854
15377
  ],
15855
15378
  "relationshipFeatures": [
@@ -17718,36 +17241,12 @@
17718
17241
  "operation": "getTotalForInvoice",
17719
17242
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
17720
17243
  },
17721
- {
17722
- "kind": "cli",
17723
- "name": "invoicelineitemcollection_getSubtotalForInvoice",
17724
- "operation": "getSubtotalForInvoice",
17725
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
17726
- },
17727
- {
17728
- "kind": "cli",
17729
- "name": "invoicelineitemcollection_getTaxForInvoice",
17730
- "operation": "getTaxForInvoice",
17731
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
17732
- },
17733
17244
  {
17734
17245
  "kind": "cli",
17735
17246
  "name": "invoicelineitemcollection_getNextSortOrder",
17736
17247
  "operation": "getNextSortOrder",
17737
17248
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
17738
17249
  },
17739
- {
17740
- "kind": "cli",
17741
- "name": "invoicelineitemcollection_recalculateAmounts",
17742
- "operation": "recalculateAmounts",
17743
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
17744
- },
17745
- {
17746
- "kind": "cli",
17747
- "name": "invoicelineitemcollection_toAccountingLineItems",
17748
- "operation": "toAccountingLineItems",
17749
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItemCollection"
17750
- },
17751
17250
  {
17752
17251
  "kind": "cli",
17753
17252
  "name": "invoicelineitemcollection_findByTenant",
@@ -17976,30 +17475,6 @@
17976
17475
  "operation": "lockForTenant",
17977
17476
  "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
17978
17477
  },
17979
- {
17980
- "kind": "cli",
17981
- "name": "taxsettingscollection_beginMoneyMigration",
17982
- "operation": "beginMoneyMigration",
17983
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
17984
- },
17985
- {
17986
- "kind": "cli",
17987
- "name": "taxsettingscollection_activateMoneyV2",
17988
- "operation": "activateMoneyV2",
17989
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
17990
- },
17991
- {
17992
- "kind": "cli",
17993
- "name": "taxsettingscollection_restoreLegacyMoneyModel",
17994
- "operation": "restoreLegacyMoneyModel",
17995
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
17996
- },
17997
- {
17998
- "kind": "cli",
17999
- "name": "taxsettingscollection_beginMoneyRollback",
18000
- "operation": "beginMoneyRollback",
18001
- "objectName": "@happyvertical/smrt-commerce:TaxSettingsCollection"
18002
- },
18003
17478
  {
18004
17479
  "kind": "cli",
18005
17480
  "name": "vendorcollection_findByProfile",
@@ -20880,12 +20355,6 @@
20880
20355
  "operation": "rebindMoneyRead",
20881
20356
  "objectName": "@happyvertical/smrt-commerce:ContractLineItem"
20882
20357
  },
20883
- {
20884
- "kind": "cli",
20885
- "name": "contractlineitem_calculateAmount",
20886
- "operation": "calculateAmount",
20887
- "objectName": "@happyvertical/smrt-commerce:ContractLineItem"
20888
- },
20889
20358
  {
20890
20359
  "kind": "cli",
20891
20360
  "name": "contractlineitem_saveDerived",
@@ -21516,36 +20985,6 @@
21516
20985
  "operation": "rebindMoneyRead",
21517
20986
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
21518
20987
  },
21519
- {
21520
- "kind": "cli",
21521
- "name": "invoicelineitem_calculateAmount",
21522
- "operation": "calculateAmount",
21523
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
21524
- },
21525
- {
21526
- "kind": "cli",
21527
- "name": "invoicelineitem_getSubtotal",
21528
- "operation": "getSubtotal",
21529
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
21530
- },
21531
- {
21532
- "kind": "cli",
21533
- "name": "invoicelineitem_getTaxAmount",
21534
- "operation": "getTaxAmount",
21535
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
21536
- },
21537
- {
21538
- "kind": "cli",
21539
- "name": "invoicelineitem_applyEditorDraft",
21540
- "operation": "applyEditorDraft",
21541
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
21542
- },
21543
- {
21544
- "kind": "cli",
21545
- "name": "invoicelineitem_getEditorDraft",
21546
- "operation": "getEditorDraft",
21547
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
21548
- },
21549
20988
  {
21550
20989
  "kind": "cli",
21551
20990
  "name": "invoicelineitem_saveDerived",
@@ -21570,12 +21009,6 @@
21570
21009
  "operation": "hasPeriod",
21571
21010
  "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
21572
21011
  },
21573
- {
21574
- "kind": "cli",
21575
- "name": "invoicelineitem_toAccountingLineItem",
21576
- "operation": "toAccountingLineItem",
21577
- "objectName": "@happyvertical/smrt-commerce:InvoiceLineItem"
21578
- },
21579
21012
  {
21580
21013
  "kind": "mcp",
21581
21014
  "name": "invoicelineitem_list",
@@ -22272,18 +21705,6 @@
22272
21705
  "operation": "delete",
22273
21706
  "objectName": "@happyvertical/smrt-commerce:TaxSettings"
22274
21707
  },
22275
- {
22276
- "kind": "cli",
22277
- "name": "taxsettings_saveMoneyModel",
22278
- "operation": "saveMoneyModel",
22279
- "objectName": "@happyvertical/smrt-commerce:TaxSettings"
22280
- },
22281
- {
22282
- "kind": "cli",
22283
- "name": "taxsettings_saveMoneyModelOnTransaction",
22284
- "operation": "saveMoneyModelOnTransaction",
22285
- "objectName": "@happyvertical/smrt-commerce:TaxSettings"
22286
- },
22287
21708
  {
22288
21709
  "kind": "mcp",
22289
21710
  "name": "taxsettings_get",
@@ -22448,24 +21869,24 @@
22448
21869
  "junctionCollections": 0,
22449
21870
  "hierarchicalObjects": 0,
22450
21871
  "polymorphicAssociations": 0,
22451
- "uuidColumns": 228
21872
+ "uuidColumns": 224
22452
21873
  },
22453
- "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). `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).\n- **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).\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**: fractional-quantity line items with safe minor-unit validation/recalculation on save; editor-mode recovery and existing-column migration are documented in [invoice quantity persistence](agents/invoice-quantity.md).\n- **Payment** / **PaymentAllocation**: tracks payments against invoices (status: see Gotchas). Carries optional backend-adapter fields for PaymentBackend-routed flows: `backendId` (rail adapter id — `base-usdc`, `btc`, `stripe`, distinct from `externalProvider` which names an accounting sync destination), `backendTxRef` (chain tx hash or gateway settlement id), `nativeAmount` / `nativeCurrency` (what actually arrived), `usdAtQuote` / `usdAtConfirmation` (drift accounting for volatile-currency rails). `Payment.usdDrift()` returns the confirmation - quote delta, or `0` when either side is unset.\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. `isDefault` is domain-managed, not API-writable: `PaymentInstrumentCollection.setDefaultForCustomer(customerId, instrumentId)` validates the target and clears the customer's other defaults (one per customer).\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: see the natural-key Gotcha.\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## Recipes (#3590)\n\n`src/recipes.ts` declares four minimal `SmrtRecipe`s, emitted into the built\n`manifest.json` and `smrt-knowledge.json` `recipes`: `commerce.customers`\n(Customer), `commerce.vendors` (Vendor), `commerce.sales` (Order, requires\ncustomers), `commerce.purchases` (PurchaseOrder, requires vendors). Headers\nonly; line items, fulfilment, invoices, and payments are later recipes. Each\nhas user-facing help in `src/<name>.recipe.md` (#3591), and the fields those\nrecipes show carry `@field({ description })` text; keep both in plain\nlanguage for end users and update them when a recipe's fields change. See\n[core recipes](../core/agents/recipes.md).\n\nSee [payment allocation integrity](agents/payment-allocations.md) for atomic splits,\nretry identity, parent corrections and supported dialects.\nSee [tax codes](agents/tax-codes.md) for tenant-owned rate components, default codes,\nand legacy-rate migration.\nSee [document money persistence](agents/document-money.md) for the per-tenant\nlegacy/v2 gate, atomic aggregate writes, issued snapshots, and frozen fields.\nSee [Contract money persistence](agents/contract-money.md) for the Contract\naggregate, purchase recoverability, and migration-only materialization boundary.\n\n## Ledger Integration\n\n`@happyvertical/smrt-ledgers` is a regular dependency, loaded lazily via dynamic `import()` (never a static import) so the package graph stays a DAG (#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 once\n and saved as `Invoice.collectionMethod` before the first push, which every\n retry reuses (never recompute it: a lost push response would record a\n method the provider was not asked for, #3190); 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 charged\n with provider tax by default (#3194): the attempt row holds the credit\n (the charge subtotal), the tax is journaled to tax payable at settlement,\n and the attempt's tax decision is recorded in its `notes` so a re-drive\n repeats the same charge request (the provider refuses a key reused with\n different tax). `claimAttempt()` is the only place that decides a\n skip and reports it (`onAutoTopUpSkipped`); `cardOnFile()` and period close\n use `defaultCardInstrument()` — never copy that predicate. A provider write-off moves the local invoice to\n `WRITTEN_OFF` (a later payment, by card or on a rail (#3188), is recorded\n and reinstates the payer; the status stays, so \"paid in full\" for a\n written-off invoice is read from its allocations).\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- `vendorId` → `@foreignKey('Vendor')` on PurchaseContract only\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- **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.\n- **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).\n- **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.\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 `PaymentIntentCollection.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- **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\n## Commercial UI\n\n`CustomerSelect`/`VendorSelect` are the registered selectors for Customer and\nVendor (`selects` in `src/ui.ts`); models carry `ui.widget` / `ui.format` hints. See\n[customer/vendor UI](agents/customer-vendor-ui.md#selectors-3602).\n\nSee [commercial UI composition](agents/commercial-ui.md) for native form recovery, amount compatibility and backend ownership. Package-owned guides under `agents/` ship with the package.\n\n## Quote and estimate UI\n\n`src/svelte/quote-types.ts` separates unparsed draft strings from explicit\nminor-unit revision DTOs. Native row submitters, caller-owned retry identity and\nextension conventions are documented in [agents/quote-ui.md](agents/quote-ui.md).\n\n## Purchasing UI\n\n`PurchaseOrderEditor` preserves allocation IDs and explicit zeroes, including\nretained reduction rows; the server validates review fingerprints before recording.\nSee [agents/purchasing-ui.md](agents/purchasing-ui.md) for caller responsibilities.\n\n## Exact cost billing\n\nPure signed cost billing and explicit tax rounding are available through the\nindependent `cost-billing` subpath; see [contract](agents/cost-billing.md).\n",
21874
+ "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). `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).\n- **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).\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**: fractional-quantity line items with safe minor-unit validation/recalculation on save; the existing-column migration is documented in [invoice quantity persistence](agents/invoice-quantity.md).\n- **Payment** / **PaymentAllocation**: tracks payments against invoices (status: see Gotchas). Carries optional backend-adapter fields for PaymentBackend-routed flows: `backendId` (rail adapter id — `base-usdc`, `btc`, `stripe`, distinct from `externalProvider` which names an accounting sync destination), `backendTxRef` (chain tx hash or gateway settlement id), `nativeAmount` / `nativeCurrency` (what actually arrived), `usdAtQuote` / `usdAtConfirmation` (drift accounting for volatile-currency rails). `Payment.usdDrift()` returns the confirmation - quote delta, or `0` when either side is unset.\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. `isDefault` is domain-managed, not API-writable: `PaymentInstrumentCollection.setDefaultForCustomer(customerId, instrumentId)` validates the target and clears the customer's other defaults (one per customer).\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: see the natural-key Gotcha.\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## Recipes (#3590)\n\n`src/recipes.ts` declares four minimal `SmrtRecipe`s, emitted into the built\n`manifest.json` and `smrt-knowledge.json` `recipes`: `commerce.customers`\n(Customer), `commerce.vendors` (Vendor), `commerce.sales` (Order, requires\ncustomers), `commerce.purchases` (PurchaseOrder, requires vendors). Headers\nonly; line items, fulfilment, invoices, and payments are later recipes. Each\nhas user-facing help in `src/<name>.recipe.md` (#3591), and the fields those\nrecipes show carry `@field({ description })` text; keep both in plain\nlanguage for end users and update them when a recipe's fields change. See\n[core recipes](../core/agents/recipes.md).\n\nSee [payment allocation integrity](agents/payment-allocations.md) for atomic splits,\nretry identity, parent corrections and supported dialects.\nSee [tax codes](agents/tax-codes.md) for tenant-owned rate components, default codes,\nand the standard codes every tenant gets.\nSee [document money persistence](agents/document-money.md) for the one money\nmodel: tax-code resolution, atomic aggregate writes, issued snapshots, and\nfrozen fields. Every Invoice and Contract money write needs a tenant. Host\nbootstrap is `ensureCommerceTenant(db, tenantId, { cookbook })`, which returns\n`{ tenantId }`. Money data written by an earlier version is refused with\n`COMMERCE_DATA_NEEDS_UPGRADE` until `smrt db:upgrade-commerce-money` converts\nit; see [money data upgrade](agents/money-upgrade.md).\nSee [Contract money persistence](agents/contract-money.md) for the Contract\naggregate and purchase recoverability.\n\n## Ledger Integration\n\n`@happyvertical/smrt-ledgers` is a regular dependency, loaded lazily via dynamic `import()` (never a static import) so the package graph stays a DAG (#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 once\n and saved as `Invoice.collectionMethod` before the first push, which every\n retry reuses (never recompute it: a lost push response would record a\n method the provider was not asked for, #3190); 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 charged\n with provider tax by default (#3194): the attempt row holds the credit\n (the charge subtotal), the tax is journaled to tax payable at settlement,\n and the attempt's tax decision is recorded in its `notes` so a re-drive\n repeats the same charge request (the provider refuses a key reused with\n different tax). `claimAttempt()` is the only place that decides a\n skip and reports it (`onAutoTopUpSkipped`); `cardOnFile()` and period close\n use `defaultCardInstrument()` — never copy that predicate. A provider write-off moves the local invoice to\n `WRITTEN_OFF` (a later payment, by card or on a rail (#3188), is recorded\n and reinstates the payer; the status stays, so \"paid in full\" for a\n written-off invoice is read from its allocations).\n- **Billing invoices are money-model invoices.** The provider taxes them\n (`providerTaxAmount`, server-managed, not API-writable, added to line tax),\n so every billing line is an explicit `taxMode: 'override'` to the seller's\n `out-of-scope` code. Charges are written before credits so the running\n subtotal never goes negative, and provider tax is written in the save that\n issues the invoice. Revenue posting is idempotent. `ledger.bookCurrency` is\n required (ISO 4217, validated by `BillingRuntime`) and must equal the\n billing currency. `toAccountingInput({ mapping })` 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- `vendorId` → `@foreignKey('Vendor')` on PurchaseContract only\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- **Money units and arithmetic**: money fields use signed integer minor units; rates and quantities remain decimal where their contracts require them. Exact rounding, tax-code resolution, safe-integer validation, and journal units are defined in the [commercial money model](agents/money-model.md) and [money calculator](agents/money-calculator.md). Do not reintroduce floating-point tolerances on money paths.\n- **Money documents need a tenant**: models are optionally tenant-scoped, but every Invoice and Contract money write requires one (`MoneyValidationError` `INVALID_MONEY_INPUT`, `Commerce money documents require a tenant`).\n- **Earlier-version money data is refused**: never add a read or conversion path for it. Detection and the one-shot upgrade are in [money data upgrade](agents/money-upgrade.md).\n- **Book currency is required**: `recognizeRevenue()` and `cancelWithRevenueReversal()` take a required `bookCurrency`, which must equal the invoice currency.\n- **Asset-native amounts stay outside line rescales**: `Payment.nativeAmount` and `PaymentIntent.paymentOptions[].nativeAmount` are per-asset values identified by `nativeCurrency`; normalize each asset to its own minor unit before any type conversion, and never apply the commerce ×100 line-money rescale to them.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax comes from tax codes**: the server resolves each line's code and computes line tax and header totals; client totals and a line's stored `taxRate` are never inputs\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 `PaymentIntentCollection.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- **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\n## Commercial UI\n\n`CustomerSelect`/`VendorSelect` are the registered selectors for Customer and\nVendor (`selects` in `src/ui.ts`); models carry `ui.widget` / `ui.format` hints. See\n[customer/vendor UI](agents/customer-vendor-ui.md#selectors-3602).\n\nSee [commercial UI composition](agents/commercial-ui.md) for native form recovery, amount compatibility and backend ownership. Package-owned guides under `agents/` ship with the package.\n\n## Quote and estimate UI\n\n`src/svelte/quote-types.ts` separates unparsed draft strings from explicit\nminor-unit revision DTOs. Native row submitters, caller-owned retry identity and\nextension conventions are documented in [agents/quote-ui.md](agents/quote-ui.md).\n\n## Purchasing UI\n\n`PurchaseOrderEditor` preserves allocation IDs and explicit zeroes, including\nretained reduction rows; the server validates review fingerprints before recording.\nSee [agents/purchasing-ui.md](agents/purchasing-ui.md) for caller responsibilities.\n\n## Exact cost billing\n\nPure signed cost billing and explicit tax rounding are available through the\nindependent `cost-billing` subpath; see [contract](agents/cost-billing.md).\n",
22454
21875
  "moduleDocs": [
22455
21876
  {
22456
21877
  "path": "agents/customer-vendor-ui.md",
22457
21878
  "module": "customer-vendor-ui",
22458
- "content": "# Customer and vendor UI\n\nImport the provider-free surfaces from `@happyvertical/smrt-commerce/svelte`:\n\n```svelte\n<script lang=\"ts\">\n import {\n CustomerDirectory,\n CustomerDetail,\n CustomerForm,\n VendorDirectory,\n VendorDetail,\n VendorForm,\n type CustomerDisplayData,\n } from '@happyvertical/smrt-commerce/svelte';\n\n let { data, form } = $props();\n</script>\n\n<CustomerForm\n mode=\"edit\"\n values={form?.values ?? data.values}\n errors={form?.errors}\n transport={{\n action: '?/update',\n method: 'post',\n hiddenFields: {\n requestId: form?.values.requestId ?? data.requestId,\n expectedTenantId: data.tenantId,\n },\n }}\n labels={{ singular: 'Client', plural: 'Clients' }}\n cancelHref={`/clients/${data.customer.id}`}\n/>\n```\n\nThe caller owns action URLs, server authorization, tenancy checks, request tokens, persistence, and retry behavior. Capability props only decide whether an affordance is rendered. The components never create a Profile, Customer, Vendor, or contact relationship and never generate request tokens; the server writes posted contact rows with `savePartyContacts()` ([contact rows](#contact-rows-3501)).\n\n`CustomerDisplayData.creditLimitMinor` and `VendorDisplayData.minimumOrderMinor` are integer currency minor units. Detail components convert them only for display and accept an explicit `minorUnitExponent`. Form values deliberately use retained strings such as `1250.00`; the server validates and converts those major-unit strings to the model's integer minor units.\n\n`VendorFormValues.leadTimeDays` is also retained text. The control supplies a numeric keyboard hint without browser number-input sanitization, and the server must parse and validate the submitted non-negative integer.\n\nForms use the provider-free `Form`, `Input`, `Select`, `Textarea`, and `Checkbox` controls from `@happyvertical/smrt-ui/forms`. They default to native `POST` submission through `preventDefault={false}`. `transport.hiddenFields`, `transport.intentName`, and the field-name maps let an application preserve its existing payload contract. Repeatable contact rows submit repeated field names in DOM order. Add and remove controls are native submitters, so they work without JavaScript; the server returns the revised rows and retained values.\n\n`CustomerForm` and `VendorForm` accept submit hooks so a host never has to find\nthe form after mount (#3501). `enhance` (`PartyFormEnhance`, action-shaped) is\napplied to the mounted native `<form>` and destroyed with it; SvelteKit's\n`enhance` from `$app/forms` fits directly, or wrap it to pass a submit callback.\nAn `ssr = false` app needs it, because a plain native post answers with the\nempty shell. `onsubmit` receives the native submit event (submission stays\nnative unless it calls `preventDefault()`), and the `getFormElement()` instance\nmethod returns the native element while mounted.\n\n```svelte\n<script lang=\"ts\">\n import { enhance } from '$app/forms';\n</script>\n\n<VendorForm values={form?.values ?? data.values} errors={form?.errors} transport={{ action: '?/save' }} {enhance} />\n```\n\nDirectory search forms accept `hiddenFields` as a string record for caller-owned GET state such as sort direction and page size. Fields whose names equal the resolved `queryName` or `statusName` are omitted so the visible search controls remain authoritative.\n\nCustomer shipping and billing addresses use the shared `CountrySelect` and\n`ProvinceSelect`. The native names remain `shippingCountry`, `shippingState`,\n`billingCountry`, and `billingState` unless `fieldNames` overrides them. Changing\ncountry changes the region control without clearing its current string; CA and US\nrender localized subdivision choices, while unsupported countries render an\neditable text field. Unknown values remain selectable/postable after a failed\nsubmission. Use `countryOptions` or `provinceOptions` only when the caller must\nrestrict the choices. `VendorForm` has no structured address fields in its public\nDTO and does not invent them.\n\nVendor currency uses the shared `CurrencySelect`. The raw retained string remains\nthe native `currency` payload, including unknown values returned after validation.\nUse `currencyOptions` for caller-authorized restrictions; omitted options use the\nshared currency catalog. Selector labels follow the active smrt-ui i18n locale.\n\nThe forms place a non-focusable default save submitter before contact row actions so Enter in a text field cannot accidentally add or remove a contact. This remains in place when the `actions` snippet customizes the visible action bar; set `transport.saveIntent` to the custom save intent.\n\nUse the `extension` snippets for application fields such as construction roles and trades. Keep those fields and their policy in the application:\n\n```svelte\n<VendorForm values={data.values} transport={{ action: '?/save' }}>\n {#snippet extension(values)}\n <ConstructionVendorFields roles={values.roles} trades={values.trades} />\n {/snippet}\n</VendorForm>\n```\n\nDirectory entries carry caller-owned detail links and pagination links. Detail edit links and form submit buttons appear only when their capability props allow them; the server must still authorize every request.\n\n`CustomerDirectory`, `CustomerDetail`, `VendorDirectory`, and `VendorDetail`\nrender a `PageHeader` (the page `h1`, a subtitle, and the add/edit action) by\ndefault. A host that frames every page itself passes `headless` to render only\nthe body, then renders its own heading and create/edit affordance; or passes a\n`header` snippet to replace the built-in header in place (#3501). `backHref`\nstays independent of both.\n\nDirectory empty and filter behavior follows this contract:\n\n| State | Empty guidance | Native form behavior |\n| --- | --- | --- |\n| No query, status, or caller filter is active | Explain that no records have been added; when `canCreate` and `addHref` are present, point to creating the first record | Search and status controls retain their normal names |\n| Query or status is active | Suggest changing search or filters | Visible query/status controls remain authoritative over same-named `hiddenFields` |\n| A caller-owned filter is active | Suggest changing search or filters | Render the `filters` snippet inside the directory form and list its names in `filterNames` so same-named hidden fields are omitted |\n| Caller supplies `emptyDescription` | Render the supplied description | Submission behavior is unchanged |\n\nThe `filters` snippet is for domain-adjacent GET controls that belong beside\nsearch and status, such as a trade classification. It does not move filtering\nor authorization into the component. `hasAdditionalFilters` describes whether\nthose controls currently narrow the result, while `filterNames` prevents a\nhidden/visible duplicate name from changing native form serialization.\n\nThe registry keys are `customer-directory`, `customer-detail`, `customer-form`, `vendor-directory`, `vendor-detail`, `vendor-form`, and `party-contact-fields`. Interactive examples are published by `@happyvertical/smrt-commerce/playground` and ship in the next generated Commerce package release. DomaCraft can replace its local routes incrementally by adapting its existing server DTOs and payload names; no identity or database migration is required.\n\nThe form playground previews handle save, add-contact, and remove-contact intents in memory. They deliberately return a simulated validation error on save so retained values and retry behavior can be inspected without a backend.\n\n## Contact rows (#3501)\n\n`Customer.contacts` / `Vendor.contacts` store `PartyContact` rows (JSON\ncolumn; additive schema, run `smrt db:migrate`). The root package exports the\nserver half of the form's contact payload:\n\n| Export | Contract |\n| --- | --- |\n| `readPartyContactRows(form, fieldNames?)` | Zips the repeated `contactId`, `contactName`, `contactLabel`, `contactEmail`, `contactPhone`, `contactAddress` values (or the form's `fieldNames` overrides) in DOM order into raw `PartyContactInput` rows. The editable contact block always posts a hidden `contactCount` marker with its row count; a missing marker (`reason: 'missing'`) or any disagreeing count (`'misaligned'`) throws `PartyContactPayloadError`, so zero rows mean an empty list only when the block rendered. |\n| `applyPartyContactIntent(rows, intent, intents?)` | `addContact` appends a blank row; `removeContact:<n>` (or a caller `parseRemoveIntent`) drops row `n`. `handled: true` means render the returned rows and do not save, including for a stale index. |\n| `validatePartyContactRows(rows)` | Trims, drops blank rows, and returns `errors` aligned with the submitted rows using codes `too_long` (limits in `PARTY_CONTACT_MAX_LENGTHS`, matching the inputs) and `invalid_email`. |\n| `savePartyContacts(collection, partyId, formOrRows, { fieldNames?, expectedTenantId? })` | Given the posted form, reads it with `readPartyContactRows` (marker enforced); an array is taken as rows already read that way. Loads the party with `collection.get()` (so the active tenant context scopes it) and replaces its contacts: posted ids are kept, new rows get a random id, unposted rows are removed. A posted id the party does not have is `unknown_contact` on `id`. Invalid input throws `PartyContactValidationError` (`errors`, nothing written); a missing or other-tenant party throws `PartyContactPartyNotFoundError`. |\n\nError codes are not messages: map `PartyContactValidationError.errors` to\nlocalized strings for `PartyFormErrors.contacts`, and return the submitted\nrows as retained `values.contacts`. A form with custom contact `fieldNames`\nmust pass the same names when reading; with mismatched names the marker is\nnot found and the read fails closed instead of removing every contact. Saves are a read-modify-write of the party\nrow (last writer wins). Map saved contacts to `CustomerDisplayData.contacts` /\n`VendorDisplayData.contacts` for the detail screens.\n\n## Selectors (#3602)\n\n`CustomerSelect` and `VendorSelect` are field-sized pickers built on the shared\nsmrt-ui `RelationInput` (`@happyvertical/smrt-ui/forms`, ARIA 1.2 combobox),\nthrough one internal `PartySelect` adapter that maps party DTOs to relation\noptions and localizes the field's text. Search, resolve, debounce, stale-result\nand create handling live in `RelationInput`, not here.\n\n| Prop | Meaning |\n| --- | --- |\n| `name`, `value` (bindable), `label`, `required`, `disabled`, `error`, `placeholder`, `description` | Field basics. `label` defaults to the localized \"Customer\"/\"Vendor\"; `error` renders a linked `role=\"alert\"`. |\n| `search(query)` | Caller-owned lookup returning party DTOs; called with `''` when the list opens and, debounced (`debounceMs`, default 250), as the person types. |\n| `resolve(id)` | Caller-owned lookup of the current value's DTO when it has not been listed; without it a value stays blank until searched. |\n| `onCreate(query)` | Optional. Renders \"+ New customer/vendor\" (`createLabel` overrides); a returned DTO is selected. |\n| `onchange(id)`, `interaction` | Change callback (`''` when cleared) and agent-interaction options. |\n\nRows show `profile.name` with localized status and, for customers, type\n(`Active · Wholesale`). Data never enters the component: no stores, no fetches,\nno tenant logic. A failed `search` shows an inline status, not an exception;\na failed `onCreate` leaves the field as it was. Text comes from the\n`commerce.select.*` messages. The selectors are registered as the\n`customer-select` and `vendor-select` slots, whose `selects` qualified names\nare emitted into the manifest's `uiSelectors`; keep `selects` a string literal\nso the scanner can read it.\n\nWidget hints (`@field({ ui: { widget } })`, #3599) are presentation-only and\nthe build rejects them on non-text fields; `currency` is a currency-code\npicker for ISO 4217 strings and is never applied to minor-unit amounts;\namounts take `ui: { format: 'money' }` instead (#3718).\nBehaviour tests run under jsdom: `pnpm exec vitest run --config\nvitest.selects.config.ts` (also part of `pnpm test:ui`).\n\n## Record page hints (#3718)\n\nMinor-unit amounts on Contract, Invoice, their line items, Payment,\nPaymentAllocation, `Customer.creditLimit` and `Vendor.minimumOrderAmount`\ncarry `ui: { format: 'money' }` (#3718; `widget: 'currency'` stays the\ncurrency-code picker on the text `currency` field). That includes the\nmoney-v2 fields (#4011): line `taxAmount`, `discountTotal` on Invoice and\nContract, `Invoice.creditedAmount`, and `DocumentTax` `taxableMinor` /\n`taxMinor` / `recoverableMinor`; the test asserts every integer named\n`*Amount`, `*Total`, `*Minor` or `subtotal` on those models (and Contract's\nSTI subtypes) is hinted. `legacyAmount` is `@internal` migration-rollback\nstate, not a user-facing amount, and stays unhinted. Payout and Payment's\nnative/USD drift amounts are not hinted: their currency is a payout rail or\nfixed USD, not the record's `currency`. The line-item FKs\n(`ContractLineItem.contractId`, `InvoiceLineItem.invoiceId`,\n`FulfillmentLineItem.fulfillmentId`) carry `role: 'items'`, so record pages\nrender them as a line-items table with a totals block;\n`src/__tests__/money-hints-manifest.test.ts` pins both.\n"
21879
+ "content": "# Customer and vendor UI\n\nImport the provider-free surfaces from `@happyvertical/smrt-commerce/svelte`:\n\n```svelte\n<script lang=\"ts\">\n import {\n CustomerDirectory,\n CustomerDetail,\n CustomerForm,\n VendorDirectory,\n VendorDetail,\n VendorForm,\n type CustomerDisplayData,\n } from '@happyvertical/smrt-commerce/svelte';\n\n let { data, form } = $props();\n</script>\n\n<CustomerForm\n mode=\"edit\"\n values={form?.values ?? data.values}\n errors={form?.errors}\n transport={{\n action: '?/update',\n method: 'post',\n hiddenFields: {\n requestId: form?.values.requestId ?? data.requestId,\n expectedTenantId: data.tenantId,\n },\n }}\n labels={{ singular: 'Client', plural: 'Clients' }}\n cancelHref={`/clients/${data.customer.id}`}\n/>\n```\n\nThe caller owns action URLs, server authorization, tenancy checks, request tokens, persistence, and retry behavior. Capability props only decide whether an affordance is rendered. The components never create a Profile, Customer, Vendor, or contact relationship and never generate request tokens; the server writes posted contact rows with `savePartyContacts()` ([contact rows](#contact-rows-3501)).\n\n`CustomerDisplayData.creditLimitMinor` and `VendorDisplayData.minimumOrderMinor` are integer currency minor units. Detail components convert them only for display and accept an explicit `minorUnitExponent`. Form values deliberately use retained strings such as `1250.00`; the server validates and converts those major-unit strings to the model's integer minor units.\n\n`VendorFormValues.leadTimeDays` is also retained text. The control supplies a numeric keyboard hint without browser number-input sanitization, and the server must parse and validate the submitted non-negative integer.\n\nForms use the provider-free `Form`, `Input`, `Select`, `Textarea`, and `Checkbox` controls from `@happyvertical/smrt-ui/forms`. They default to native `POST` submission through `preventDefault={false}`. `transport.hiddenFields`, `transport.intentName`, and the field-name maps let an application preserve its existing payload contract. Repeatable contact rows submit repeated field names in DOM order. Add and remove controls are native submitters, so they work without JavaScript; the server returns the revised rows and retained values.\n\n`CustomerForm` and `VendorForm` accept submit hooks so a host never has to find\nthe form after mount (#3501). `enhance` (`PartyFormEnhance`, action-shaped) is\napplied to the mounted native `<form>` and destroyed with it; SvelteKit's\n`enhance` from `$app/forms` fits directly, or wrap it to pass a submit callback.\nAn `ssr = false` app needs it, because a plain native post answers with the\nempty shell. `onsubmit` receives the native submit event (submission stays\nnative unless it calls `preventDefault()`), and the `getFormElement()` instance\nmethod returns the native element while mounted.\n\n```svelte\n<script lang=\"ts\">\n import { enhance } from '$app/forms';\n</script>\n\n<VendorForm values={form?.values ?? data.values} errors={form?.errors} transport={{ action: '?/save' }} {enhance} />\n```\n\nDirectory search forms accept `hiddenFields` as a string record for caller-owned GET state such as sort direction and page size. Fields whose names equal the resolved `queryName` or `statusName` are omitted so the visible search controls remain authoritative.\n\nCustomer shipping and billing addresses use the shared `CountrySelect` and\n`ProvinceSelect`. The native names remain `shippingCountry`, `shippingState`,\n`billingCountry`, and `billingState` unless `fieldNames` overrides them. Changing\ncountry changes the region control without clearing its current string; CA and US\nrender localized subdivision choices, while unsupported countries render an\neditable text field. Unknown values remain selectable/postable after a failed\nsubmission. Use `countryOptions` or `provinceOptions` only when the caller must\nrestrict the choices. `VendorForm` has no structured address fields in its public\nDTO and does not invent them.\n\nVendor currency uses the shared `CurrencySelect`. The raw retained string remains\nthe native `currency` payload, including unknown values returned after validation.\nUse `currencyOptions` for caller-authorized restrictions; omitted options use the\nshared currency catalog. Selector labels follow the active smrt-ui i18n locale.\n\nThe forms place a non-focusable default save submitter before contact row actions so Enter in a text field cannot accidentally add or remove a contact. This remains in place when the `actions` snippet customizes the visible action bar; set `transport.saveIntent` to the custom save intent.\n\nUse the `extension` snippets for application fields such as construction roles and trades. Keep those fields and their policy in the application:\n\n```svelte\n<VendorForm values={data.values} transport={{ action: '?/save' }}>\n {#snippet extension(values)}\n <ConstructionVendorFields roles={values.roles} trades={values.trades} />\n {/snippet}\n</VendorForm>\n```\n\nDirectory entries carry caller-owned detail links and pagination links. Detail edit links and form submit buttons appear only when their capability props allow them; the server must still authorize every request.\n\n`CustomerDirectory`, `CustomerDetail`, `VendorDirectory`, and `VendorDetail`\nrender a `PageHeader` (the page `h1`, a subtitle, and the add/edit action) by\ndefault. A host that frames every page itself passes `headless` to render only\nthe body, then renders its own heading and create/edit affordance; or passes a\n`header` snippet to replace the built-in header in place (#3501). `backHref`\nstays independent of both.\n\nDirectory empty and filter behavior follows this contract:\n\n| State | Empty guidance | Native form behavior |\n| --- | --- | --- |\n| No query, status, or caller filter is active | Explain that no records have been added; when `canCreate` and `addHref` are present, point to creating the first record | Search and status controls retain their normal names |\n| Query or status is active | Suggest changing search or filters | Visible query/status controls remain authoritative over same-named `hiddenFields` |\n| A caller-owned filter is active | Suggest changing search or filters | Render the `filters` snippet inside the directory form and list its names in `filterNames` so same-named hidden fields are omitted |\n| Caller supplies `emptyDescription` | Render the supplied description | Submission behavior is unchanged |\n\nThe `filters` snippet is for domain-adjacent GET controls that belong beside\nsearch and status, such as a trade classification. It does not move filtering\nor authorization into the component. `hasAdditionalFilters` describes whether\nthose controls currently narrow the result, while `filterNames` prevents a\nhidden/visible duplicate name from changing native form serialization.\n\nThe registry keys are `customer-directory`, `customer-detail`, `customer-form`, `vendor-directory`, `vendor-detail`, `vendor-form`, and `party-contact-fields`. Interactive examples are published by `@happyvertical/smrt-commerce/playground` and ship in the next generated Commerce package release. DomaCraft can replace its local routes incrementally by adapting its existing server DTOs and payload names; no identity or database migration is required.\n\nThe form playground previews handle save, add-contact, and remove-contact intents in memory. They deliberately return a simulated validation error on save so retained values and retry behavior can be inspected without a backend.\n\n## Contact rows (#3501)\n\n`Customer.contacts` / `Vendor.contacts` store `PartyContact` rows (JSON\ncolumn; additive schema, run `smrt db:migrate`). The root package exports the\nserver half of the form's contact payload:\n\n| Export | Contract |\n| --- | --- |\n| `readPartyContactRows(form, fieldNames?)` | Zips the repeated `contactId`, `contactName`, `contactLabel`, `contactEmail`, `contactPhone`, `contactAddress` values (or the form's `fieldNames` overrides) in DOM order into raw `PartyContactInput` rows. The editable contact block always posts a hidden `contactCount` marker with its row count; a missing marker (`reason: 'missing'`) or any disagreeing count (`'misaligned'`) throws `PartyContactPayloadError`, so zero rows mean an empty list only when the block rendered. |\n| `applyPartyContactIntent(rows, intent, intents?)` | `addContact` appends a blank row; `removeContact:<n>` (or a caller `parseRemoveIntent`) drops row `n`. `handled: true` means render the returned rows and do not save, including for a stale index. |\n| `validatePartyContactRows(rows)` | Trims, drops blank rows, and returns `errors` aligned with the submitted rows using codes `too_long` (limits in `PARTY_CONTACT_MAX_LENGTHS`, matching the inputs) and `invalid_email`. |\n| `savePartyContacts(collection, partyId, formOrRows, { fieldNames?, expectedTenantId? })` | Given the posted form, reads it with `readPartyContactRows` (marker enforced); an array is taken as rows already read that way. Loads the party with `collection.get()` (so the active tenant context scopes it) and replaces its contacts: posted ids are kept, new rows get a random id, unposted rows are removed. A posted id the party does not have is `unknown_contact` on `id`. Invalid input throws `PartyContactValidationError` (`errors`, nothing written); a missing or other-tenant party throws `PartyContactPartyNotFoundError`. |\n\nError codes are not messages: map `PartyContactValidationError.errors` to\nlocalized strings for `PartyFormErrors.contacts`, and return the submitted\nrows as retained `values.contacts`. A form with custom contact `fieldNames`\nmust pass the same names when reading; with mismatched names the marker is\nnot found and the read fails closed instead of removing every contact. Saves are a read-modify-write of the party\nrow (last writer wins). Map saved contacts to `CustomerDisplayData.contacts` /\n`VendorDisplayData.contacts` for the detail screens.\n\n## Selectors (#3602)\n\n`CustomerSelect` and `VendorSelect` are field-sized pickers built on the shared\nsmrt-ui `RelationInput` (`@happyvertical/smrt-ui/forms`, ARIA 1.2 combobox),\nthrough one internal `PartySelect` adapter that maps party DTOs to relation\noptions and localizes the field's text. Search, resolve, debounce, stale-result\nand create handling live in `RelationInput`, not here.\n\n| Prop | Meaning |\n| --- | --- |\n| `name`, `value` (bindable), `label`, `required`, `disabled`, `error`, `placeholder`, `description` | Field basics. `label` defaults to the localized \"Customer\"/\"Vendor\"; `error` renders a linked `role=\"alert\"`. |\n| `search(query)` | Caller-owned lookup returning party DTOs; called with `''` when the list opens and, debounced (`debounceMs`, default 250), as the person types. |\n| `resolve(id)` | Caller-owned lookup of the current value's DTO when it has not been listed; without it a value stays blank until searched. |\n| `onCreate(query)` | Optional. Renders \"+ New customer/vendor\" (`createLabel` overrides); a returned DTO is selected. |\n| `onchange(id)`, `interaction` | Change callback (`''` when cleared) and agent-interaction options. |\n\nRows show `profile.name` with localized status and, for customers, type\n(`Active · Wholesale`). Data never enters the component: no stores, no fetches,\nno tenant logic. A failed `search` shows an inline status, not an exception;\na failed `onCreate` leaves the field as it was. Text comes from the\n`commerce.select.*` messages. The selectors are registered as the\n`customer-select` and `vendor-select` slots, whose `selects` qualified names\nare emitted into the manifest's `uiSelectors`; keep `selects` a string literal\nso the scanner can read it.\n\nWidget hints (`@field({ ui: { widget } })`, #3599) are presentation-only and\nthe build rejects them on non-text fields; `currency` is a currency-code\npicker for ISO 4217 strings and is never applied to minor-unit amounts;\namounts take `ui: { format: 'money' }` instead (#3718).\nBehaviour tests run under jsdom: `pnpm exec vitest run --config\nvitest.selects.config.ts` (also part of `pnpm test:ui`).\n\n## Record page hints (#3718)\n\nMinor-unit amounts on Contract, Invoice, their line items, Payment,\nPaymentAllocation, `Customer.creditLimit` and `Vendor.minimumOrderAmount`\ncarry `ui: { format: 'money' }` (#3718; `widget: 'currency'` stays the\ncurrency-code picker on the text `currency` field). That includes the\nmoney-v2 fields (#4011): line `taxAmount`, `discountTotal` on Invoice and\nContract, `Invoice.creditedAmount`, and `DocumentTax` `taxableMinor` /\n`taxMinor` / `recoverableMinor`; the test asserts every integer named\n`*Amount`, `*Total`, `*Minor` or `subtotal` on those models (and Contract's\nSTI subtypes) is hinted. Payout and Payment's\nnative/USD drift amounts are not hinted: their currency is a payout rail or\nfixed USD, not the record's `currency`. The line-item FKs\n(`ContractLineItem.contractId`, `InvoiceLineItem.invoiceId`,\n`FulfillmentLineItem.fulfillmentId`) carry `role: 'items'`, so record pages\nrender them as a line-items table with a totals block;\n`src/__tests__/money-hints-manifest.test.ts` pins both.\n"
22459
21880
  },
22460
21881
  {
22461
21882
  "path": "README.md",
22462
21883
  "module": "README",
22463
- "content": "# @happyvertical/smrt-commerce\n\nCommerce models for the s-m-r-t framework. Covers customers, vendors, contracts (10 STI types), invoices with ledger integration, payments, and fulfillment tracking.\n\n## Installation\n\n```bash\npnpm add @happyvertical/smrt-commerce\n```\n\n## Usage\n\n```typescript\nimport {\n Customer, CustomerCollection,\n Order, ContractCollection,\n Invoice, InvoiceCollection,\n Payment, PaymentCollection,\n ContractStatus, InvoiceStatus, PaymentMethod\n} from '@happyvertical/smrt-commerce';\n\n// Create a customer linked to a profile\nconst customers = await CustomerCollection.create({ db });\nconst customer = await customers.create({\n profileId: 'profile-uuid',\n creditLimit: 1000000, // $10,000.00 — money is integer minor units\n paymentTerms: 'Net 30',\n});\nawait customer.save();\n\n// Create an order (STI contract type)\nconst contracts = await ContractCollection.create({ db });\nconst order = await contracts.create({\n _meta_type: 'Order',\n customerId: customer.id,\n subtotal: 100000, // $1,000.00\n taxAmount: 5000, // $50.00\n totalAmount: 105000, // $1,050.00\n currency: 'CAD',\n});\nawait order.save();\n\n// Create an invoice for the order\nconst invoices = await InvoiceCollection.create({ db });\nconst invoiceNumber = await invoices.generateInvoiceNumber();\nconst invoice = await invoices.create({\n customerId: customer.id,\n contractId: order.id,\n invoiceNumber,\n subtotal: 100000, // $1,000.00\n taxAmount: 5000, // $50.00\n totalAmount: 105000, // $1,050.00\n});\nawait invoice.save();\n\n// Recognize revenue (creates balanced journal in smrt-ledgers)\nawait invoice.recognizeRevenue({\n arAccountId: 'ar-account-id',\n revenueAccountId: 'revenue-account-id',\n taxAccountId: 'tax-account-id',\n});\n\n// Record a payment\nconst payments = await PaymentCollection.create({ db });\nconst payment = await payments.create({\n contractId: order.id,\n customerId: customer.id,\n amount: 105000,\n method: PaymentMethod.CREDIT_CARD,\n});\nawait payment.save();\n\n// Record payment with ledger integration\nawait payment.recordPayment({\n ledgerId: 'ledger-id',\n receivablesAccountId: 'ar-account-id',\n cashAccountId: 'bank-account-id',\n});\n```\n\n## Commercial money calculator\n\nThe public `/money` entry is a pure calculator for line amounts, discounts,\ncomponent tax, document rounding, and totals. It consumes and returns integer\nminor-unit money values; tax and discount percentages are exact text (`'5'`\nmeans 5%). It does not persist or load documents.\n\n```ts\nimport { calculateMoneyDocument } from '@happyvertical/smrt-commerce/money';\n\nconst totals = calculateMoneyDocument({\n currency: 'CAD',\n lineAmountType: 'exclusive',\n roundingScope: 'line',\n lines: [{\n id: 'consulting', quantity: 1, unitPrice: 10000,\n discount: { kind: 'none' },\n taxCode: {\n id: 'gst', kind: 'taxable',\n components: [{ taxComponentId: 'gst', percent: '5', compound: false }],\n },\n }],\n});\n```\n\n`resolveLineTaxCode()` resolves caller-loaded codes without I/O, and the\nSvelte `InvoiceEditor` opts into the v2 money controls through its `money`\nprop. Frozen editor documents render a supplied stored snapshot without\nrecalculation. See the [commerce money calculator guide](../../docs/content/commerce-money.md)\nfor pricing modes, rounding, input and result types, the native-form editor\ncontract, and the boundaries this package does not define.\n\n### Tenant tax catalogue and defaults\n\nThe package root also exports tenant-scoped `TaxComponent`, `TaxCode`, and\n`TaxSettings` models and their collections. Components hold exact percentage\nrates and optional liability accounts; codes combine compatible components; the\none settings row per tenant supplies sales/purchase defaults, amount mode,\nrounding scope, and labelled registration numbers. A referenced component rate\nis immutable: archive it and create a replacement for a rate change.\n\n`ensureTenantTaxCodes(db, tenantId)` idempotently creates the baseline\nnon-taxable codes. `applyTaxCodeExamplePack(db, tenantId)` installs illustrative\nregional examples for review, and `applyCommerceCookbookSettings(db, tenantId,\ncookbook)` explicitly materializes a validated cookbook's defaults after schema\nsetup and authentication establish the tenant. See the [commerce money\ncalculator guide](../../docs/content/commerce-money.md#tenant-tax-catalogue) for\nthe supported kinds, seed behavior, and cookbook rate units.\n\n### Persisted Invoice v2\n\nThe public `Invoice` and `InvoiceLineItem` save paths use the server-owned\n`TaxSettings.moneyModel` gate. Missing settings and `legacy` preserve the\nexisting behavior; `migrating` rejects Invoice money writes; `v2` atomically\nrecalculates the draft Invoice aggregate from its lines and current tax inputs.\nThe marker is not writable through generated surfaces or ordinary saves of\nan existing settings row.\n\nIssuing an Invoice (leaving `DRAFT`) or assigning `arJournalId` freezes its\nmoney snapshot. Header and line money changes then fail with `FROZEN_DOCUMENT`,\nwhile later lifecycle and payment updates retain the snapshot. Provider tax\nstays separate in `providerTaxAmount` and contributes to the Invoice headers\nwithout creating an application tax code. In v2,\n`InvoiceLineItemCollection.getTotalForInvoice()` sums line amounts for\ncompatibility; use persisted `Invoice.totalAmount` for the amount due. The\n[commerce money calculator guide](../../docs/content/commerce-money.md#persisted-invoice-money)\nand [document-money contract](agents/document-money.md) cover persisted Invoice-v2\nsemantics, credits, refunds, and posting. The materialized Invoice tax-report\ndescriptor and tenant-safe drilldown are documented in\n[tax-summary contract](agents/tax-summary.md). Tenant migration commands,\nContract money persistence, PurchaseContract tax contributors, and the\nmaterialized tax-summary host and drilldown are available. Applications opt in\nby registering `createTaxSummaryMaterializedReportHost()` in their server-only\nreport-host registry; the route binds the authenticated tenant and read\npermission. See the tax-summary contract for registration and migration details.\n\n## API\n\n### Models\n\n#### Supplier quotations\n\n`SupplierQuote` extends `PurchaseContract`, requires `vendorId`, and has\n`ContractType.SUPPLIER_QUOTE`. Create it through `PurchaseContractCollection`\n(or `ContractCollection`) with `_meta_type:\n'@happyvertical/smrt-commerce:SupplierQuote'`. It inherits `expiryDate`,\n`reference`, `notes`, and `terms`; it has no customer relationship. Purchase\nbranch reads include quotes and sales branch reads exclude them.\n\nKeep received quote revisions and shared quote identity in your application.\nAwarding a quote creates a separate `PurchaseOrder`; accepting or saving a\nquote never converts its identity into an order. Quotes use purchase-side\nmoney calculation and frozen snapshots, but never contribute incurred purchase\ntax to reports, regardless of status. Existing Estimate and PurchaseOrder\nreport eligibility is unchanged.\n\n#### Upgrading contract parties (#3898)\n\n`Contract` no longer declares `customerId` or `vendorId`. Narrow a contract to\n`SalesContract` to read `customerId`, or to `PurchaseContract` to read `vendorId`.\nThe existing leaf qualified names and the `contracts` table stay the same; a\n`db:migrate --dry-run` against a v0.55.19 schema reports no DDL changes on\nSQLite or PostgreSQL. Sales leaves no longer accept `vendorId`; purchase leaves\nno longer accept `customerId`. `Order`, `Estimate`, `Agreement`, `Lease`, and\n`WholesaleOrder` require a customer on save. `PurchaseOrder` and `SupplierQuote`\nrequire a vendor.\n`Cart`, `LicenseSale`, and `ProductionOrder` retain optional parties on their side.\n\nBefore upgrading existing data, run the read-only\n`preflightCommercePartyLinks(db)` and inspect its stray party links, missing\nrequired parties, base-typed rows, discriminator mismatches, and invalid\ndocument references. Its `unqualifiedDiscriminatorRows` lists legacy simple\n`_meta_type` values such as `Order` and `PurchaseOrder`; retype them to the\nqualified class name before upgrading, since branch collections cannot read\nthem. The report checks invoice, payment, fulfillment, ad group,\nand expense links when those tables exist. A linked base-typed or wrong-branch\ncontract blocks the next save of its document even when the link is unchanged.\nRegistered custom descendants of either contract branch are accepted in link\nchecks; load those model classes before running the preflight.\nTenant-owned documents linked to global or other-tenant contracts are also\nreported because the save-time lookup is tenant-scoped. Retype the contract to\nthe correct leaf, assign it to the document's tenant, or repair the link before\ndeployment. The deprecated `ContractCollection` party finders now return the\nappropriate branch\nonly, so retype legacy base rows with party links before relying on those\nfinders. Clear stray party columns and fill required parties before deploying\nwriters that save affected rows. Missing required parties remain readable but\nfail their next save. Regenerate cached manifests, planner catalogs, and\ngenerated screens after upgrading.\nThis preflight never edits rows; an operator chooses the data correction.\n\n| Export | Description |\n|--------|------------|\n| `Customer` | Customer record with creditLimit, paymentTerms, tax exemption, addresses, contacts |\n| `Vendor` | Vendor/supplier with leadTimeDays, minimumOrderAmount, currency, contacts |\n| `Contract` | STI base class for all commercial agreements |\n| `SalesContract` | Abstract customer-side Contract branch; owns `customerId` |\n| `PurchaseContract` | Abstract supplier-side Contract branch; owns `vendorId` |\n| `Estimate` | Quote or proposal sent to a customer |\n| `Order` | Customer purchase order |\n| `Lease` | Rental or lease agreement |\n| `Agreement` | Service or maintenance agreement |\n| `SupplierQuote` | Quotation received from a vendor; distinct from an awarded purchase order |\n| `PurchaseOrder` | Order sent to a vendor/supplier |\n| `ContractLineItem` | Line item within a contract |\n| `Invoice` | Billing document with status lifecycle and ledger integration |\n| `InvoiceLineItem` | Individual invoice line item |\n| `Payment` | Payment record with method, status, and optional ledger journal |\n| `PaymentAllocation` | Payment-to-invoice allocation |\n| `Fulfillment` | Shipment/delivery tracking with carrier and address |\n| `FulfillmentLineItem` | Individual fulfillment line item |\n| `BillingAccount` | A payer's account with a seller: customer, provider customer, terms, standing |\n| `BillingPeriodClose` | One payer's invoice for one period and currency; also the billing job target |\n| `BillingLineSource` | The claim that a charge or plan period is billed on exactly one close |\n\n### Collections\n\n`CustomerCollection`, `VendorCollection`, `ContractCollection`, `SalesContractCollection`, `PurchaseContractCollection`, `InvoiceCollection`, `InvoiceLineItemCollection`, `PaymentCollection`, `PaymentAllocationCollection`, `FulfillmentCollection`\n\n`PaymentAllocationCollection.allocatePayment()` splits a payment across invoices\nin one transaction using stable allocation IDs for retries. Direct allocation\nsaves also enforce matching owners, customers and currencies and both balance\ncaps. Read [payment allocation integrity](agents/payment-allocations.md) for\nreplay, parent revision refresh and compatibility requirements.\n\n\n### Enums\n\n| Export | Values |\n|--------|--------|\n| `ContractType` | `estimate`, `order`, `lease`, `agreement`, `supplier_quote`, `purchase_order`, `wholesale_order`, `production_order`, `cart`, `license_sale` |\n| `ContractStatus` | `draft`, `sent`, `accepted`, `declined`, `completed`, `cancelled` |\n| `CustomerStatus` | `active`, `inactive`, `suspended` |\n| `VendorStatus` | `active`, `inactive`, `suspended` |\n| `InvoiceStatus` | `draft`, `sent`, `viewed`, `partial`, `paid`, `overdue`, `cancelled`, `written_off` |\n| `PaymentMethod` | `cash`, `check`, `credit_card`, `bank_transfer`, `crypto`, `other` |\n| `PaymentStatus` | `pending`, `completed`, `failed`, `refunded`, `cancelled` |\n| `FulfillmentStatus` | `pending`, `processing`, `shipped`, `delivered`, `cancelled` |\n| `FulfillmentType` | `shipment`, `delivery`, `pickup`, `digital`, `service` |\n\n### Types and Constants\n\n| Export | Description |\n|--------|------------|\n| `Address` | Shared address interface (street1, street2, city, state, postalCode, country) |\n| `RecognizeRevenueOptions` | Account IDs for invoice revenue recognition |\n| `RecordPaymentOptions` | Ledger and account IDs for payment recording |\n| `InvoiceNumberOptions` | Options for `generateInvoiceNumber()` (prefix, format) |\n| `UNPAID_STATUSES` | Array of invoice statuses considered unpaid (sent, viewed, partial, overdue) |\n| `COMMERCE_MODULE_META` | UI module metadata |\n| `COMMERCE_UI_SLOTS` | UI slot definitions |\n\n### Ledger Integration\n\nInvoice and Payment integrate with `@happyvertical/smrt-ledgers` via dynamic import (optional dependency). Invoice stores `arJournalId` and `revenueJournalId` as plain string references to ledger journals. `recognizeRevenue()` creates a balanced AR entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable). `recordPayment()` creates a balanced cash receipt entry (DR: Cash, CR: Accounts Receivable). Both methods return null or throw if the ledgers package is not installed.\n\n`Payment.recordPayment()` claims the payment revision and commits the journal,\nentries, and completed payment in one database transaction. A failed completion\nrolls back the posting and restores the instance's settlement fields for retry.\nThe claimed persisted status is authoritative even if a caller locally resets\nstatus or marks the instance failed without saving. Concurrent or stale instances\nfail instead of posting twice; reload the payment\nand retry only if it is still pending. An already completed payment retains the\n`Payment already recorded` error. A caller's enclosing transaction remains the\nowner: if it rolls back after settlement returns, reload the payment before retry.\nThese guarantees cover new postings, not orphaned journals written by older\nversions. The returned journal uses the caller's database executor.\n\n### Cross-Package References\n\nCustomer and Vendor link to `@happyvertical/smrt-profiles` via plain `profileId` string. Invoice and Payment reference `@happyvertical/smrt-ledgers` journals via plain string IDs (`arJournalId`, `revenueJournalId`, `journalId`). All models use `@TenantScoped({ mode: 'optional' })` with nullable `tenantId`.\n\n## Billing-period close\n\n`BillingRuntime` turns approved `@happyvertical/smrt-subscriptions` charges into\ninvoices for each billing owner, pushes them to a payment provider with\nprovider-calculated tax, posts revenue to the ledger, and applies the\nprovider's webhook events. Every provider call goes through\n`@happyvertical/accounting`; `createStripeBillingProvider()` adapts its Stripe\nprovider to the `BillingProvider` port. Money crossing the port is integer\nminor units.\n\n```ts\nimport { getAccountingProvider } from '@happyvertical/accounting';\nimport { SpendingPolicyEvaluator } from '@happyvertical/smrt-subscriptions';\nimport {\n BillingRuntime,\n createStripeBillingProvider,\n enqueueBillingPeriodClose,\n registerBillingRuntime,\n} from '@happyvertical/smrt-commerce';\n\nconst stripe = await getAccountingProvider({ type: 'stripe', secretKey });\nconst billing = await BillingRuntime.create({\n db,\n sellerTenantId: platformTenantId,\n kind: 'provider', // or 'reseller' to bill a reseller's children\n provider: createStripeBillingProvider({ stripe, webhookSecret }),\n billingRelationships, // smrt-tenancy BillingRelationshipService\n ledger: {\n arAccountId,\n revenueAccountId,\n taxAccountId,\n cashAccountId,\n prepaidCreditAccountId,\n },\n onPayerStanding: async ({ payerTenantId, standing, db }) => {\n // suspend or reinstate the payer; runs in the event transaction\n },\n});\n\n// One account per payer: its customer, tax location, and terms.\nawait billing.upsertAccount({\n payerTenantId,\n name: 'Network Co',\n email: 'billing@example.test',\n billingAddress: { country: 'CA', postalCode: 'T0L 0A0' },\n});\n\n// Optional: bill this payer from its activation date instead of calendar\n// months, prorating flat plans added mid-period (see \"Billing cycles\").\nawait billing.upsertAccount({\n payerTenantId,\n name: 'Network Co',\n billingAnchorAt: activatedAt,\n prorateFlatPlans: true,\n});\n\n// Close each payer's last ended period — safe to run on every schedule tick.\nregisterBillingRuntime('platform', billing);\nawait enqueueBillingPeriodClose({ runtime: 'platform' });\n\n// Webhook route: verify, enqueue, respond 2xx.\nconst intake = await billing.acceptWebhook(\n rawBody,\n request.headers.get('stripe-signature'),\n);\nif (intake.type?.endsWith(':unverified_credit_purchase')) {\n // alert: a paid checkout claimed to buy credit but failed verification\n}\nawait billing.processEvents(); // or enqueueBillingEvents({ runtime: 'platform' })\n\n// Prepaid credit through provider checkout: tax is added on top when the\n// payer's account is taxed; `savePaymentMethod` keeps the card for top-ups.\nconst { url } = await billing.createCreditCheckout({\n spendingPolicyId,\n amount: 5000,\n purchaseId: cartId,\n savePaymentMethod: true,\n successUrl,\n cancelUrl,\n});\n\n// Card on file at signup: a setup checkout that charges nothing. Its\n// completion event makes the card the payer's default and adopts the billing\n// address collected at checkout as the tax location.\nconst setup = await billing.createCardSetupCheckout({\n payerTenantId,\n currency: 'CAD',\n setupId: signupId,\n successUrl,\n cancelUrl,\n});\n\n// Automatic top-ups: charge the saved card when a balance would run out.\n// Accounts are taxed by default (`automaticTax`); a taxed account is charged\n// the credit plus provider-calculated tax (#3194).\nconst evaluator = await SpendingPolicyEvaluator.create({\n db,\n autoTopUp: billing.autoTopUpHook({ amount: () => 2500 }),\n});\n```\n\n- **What is billed.** A `provider` runtime bills `ClientCharge`s (and their\n `BillingAdjustment`s) to the payer each charge names, plus monthly flat plans\n it owns, to each subscriber's billing owner. A `reseller` runtime bills the\n `RetailCharge`s its children owe it plus its own flat plans. Charges approved\n before the period end are billed once, whatever period that is; flat plans\n are billed in arrears for the payer's closed period (trials and\n provider-managed subscriptions are skipped). One invoice per payer and\n currency carries both kinds of lines.\n- **Replay safety.** Close, invoice, and line ids are derived from the payer,\n currency, and period; each charge is claimed by exactly one close; the\n provider invoice uses the close id as its idempotency key; and every step is\n persisted before the next, so a retry resumes where it stopped. A lease stops\n two workers advancing the same close. A close without a period also resumes\n every unfinished close whose period has ended, even one the payer's current\n schedule no longer produces (#3116). A payer whose charges net to a credit\n gets no invoice; the credit is carried to its next invoice.\n- **Billing cycles (#3116).** A payer's periods follow its account's\n `billingAnchorAt`. Unset (the default) they are UTC calendar months. Set,\n they run monthly from the anchor at its UTC time of day; a day the month\n lacks is clamped to its last day, always from the anchor itself (an anchor\n on Jan 31 bills Jan 31 → Feb 28 → Mar 31). Before the anchor the schedule is\n calendar months, and the anchor's own month ends at the anchor (a *stub*).\n `closePeriod()` / `enqueueBillingPeriodClose()` with no period close each\n payer's **last ended period on its own schedule**, so payers on different\n anchors are closed by the same daily tick. An explicit\n `{ periodStart, periodEnd }` closes that period for calendar payers (flat\n plans only when it is a calendar month, as before) and for anchored payers\n whose schedule has exactly that period. `billingPeriodFor(payer, at)` returns\n a payer's current period; `billingPeriodContaining()`,\n `lastEndedBillingPeriod()`, and `prorateMinorUnits()` are exported.\n- **Proration.** A flat plan is priced `price × billed time / cycle length`,\n in integer minor units rounded half up (exact integer arithmetic; the\n cycle is the period's own month, so February prorates over 28 or 29 days).\n With `prorateFlatPlans` off (default) the billed time is the whole period\n whenever the subscription was active in it and out of trial at its start —\n the pre-#3116 behavior. With it on, the billed time is exactly the time the\n subscription was active and out of trial: a subscription starting, leaving\n trial, or canceled mid-period is billed pro rata. Proration happens before\n the account's flat discount. Split into parts, prorated amounts can differ\n from the full price by a minor unit.\n- **First period.** *Anchor at signup* (set `billingAnchorAt` to the\n signup/activation instant): the first period is a full period from signup;\n nothing before it is billed. *Bill from signup to the next anchor* (keep a\n fixed anchor, or calendar months, and set `prorateFlatPlans`): the first\n period is billed pro rata from signup. Items added later are prorated to the\n payer's anchor when `prorateFlatPlans` is on and billed a full period\n otherwise.\n- **Mid-period changes.** Billing is in arrears, so a cancellation needs no\n credit: with proration the canceled period is billed up to `canceledAt`,\n without it in full. A plan change is billed at the plan in effect at close\n for the whole window (no plan-change proration yet, #3119), and a cancellation\n back-dated into an already billed period is not credited — issue an\n adjustment.\n- **No double billing across schedule changes.** Every flat-plan claim records\n the exact time it bills, and a subscription's claims are numbered, so only\n one worker can write the next claim and two can never claim the same time.\n Moving an account from calendar months to an anchor, moving an anchor, or\n moving a subscription to a payer on another schedule bills only time not yet\n billed, prorated. For example, calendar months through February and then an\n anchor on Mar 20 bills the Mar 1 → Mar 20 stub pro rata, then Mar 20 → Apr\n 20. Time around an already billed window is billed as separate lines, each\n with its own service period. Stop workers running a pre-#3116 version\n before relying on this: they claim flat plans outside the numbering.\n- **Service periods.** Each flat-plan invoice line carries its billed window\n as `periodStart`/`periodEnd`, down to the provider port and on to Stripe's\n invoice item `period`.\n- **Upgrading to #3116.** Additive schema only: `_smrt_billing_accounts` gains\n `billing_anchor_at` (nullable) and `prorate_flat_plans` (default false), and\n `_smrt_billing_line_sources` gains `chain_sequence` (default 0) and an index\n on `(line_key, chain_sequence)`; run `smrt db:migrate`.\n Existing accounts stay on calendar months, and claims written before the\n upgrade (`<subscriptionId>:<periodStart>`, number 0) count as billed\n coverage, so nothing is billed again after it.\n- **Tax** comes from the provider (Stripe Tax) using the account customer's\n `defaultBillingAddress`; the invoice records it as `providerTaxAmount`.\n- **Events** are verified, stored in smrt-jobs' durable delivery inbox, and\n applied against current provider state: paid invoices record a payment and\n allocation; failures and overdue notices mark the payer `past_due` and its\n flat-plan subscriptions `past_due`; an invoice written off at the provider\n (`markInvoiceUncollectible(invoiceId)`, or in Stripe) is written off\n locally and marks the payer `uncollectible` (no bad-debt journal is\n posted; AR stays until it is paid or an operator journals it); payment\n reinstates them, including a late payment of the written-off invoice. Dunning is the provider's.\n Subscribe the webhook endpoint to `invoice.*`, `customer.subscription.*`,\n `checkout.session.completed`, `checkout.session.async_payment_succeeded`,\n and `payment_intent.*`.\n- **Prepaid credit** purchases credit a `period: 'balance'` spending policy\n once per checkout session, paid by the policy's tenant or, for a delegated\n balance, the parent that set it. Amounts are ISO 4217 minor units in any\n currency the provider supports. When the payer's account is taxed\n (`automaticTax`, not tax-exempt; override per call), provider tax is\n charged on top and collected at checkout with the buyer's address: the\n credit is the amount bought, the payment is the total collected, and the\n tax is booked to the tax account. For a payer with a provider customer but\n no local tax location, the address collected at checkout becomes its tax\n location. `savePaymentMethod` requires a provider that can re-read the\n checkout and set a default payment method, and is refused otherwise.\n- **Card on file (#3139).** `createCardSetupCheckout()` (or a credit purchase\n with `savePaymentMethod`) saves the payer's card without charging it. On\n completion the card becomes the provider customer's default and the\n payer's default `PaymentInstrument`. When the checkout collected the billing\n address (a taxed payer with no tax location), it becomes the payer's tax\n location. `cardOnFile(payerTenantId)` returns `{ onFile, savedAt? }` from\n the same check automatic charges use (a default card with the account's\n provider customer), for \"card on file\" displays and signup activation;\n the payer or a system context may call it (#3187).\n- **Automatically charged invoices.** With `autoChargeInvoices: true` on the\n runtime, a payer with a default card is invoiced `charge_automatically`:\n the provider charges the card after the invoice is sent, on its own\n schedule, and retries failures. Each pushed invoice records the method in\n `Invoice.collectionMethod` (`charge_automatically` or `send_invoice`; empty\n for invoices pushed before #3190). Do not offer another payment rail on a\n `charge_automatically` invoice the provider is still collecting.\n Upgrading adds the `collection_method` column to `invoices` (run\n `smrt db:migrate`). Activate service on the `current` standing\n from the `paid` event, not on send; a failure marks the payer `past_due`.\n- **Automatic top-ups.** Taxed accounts (the default: `automaticTax` on, not\n tax-exempt) are charged the credit plus provider-calculated tax (#3194):\n the balance is credited the credit, the payment records the credit, and\n the tax is booked to the tax account. `taxedAccounts: 'skip'` does not top\n them up, and `'charge_untaxed'` charges them without tax. A taxed payer\n without a tax location is not charged. A location Stripe rejects fails the\n attempt with `failureCode: 'customer_tax_location_invalid'`\n (`onAutoTopUpFailed`), and it is retried only after `retryAfterMs`. Pass\n `onAutoTopUpSkipped` to learn when a needed top-up will not be charged\n because of the payer's account. Its `reason` is `taxed_account`, `no_card`,\n `no_account`, or `tax_location_invalid`, and more may be added. Use it, for\n example, to ask the payer to top up by hand or to add a billing address; it is not called while a charge is processing, while a\n declined card waits out `retryAfterMs`, or when the top-up is no longer\n needed or `amount` declines it (#3189).\n `billing.autoTopUpHook()` is the `autoTopUp` hook for\n `SpendingPolicyEvaluator`: when a balance would run out it charges the\n payer's saved card off-session (the delegating parent's, for a delegated\n balance) and credits the balance only when the charge succeeds. A charge\n still processing is credited from its `payment_intent.succeeded` event; a\n decline or an authentication requirement credits nothing, closes the\n attempt, and calls the runtime's `onAutoTopUpFailed` (ask the payer to\n re-save their card with a setup checkout). One attempt runs per policy at a\n time, retries of a declined card wait `retryAfterMs` (default one hour),\n and the credit is granted exactly once however the outcome arrives.\n- **Product tax codes (#3194).** `taxCodes: { credit, invoice }` on the\n runtime classifies prepaid credit (checkout purchases and taxed top-ups)\n and billing-period invoice lines for Stripe Tax (`txcd_...`). Without it,\n the Stripe account's default product tax code applies.\n- **Upgrading to #3194.** Requires `@happyvertical/accounting` 0.96 or later.\n No schema change. Taxed accounts are now topped up with tax. To keep the\n previous behaviour, pass `taxedAccounts: 'skip'`.\n- **Upgrading to #3139.** Requires `@happyvertical/accounting` 0.92 or later.\n No schema change. Credit checkouts are now taxed for taxed accounts (pass\n `automaticTax: false` to keep them untaxed), and Stripe customers are\n created idempotently.\n\n### Payment rails: BTC and other crypto (#3138)\n\nA seller runtime can take payments on more than one **rail**. The issuing\n`provider` (Stripe) still issues, taxes, and emails every invoice; extra rails\nin `paymentProviders` let a payer choose, per payment, to pay an invoice or buy\nprepaid credit another way. The crypto rail runs short-lived, fiat-priced\ncheckouts through `@happyvertical/payments`' provider-neutral\n`CryptoCheckoutGateway` — BTCPay Server at launch — so another gateway can\nreplace BTCPay without changing this package or its consumers.\n\n```ts\nimport {\n BillingRuntime,\n createBtcPayBillingProvider,\n createStripeBillingProvider,\n} from '@happyvertical/smrt-commerce';\n\nconst btcpay = createBtcPayBillingProvider({\n baseUrl, apiKey, storeId, // one BTCPay store per seller entity\n webhookSecret, metadataSecret, // from the host's secret store, by name\n speedPolicy: 'LowMediumSpeed', // settle after 2 confirmations (BTCPay offers 0/1/2/6)\n rateSource: 'kraken',\n minimumAmount: { CAD: 500, USD: 500 },\n});\nconst billing = await BillingRuntime.create({\n db, sellerTenantId, kind: 'provider',\n provider: createStripeBillingProvider({ stripe, webhookSecret }),\n paymentProviders: [btcpay],\n paymentPolicy: { latePaymentPolicy: 'review', refundBasis: 'original_native' },\n billingRelationships,\n ledger: { ...ledger, cryptoHoldingsAccountId, fxGainLossAccountId, feesAccountId },\n});\n\n// Prepaid credit or an issued invoice, paid with BTC:\nawait billing.createCreditCheckout({ ...purchase, provider: 'btcpay' });\nawait billing.createInvoicePayment({ invoiceId, provider: 'btcpay', purchaseId, successUrl, cancelUrl });\n\n// Webhook route for the rail (BTCPay signs with its own header):\nawait billing.acceptWebhook(rawBody, '', { provider: 'btcpay', headers: request.headers });\nawait billing.processEvents();\n```\n\n- **Refusals are typed (#3185).** `createInvoicePayment()` and\n `createCreditCheckout()` throw `BillingPaymentRefusedError` for requests the\n payer can correct, with a stable `code`: `not_found`, `not_payable`,\n `payment_in_progress`, `nothing_due`, `invalid_amount`, `below_minimum`\n (under the rail's `minimumAmount`), or `no_account`. Show its message as a\n 4xx; any other error is a server failure, and `TenantIsolationError` still\n means the caller is not the payer. Treat an unknown code like\n `not_payable`.\n- **Settlement is the gateway's.** Credit is granted and invoices are paid\n only when the gateway reports `settled` under its own confirmation policy;\n this package never counts confirmations. BTCPay offers 0, 1, 2 or 6\n confirmations; the default is `LowMediumSpeed` (2 confirmations).\n- **Payment attempts.** Every rail checkout is a `BillingPaymentAttempt`\n recording the locked fiat price, fiat and native amounts received, rate and\n rate source, each payment's txid, rail and fee, a timeline of status changes,\n and what settlement produced. `listPaymentAttempts()` feeds \"payment\n confirming\" displays and operator queues; every filter applies before its\n `limit` and `offset`, so paging through `{ unresolved: true }` lists every\n open exception (flagged, not yet resolved) however old. `refreshPaymentAttempts()` is the polling\n fallback for missed webhooks.\n- **Dunning pauses** for an invoice while a payment for it is confirming\n (0-conf seen); if the payment then expires or is invalidated the payer's\n standing is re-applied.\n- **Invoices paid on a rail** are closed at the issuer with\n `markInvoicePaidOutOfBand` (Stripe `paid_out_of_band`), so Stripe stops\n collecting; Stripe's own `paid` event then records nothing more.\n `createInvoicePayment` refuses when the issuing provider cannot do that.\n A written-off (uncollectible) invoice with an amount due can be paid on a\n rail too, as it can by card at Stripe (#3188): the payment is recorded,\n Stripe's invoice is closed out of band, and the payer returns to `current`;\n the invoice keeps its `WRITTEN_OFF` status (its `amountPaid` shows the\n recovery).\n- **Exceptions never auto-refund.** Underpaid, overpaid, late, manually marked\n (at the gateway), invalidated-after-settlement, and paid-twice attempts are\n flagged for an operator (`resolvePaymentAttempt`). Per-seller\n `paymentPolicy` sets under/overpayment tolerances (basis points), late\n payments (`review` | `accept`), manually marked checkouts, hold vs convert\n (`conversionHandler`), and the refund basis.\n- **Ledger.** Receipts are booked at the locked fiat amount into\n `cryptoHoldingsAccountId` (default `cashAccountId`); money received above\n the price is booked as customer credit (prepaid-credit liability).\n `recordManualRefund()` refunds from that excess first, then from purchased\n credit (or an invoice payment kept as credit because the invoice was paid\n elsewhere) — capped cumulatively, once per reference; an invoice payment\n applied to its invoice is reversed at the issuer, not here.\n `recordCryptoConversion()` posts a conversion with FX gain/loss.\n- **Upgrading.** Additive schema: `_smrt_billing_payment_attempts` (run\n `smrt db:migrate`). A runtime without `paymentProviders` never reads it.\n- **One live payment per invoice.** The issuer's own `paid` event for an\n invoice a rail closed out of band records nothing (the rail records it); an\n out-of-band close made by someone else (wire, cheque) is recorded as an\n `OTHER`-method payment; a real card payment made while a rail payment\n confirms is recorded and the rail money is flagged `invoice_already_paid`.\n An issuer close whose rail payment never settles is flagged\n `out_of_band_without_settlement`: reopen the invoice at the issuer, then\n `resolvePaymentAttempt()` it, after which a later out-of-band close is\n recorded normally.\n- **Entities.** Each legal entity is its own seller runtime with its own\n store, keys, and ledger. Inter-entity resale is ordinary reseller billing\n between two sellers, payable on any rail the selling entity offers.\n\nSee [`AGENTS.md`](./AGENTS.md#billing-period-close-3060) for invariants and\nknown limits.\n\n## Dependencies\n\n- `@happyvertical/smrt-core` -- ORM and code generation\n- `@happyvertical/smrt-tenancy` -- multi-tenant scoping\n- `@happyvertical/smrt-subscriptions` -- charges, plans, and credit balances billed by period close\n- `@happyvertical/smrt-jobs` -- period-close jobs and the provider event inbox\n- `@happyvertical/accounting` -- payment-provider (Stripe) calls\n- `@happyvertical/smrt-types` -- shared type definitions\n- Peer: `@happyvertical/smrt-ledgers`, `@happyvertical/smrt-profiles`, `@happyvertical/smrt-svelte`\n\n## Contributor guide\n\nSee [`AGENTS.md`](./AGENTS.md) for package architecture, invariants, validation,\nand contributor guidance.\n\n## Pricing version UI\n\n`PricingVersionSummary` and `PricingVersionDecision` compose the quote revision\ncontract without adding a pricing ledger. Compare versions using\n`QuoteRevisionComparison`; the application owns every decision and native form\naction. See [pricing version presentation](agents/pricing-versions.md).\n\n## Commercial UI composition\n\nCustomer addresses and vendor/quote currencies use the shared localized code\nselectors while preserving native field names and rejected raw drafts. See\n[commercial UI contracts and backend boundaries](agents/commercial-ui.md) for\nnative forms, recovery, amount units and consumer adoption, plus the focused\n[customer/vendor](agents/customer-vendor-ui.md) and\n[quotation](agents/quote-ui.md) contracts for selector restrictions.\n\n## Customer and vendor contacts\n\n`Customer.contacts` and `Vendor.contacts` hold the people to reach at a party\n(`PartyContact`: `id`, `name`, `label`, `email`, `phone`, `address`) as a JSON\ncolumn; existing databases need `smrt db:migrate` for the new `contacts`\ncolumns. The form's contact rows are written with `savePartyContacts()`:\n\n```typescript\nimport {\n applyPartyContactIntent,\n PartyContactValidationError,\n readPartyContactRows,\n savePartyContacts,\n} from '@happyvertical/smrt-commerce';\n\nconst form = await request.formData();\n// Throws PartyContactPayloadError when the form's contactCount marker is absent\n// (block not rendered, or fieldNames differ), so a save never deletes by accident.\nconst rows = readPartyContactRows(form); // pass the form's fieldNames if overridden\nconst { contacts, handled } = applyPartyContactIntent(rows, String(form.get('intent')));\nif (handled) return { values: { ...values, contacts } }; // add/remove round trip, no save\ntry {\n await savePartyContacts(vendors, vendorId, rows, { expectedTenantId });\n} catch (error) {\n if (error instanceof PartyContactValidationError)\n return fail(400, { values: { ...values, contacts: rows }, errors: { contacts: toMessages(error.errors) } });\n throw error;\n}\n```\n\n`savePartyContacts` also accepts the `FormData` itself (with `fieldNames`) and\nreads it the same way. The rows replace the party's contacts: posted ids are kept, new rows get an\nid, unposted rows are removed, blank rows are dropped. See\n[customer/vendor UI](agents/customer-vendor-ui.md#contact-rows-3501).\n\n## Customer and vendor selectors\n\n`CustomerSelect` and `VendorSelect` from `@happyvertical/smrt-commerce/svelte`\nare searchable, field-sized pickers for a customer or vendor relation field.\nThey are registered in `COMMERCE_UI_SLOTS` with `selects:\n'@happyvertical/smrt-commerce:Customer'` / `':Vendor'`, so generic forms (via\n`findSelectorFor` over the manifest's `uiSelectors`) use them for any\n`@foreignKey` or `@crossPackageRef` field targeting those models.\n\nThey are presentation only. The caller owns the data: `search(query)` returns\nthe existing party DTOs (`CustomerDisplayData` / `VendorDisplayData`, whose\n`profile.name` is the label), and `resolve(id)` returns the record behind the\ncurrent value. Each row shows the profile name with status (and customer type).\nThe committed id, never the name, posts under `name`. Pass `onCreate` to offer a\n\"New customer\" / \"New vendor\" action; it receives the searched text, and a\nreturned record is selected.\n\n```svelte\n<CustomerSelect\n name=\"customerId\"\n bind:value={customerId}\n search={(query) => customers.search(query)}\n resolve={(id) => customers.byId(id)}\n onCreate={(query) => openCustomerDialog(query)}\n/>\n```\n\nThe same models carry `@field({ ui: { widget } })` hints in the manifest:\n`textarea` on `Contract` (and subclass) `terms` and `notes`, `Customer`/`Vendor`\n`notes`, and `Invoice` notes and terms; `currency` (an ISO 4217 code picker) on\n`Contract`, `Vendor`, `Invoice` and `Payment` `currency`; `email`/`phone` on\n`Vendor.defaultContactEmail`/`defaultContactPhone`. See\n[customer and vendor selectors](agents/customer-vendor-ui.md#selectors-3602).\n\n## Invoice preparation UI\n\n`InvoiceLineEditor` and `InvoiceEditor` from `@happyvertical/smrt-commerce/svelte`\nprovide editable descriptions/SKUs, fractional quantities, unit prices, flat or\npercentage discounts, explicit or inherited tax, customer, dates, currency and\nterms. Calculated previews use the same exact minor-unit arithmetic exported\nfor server handlers from `@happyvertical/smrt-commerce/invoices`.\n`InvoiceAllocationFields` and `InvoiceReview` extend this foundation with\ncaller-owned sources and approvals. See [the editor contract](agents/invoice-preparation.md),\n[calculation rules](agents/invoice-calculations.md), and the required\n[existing-database storage migration](agents/invoice-quantity.md).\n\n## Exact cost billing\n\nThe independent `@happyvertical/smrt-commerce/cost-billing` entry point provides\nexact signed reimbursement/fee calculations, ordered generic markup provenance,\nand explicit line or invoice tax rounding. Its opt-in ordered tax calculator\nsupports subset compounding with caller-selected signed invoice allocation.\nSee [the cost billing contract](agents/cost-billing.md).\nIt does not alter Invoice persistence, editor defaults or settlement behavior.\n\n### Purchase-order header editing\n\nImport `PurchaseOrderHeaderForm` from `@happyvertical/smrt-commerce/svelte` for a\nnative POST of vendor, reference, notes, expected date and ship-to fields. Pass\n`action`, `vendors` and raw `values`; return canonical field `errors` and the same\n`hiddenFields` after server validation. This header-only form leaves purchase\nallocations and totals unchanged. `PurchaseOrder` stores `expectedDate` and\n`shipTo` in existing STI metadata. See [the purchasing contract](agents/purchasing-ui.md#purchase-order-header)\nfor validation, retained values and transport details.\n"
21884
+ "content": "# @happyvertical/smrt-commerce\n\nCommerce models for the s-m-r-t framework. Covers customers, vendors, contracts (10 STI types), invoices with ledger integration, payments, and fulfillment tracking.\n\n## Installation\n\n```bash\npnpm add @happyvertical/smrt-commerce\n```\n\n## Usage\n\n```typescript\nimport { withTenant } from '@happyvertical/smrt-tenancy';\nimport {\n CustomerCollection,\n ContractCollection,\n InvoiceCollection,\n InvoiceLineItemCollection,\n PaymentCollection,\n PaymentMethod,\n ensureCommerceTenant,\n} from '@happyvertical/smrt-commerce';\n\n// Once per tenant, after the host creates it: tax settings and standard codes.\nawait ensureCommerceTenant(db, tenantId);\n\n// Money documents (invoices, contracts, and their lines) belong to a tenant.\nawait withTenant({ tenantId }, async () => {\n // Create a customer linked to a profile\n const customers = await CustomerCollection.create({ db });\n const customer = await customers.create({\n profileId: 'profile-uuid',\n creditLimit: 1000000, // $10,000.00 — money is integer minor units\n paymentTerms: 'Net 30',\n });\n\n // Create an order (STI contract type)\n const contracts = await ContractCollection.create({ db });\n const order = await contracts.create({\n _meta_type: 'Order',\n customerId: customer.id,\n currency: 'CAD',\n });\n\n // Create an invoice. The server resolves each line's tax code and computes\n // line tax and the header totals; header amounts are never inputs.\n const invoices = await InvoiceCollection.create({ db });\n const invoice = await invoices.create({\n customerId: customer.id,\n contractId: order.id,\n invoiceNumber: await invoices.generateInvoiceNumber(),\n currency: 'CAD',\n });\n const lines = await InvoiceLineItemCollection.create({ db });\n await lines.create({\n invoiceId: invoice.id,\n description: 'Consulting',\n quantity: 10,\n unitPrice: 10000, // $100.00\n });\n\n // Issuing freezes the money snapshot; then recognize revenue (creates a\n // balanced journal in smrt-ledgers from the frozen facts).\n const issued = await invoices.get({ id: invoice.id }, { cache: false });\n issued.markSent();\n await issued.save();\n await issued.recognizeRevenue({\n arAccountId: 'ar-account-id',\n revenueAccountId: 'revenue-account-id',\n taxAccountId: 'tax-account-id',\n bookCurrency: 'CAD', // must equal the invoice currency\n });\n\n // Record a payment with ledger integration\n const payments = await PaymentCollection.create({ db });\n const payment = await payments.create({\n contractId: order.id,\n customerId: customer.id,\n amount: issued.totalAmount,\n method: PaymentMethod.CREDIT_CARD,\n });\n await payment.recordPayment({\n ledgerId: 'ledger-id',\n receivablesAccountId: 'ar-account-id',\n cashAccountId: 'bank-account-id',\n });\n});\n```\n\n## Commercial money calculator\n\nThe public `/money` entry is a pure calculator for line amounts, discounts,\ncomponent tax, document rounding, and totals. It consumes and returns integer\nminor-unit money values; tax and discount percentages are exact text (`'5'`\nmeans 5%). It does not persist or load documents.\n\n```ts\nimport { calculateMoneyDocument } from '@happyvertical/smrt-commerce/money';\n\nconst totals = calculateMoneyDocument({\n currency: 'CAD',\n lineAmountType: 'exclusive',\n roundingScope: 'line',\n lines: [{\n id: 'consulting', quantity: 1, unitPrice: 10000,\n discount: { kind: 'none' },\n taxCode: {\n id: 'gst', kind: 'taxable',\n components: [{ taxComponentId: 'gst', percent: '5', compound: false }],\n },\n }],\n});\n```\n\n`resolveLineTaxCode()` resolves caller-loaded codes without I/O, and the\nSvelte `InvoiceEditor` renders the money controls from its required `money`\nprop. Frozen editor documents render a supplied stored snapshot without\nrecalculation. See the [commerce money calculator guide](../../docs/content/commerce-money.md)\nfor pricing modes, rounding, input and result types, the native-form editor\ncontract, and the boundaries this package does not define.\n\n### Tenant tax catalogue and defaults\n\nThe package root also exports tenant-scoped `TaxComponent`, `TaxCode`, and\n`TaxSettings` models and their collections. Components hold exact percentage\nrates and optional liability accounts; codes combine compatible components; the\none settings row per tenant supplies sales/purchase defaults, amount mode,\nrounding scope, and labelled registration numbers. A referenced component rate\nis immutable: archive it and create a replacement for a rate change.\n\n`ensureTenantTaxCodes(db, tenantId)` idempotently creates the standard\nnon-taxable codes, `exempt`, `zero-rated`, and `out-of-scope`\n(`STANDARD_TAX_CODES`). `applyTaxCodeExamplePack(db, tenantId)` installs illustrative\nregional examples for review, and `applyCommerceCookbookSettings(db, tenantId,\ncookbook)` explicitly materializes a validated cookbook's defaults after schema\nsetup and authentication establish the tenant. See the [commerce money\ncalculator guide](../../docs/content/commerce-money.md#tenant-tax-catalogue) for\nthe supported kinds, seed behavior, and cookbook rate units.\n\n### Persisted Invoice money\n\nCommerce has one money model. Tax codes resolve each line's tax: the line's\noverride, else `exempt` for an exempt customer, else the product's code, the\ncustomer's code, then the tenant default. The server computes line\n`taxAmount` and the header totals and ignores any client total. Every Invoice\nand Contract money write needs a tenant; without one it fails with\n`MoneyValidationError` `INVALID_MONEY_INPUT`.\n`ensureCommerceTenant(db, tenantId, { cookbook })` is the one bootstrap call a\nhost makes after creating a tenant. It seeds the tenant's tax settings, the\nstandard codes, and the cookbook's defaults, and returns `{ tenantId }`.\n\nIssuing an Invoice (leaving `DRAFT`) or assigning `arJournalId` freezes its\nmoney snapshot. Header and line money changes then fail with `FROZEN_DOCUMENT`,\nwhile later lifecycle and payment updates retain the snapshot. Provider tax\nstays separate in `providerTaxAmount` and contributes to the Invoice headers\nwithout creating an application tax code.\n`InvoiceLineItemCollection.getTotalForInvoice()` sums line amounts for\ncompatibility; use persisted `Invoice.totalAmount` for the amount due.\n`recognizeRevenue()` and `cancelWithRevenueReversal()` require a `bookCurrency`\nequal to the invoice currency, and `toAccountingInput({ mapping })` requires\nexplicit QuickBooks realm mapping. The\n[commerce money calculator guide](../../docs/content/commerce-money.md#persisted-invoice-money)\nand [document-money contract](agents/document-money.md) cover persisted Invoice\nsemantics, credits, refunds, and posting. The materialized Invoice tax-report\ndescriptor and tenant-safe drilldown are documented in\n[tax-summary contract](agents/tax-summary.md). Contract money persistence,\nPurchaseContract tax contributors, and the materialized tax-summary host and\ndrilldown are available. Applications opt in by registering\n`createTaxSummaryMaterializedReportHost()` in their server-only report-host\nregistry; the route binds the authenticated tenant and read permission. See the\ntax-summary contract for registration details.\n\n### Upgrading money data from an earlier version\n\nCommerce refuses money data written by an earlier version with\n`COMMERCE_DATA_NEEDS_UPGRADE` instead of reading it. That is a schema that\nstill has the earlier money columns or the `commerce_money_migration_documents` table,\nan invoice or contract without a tenant, or a tenant with invoices or contracts but no tax settings row or with\ndocuments in the earlier format. `smrt db:status`\nreports it, so `smrt app doctor` reports `migration-required`. Run the normal\nschema migration, then upgrade once:\n\n```sh\nsmrt db:upgrade-commerce-money --check # report only\nsmrt db:upgrade-commerce-money # every tenant\n```\n\nEach tenant converts in its own transaction. Stored rates become tax codes,\nissued documents keep every stored total, and drafts are recomputed. The\nearlier columns are dropped once no tenant needs the upgrade. See\n[money data upgrade](agents/money-upgrade.md) and the\n[CLI guide](../cli/agents/commerce-money-upgrade.md).\n\n## API\n\n### Models\n\n#### Supplier quotations\n\n`SupplierQuote` extends `PurchaseContract`, requires `vendorId`, and has\n`ContractType.SUPPLIER_QUOTE`. Create it through `PurchaseContractCollection`\n(or `ContractCollection`) with `_meta_type:\n'@happyvertical/smrt-commerce:SupplierQuote'`. It inherits `expiryDate`,\n`reference`, `notes`, and `terms`; it has no customer relationship. Purchase\nbranch reads include quotes and sales branch reads exclude them.\n\nKeep received quote revisions and shared quote identity in your application.\nAwarding a quote creates a separate `PurchaseOrder`; accepting or saving a\nquote never converts its identity into an order. Quotes use purchase-side\nmoney calculation and frozen snapshots, but never contribute incurred purchase\ntax to reports, regardless of status. Existing Estimate and PurchaseOrder\nreport eligibility is unchanged.\n\n#### Upgrading contract parties (#3898)\n\n`Contract` no longer declares `customerId` or `vendorId`. Narrow a contract to\n`SalesContract` to read `customerId`, or to `PurchaseContract` to read `vendorId`.\nThe existing leaf qualified names and the `contracts` table stay the same; a\n`db:migrate --dry-run` against a v0.55.19 schema reports no DDL changes on\nSQLite or PostgreSQL. Sales leaves no longer accept `vendorId`; purchase leaves\nno longer accept `customerId`. `Order`, `Estimate`, `Agreement`, `Lease`, and\n`WholesaleOrder` require a customer on save. `PurchaseOrder` and `SupplierQuote`\nrequire a vendor.\n`Cart`, `LicenseSale`, and `ProductionOrder` retain optional parties on their side.\n\nBefore upgrading existing data, run the read-only\n`preflightCommercePartyLinks(db)` and inspect its stray party links, missing\nrequired parties, base-typed rows, discriminator mismatches, and invalid\ndocument references. Its `unqualifiedDiscriminatorRows` lists legacy simple\n`_meta_type` values such as `Order` and `PurchaseOrder`; retype them to the\nqualified class name before upgrading, since branch collections cannot read\nthem. The report checks invoice, payment, fulfillment, ad group,\nand expense links when those tables exist. A linked base-typed or wrong-branch\ncontract blocks the next save of its document even when the link is unchanged.\nRegistered custom descendants of either contract branch are accepted in link\nchecks; load those model classes before running the preflight.\nTenant-owned documents linked to global or other-tenant contracts are also\nreported because the save-time lookup is tenant-scoped. Retype the contract to\nthe correct leaf, assign it to the document's tenant, or repair the link before\ndeployment. The deprecated `ContractCollection` party finders now return the\nappropriate branch\nonly, so retype legacy base rows with party links before relying on those\nfinders. Clear stray party columns and fill required parties before deploying\nwriters that save affected rows. Missing required parties remain readable but\nfail their next save. Regenerate cached manifests, planner catalogs, and\ngenerated screens after upgrading.\nThis preflight never edits rows; an operator chooses the data correction.\n\n| Export | Description |\n|--------|------------|\n| `Customer` | Customer record with creditLimit, paymentTerms, tax exemption, addresses, contacts |\n| `Vendor` | Vendor/supplier with leadTimeDays, minimumOrderAmount, currency, contacts |\n| `Contract` | STI base class for all commercial agreements |\n| `SalesContract` | Abstract customer-side Contract branch; owns `customerId` |\n| `PurchaseContract` | Abstract supplier-side Contract branch; owns `vendorId` |\n| `Estimate` | Quote or proposal sent to a customer |\n| `Order` | Customer purchase order |\n| `Lease` | Rental or lease agreement |\n| `Agreement` | Service or maintenance agreement |\n| `SupplierQuote` | Quotation received from a vendor; distinct from an awarded purchase order |\n| `PurchaseOrder` | Order sent to a vendor/supplier |\n| `ContractLineItem` | Line item within a contract |\n| `Invoice` | Billing document with status lifecycle and ledger integration |\n| `InvoiceLineItem` | Individual invoice line item |\n| `Payment` | Payment record with method, status, and optional ledger journal |\n| `PaymentAllocation` | Payment-to-invoice allocation |\n| `Fulfillment` | Shipment/delivery tracking with carrier and address |\n| `FulfillmentLineItem` | Individual fulfillment line item |\n| `BillingAccount` | A payer's account with a seller: customer, provider customer, terms, standing |\n| `BillingPeriodClose` | One payer's invoice for one period and currency; also the billing job target |\n| `BillingLineSource` | The claim that a charge or plan period is billed on exactly one close |\n\n### Collections\n\n`CustomerCollection`, `VendorCollection`, `ContractCollection`, `SalesContractCollection`, `PurchaseContractCollection`, `InvoiceCollection`, `InvoiceLineItemCollection`, `PaymentCollection`, `PaymentAllocationCollection`, `FulfillmentCollection`\n\n`PaymentAllocationCollection.allocatePayment()` splits a payment across invoices\nin one transaction using stable allocation IDs for retries. Direct allocation\nsaves also enforce matching owners, customers and currencies and both balance\ncaps. Read [payment allocation integrity](agents/payment-allocations.md) for\nreplay, parent revision refresh and compatibility requirements.\n\n\n### Enums\n\n| Export | Values |\n|--------|--------|\n| `ContractType` | `estimate`, `order`, `lease`, `agreement`, `supplier_quote`, `purchase_order`, `wholesale_order`, `production_order`, `cart`, `license_sale` |\n| `ContractStatus` | `draft`, `sent`, `accepted`, `declined`, `completed`, `cancelled` |\n| `CustomerStatus` | `active`, `inactive`, `suspended` |\n| `VendorStatus` | `active`, `inactive`, `suspended` |\n| `InvoiceStatus` | `draft`, `sent`, `viewed`, `partial`, `paid`, `overdue`, `cancelled`, `written_off` |\n| `PaymentMethod` | `cash`, `check`, `credit_card`, `bank_transfer`, `crypto`, `other` |\n| `PaymentStatus` | `pending`, `completed`, `failed`, `refunded`, `cancelled` |\n| `FulfillmentStatus` | `pending`, `processing`, `shipped`, `delivered`, `cancelled` |\n| `FulfillmentType` | `shipment`, `delivery`, `pickup`, `digital`, `service` |\n\n### Types and Constants\n\n| Export | Description |\n|--------|------------|\n| `Address` | Shared address interface (street1, street2, city, state, postalCode, country) |\n| `RecognizeRevenueOptions` | Account IDs for invoice revenue recognition |\n| `RecordPaymentOptions` | Ledger and account IDs for payment recording |\n| `InvoiceNumberOptions` | Options for `generateInvoiceNumber()` (prefix, format) |\n| `UNPAID_STATUSES` | Array of invoice statuses considered unpaid (sent, viewed, partial, overdue) |\n| `COMMERCE_MODULE_META` | UI module metadata |\n| `COMMERCE_UI_SLOTS` | UI slot definitions |\n\n### Ledger Integration\n\nInvoice and Payment integrate with `@happyvertical/smrt-ledgers` via dynamic import (optional dependency). Invoice stores `arJournalId` and `revenueJournalId` as plain string references to ledger journals. `recognizeRevenue()` creates a balanced AR entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable). `recordPayment()` creates a balanced cash receipt entry (DR: Cash, CR: Accounts Receivable). Both methods return null or throw if the ledgers package is not installed.\n\n`Payment.recordPayment()` claims the payment revision and commits the journal,\nentries, and completed payment in one database transaction. A failed completion\nrolls back the posting and restores the instance's settlement fields for retry.\nThe claimed persisted status is authoritative even if a caller locally resets\nstatus or marks the instance failed without saving. Concurrent or stale instances\nfail instead of posting twice; reload the payment\nand retry only if it is still pending. An already completed payment retains the\n`Payment already recorded` error. A caller's enclosing transaction remains the\nowner: if it rolls back after settlement returns, reload the payment before retry.\nThese guarantees cover new postings, not orphaned journals written by older\nversions. The returned journal uses the caller's database executor.\n\n### Cross-Package References\n\nCustomer and Vendor link to `@happyvertical/smrt-profiles` via plain `profileId` string. Invoice and Payment reference `@happyvertical/smrt-ledgers` journals via plain string IDs (`arJournalId`, `revenueJournalId`, `journalId`). All models use `@TenantScoped({ mode: 'optional' })` with nullable `tenantId`.\n\n## Billing-period close\n\n`BillingRuntime` turns approved `@happyvertical/smrt-subscriptions` charges into\ninvoices for each billing owner, pushes them to a payment provider with\nprovider-calculated tax, posts revenue to the ledger, and applies the\nprovider's webhook events. Every provider call goes through\n`@happyvertical/accounting`; `createStripeBillingProvider()` adapts its Stripe\nprovider to the `BillingProvider` port. Money crossing the port is integer\nminor units.\n\n```ts\nimport { getAccountingProvider } from '@happyvertical/accounting';\nimport { SpendingPolicyEvaluator } from '@happyvertical/smrt-subscriptions';\nimport {\n BillingRuntime,\n createStripeBillingProvider,\n enqueueBillingPeriodClose,\n registerBillingRuntime,\n} from '@happyvertical/smrt-commerce';\n\nconst stripe = await getAccountingProvider({ type: 'stripe', secretKey });\nconst billing = await BillingRuntime.create({\n db,\n sellerTenantId: platformTenantId,\n kind: 'provider', // or 'reseller' to bill a reseller's children\n provider: createStripeBillingProvider({ stripe, webhookSecret }),\n billingRelationships, // smrt-tenancy BillingRelationshipService\n ledger: {\n bookCurrency: 'USD', // ISO 4217; must equal the billing currency\n arAccountId,\n revenueAccountId,\n taxAccountId,\n cashAccountId,\n prepaidCreditAccountId,\n },\n onPayerStanding: async ({ payerTenantId, standing, db }) => {\n // suspend or reinstate the payer; runs in the event transaction\n },\n});\n\n// One account per payer: its customer, tax location, and terms.\nawait billing.upsertAccount({\n payerTenantId,\n name: 'Network Co',\n email: 'billing@example.test',\n billingAddress: { country: 'CA', postalCode: 'T0L 0A0' },\n});\n\n// Optional: bill this payer from its activation date instead of calendar\n// months, prorating flat plans added mid-period (see \"Billing cycles\").\nawait billing.upsertAccount({\n payerTenantId,\n name: 'Network Co',\n billingAnchorAt: activatedAt,\n prorateFlatPlans: true,\n});\n\n// Close each payer's last ended period — safe to run on every schedule tick.\nregisterBillingRuntime('platform', billing);\nawait enqueueBillingPeriodClose({ runtime: 'platform' });\n\n// Webhook route: verify, enqueue, respond 2xx.\nconst intake = await billing.acceptWebhook(\n rawBody,\n request.headers.get('stripe-signature'),\n);\nif (intake.type?.endsWith(':unverified_credit_purchase')) {\n // alert: a paid checkout claimed to buy credit but failed verification\n}\nawait billing.processEvents(); // or enqueueBillingEvents({ runtime: 'platform' })\n\n// Prepaid credit through provider checkout: tax is added on top when the\n// payer's account is taxed; `savePaymentMethod` keeps the card for top-ups.\nconst { url } = await billing.createCreditCheckout({\n spendingPolicyId,\n amount: 5000,\n purchaseId: cartId,\n savePaymentMethod: true,\n successUrl,\n cancelUrl,\n});\n\n// Card on file at signup: a setup checkout that charges nothing. Its\n// completion event makes the card the payer's default and adopts the billing\n// address collected at checkout as the tax location.\nconst setup = await billing.createCardSetupCheckout({\n payerTenantId,\n currency: 'CAD',\n setupId: signupId,\n successUrl,\n cancelUrl,\n});\n\n// Automatic top-ups: charge the saved card when a balance would run out.\n// Accounts are taxed by default (`automaticTax`); a taxed account is charged\n// the credit plus provider-calculated tax (#3194).\nconst evaluator = await SpendingPolicyEvaluator.create({\n db,\n autoTopUp: billing.autoTopUpHook({ amount: () => 2500 }),\n});\n```\n\n- **What is billed.** A `provider` runtime bills `ClientCharge`s (and their\n `BillingAdjustment`s) to the payer each charge names, plus monthly flat plans\n it owns, to each subscriber's billing owner. A `reseller` runtime bills the\n `RetailCharge`s its children owe it plus its own flat plans. Charges approved\n before the period end are billed once, whatever period that is; flat plans\n are billed in arrears for the payer's closed period (trials and\n provider-managed subscriptions are skipped). One invoice per payer and\n currency carries both kinds of lines.\n- **Replay safety.** Close, invoice, and line ids are derived from the payer,\n currency, and period; each charge is claimed by exactly one close; the\n provider invoice uses the close id as its idempotency key; and every step is\n persisted before the next, so a retry resumes where it stopped. A lease stops\n two workers advancing the same close. A close without a period also resumes\n every unfinished close whose period has ended, even one the payer's current\n schedule no longer produces (#3116). A payer whose charges net to a credit\n gets no invoice; the credit is carried to its next invoice.\n- **Billing cycles (#3116).** A payer's periods follow its account's\n `billingAnchorAt`. Unset (the default) they are UTC calendar months. Set,\n they run monthly from the anchor at its UTC time of day; a day the month\n lacks is clamped to its last day, always from the anchor itself (an anchor\n on Jan 31 bills Jan 31 → Feb 28 → Mar 31). Before the anchor the schedule is\n calendar months, and the anchor's own month ends at the anchor (a *stub*).\n `closePeriod()` / `enqueueBillingPeriodClose()` with no period close each\n payer's **last ended period on its own schedule**, so payers on different\n anchors are closed by the same daily tick. An explicit\n `{ periodStart, periodEnd }` closes that period for calendar payers (flat\n plans only when it is a calendar month, as before) and for anchored payers\n whose schedule has exactly that period. `billingPeriodFor(payer, at)` returns\n a payer's current period; `billingPeriodContaining()`,\n `lastEndedBillingPeriod()`, and `prorateMinorUnits()` are exported.\n- **Proration.** A flat plan is priced `price × billed time / cycle length`,\n in integer minor units rounded half up (exact integer arithmetic; the\n cycle is the period's own month, so February prorates over 28 or 29 days).\n With `prorateFlatPlans` off (default) the billed time is the whole period\n whenever the subscription was active in it and out of trial at its start —\n the pre-#3116 behavior. With it on, the billed time is exactly the time the\n subscription was active and out of trial: a subscription starting, leaving\n trial, or canceled mid-period is billed pro rata. Proration happens before\n the account's flat discount. Split into parts, prorated amounts can differ\n from the full price by a minor unit.\n- **First period.** *Anchor at signup* (set `billingAnchorAt` to the\n signup/activation instant): the first period is a full period from signup;\n nothing before it is billed. *Bill from signup to the next anchor* (keep a\n fixed anchor, or calendar months, and set `prorateFlatPlans`): the first\n period is billed pro rata from signup. Items added later are prorated to the\n payer's anchor when `prorateFlatPlans` is on and billed a full period\n otherwise.\n- **Mid-period changes.** Billing is in arrears, so a cancellation needs no\n credit: with proration the canceled period is billed up to `canceledAt`,\n without it in full. A plan change is billed at the plan in effect at close\n for the whole window (no plan-change proration yet, #3119), and a cancellation\n back-dated into an already billed period is not credited — issue an\n adjustment.\n- **No double billing across schedule changes.** Every flat-plan claim records\n the exact time it bills, and a subscription's claims are numbered, so only\n one worker can write the next claim and two can never claim the same time.\n Moving an account from calendar months to an anchor, moving an anchor, or\n moving a subscription to a payer on another schedule bills only time not yet\n billed, prorated. For example, calendar months through February and then an\n anchor on Mar 20 bills the Mar 1 → Mar 20 stub pro rata, then Mar 20 → Apr\n 20. Time around an already billed window is billed as separate lines, each\n with its own service period. Stop workers running a pre-#3116 version\n before relying on this: they claim flat plans outside the numbering.\n- **Service periods.** Each flat-plan invoice line carries its billed window\n as `periodStart`/`periodEnd`, down to the provider port and on to Stripe's\n invoice item `period`.\n- **Upgrading to #3116.** Additive schema only: `_smrt_billing_accounts` gains\n `billing_anchor_at` (nullable) and `prorate_flat_plans` (default false), and\n `_smrt_billing_line_sources` gains `chain_sequence` (default 0) and an index\n on `(line_key, chain_sequence)`; run `smrt db:migrate`.\n Existing accounts stay on calendar months, and claims written before the\n upgrade (`<subscriptionId>:<periodStart>`, number 0) count as billed\n coverage, so nothing is billed again after it.\n- **Tax** comes from the provider (Stripe Tax) using the account customer's\n `defaultBillingAddress`; the invoice records it as `providerTaxAmount` in the\n save that issues it. Billing invoices are ordinary money-model invoices in\n the seller's tenant; period close seeds its standard codes if needed. Every\n billing line is an explicit override to the seller's `out-of-scope` code, so\n the seller's default code never taxes it a second time. Charges are written\n before credits, so the running subtotal never goes negative. Revenue posting\n uses `ledger.bookCurrency` and is idempotent.\n- **Events** are verified, stored in smrt-jobs' durable delivery inbox, and\n applied against current provider state: paid invoices record a payment and\n allocation; failures and overdue notices mark the payer `past_due` and its\n flat-plan subscriptions `past_due`; an invoice written off at the provider\n (`markInvoiceUncollectible(invoiceId)`, or in Stripe) is written off\n locally and marks the payer `uncollectible` (no bad-debt journal is\n posted; AR stays until it is paid or an operator journals it); payment\n reinstates them, including a late payment of the written-off invoice. Dunning is the provider's.\n Subscribe the webhook endpoint to `invoice.*`, `customer.subscription.*`,\n `checkout.session.completed`, `checkout.session.async_payment_succeeded`,\n and `payment_intent.*`.\n- **Prepaid credit** purchases credit a `period: 'balance'` spending policy\n once per checkout session, paid by the policy's tenant or, for a delegated\n balance, the parent that set it. Amounts are ISO 4217 minor units in any\n currency the provider supports. When the payer's account is taxed\n (`automaticTax`, not tax-exempt; override per call), provider tax is\n charged on top and collected at checkout with the buyer's address: the\n credit is the amount bought, the payment is the total collected, and the\n tax is booked to the tax account. For a payer with a provider customer but\n no local tax location, the address collected at checkout becomes its tax\n location. `savePaymentMethod` requires a provider that can re-read the\n checkout and set a default payment method, and is refused otherwise.\n- **Card on file (#3139).** `createCardSetupCheckout()` (or a credit purchase\n with `savePaymentMethod`) saves the payer's card without charging it. On\n completion the card becomes the provider customer's default and the\n payer's default `PaymentInstrument`. When the checkout collected the billing\n address (a taxed payer with no tax location), it becomes the payer's tax\n location. `cardOnFile(payerTenantId)` returns `{ onFile, savedAt? }` from\n the same check automatic charges use (a default card with the account's\n provider customer), for \"card on file\" displays and signup activation;\n the payer or a system context may call it (#3187).\n- **Automatically charged invoices.** With `autoChargeInvoices: true` on the\n runtime, a payer with a default card is invoiced `charge_automatically`:\n the provider charges the card after the invoice is sent, on its own\n schedule, and retries failures. Each pushed invoice records the method in\n `Invoice.collectionMethod` (`charge_automatically` or `send_invoice`; empty\n for invoices pushed before #3190). Do not offer another payment rail on a\n `charge_automatically` invoice the provider is still collecting.\n Upgrading adds the `collection_method` column to `invoices` (run\n `smrt db:migrate`). Activate service on the `current` standing\n from the `paid` event, not on send; a failure marks the payer `past_due`.\n- **Automatic top-ups.** Taxed accounts (the default: `automaticTax` on, not\n tax-exempt) are charged the credit plus provider-calculated tax (#3194):\n the balance is credited the credit, the payment records the credit, and\n the tax is booked to the tax account. `taxedAccounts: 'skip'` does not top\n them up, and `'charge_untaxed'` charges them without tax. A taxed payer\n without a tax location is not charged. A location Stripe rejects fails the\n attempt with `failureCode: 'customer_tax_location_invalid'`\n (`onAutoTopUpFailed`), and it is retried only after `retryAfterMs`. Pass\n `onAutoTopUpSkipped` to learn when a needed top-up will not be charged\n because of the payer's account. Its `reason` is `taxed_account`, `no_card`,\n `no_account`, or `tax_location_invalid`, and more may be added. Use it, for\n example, to ask the payer to top up by hand or to add a billing address; it is not called while a charge is processing, while a\n declined card waits out `retryAfterMs`, or when the top-up is no longer\n needed or `amount` declines it (#3189).\n `billing.autoTopUpHook()` is the `autoTopUp` hook for\n `SpendingPolicyEvaluator`: when a balance would run out it charges the\n payer's saved card off-session (the delegating parent's, for a delegated\n balance) and credits the balance only when the charge succeeds. A charge\n still processing is credited from its `payment_intent.succeeded` event; a\n decline or an authentication requirement credits nothing, closes the\n attempt, and calls the runtime's `onAutoTopUpFailed` (ask the payer to\n re-save their card with a setup checkout). One attempt runs per policy at a\n time, retries of a declined card wait `retryAfterMs` (default one hour),\n and the credit is granted exactly once however the outcome arrives.\n- **Product tax codes (#3194).** `taxCodes: { credit, invoice }` on the\n runtime classifies prepaid credit (checkout purchases and taxed top-ups)\n and billing-period invoice lines for Stripe Tax (`txcd_...`). Without it,\n the Stripe account's default product tax code applies.\n- **Upgrading to #3194.** Requires `@happyvertical/accounting` 0.96 or later.\n No schema change. Taxed accounts are now topped up with tax. To keep the\n previous behaviour, pass `taxedAccounts: 'skip'`.\n- **Upgrading to #3139.** Requires `@happyvertical/accounting` 0.92 or later.\n No schema change. Credit checkouts are now taxed for taxed accounts (pass\n `automaticTax: false` to keep them untaxed), and Stripe customers are\n created idempotently.\n\n### Payment rails: BTC and other crypto (#3138)\n\nA seller runtime can take payments on more than one **rail**. The issuing\n`provider` (Stripe) still issues, taxes, and emails every invoice; extra rails\nin `paymentProviders` let a payer choose, per payment, to pay an invoice or buy\nprepaid credit another way. The crypto rail runs short-lived, fiat-priced\ncheckouts through `@happyvertical/payments`' provider-neutral\n`CryptoCheckoutGateway` — BTCPay Server at launch — so another gateway can\nreplace BTCPay without changing this package or its consumers.\n\n```ts\nimport {\n BillingRuntime,\n createBtcPayBillingProvider,\n createStripeBillingProvider,\n} from '@happyvertical/smrt-commerce';\n\nconst btcpay = createBtcPayBillingProvider({\n baseUrl, apiKey, storeId, // one BTCPay store per seller entity\n webhookSecret, metadataSecret, // from the host's secret store, by name\n speedPolicy: 'LowMediumSpeed', // settle after 2 confirmations (BTCPay offers 0/1/2/6)\n rateSource: 'kraken',\n minimumAmount: { CAD: 500, USD: 500 },\n});\nconst billing = await BillingRuntime.create({\n db, sellerTenantId, kind: 'provider',\n provider: createStripeBillingProvider({ stripe, webhookSecret }),\n paymentProviders: [btcpay],\n paymentPolicy: { latePaymentPolicy: 'review', refundBasis: 'original_native' },\n billingRelationships,\n ledger: { ...ledger, cryptoHoldingsAccountId, fxGainLossAccountId, feesAccountId },\n});\n\n// Prepaid credit or an issued invoice, paid with BTC:\nawait billing.createCreditCheckout({ ...purchase, provider: 'btcpay' });\nawait billing.createInvoicePayment({ invoiceId, provider: 'btcpay', purchaseId, successUrl, cancelUrl });\n\n// Webhook route for the rail (BTCPay signs with its own header):\nawait billing.acceptWebhook(rawBody, '', { provider: 'btcpay', headers: request.headers });\nawait billing.processEvents();\n```\n\n- **Refusals are typed (#3185).** `createInvoicePayment()` and\n `createCreditCheckout()` throw `BillingPaymentRefusedError` for requests the\n payer can correct, with a stable `code`: `not_found`, `not_payable`,\n `payment_in_progress`, `nothing_due`, `invalid_amount`, `below_minimum`\n (under the rail's `minimumAmount`), or `no_account`. Show its message as a\n 4xx; any other error is a server failure, and `TenantIsolationError` still\n means the caller is not the payer. Treat an unknown code like\n `not_payable`.\n- **Settlement is the gateway's.** Credit is granted and invoices are paid\n only when the gateway reports `settled` under its own confirmation policy;\n this package never counts confirmations. BTCPay offers 0, 1, 2 or 6\n confirmations; the default is `LowMediumSpeed` (2 confirmations).\n- **Payment attempts.** Every rail checkout is a `BillingPaymentAttempt`\n recording the locked fiat price, fiat and native amounts received, rate and\n rate source, each payment's txid, rail and fee, a timeline of status changes,\n and what settlement produced. `listPaymentAttempts()` feeds \"payment\n confirming\" displays and operator queues; every filter applies before its\n `limit` and `offset`, so paging through `{ unresolved: true }` lists every\n open exception (flagged, not yet resolved) however old. `refreshPaymentAttempts()` is the polling\n fallback for missed webhooks.\n- **Dunning pauses** for an invoice while a payment for it is confirming\n (0-conf seen); if the payment then expires or is invalidated the payer's\n standing is re-applied.\n- **Invoices paid on a rail** are closed at the issuer with\n `markInvoicePaidOutOfBand` (Stripe `paid_out_of_band`), so Stripe stops\n collecting; Stripe's own `paid` event then records nothing more.\n `createInvoicePayment` refuses when the issuing provider cannot do that.\n A written-off (uncollectible) invoice with an amount due can be paid on a\n rail too, as it can by card at Stripe (#3188): the payment is recorded,\n Stripe's invoice is closed out of band, and the payer returns to `current`;\n the invoice keeps its `WRITTEN_OFF` status (its `amountPaid` shows the\n recovery).\n- **Exceptions never auto-refund.** Underpaid, overpaid, late, manually marked\n (at the gateway), invalidated-after-settlement, and paid-twice attempts are\n flagged for an operator (`resolvePaymentAttempt`). Per-seller\n `paymentPolicy` sets under/overpayment tolerances (basis points), late\n payments (`review` | `accept`), manually marked checkouts, hold vs convert\n (`conversionHandler`), and the refund basis.\n- **Ledger.** Receipts are booked at the locked fiat amount into\n `cryptoHoldingsAccountId` (default `cashAccountId`); money received above\n the price is booked as customer credit (prepaid-credit liability).\n `recordManualRefund()` refunds from that excess first, then from purchased\n credit (or an invoice payment kept as credit because the invoice was paid\n elsewhere) — capped cumulatively, once per reference; an invoice payment\n applied to its invoice is reversed at the issuer, not here.\n `recordCryptoConversion()` posts a conversion with FX gain/loss.\n- **Upgrading.** Additive schema: `_smrt_billing_payment_attempts` (run\n `smrt db:migrate`). A runtime without `paymentProviders` never reads it.\n- **One live payment per invoice.** The issuer's own `paid` event for an\n invoice a rail closed out of band records nothing (the rail records it); an\n out-of-band close made by someone else (wire, cheque) is recorded as an\n `OTHER`-method payment; a real card payment made while a rail payment\n confirms is recorded and the rail money is flagged `invoice_already_paid`.\n An issuer close whose rail payment never settles is flagged\n `out_of_band_without_settlement`: reopen the invoice at the issuer, then\n `resolvePaymentAttempt()` it, after which a later out-of-band close is\n recorded normally.\n- **Entities.** Each legal entity is its own seller runtime with its own\n store, keys, and ledger. Inter-entity resale is ordinary reseller billing\n between two sellers, payable on any rail the selling entity offers.\n\nSee [`AGENTS.md`](./AGENTS.md#billing-period-close-3060) for invariants and\nknown limits.\n\n## Dependencies\n\n- `@happyvertical/smrt-core` -- ORM and code generation\n- `@happyvertical/smrt-tenancy` -- multi-tenant scoping\n- `@happyvertical/smrt-subscriptions` -- charges, plans, and credit balances billed by period close\n- `@happyvertical/smrt-jobs` -- period-close jobs and the provider event inbox\n- `@happyvertical/accounting` -- payment-provider (Stripe) calls\n- `@happyvertical/smrt-types` -- shared type definitions\n- Peer: `@happyvertical/smrt-ledgers`, `@happyvertical/smrt-profiles`, `@happyvertical/smrt-svelte`\n\n## Contributor guide\n\nSee [`AGENTS.md`](./AGENTS.md) for package architecture, invariants, validation,\nand contributor guidance.\n\n## Pricing version UI\n\n`PricingVersionSummary` and `PricingVersionDecision` compose the quote revision\ncontract without adding a pricing ledger. Compare versions using\n`QuoteRevisionComparison`; the application owns every decision and native form\naction. See [pricing version presentation](agents/pricing-versions.md).\n\n## Commercial UI composition\n\nCustomer addresses and vendor/quote currencies use the shared localized code\nselectors while preserving native field names and rejected raw drafts. See\n[commercial UI contracts and backend boundaries](agents/commercial-ui.md) for\nnative forms, recovery, amount units and consumer adoption, plus the focused\n[customer/vendor](agents/customer-vendor-ui.md) and\n[quotation](agents/quote-ui.md) contracts for selector restrictions.\n\n## Customer and vendor contacts\n\n`Customer.contacts` and `Vendor.contacts` hold the people to reach at a party\n(`PartyContact`: `id`, `name`, `label`, `email`, `phone`, `address`) as a JSON\ncolumn; existing databases need `smrt db:migrate` for the new `contacts`\ncolumns. The form's contact rows are written with `savePartyContacts()`:\n\n```typescript\nimport {\n applyPartyContactIntent,\n PartyContactValidationError,\n readPartyContactRows,\n savePartyContacts,\n} from '@happyvertical/smrt-commerce';\n\nconst form = await request.formData();\n// Throws PartyContactPayloadError when the form's contactCount marker is absent\n// (block not rendered, or fieldNames differ), so a save never deletes by accident.\nconst rows = readPartyContactRows(form); // pass the form's fieldNames if overridden\nconst { contacts, handled } = applyPartyContactIntent(rows, String(form.get('intent')));\nif (handled) return { values: { ...values, contacts } }; // add/remove round trip, no save\ntry {\n await savePartyContacts(vendors, vendorId, rows, { expectedTenantId });\n} catch (error) {\n if (error instanceof PartyContactValidationError)\n return fail(400, { values: { ...values, contacts: rows }, errors: { contacts: toMessages(error.errors) } });\n throw error;\n}\n```\n\n`savePartyContacts` also accepts the `FormData` itself (with `fieldNames`) and\nreads it the same way. The rows replace the party's contacts: posted ids are kept, new rows get an\nid, unposted rows are removed, blank rows are dropped. See\n[customer/vendor UI](agents/customer-vendor-ui.md#contact-rows-3501).\n\n## Customer and vendor selectors\n\n`CustomerSelect` and `VendorSelect` from `@happyvertical/smrt-commerce/svelte`\nare searchable, field-sized pickers for a customer or vendor relation field.\nThey are registered in `COMMERCE_UI_SLOTS` with `selects:\n'@happyvertical/smrt-commerce:Customer'` / `':Vendor'`, so generic forms (via\n`findSelectorFor` over the manifest's `uiSelectors`) use them for any\n`@foreignKey` or `@crossPackageRef` field targeting those models.\n\nThey are presentation only. The caller owns the data: `search(query)` returns\nthe existing party DTOs (`CustomerDisplayData` / `VendorDisplayData`, whose\n`profile.name` is the label), and `resolve(id)` returns the record behind the\ncurrent value. Each row shows the profile name with status (and customer type).\nThe committed id, never the name, posts under `name`. Pass `onCreate` to offer a\n\"New customer\" / \"New vendor\" action; it receives the searched text, and a\nreturned record is selected.\n\n```svelte\n<CustomerSelect\n name=\"customerId\"\n bind:value={customerId}\n search={(query) => customers.search(query)}\n resolve={(id) => customers.byId(id)}\n onCreate={(query) => openCustomerDialog(query)}\n/>\n```\n\nThe same models carry `@field({ ui: { widget } })` hints in the manifest:\n`textarea` on `Contract` (and subclass) `terms` and `notes`, `Customer`/`Vendor`\n`notes`, and `Invoice` notes and terms; `currency` (an ISO 4217 code picker) on\n`Contract`, `Vendor`, `Invoice` and `Payment` `currency`; `email`/`phone` on\n`Vendor.defaultContactEmail`/`defaultContactPhone`. See\n[customer and vendor selectors](agents/customer-vendor-ui.md#selectors-3602).\n\n## Invoice preparation UI\n\n`InvoiceEditor` from `@happyvertical/smrt-commerce/svelte` is the invoice form,\nbuilt on the commercial money model, v2, which is the only invoice money model.\nIt requires a `money` prop (`MoneyEditorProps`): a lossless draft with\ndescriptions, fractional quantities, unit prices, percentage or amount\ndiscounts and a tax code per line (inherited or overridden), plus the\nauthorized tax codes. Its previews run the same exact calculator the server\nuses, and the server re-resolves tax codes and recomputes every stored amount.\nAn issued document renders only its frozen stored snapshot.\n`InvoiceAllocationFields` and `InvoiceReview` add caller-owned sources and\napprovals. See [the editor contract](agents/invoice-preparation.md), the\n[line calculation helper](agents/invoice-calculations.md), and the\n[existing-database storage migration](agents/invoice-quantity.md).\n\n## Exact cost billing\n\nThe independent `@happyvertical/smrt-commerce/cost-billing` entry point provides\nexact signed reimbursement/fee calculations, ordered generic markup provenance,\nand explicit line or invoice tax rounding. Its opt-in ordered tax calculator\nsupports subset compounding with caller-selected signed invoice allocation.\nSee [the cost billing contract](agents/cost-billing.md).\nIt does not alter Invoice persistence, editor defaults or settlement behavior.\n\n### Purchase-order header editing\n\nImport `PurchaseOrderHeaderForm` from `@happyvertical/smrt-commerce/svelte` for a\nnative POST of vendor, reference, notes, expected date and ship-to fields. Pass\n`action`, `vendors` and raw `values`; return canonical field `errors` and the same\n`hiddenFields` after server validation. This header-only form leaves purchase\nallocations and totals unchanged. `PurchaseOrder` stores `expectedDate` and\n`shipTo` in existing STI metadata. See [the purchasing contract](agents/purchasing-ui.md#purchase-order-header)\nfor validation, retained values and transport details.\n"
22464
21885
  },
22465
21886
  {
22466
21887
  "path": "agents/invoice-quantity.md",
22467
21888
  "module": "invoice-quantity",
22468
- "content": "# Invoice quantity persistence and editor recovery\n\nInvoiceLineItem accepts nonnegative decimal quantities with at most six decimal\nplaces. The `1.0` default declares a floating-point quantity column. Unit price,\nflat discount, and calculated amount remain safe integer minor units. Save always\nvalidates resolved fields and recalculates amount; a supplied amount is never\ntrusted. Gross quantity × price and then tax on discounted gross round to minor\nunits, with ties toward positive infinity (including existing negative-price\ncredit lines). Tax is a resolved fraction in [0, 1], with eight decimal places.\nAccounting export retains an explicit zero rate instead of omitting it.\n\n`applyEditorDraft(draft, authorizedContext)` resolves the shared invoice draft\nhelper into description/SKU/quantity/price/flat discount/tax and stores cloned\n`invoiceEditorStateJson` TEXT, accessed through a guarded typed `invoiceEditorState` getter/setter. It does not save.\n`getEditorDraft(authorizedContext)` returns a clone only when currency, inherited\ntax authority, and all source fields still agree. Otherwise it returns null so the\ncaller can reconstruct a flat-discount/explicit-tax editor from resolved fields.\nNormal save clears stale editor state and never derives money from metadata.\nAll other object metadata, authorization, and transaction ownership are unchanged.\n\n## Existing integer columns\n\nRun a backup and pause invoice writers for this explicit migration. With a root\ndatabase handle and its known engine (`sqlite`, `postgres`, or `duckdb`), call\n`preflightInvoiceEditorStorage(db, engine)`, resolve every reported invalid row,\nthen `migrateInvoiceEditorStorage(db, engine)`. The preflight rejects negative,\nnonfinite, unsafe, excess-precision, and numerically non-roundtripping quantities.\nThe migration uses a transaction (and a PostgreSQL writer lock) and never rescales\nor rounds stored values. PostgreSQL widens INTEGER/BIGINT to DOUBLE PRECISION;\nDuckDB widens to DOUBLE. Repeating it leaves an already widened declaration alone.\nAn absent table is reported as absent and is never marked migrated.\n\nSQLite uses the public core table-rebuild planner to change the declaration to\nREAL while preserving existing columns, constraints, indexes, and triggers. If\nincoming foreign keys make that rebuild unsafe, the migration throws with the\nplanner reason and rolls back; it does not disable constraints or lose children.\nResolve that maintenance condition before deploying fractional writers. The same\ntransaction adds the nullable TEXT `invoice_editor_state_json` column with an empty\nstring default on every engine. This column is required for updated model saves.\nAn existing floating quantity column still receives the missing state column.\nNo schema mutation runs implicitly during runtime initialization or model save.\n\nThe actual DuckDB quantity/storage migration is tested. Complete Commerce model\npersistence on DuckDB is currently unsupported because its hard foreign keys use\nON UPDATE CASCADE, which DuckDB cannot preserve; the schema builder refuses that\ncontract. SQLite and PostgreSQL exercise complete save/reload model behavior.\n"
21889
+ "content": "# Invoice quantity persistence\n\nInvoiceLineItem accepts nonnegative decimal quantities with at most six decimal\nplaces. The `1.0` default declares a floating-point quantity column. Unit price,\ndiscount and amount stay safe integer minor units. Every save goes through the\ninvoice money aggregate, which recomputes the line and ignores\nany supplied amount. Tax comes from the line's resolved tax code, never from a\nstored rate. See the [commercial money model](money-model.md).\n\n## Existing integer columns\n\nRun a backup and pause invoice writers for this explicit migration. With a root\ndatabase handle and its known engine (`sqlite`, `postgres`, or `duckdb`), call\n`preflightInvoiceEditorStorage(db, engine)`, resolve every reported invalid row,\nthen `migrateInvoiceEditorStorage(db, engine)`. The preflight rejects negative,\nnonfinite, unsafe, excess-precision, and numerically non-roundtripping quantities.\nThe migration uses a transaction (and a PostgreSQL writer lock) and never rescales\nor rounds stored values. PostgreSQL widens INTEGER/BIGINT to DOUBLE PRECISION;\nDuckDB widens to DOUBLE. Repeating it leaves an already widened declaration alone.\nAn absent table is reported as absent and is never marked migrated.\n\nSQLite uses the public core table-rebuild planner to change the declaration to\nREAL while preserving existing columns, constraints, indexes, and triggers. If\nincoming foreign keys make that rebuild unsafe, the migration throws with the\nplanner reason and rolls back; it does not disable constraints or lose children.\nResolve that maintenance condition before deploying fractional writers. The\nmigration no longer adds an editor-state column. An `invoice_editor_state_json`\ncolumn that an earlier version added is left in place and unused. No schema\nmutation runs implicitly during runtime initialization or model save.\n\nThe actual DuckDB quantity/storage migration is tested. Complete Commerce model\npersistence on DuckDB is currently unsupported because its hard foreign keys use\nON UPDATE CASCADE, which DuckDB cannot preserve; the schema builder refuses that\ncontract. SQLite and PostgreSQL exercise complete save/reload model behavior.\n"
22469
21890
  },
22470
21891
  {
22471
21892
  "path": "agents/payment-allocations.md",
@@ -22475,37 +21896,37 @@
22475
21896
  {
22476
21897
  "path": "agents/tax-codes.md",
22477
21898
  "module": "tax-codes",
22478
- "content": "# Tax codes\n\n`TaxComponent` owns an immutable percent and optional liability account. To\nchange a rate, archive the old component, create a replacement and update the\neditable `TaxCode.components` list. Never edit a used component's percent.\n\nEvery tenant seeds `exempt`, `zero-rated`, `out-of-scope`, and\n`legacy-unreconciled`. Those kinds have no components because they report\ndifferently. A legacy zero rate means out-of-scope unless the linked customer\nwas explicitly exempt; malformed or out-of-range legacy rates stay visible\nthrough `legacy-unreconciled` and the mapping report.\n\n`TaxSettings` owns tenant defaults, tax registrations, default line amount type\nand rounding scope. Cookbook policies adapt the portable settings document;\nthey do not replace persisted tax settings. The per-tenant migration marker is\nowned by the money-snapshot migration. Ordinary saves and generated creates\ncannot change it: a new instance whose tenant natural key would upsert over a\n`migrating` or `v2` row is refused rather than resetting that tenant to legacy.\nThe ordinary save transaction locks and re-reads the tenant marker before its\nnatural-key upsert, serializing that check with migration claim and activation.\nIf the tenant has no marker to lock, its first ordinary save is insert-only: a\nconcurrent migration that creates the marker wins and the stale save is refused\ninstead of adopting and overwriting the migration-owned row.\n\n`applyCommerceCookbookSettings(db, tenantId, cookbook)` is the explicit\nserver-side activation boundary for core's portable `taxRate`, inclusion, and\nrounding policies. A host calls it after schema setup, once it has the\nauthoritative tenant from its authenticated bootstrap flow:\n\n```ts\nimport { applyCommerceCookbookSettings } from '@happyvertical/smrt-commerce';\nimport cookbook from './smrt.cookbook.json' with { type: 'json' };\n\nawait applyCommerceCookbookSettings(db, session.tenantId, cookbook);\n```\n\n`smrt cookbook apply` writes project files and does not have a database or\ntenant, so it deliberately performs no storage writes. Activation creates or\nreuses a component and default tax code, then updates tenant `TaxSettings` in\none transaction. A zero default rate selects `out-of-scope`; inclusion remains\nthe cookbook's explicit inclusive/exclusive choice because individual lines can\nstill override the default code.\n\n`COMMON_TAX_CODE_EXAMPLE_PACK` demonstrates Canada GST, BC GST+PST, Ontario\nHST, UK VAT, Australian GST and New Zealand GST. Import it idempotently with\n`applyTaxCodeExamplePack(db, tenantId)`, then review local applicability and\nassign liability accounts before selecting a code for production documents.\nThe pack supplies examples, not jurisdiction determination.\n"
21899
+ "content": "# Tax codes\n\n`TaxComponent` owns an immutable percent and optional liability account. To\nchange a rate, archive the old component, create a replacement and update the\neditable `TaxCode.components` list. Never edit a used component's percent.\n\nEvery tenant seeds `exempt`, `zero-rated`, and `out-of-scope`\n(`STANDARD_TAX_CODES`, seeded by `ensureTenantTaxCodes(db, tenantId)` in\n`src/tax/standard-codes.ts`). Those kinds have no components because they\nreport differently. They are reserved: billing lines and zero rates rely on\nthem never carrying tax, so `ensureTenantTaxCodes` (and through it the\nbootstrap, billing period close and the upgrade) refuses a standard code\nwhose kind or components were changed. `TaxCodeCollection.findByCode(code, tenantId?)` finds a\ncode by its tenant-unique `code`, optionally for an explicit tenant.\nThe [money data upgrade](money-upgrade.md) adds a one-component\n`rate-<percent>` code for each nonzero rate it finds in earlier-version data.\n\n`TaxSettings` owns tenant defaults, tax registrations, default line amount type\nand rounding scope. Cookbook policies adapt the portable settings document;\nthey do not replace persisted tax settings. There is one settings row per\ntenant, created once under a tenant lock (see\n[document money](document-money.md#tenant-settings-and-bootstrap-4009)).\n\n`applyCommerceCookbookSettings(db, tenantId, cookbook)` is the explicit\nserver-side activation boundary for core's portable `taxRate`, inclusion, and\nrounding policies. A host calls it after schema setup, once it has the\nauthoritative tenant from its authenticated bootstrap flow:\n\n```ts\nimport { applyCommerceCookbookSettings } from '@happyvertical/smrt-commerce';\nimport cookbook from './smrt.cookbook.json' with { type: 'json' };\n\nawait applyCommerceCookbookSettings(db, session.tenantId, cookbook);\n```\n\n`ensureCommerceTenant(db, tenantId, { cookbook })` wraps this call with the\nsettings row and standard code seed; prefer it for a newly created tenant\n(#4009).\n\n`smrt cookbook apply` writes project files and does not have a database or\ntenant, so it deliberately performs no storage writes. Activation creates or\nreuses a component and default tax code, then updates tenant `TaxSettings` in\none transaction. A zero default rate selects `out-of-scope`; inclusion remains\nthe cookbook's explicit inclusive/exclusive choice because individual lines can\nstill override the default code.\n\n`COMMON_TAX_CODE_EXAMPLE_PACK` demonstrates Canada GST, BC GST+PST, Ontario\nHST, UK VAT, Australian GST and New Zealand GST. Import it idempotently with\n`applyTaxCodeExamplePack(db, tenantId)`, then review local applicability and\nassign liability accounts before selecting a code for production documents.\nThe pack supplies examples, not jurisdiction determination.\n"
22479
21900
  },
22480
21901
  {
22481
21902
  "path": "agents/document-money.md",
22482
21903
  "module": "document-money",
22483
- "content": "# Document money persistence\n\nTenant money behavior is selected by the server-owned `TaxSettings.moneyModel`\nmarker. Missing settings and `legacy` preserve the existing line-save behavior.\n`migrating` is a persisted maintenance guard: invoice money writes fail until\nthe owning migration restores `legacy` or activates `v2`. Generated surfaces\ncannot write either the marker or its owner token.\n\nEvery Invoice/InvoiceLineItem get, list, preview, mutation, posting, and\naccounting-sync phase acquires the same tenant settings row before document\nrows. The barrier is held through the owning transaction, so migration cannot\nstart between the marker check and a money read/write. Nested coordinators\nreuse an explicitly registered transaction executor; adapter shape is never\nused as transaction proof. When the settings table exists, the first legacy\nruntime boundary materializes the deterministic legacy row solely as this\ndurable serialization point. Older schemas without the table remain legacy\nand runtime never creates schema.\n\nIn `v2`, saving an invoice line locks and reloads its parent on the caller's\ntransaction, reloads every sibling, resolves current tax defaults, calculates\nthe document once, and writes line snapshots, `DocumentTax`, and header totals\natomically. Tax resolution order is line override, customer exemption (unless\nthe line overrides), product, customer, then app default. Product reads use\nthe products package's collection on that same transaction executor;\ntenant-owned and catalog-global products are accepted.\n\nThe first transition away from `DRAFT`, or setting `arJournalId`, freezes the\nmoney document. The invoice and every line receive the same stable\n`moneySnapshotId`. Later payment and lifecycle saves keep that identity.\nThat persisted snapshot identity remains the freeze authority if payment\nallocation changes move the lifecycle status back to `DRAFT`; it does not turn\nan issued document back into an editable money draft.\nFrozen fields include party/customer identity, currency, issue and due dates,\ndocument amount/rounding settings, seller and party tax snapshots, line\ndescription/SKU/product, quantity/pricing/discount modes, tax code and component\nsnapshots, source period, revenue account, and sort order. Header and child\nmoney edits, child moves, additions, and deletions then fail with\n`FROZEN_DOCUMENT`; status, payment, reminder, and delivery metadata continue\nthrough their existing lifecycle guards.\n\nCredit notes reuse `Invoice` with `kind: 'credit_note'` and a frozen\n`creditedInvoiceId`; their `DocumentTax.documentType` is `credit_note`. Stored\nheader amounts remain positive and posting/report direction comes from `kind`.\nA targeted credit line carries `creditedLineId` plus a server-derived frozen\ncalculation basis. The credit document inherits and re-enforces its source\n`lineAmountType` and `taxRoundingScope`; later tenant defaults cannot reinterpret\nthe frozen unit price or tax allocation. Quantity credits reuse the source price\nand allocate its frozen flat or percentage discount from cumulative credited\nquantity; this prevents repeated rounding from exceeding the source discount,\nand the final quantity receives its exact remaining discount. Amount credits use\nan explicit tax-exclusive magnitude and restore the source line sign. A quantity\nrequest is also bounded by tax-exclusive value already consumed through amount\ncredits, so mixing the two bases cannot exceed the source line. Tax components,\nlabels, rates, and accounts come from the issued source snapshot. When a request\nexhausts every remaining original line, the final line also receives the exact\nremaining document/component residual.\nFree-form goodwill lines require an explicit tax code and do not consume source\nline capacity.\n\nIssued credit notes reduce the source `getBalance()` and update its\nserver-managed `creditedAmount` under the same locks. Refunds are positive\n`Payment` rows with `kind: 'refund'`, allocated only to credit notes; their\navailable amount is the unapplied issued credit after source settlement and\nprior settled refunds. Incoming payments debit cash and credit receivables;\nrefund journals reverse those sides.\n\n`previewInvoiceMoney({ db, tenantId, invoiceId, draft })` applies a proposed\ndraft overlay through the owning resolver/calculator without writes and rejects\nfrozen documents. A supplied line id must name a stored line exactly once; omit\nthe id for a new preview-only line. `getInvoiceMoneyFacts({ db, tenantId,\ninvoiceId })` reads the authoritative stored header, lines, `moneySnapshotId`,\nand `DocumentTax` rows without recalculation. Both facades require an active\n`v2` tenant and fail closed while migration is in progress, so legacy or\npartially migrated amounts are never presented as money-v2 facts.\n\n### Record-screen composition\n\n`loadInvoiceRecordScreenData({ db, tenantId, invoiceId })` is the commerce\nserver adapter for a generic invoice record page. A caller must already have\nthat exact active tenant; the adapter never switches principals based on its\nargument. It holds the shared v2 money read barrier while it reads the stored\nheader, lines, `DocumentTax`, and related credit-note summaries, so a migration\ncannot mix a stale header with a different snapshot of those rows. Its totals\nare a direct projection of persisted minor-unit values and component snapshots;\nit does not resolve a code or recalculate tax.\n\n`InvoiceRecordScreen` from `@happyvertical/smrt-commerce/svelte` composes that\nprojection with `RecordScreen.lineItemStoredTotals`, the released generic\nline-items widget contract. A host passes its own generic screen wiring and\nmay expose `onCreateCreditNote` only with a server-derived\n`canCreateCreditNote` authorization result. The wrapper hides the action for\ncredit-note records and never treats a client callback as authorization.\n\n`InvoiceLineItem.amount` is tax-exclusive net money in `v2`.\n`taxAmount` is separate, while `legacyAmount` is reserved for rollback during\nthe compatibility migration. `taxCodeId` and component identity may be null for\na genuine unassigned zero-tax line. Optional `productId` and\n`revenueAccountId` are nullable UUID references so PostgreSQL never receives an\nempty-string sentinel.\n\n`DocumentTax` is derived and read-only. Its rows snapshot the code/component\nlabels, percentage, liability account, taxable minor units, and tax minor\nunits. Provider tax remains the invoice header's `providerTaxAmount`; commerce\ndoes not invent a tax code or component for provider-owned tax.\n\nRevenue recognition for `v2` reads only the frozen invoice, lines, and\n`DocumentTax` rows. It groups each line's stored net by its snapshotted revenue\naccount (falling back to the caller's revenue account), groups component tax by\nthe snapshotted liability account (falling back to the caller's tax account),\nand posts provider tax to the fallback once. Non-taxable zero rows do not post.\nEvery entry is an exact non-negative debit or credit in the invoice currency.\nThe caller must supply its authoritative `bookCurrency`; commerce rejects a\nmissing, unsupported, or different currency because ledgers has no book model.\n\nThe revenue journal identity derives from the tenant, invoice, and stable\n`moneySnapshotId`. Creation, posting, and the invoice's `arJournalId` link share\nthe invoice parent lock and transaction, so retries converge. Account IDs from\nfrozen snapshots are still authorization checked: every used account must be\nactive and owned by the invoice tenant.\n\nA posted `v2` invoice cannot move directly to `CANCELLED`. Call\n`cancelWithRevenueReversal({ bookCurrency })`; it reads the original posted\njournal, posts one deterministic journal with every debit and credit reversed,\nand stores `arReversalJournalId` with the status transition atomically. The\noriginal journal remains posted. Failed link or reversal writes roll back the\nentire operation, and retry returns the same reversal. Legacy tenants keep the\nexisting recognition and cancellation behavior.\n"
21904
+ "content": "# Document money persistence\n\nCommerce has one money model. Tax codes resolve each line's tax, the server\ncomputes line `taxAmount` and the header totals, and issuing a document freezes\nit with a money snapshot. Every Invoice and Contract money write needs a\ntenant; a tenantless write fails with `MoneyValidationError`\n`INVALID_MONEY_INPUT` (`Commerce money documents require a tenant`).\n\n### Tenant settings and bootstrap (#4009)\n\nEach tenant has one `TaxSettings` row. It is created once, wherever the first\ncaller reaches it: `getOrCreateForTenant`, the cookbook path, a generated\ncreate, or the runtime barrier ahead of a tenant's first document. Creation\nruns in one transaction under a tenant advisory lock on PostgreSQL (the\nembedded write queue elsewhere), re-reads before inserting, and inserts the\ndeterministic settings id, so racing first writers converge on one row.\n\n`ensureCommerceTenant(db, tenantId, { cookbook? })` is the one host bootstrap\ncall after tenant creation. It seeds the settings row, the standard tax codes\n(`exempt`, `zero-rated`, `out-of-scope`), and the cookbook's commerce tax\nsettings when one is given. It returns `{ tenantId }` and is idempotent;\nre-running it with a cookbook re-applies the cookbook defaults.\n\nMoney data written by an earlier version is refused, never read. A schema with\nthe earlier version's money columns or table, or an Invoice or Contract row\nwithout a tenant, refuses the whole database. A tenant with Invoice or Contract\nrows but no settings row, or with old-format documents (an issued document\nwithout a money snapshot, or a line with neither a tax code nor a component\nsnapshot), is refused on its own. All fail with `COMMERCE_DATA_NEEDS_UPGRADE`\nand name `smrt db:upgrade-commerce-money`.\nSee [money data upgrade](money-upgrade.md). Rows written with raw SQL bypass\nthe model and are not recalculated.\n\nEvery Invoice/InvoiceLineItem get, list, preview, mutation, posting, and\naccounting-sync phase checks for old-format data, then acquires the tenant\nsettings row before document rows. The lock is held through the owning\ntransaction. Nested coordinators reuse an explicitly registered transaction\nexecutor; adapter shape is never used as transaction proof. Runtime code never\ncreates schema.\n\nSaving an invoice line locks and reloads its parent on the caller's\ntransaction, reloads every sibling, resolves current tax defaults, calculates\nthe document once, and writes line snapshots, `DocumentTax`, and header totals\natomically. Tax resolution order is line override, customer exemption (unless\nthe line overrides), product, customer code, then the tenant default; `none`\nmode uses the tenant's `out-of-scope` code. Product reads use the products\npackage's collection on that same transaction executor; tenant-owned and\ncatalog-global products are accepted.\n\nThe first transition away from `DRAFT`, or setting `arJournalId`, freezes the\nmoney document. The invoice and every line receive the same stable\n`moneySnapshotId`. Later payment and lifecycle saves keep that identity.\nThat persisted snapshot identity remains the freeze authority if payment\nallocation changes move the lifecycle status back to `DRAFT`; it does not turn\nan issued document back into an editable money draft.\nFrozen fields include party/customer identity, currency, issue and due dates,\ndocument amount/rounding settings, seller and party tax snapshots, line\ndescription/SKU/product, quantity/pricing/discount modes, tax code and component\nsnapshots, source period, revenue account, and sort order. Header and child\nmoney edits, child moves, additions, and deletions then fail with\n`FROZEN_DOCUMENT`; status, payment, reminder, and delivery metadata continue\nthrough their existing lifecycle guards.\n\nCredit notes reuse `Invoice` with `kind: 'credit_note'` and a frozen\n`creditedInvoiceId`; their `DocumentTax.documentType` is `credit_note`. Stored\nheader amounts remain positive and posting/report direction comes from `kind`.\nA targeted credit line carries `creditedLineId` plus a server-derived frozen\ncalculation basis. The credit document inherits and re-enforces its source\n`lineAmountType` and `taxRoundingScope`; later tenant defaults cannot reinterpret\nthe frozen unit price or tax allocation. Quantity credits reuse the source price\nand allocate its frozen flat or percentage discount from cumulative credited\nquantity; this prevents repeated rounding from exceeding the source discount,\nand the final quantity receives its exact remaining discount. Amount credits use\nan explicit tax-exclusive magnitude and restore the source line sign. A quantity\nrequest is also bounded by tax-exclusive value already consumed through amount\ncredits, so mixing the two bases cannot exceed the source line. Tax components,\nlabels, rates, and accounts come from the issued source snapshot. When a request\nexhausts every remaining original line, the final line also receives the exact\nremaining document/component residual.\nFree-form goodwill lines require an explicit tax code and do not consume source\nline capacity.\n\nIssued credit notes reduce the source `getBalance()` and update its\nserver-managed `creditedAmount` under the same locks. Refunds are positive\n`Payment` rows with `kind: 'refund'`, allocated only to credit notes; their\navailable amount is the unapplied issued credit after source settlement and\nprior settled refunds. Incoming payments debit cash and credit receivables;\nrefund journals reverse those sides.\n\n`previewInvoiceMoney({ db, tenantId, invoiceId, draft })` applies a proposed\ndraft overlay through the owning resolver/calculator without writes and rejects\nfrozen documents. A supplied line id must name a stored line exactly once; omit\nthe id for a new preview-only line. `getInvoiceMoneyFacts({ db, tenantId,\ninvoiceId })` reads the authoritative stored header, lines, `moneySnapshotId`,\nand `DocumentTax` rows without recalculation. Both facades take the tenant\nsettings lock and refuse a tenant whose data needs the upgrade.\n\n### Record-screen composition\n\n`loadInvoiceRecordScreenData({ db, tenantId, invoiceId })` is the commerce\nserver adapter for a generic invoice record page. A caller must already have\nthat exact active tenant; the adapter never switches principals based on its\nargument. It holds the tenant money lock while it reads the stored header,\nlines, `DocumentTax`, and related credit-note summaries, so it never mixes a\nstale header with a different snapshot of those rows. Its totals\nare a direct projection of persisted minor-unit values and component snapshots;\nit does not resolve a code or recalculate tax.\n\n`InvoiceRecordScreen` from `@happyvertical/smrt-commerce/svelte` composes that\nprojection with `RecordScreen.lineItemStoredTotals`, the released generic\nline-items widget contract. A host passes its own generic screen wiring and\nmay expose `onCreateCreditNote` only with a server-derived\n`canCreateCreditNote` authorization result. The wrapper hides the action for\ncredit-note records and never treats a client callback as authorization.\n\n`InvoiceLineItem.amount` is the post-discount amount in the document's\n`lineAmountType`: tax-exclusive net for `exclusive`, tax-inclusive for\n`inclusive`. `taxAmount` is separate. `taxRate` is derived by the server from\nthe resolved components (`0.05` means 5%); it is never an input. `taxCodeId` and component identity may be null for\na genuine unassigned zero-tax line. Optional `productId` and\n`revenueAccountId` are nullable UUID references so PostgreSQL never receives an\nempty-string sentinel.\n\n`DocumentTax` is derived and read-only. Its rows snapshot the code/component\nlabels, percentage, liability account, taxable minor units, and tax minor\nunits. Provider tax remains the invoice header's `providerTaxAmount`; commerce\ndoes not invent a tax code or component for provider-owned tax.\n\nRevenue recognition reads only the frozen invoice, lines, and\n`DocumentTax` rows. It groups each line's stored net by its snapshotted revenue\naccount (falling back to the caller's revenue account), groups component tax by\nthe snapshotted liability account (falling back to the caller's tax account),\nand posts provider tax to the fallback once. Non-taxable zero rows do not post.\nEvery entry is an exact non-negative debit or credit in the invoice currency.\nThe caller must supply its authoritative `bookCurrency`; commerce rejects a\nmissing, unsupported, or different currency because ledgers has no book model.\n\nThe revenue journal identity derives from the tenant, invoice, and stable\n`moneySnapshotId`. Creation, posting, and the invoice's `arJournalId` link share\nthe invoice parent lock and transaction, so retries converge. Account IDs from\nfrozen snapshots are still authorization checked: every used account must be\nactive and owned by the invoice tenant.\n\nA posted invoice cannot move directly to `CANCELLED`. Call\n`cancelWithRevenueReversal({ bookCurrency })`; it reads the original posted\njournal, posts one deterministic journal with every debit and credit reversed,\nand stores `arReversalJournalId` with the status transition atomically. The\noriginal journal remains posted. Failed link or reversal writes roll back the\nentire operation, and retry returns the same reversal.\n\nBilling period close writes ordinary money-model invoices. The payment provider\ntaxes them, so each billing line is an explicit override to the seller's\n`out-of-scope` code, and the provider's tax is stored in `providerTaxAmount`\nin the save that issues the invoice.\n"
21905
+ },
21906
+ {
21907
+ "path": "agents/money-upgrade.md",
21908
+ "module": "money-upgrade",
21909
+ "content": "# Money data upgrade\n\nCommerce has one money model. It does not read money data written by an\nearlier version, and it does not convert that data on the fly. It refuses it\nand names the upgrade command. The operator steps are in the CLI guide,\n[commerce money upgrade command](../../cli/agents/commerce-money-upgrade.md).\n\n## Detection\n\n`src/upgrade/detect.ts` owns detection. Four shapes count as old data:\n\n- **Old schema.** The database still has `tax_settings.money_model`,\n `tax_settings.money_migration_id`, `*_line_items.legacy_amount`, or the\n `commerce_money_migration_documents` table. This refuses the whole database.\n- **Documents without a tenant.** An Invoice or Contract row, header or line,\n with no tenant. The current model writes none, and the upgrade cannot convert\n one: assign it to a tenant or delete it. This refuses the whole database and\n keeps the old schema in place until it is resolved.\n- **Tenant without settings.** A tenant has Invoice or Contract rows but no\n `tax_settings` row. This refuses that tenant only.\n- **Old-format documents.** An issued Invoice or Contract without a money\n snapshot, or a line with neither a tax code nor a component snapshot. The\n current model never writes either, so these stay detectable after `db:migrate --drop-columns`\n removed the old columns. This refuses that tenant only.\n\n`assertCommerceMoneySchemaCurrent(db)` checks the schema.\n`assertTenantMoneyCurrent(db, tenantId)` checks the schema, then the tenant.\nOn PostgreSQL every cross-tenant scan runs on a connection of its own (opened\nfrom the database URL), in a transaction that raises `smrt.system_context`,\nso the caller's connection and row policies are never touched; concurrent\nchecks for one database and tenant share a single scan. Both cache a passing\nresult per database (keyed by its connection URL, so\nrequest executors share it) and tenant, so the check runs once per process.\nInvoice, Contract and line models also run the check from `initialize()` on\nevery row hydrated from the database, which covers unscoped reads and every\ncollection class, including generated STI subtype collections. A\ntenantless collection read still checks the schema, and a tenant's first\n`TaxSettings` row is refused while that tenant holds old documents. Every runtime money read and write, the money MCP tools, the tax\nsummary, and `ensureCommerceTenant()` call them first.\n\nA refusal is a `CommerceDataNeedsUpgradeError` with `code`\n`COMMERCE_DATA_NEEDS_UPGRADE`. Its message ends with the command, for example\n`Run smrt db:upgrade-commerce-money --tenant-id <id> to upgrade it.`\n`commerceMoneyUpgradeCommand(tenantId?)` builds that command text.\n\n`findCommerceMoneyUpgradeNeeds(db)` scans the whole database. It is\noperator-only: it ignores the caller's row permissions, so never wire it into\na principal-facing surface. It returns `oldSchema`, `tenantlessDocuments`\n(rows to assign or delete) and each tenant that needs the upgrade, with a\nreason (`legacy-marker`, `interrupted-migration`, `missing-settings`, or\n`old-format-documents`).\n`smrt db:status` uses it to report the `commerce-money-upgrade` error\nprecondition, so `smrt app doctor` reports `migration-required`.\n\n## Upgrade\n\n`upgradeCommerceMoney(db, { tenantId? })` in `src/upgrade/money-v2.ts` is the\none-shot conversion behind `smrt db:upgrade-commerce-money`. Run the normal\nschema migration first. Each tenant converts in its own transaction:\n\n1. Its tax settings row and standard codes are seeded.\n2. Each stored fractional rate maps to a code. A zero rate becomes `exempt`\n for an exempt customer, otherwise `out-of-scope`. Any other rate becomes a\n one-component `rate-<percent>` code, created once.\n3. Issued documents are backfilled from their stored values: line net, line\n tax, tax-code snapshot, `DocumentTax` rows, and a money snapshot. No stored\n total changes.\n4. Drafts are recomputed through the runtime from their prices. Only\n old-format lines (no tax code and no component snapshot) are mapped; a\n draft header keeps its amount type and rounding. The result lists each\n draft whose total changed.\n\nAn issued document whose stored lines do not add up exactly to its stored\nheader fails its tenant. So do an invalid stored rate, an invoice posted under\nthe earlier version (its revenue journal carries that version's provenance,\nwhich current posting and reversal refuse; void it and clear the link, or reset\nthe data), an existing `rate-<percent>` code or component that does not\nmean exactly that rate, and an edited standard `exempt` or `out-of-scope`\ncode. Nothing in that tenant changes, and the result lists\neach failing document. A tenant whose earlier\nmigration never finished also fails; roll it back with the version that started\nit, or reset its data. Converted tenants are skipped on a rerun.\n\nOnce no tenant needs the upgrade, the old columns and table are dropped. The\nupgrade leaves no mode flag. Read `failures` and `remaining` on the result; the\nCLI exits nonzero on either. New `rate-<percent>` components have no liability\naccount and are listed in `componentsWithoutLiabilityAccount`; posting falls\nback to the caller's `taxAccountId` until one is assigned.\n\nThe runtime never imports `money-v2.ts`. Delete the `upgrade/` directory, the\nCLI command, and the detection call sites together once no supported version\ncan have written old-format data.\n"
22484
21910
  },
22485
21911
  {
22486
21912
  "path": "agents/contract-money.md",
22487
21913
  "module": "contract-money",
22488
- "content": "# Contract money persistence\n\nContract money runs through `contract-money.ts`. A tenant operation enters\n`withTenantMoneyRuntime` before it locks and reloads its Contract and sibling\nlines on the scoped executor. The legacy barrier variants retain the old line\ncalculation; only active v2 tenants resolve tax codes and replace the Contract\naggregate plus `DocumentTax` rows in the same transaction.\n\nDraft Contract line writes recompute every sibling. `subtotal` is tax-exclusive\nnet after discount, `taxAmount` is the component total, and `totalAmount` is\ntheir exact sum. Purchase component tax is recoverable only when the frozen\ncomponent says so. A Contract leaving draft snapshots registrations, party tax\nidentity, per-line component amounts, and `moneySnapshotId`; later money edits\nare refused with `FROZEN_DOCUMENT`.\n\n`materializeContractMoneySnapshot` is migration-only. Its caller supplies the\nalready transaction-bound migration owner executor; it verifies that owner,\nlocks/reloads the Contract, and never starts a runtime transaction.\n\n`SupplierQuote` uses purchase defaults and recoverability, including during\nlegacy-to-v2 migration. Non-draft quotes freeze just like other Contracts, but\nquote snapshots (including custom descendants) are excluded by their hydrated\nSTI subtype, not the mutable `contractType` scalar, from report candidates and\npurchase tax facts. Awarding requires a separate PurchaseOrder.\n"
21914
+ "content": "# Contract money persistence\n\nContract money runs through `contract-money.ts`. A tenant operation enters\n`withTenantMoneyRuntime` before it locks and reloads its Contract and sibling\nlines on the scoped executor, resolves tax codes, and replaces the Contract\naggregate plus `DocumentTax` rows in the same transaction. Every Contract money\nwrite needs a tenant, and a tenant whose data needs the upgrade is refused (see\n[money data upgrade](money-upgrade.md)).\n\nDraft Contract line writes recompute every sibling. `subtotal` is tax-exclusive\nnet after discount, `taxAmount` is the component total, and `totalAmount` is\ntheir exact sum. Purchase component tax is recoverable only when the frozen\ncomponent says so. A Contract leaving draft snapshots registrations, party tax\nidentity, per-line component amounts, and `moneySnapshotId`; later money edits\nare refused with `FROZEN_DOCUMENT`.\n\n`SupplierQuote` uses purchase defaults and recoverability. Non-draft quotes\nfreeze just like other Contracts, but quote snapshots (including custom\ndescendants) are excluded by their hydrated STI subtype, not the mutable\n`contractType` scalar, from report candidates and purchase tax facts. Awarding\nrequires a separate PurchaseOrder.\n"
22489
21915
  },
22490
21916
  {
22491
21917
  "path": "agents/money-model.md",
22492
21918
  "module": "money-model",
22493
- "content": "# Commercial money model\n\nThis guide names the public commerce money contract. The exact input and result\nshapes, validation codes, and worked calculator examples live in the\n[calculator guide](money-calculator.md) and\n[package documentation](../../../docs/content/commerce-money.md). Money values\nare signed safe-integer minor units; calculator tax percentages are exact text,\nso `'5'` means 5%. The retained `InvoiceLineItem.taxRate` and\n`ContractLineItem.taxRate` fields are different legacy persisted fractions:\n`0.05` means 5%.\n\n## Calculation contract\n\n`calculateMoneyDocument()` accepts `exclusive`, `inclusive`, and `none` line\namount modes. It first computes quantity times unit price, applies the one\nchosen discount, and returns the post-discount amount, tax components, net, and\nline total. `exclusive` line amounts are tax-exclusive net; `inclusive` line\namounts are the visible gross and derive net by removing tax; `none` has zero\ntax and requires an out-of-scope code. `subtotalMinor`, `taxMinor`, and\n`totalMinor` are document totals. `discountTotalMinor` is informational because\nthe line discounts were already applied; consumers must not subtract it again.\n\nThe `line` rounding scope rounds each line/component. The `document` scope\nrounds each shared component's exact total, truncates each allocation toward\nzero, and gives signed residual minor units to the largest remainders, with\nline-ID code-unit order breaking ties. It leaves no document residual. Across\n`n` small lines, the two scopes can differ by up to `ceil(n / 2)` minor units\nper component. Store the chosen scope; do not silently substitute a later\nsetting. `MoneyValidationError.code` is the public branch point for invalid\nmoney, discounts, tax-code state and scope, unsupported compound tax, frozen\nrecords, and unsafe values.\n\nTax-code resolution is pure once the caller has loaded authorized codes. It\nuses a line override, customer exemption, product code, customer code, and app\ndefault in that order. An unresolved exclusive or inclusive line is a genuine\nzero-tax, null-code case; it is not an out-of-scope inference. The tax-code\ncatalogue, seeds, registrations, and cookbook boundary are documented in\n[tax codes](tax-codes.md).\n\n## Stored Invoice facts\n\n`TaxSettings.moneyModel` selects the Invoice write behavior. `legacy` keeps the\ncompatibility path; `migrating` rejects money writes; `v2` resolves a draft's\ncurrent settings once and atomically writes the header, line snapshots, and\n`DocumentTax` rows. The detailed persistence, freeze, preview, credit-note, and\nrefund contracts are in [document money](document-money.md).\n\nOnce an Invoice leaves `DRAFT`, or receives an AR journal, its money snapshot\nis frozen. Reads use those stored values and must not recompute issued tax from\nnew defaults. `DocumentTax` is a derived, read-only per-document/code/component\nsnapshot with currency, code and component identities and labels, kind, exact\npercent, liability account, taxable, tax, and recoverable minor-unit values.\nIt also represents non-taxable and unassigned zero-tax rows so reporting does\nnot invent a taxable component.\n\nA credit note is an `Invoice` with `kind: 'credit_note'`. It stores positive\namounts; posting, reporting, and accounting adapters apply that direction once.\nIssued-credit capacity and refunds are stable Invoice-v2 behavior. A refund is\na positive `Payment` of kind `refund`, allocated only to a credit note. See\n[document money](document-money.md) before presenting either balance as an\nordinary incoming payment.\n\n## Invoice MCP `get` projection\n\nThe standard Invoice MCP `get` tool uses `serializeInvoiceMcpGet`. For a\nlegacy tenant, or when tenant settings are absent, it returns the ordinary\npublic Invoice projection and omits the `money` alias. For a v2 tenant, the\noptional `money` value is the stored `InvoiceMoneyFacts` projection: its\nheader, line snapshots, and `DocumentTax` facts are read from the issued\nsnapshot and are never recalculated.\n\nA tenant in the `migrating` gate refuses this read with the stable\n`FROZEN_DOCUMENT` code and the public message `Tenant money migration is in\nprogress`. If the caller's `PublicJsonOptions` hide any Invoice,\n`InvoiceLineItem`, or `DocumentTax` field needed by the money projection, the\nserializer returns the base Invoice and omits the entire `money` alias. It does\nnot expose restricted values through a partial plain-object alias.\n\nThis applies only to standard MCP `get`. Invoice list and MCP write behavior\nremain their existing public contracts. The general serializer configuration,\nauthorization order, and projection rule are in [core MCP get\nserializers](../../core/agents/generators.md#mcp-get-serializers).\n\n## Reporting and integrations\n\nThe materialized `TaxSummaryReport` and its public descriptor/drilldown bridge\nread stored Invoice `DocumentTax` facts under the tenant v2 gate. See\n[tax summary aggregation](tax-summary.md). Commerce exports\n`createTaxSummaryMaterializedReportHost()` for an application-owned,\nserver-only host; register that host in the app's fixed report-host registry to\nopt into the generic `MaterializedReportTable` and its tenant-scoped drilldown\nroute. The host binds the authenticated tenant, principal, and\n`taxsummaryreports.read` permission; browser input supplies only a row id\nand visible column id.\n\nThe accounting snapshot mapper consumes frozen facts and never re-calculates a\nrate or allocation. It maps an inclusive line to the accounting SDK's\n`TaxInclusiveAmt`; tenant mappings provide provider tax-code and component-rate\nidentities. The QuickBooks sync boundary persists SDK 0.103.7 invoice or credit\nmemo request descriptors before provider I/O, refuses competing writes, and\nrequires explicit reconciliation after an unknown outcome. Its supported\ninputs and recovery contract are documented in [accounting snapshot\nmapper](accounting-snapshot-mapper.md).\n\n## Deliberate boundaries\n\nThe tenant migration and rollback contract is documented in\n[tenant line-amount migration](line-amount-migration.md).\nThe marker is a maintenance gate, not a customer-controlled setting. Contract\nmoney persistence remains a separate aggregate contract; production report\nhosting is an application opt-in over the public host described above. The\nmodel also does not provide currency conversion, cash-basis/deposit accounting,\npartial recoverability, period locking, or compound tax.\n"
21919
+ "content": "# Commercial money model\n\nThis guide names the public commerce money contract. The exact input and result\nshapes, validation codes, and worked calculator examples live in the\n[calculator guide](money-calculator.md) and\n[package documentation](../../../docs/content/commerce-money.md). Money values\nare signed safe-integer minor units; calculator tax percentages are exact text,\nso `'5'` means 5%. The `InvoiceLineItem.taxRate` and\n`ContractLineItem.taxRate` fields are server-derived fractions of the resolved\ncomponents (`0.05` means 5%); they are never tax inputs.\n\n## Calculation contract\n\n`calculateMoneyDocument()` accepts `exclusive`, `inclusive`, and `none` line\namount modes. It first computes quantity times unit price, applies the one\nchosen discount, and returns the post-discount amount, tax components, net, and\nline total. `exclusive` line amounts are tax-exclusive net; `inclusive` line\namounts are the visible gross and derive net by removing tax; `none` has zero\ntax and requires an out-of-scope code. `subtotalMinor`, `taxMinor`, and\n`totalMinor` are document totals. `discountTotalMinor` is informational because\nthe line discounts were already applied; consumers must not subtract it again.\n\nThe `line` rounding scope rounds each line/component. The `document` scope\nrounds each shared component's exact total, truncates each allocation toward\nzero, and gives signed residual minor units to the largest remainders, with\nline-ID code-unit order breaking ties. It leaves no document residual. Across\n`n` small lines, the two scopes can differ by up to `ceil(n / 2)` minor units\nper component. Store the chosen scope; do not silently substitute a later\nsetting. `MoneyValidationError.code` is the public branch point for invalid\nmoney, discounts, tax-code state and scope, unsupported compound tax, frozen\nrecords, and unsafe values.\n\nTax-code resolution is pure once the caller has loaded authorized codes. It\nuses a line override, customer exemption, product code, customer code, and app\ndefault in that order. An unresolved exclusive or inclusive line is a genuine\nzero-tax, null-code case; it is not an out-of-scope inference. The tax-code\ncatalogue, seeds, registrations, and cookbook boundary are documented in\n[tax codes](tax-codes.md).\n\n## Stored Invoice facts\n\nThere is one money model. Saving a draft resolves its current tax settings\nonce and atomically writes the header, line snapshots, and `DocumentTax` rows.\nEvery money document needs a tenant, and data written by an earlier version is\nrefused until it is upgraded ([money data upgrade](money-upgrade.md)). The detailed persistence, freeze, preview, credit-note, and\nrefund contracts are in [document money](document-money.md).\n\nOnce an Invoice leaves `DRAFT`, or receives an AR journal, its money snapshot\nis frozen. Reads use those stored values and must not recompute issued tax from\nnew defaults. `DocumentTax` is a derived, read-only per-document/code/component\nsnapshot with currency, code and component identities and labels, kind, exact\npercent, liability account, taxable, tax, and recoverable minor-unit values.\nIt also represents non-taxable and unassigned zero-tax rows so reporting does\nnot invent a taxable component.\n\nA credit note is an `Invoice` with `kind: 'credit_note'`. It stores positive\namounts; posting, reporting, and accounting adapters apply that direction once.\nIssued-credit capacity and refunds are stable Invoice behavior. A refund is\na positive `Payment` of kind `refund`, allocated only to a credit note. See\n[document money](document-money.md) before presenting either balance as an\nordinary incoming payment.\n\n## Invoice MCP `get` projection\n\nThe standard Invoice MCP `get` tool uses `serializeInvoiceMcpGet`. For a\ntenant invoice, the optional `money` value is the stored `InvoiceMoneyFacts`\nprojection: its header, line snapshots, and `DocumentTax` facts are read from\nthe stored snapshot and are never recalculated. An invoice without an id or\ntenant returns the ordinary public Invoice projection.\n\nA tenant whose data needs the upgrade refuses this read with\n`COMMERCE_DATA_NEEDS_UPGRADE`. If the caller's `PublicJsonOptions` hide any Invoice,\n`InvoiceLineItem`, or `DocumentTax` field needed by the money projection, the\nserializer returns the base Invoice and omits the entire `money` alias. It does\nnot expose restricted values through a partial plain-object alias.\n\nThis applies only to standard MCP `get`. Invoice list and MCP write behavior\nremain their existing public contracts. The general serializer configuration,\nauthorization order, and projection rule are in [core MCP get\nserializers](../../core/agents/generators.md#mcp-get-serializers).\n\n## Reporting and integrations\n\nThe materialized `TaxSummaryReport` and its public descriptor/drilldown bridge\nread stored Invoice `DocumentTax` facts under the tenant money lock. See\n[tax summary aggregation](tax-summary.md). Commerce exports\n`createTaxSummaryMaterializedReportHost()` for an application-owned,\nserver-only host; register that host in the app's fixed report-host registry to\nopt into the generic `MaterializedReportTable` and its tenant-scoped drilldown\nroute. The host binds the authenticated tenant, principal, and\n`taxsummaryreports.read` permission; browser input supplies only a row id\nand visible column id.\n\nThe accounting snapshot mapper consumes frozen facts and never re-calculates a\nrate or allocation. It maps an inclusive line to the accounting SDK's\n`TaxInclusiveAmt`; tenant mappings provide provider tax-code and component-rate\nidentities. The QuickBooks sync boundary persists SDK 0.103.7 invoice or credit\nmemo request descriptors before provider I/O, refuses competing writes, and\nrequires explicit reconciliation after an unknown outcome. Its supported\ninputs and recovery contract are documented in [accounting snapshot\nmapper](accounting-snapshot-mapper.md).\n\n## Deliberate boundaries\n\nUpgrading data written by an earlier version is documented in\n[money data upgrade](money-upgrade.md). Contract money persistence remains a separate aggregate contract; production report\nhosting is an application opt-in over the public host described above. The\nmodel also does not provide currency conversion, cash-basis/deposit accounting,\npartial recoverability, period locking, or compound tax.\n"
22494
21920
  },
22495
21921
  {
22496
21922
  "path": "agents/money-calculator.md",
22497
21923
  "module": "money-calculator",
22498
- "content": "# Money calculator\n\n`src/money.ts` is the Svelte-free authority for commercial document arithmetic.\nImport it from `@happyvertical/smrt-commerce/money` (or the package root).\n\n- Inputs are signed safe-integer minor units. Quantity and percent values have at\n most six decimal places; calculations use exact `BigInt` rationals and round\n half away from zero. Currency must be a supported ISO 4217 code with a defined\n minor unit; the exponent validates the document currency but does not rescale\n already-minor-unit inputs.\n- `lineAmountType` is `exclusive`, `inclusive`, or `none`. Inclusive `amountMinor`\n is the customer-visible gross; `netMinor = amountMinor - taxMinor`.\n- `roundingScope: 'line'` rounds each line/component. `document` rounds the exact\n sum for each shared component, then allocates from truncation toward zero by\n signed largest remainder. Ties use code-unit line ID order, so input order does\n not affect allocation. The residual's sign, including mixed signed lines, is\n authoritative.\n- `taxes` is split by tax code and component for persistence. Shared components\n are rounded across codes at document scope. Non-taxable codes use a null\n component row with zero tax and taxable minor units equal to summed line net.\n A genuinely unassigned exclusive/inclusive line uses `taxCode: null` and emits\n `{ taxCodeId: null, taxComponentId: null }`; no synthetic ID is persisted.\n- Resolve already-loaded codes with `resolveLineTaxCode()`. `none` mode always\n chooses the supplied out-of-scope code before an override. Active/scope checks\n happen during resolution; issued snapshots are not passed back through it.\n Missing exempt/out-of-scope seeds fail closed and never fall through to a\n taxable default.\n- Discounts are either percent or signed minor units. An amount discount carries\n the gross sign. Editors may accept a positive magnitude, then apply the sign.\n\nAll business rejections use `MoneyValidationError`. The stable tax-code shape\ncodes include `TAX_CODE_DUPLICATE_COMPONENT`,\n`TAX_CODE_NON_TAXABLE_COMPONENTS`, and `TAX_CODE_TAXABLE_EMPTY`, in addition to\n`TAX_CODE_NOT_FOUND`, `TAX_CODE_KIND`, `TAX_COMPONENT_RATE_CONFLICT`, and the\nshared input, discount, percent, compound, state, scope, safety, freeze, and\ncurrency codes exported as `MoneyValidationCode`.\n\nThe existing invoice functions in `src/svelte/invoices/calculations.ts` remain\ncompatibility adapters for the legacy single-component exclusive model until the\ntenant migration gate enables persisted money-v2 fields. In particular,\n`calculateInvoiceMinorLine(input)` defaults to the legacy negative-tie rule for\nexisting model saves. Migration-aware callers explicitly pass\n`{ moneyModel: 'v2' }`; there is no process-global mode. V2 callers pass the\nstored signed discount through unchanged; editor magnitude normalization happens\nbefore constructing that stored input.\n"
22499
- },
22500
- {
22501
- "path": "agents/line-amount-migration.md",
22502
- "module": "line-amount-migration",
22503
- "content": "# Tenant line-amount migration\n\nThe operational CLI entrypoints are documented in\n`@happyvertical/smrt-cli/agents/commerce-money-migration.md`. A CLI migration\nchooses and prints its owner UUID before the tenant is claimed, so a failure\nafter the claim can be resumed with the exact same `--migration-id`; a\ndifferent owner is rejected.\n\n`preflightCommerceLineAmounts(db, { tenantId?, out? })` is read-only. It does\nnot create `TaxSettings`. Review its structured report (and optional explicit\n`jsonPath`/`csvPath`) before migration. `UNRECONCILED`, orphan or cross-tenant\nlines, missing tenancy, an ambiguous base Contract direction, and an\n`in_flight`/`unknown` accounting sync block the tenant before any marker or\ndocument mutation. Draft total changes also require `acceptDraftChanges`.\n\n`migrateCommerceLineAmounts(db, { tenantId, acceptDraftChanges?, migrationId? })`\nclaims the tenant marker, maps legacy rates, and processes each document under\nthe shared tenant barrier. A sensitive internal\n`MoneyMigrationDocument` record commits its checksummed pre-image before the\ndocument transaction. The applied transaction stores a checksummed post-image;\nan exact retry skips it, while any mismatch fails closed. Runtime reads and\nwrites remain blocked until every issued document verifies and the marker is\nwritten `v2` last.\n\nIssued migration preserves every previously stored header field, timestamp,\nrevision and journal link. The only header writes are the additive v2 snapshot\ncolumns `discount_total`, `line_amount_type`, `tax_rounding_scope`,\n`seller_tax_registrations_json`, `party_tax_id`, and `money_snapshot_id`.\nIssued legacy snapshots always record `line_amount_type=exclusive` and\n`tax_rounding_scope=line`, which describe the historical storage contract;\ncurrent tenant defaults apply only when calculating new v2 drafts.\nDrafts are recomputed through the owning aggregate after their legacy rate is\npromoted to an explicit code override. When a tenant has both exempt and\nordinary zero-rate history, migration selects the durable code from each\ndocument's linked customer: exempt customers remain `exempt`; other zero-rate\ndocuments remain `out-of-scope`.\n\n`rollbackCommerceLineAmounts(db, { tenantId, migrationId })` claims the tenant\nbarrier and validates journal existence plus every current post-image in one\ntransaction. Every current tenant invoice and contract must have a matching\njournal entry; an unjournaled header was created under v2 and makes rollback\nunsafe, so rollback refuses before changing the marker or document. Any other\nrefusal before restoration begins also rolls the claim back, so the tenant\nremains on v2 and ordinary runtime reads stay available. Once restoration\nbegins, each document commits separately under the same owner; an interruption\nleaves the marker in `migrating`, and retrying that owner skips durable\n`rolled_back` journal entries. Exact pre-images restore the header, lines,\ntaxes, timestamps and revisions, then the marker returns to `legacy`.\nThe journal is installed by normal schema migration and retained through the\nrollback window. Runtime code never creates its table.\n"
21924
+ "content": "# Money calculator\n\n`src/money.ts` is the Svelte-free authority for commercial document arithmetic.\nImport it from `@happyvertical/smrt-commerce/money` (or the package root).\n\n- Inputs are signed safe-integer minor units. Quantity and percent values have at\n most six decimal places; calculations use exact `BigInt` rationals and round\n half away from zero. Currency must be a supported ISO 4217 code with a defined\n minor unit; the exponent validates the document currency but does not rescale\n already-minor-unit inputs.\n- `lineAmountType` is `exclusive`, `inclusive`, or `none`. Inclusive `amountMinor`\n is the customer-visible gross; `netMinor = amountMinor - taxMinor`.\n- `roundingScope: 'line'` rounds each line/component. `document` rounds the exact\n sum for each shared component, then allocates from truncation toward zero by\n signed largest remainder. Ties use code-unit line ID order, so input order does\n not affect allocation. The residual's sign, including mixed signed lines, is\n authoritative.\n- `taxes` is split by tax code and component for persistence. Shared components\n are rounded across codes at document scope. Non-taxable codes use a null\n component row with zero tax and taxable minor units equal to summed line net.\n A genuinely unassigned exclusive/inclusive line uses `taxCode: null` and emits\n `{ taxCodeId: null, taxComponentId: null }`; no synthetic ID is persisted.\n- Resolve already-loaded codes with `resolveLineTaxCode()`. `none` mode always\n chooses the supplied out-of-scope code before an override. Active/scope checks\n happen during resolution; issued snapshots are not passed back through it.\n Missing exempt/out-of-scope seeds fail closed and never fall through to a\n taxable default.\n- Discounts are either percent or signed minor units. An amount discount carries\n the gross sign. Editors may accept a positive magnitude, then apply the sign.\n\nAll business rejections use `MoneyValidationError`. The stable tax-code shape\ncodes include `TAX_CODE_DUPLICATE_COMPONENT`,\n`TAX_CODE_NON_TAXABLE_COMPONENTS`, and `TAX_CODE_TAXABLE_EMPTY`, in addition to\n`TAX_CODE_NOT_FOUND`, `TAX_CODE_KIND`, `TAX_COMPONENT_RATE_CONFLICT`, and the\nshared input, discount, percent, compound, state, scope, safety, freeze, and\ncurrency codes exported as `MoneyValidationCode`.\n\n`calculateInvoiceMinorLine(input)` in `src/svelte/invoices/calculations.ts`\n(also exported from `@happyvertical/smrt-commerce/invoices`) runs one line\nthrough this calculator with a single-component code built from its resolved\nfraction. It takes no options. Its discount is signed minor units and passes\nthrough unchanged, and its ties round half away from zero for negative\nlines too. Editor magnitude normalization happens before constructing that\ninput. See [invoice line calculation](invoice-calculations.md).\n"
22504
21925
  },
22505
21926
  {
22506
21927
  "path": "agents/commercial-ui.md",
22507
21928
  "module": "commercial-ui",
22508
- "content": "# Commercial UI composition\n\nThe commercial UI release is tracked in [#3423](https://github.com/happyvertical/smrt/issues/3423).\nComponents compose the provider-free controls in `@happyvertical/smrt-ui/forms`\n(including `Combobox` and the searchable `RelationInput`, which takes\ncaller-supplied `search`/`resolve`) and visual controls in\n`@happyvertical/smrt-ui`. They do not require SvelteKit, a database, or the\ntop-level `smrt-svelte` integration package. Shared form controls live in\nsmrt-ui; build on them rather than re-implementing their logic locally.\n\n## Native forms and recovery\n\nThe application owns action URLs, HTTP methods, submitter names/values, field\nnames, request identity, tenant context and returned validation errors. Native\nforms use the base `Form` with `preventDefault={false}`. Enhancement is optional;\na handler that takes over submission is responsible for preventing navigation.\n\nA failed action returns the submitted strings, including invalid amounts and\nrepeated fields, alongside errors. Components must not reconstruct these values\nfrom saved models or generate a replacement request token. Unknown commit outcomes\nretain the same identity and payload until the application resolves the result.\nFile inputs cannot be repopulated after navigation: explain that the file must be\nselected again and retain the caller's recovery identity.\n\nThe existing [form retry contract](../../../docs/content/form-retry.md) provides\noptional browser enhancement. It does not replace server-side tenant/actor checks\nor idempotency. Permission props control presentation only; applications must\nsupply already-authorized data and reauthorize every submitted action.\n\n## Amounts and public compatibility\n\nNew commercial editors use explicitly documented integer minor-unit amounts.\nEditable values remain strings until the owning server validates them; blank,\ninvalid and zero are distinct. Currency and its display precision are explicit.\nDo not mix currencies in totals or infer an unknown allocation as zero.\n\nThe original invoice display DTOs in `src/svelte/types.ts` and their components\naccept decimal major-unit amounts. Preserve that published behavior. Applications\nadapting model amounts into those legacy components must explicitly convert units;\ndo not pass a model's integer amount directly or silently change a legacy prop's\nmeaning. New editor DTOs are separate contracts.\n\nApproved or otherwise retained invoice facts use the explicit `retained` display\nbranches and DTOs from `src/svelte/invoice-display.ts`. Their amounts are safe\nintegers in currency minor units, formatted through the package ISO exponent\ntable without floating-point division. Retained line subtotals, discounts, tax\nrows, holdbacks, totals, paid amounts and payable amounts are caller-authoritative;\nthe components never derive one from another, and an omitted fact stays omitted.\nApplication-specific evidence belongs in the typed detail snippets.\n\n## Domain boundaries\n\nCustomer and Vendor reference Profiles identity data. A consumer's Client record\nis not automatically a Commerce Customer; UI adoption performs no identity merge\nor persistence migration. Domain-specific contact fields, trades, project/package\nallocations and approval policies enter through typed adapters and extension slots.\n\nQuote/estimate preparation, customer acceptance, vendor award, expense review and\ninvoice approval are separate decisions. A UI status or selection must never\nimplicitly perform another transition. The server supplies current decision and\nsource availability, including restricted-reader presentation.\n\n## Backend capability assessment\n\nAssessment baseline: SMRT `98c872be4` and reviewed DomaCraft epic-138\n`b03b322f75fbfdcb9193b18e73d3ec818ef18811`.\n\n| Responsibility | Existing capability | Release decision |\n| --- | --- | --- |\n| Commitment versus actual | `ExpenseCollection.commitmentPosition()` sums reviewed, non-duplicate expenses in the commitment currency, optionally per line. | Reuse Expenses; keep DomaCraft award lineage and package-cap constraints in its adapter. |\n| Receipt identity and duplicate review | `ExpenseReceiptCollection` owns receipt joins and duplicate detection; `Expense` owns guarded review transitions. | Reuse these contracts. Private upload authorization, draft capacity and storage compensation stay in application composition. |\n| Idempotent submission and uncertain outcomes | Core `runOnce()` and UI `createFormRetry()` provide shared server/browser primitives. | Preserve the caller's request identity. Do not replace DomaCraft's transaction-bound financial submission logic without proving equivalent transaction and recovery semantics. |\n| Invoice source reservations | DomaCraft `finance.ts` reserves its own cost allocations under project transaction locks; Commerce Invoice has no equivalent source-allocation contract. | Keep the reservation service local: extracting it changes a published financial contract and requires an agreed cross-domain source model. UI accepts supplied source availability. |\n| Source-change approval invalidation | DomaCraft `withdrawInvoiceApprovalForSource()` and invoice fingerprint checks bind project policy, actor decisions and retained sources. | Keep this policy local. Shared presentation renders server-supplied approval state and cannot authorize execution. |\n\nNo generic backend redesign is required by these UI slices. A future extraction\nneeds evidence that a contract works beyond the application-specific project and\npolicy model; UI reuse alone does not establish that requirement.\n\n## Validation and consumer adoption\n\nEach slice covers its public exports, SSR/native payload, retained errors,\nread-only and empty states, keyboard operation and a 390px viewport. The release\nalso validates the complete touched packages and integrated workbench. A browser\nviewport test does not establish physical-device camera behavior.\n\nDomaCraft adopts a published release through normal dependency admission. Its\nnative financial, authorization, tenant and uncertain-outcome tests remain required;\nshared component tests do not establish those server-side properties.\n"
21929
+ "content": "# Commercial UI composition\n\nThe commercial UI release is tracked in [#3423](https://github.com/happyvertical/smrt/issues/3423).\nComponents compose the provider-free controls in `@happyvertical/smrt-ui/forms`\n(including `Combobox` and the searchable `RelationInput`, which takes\ncaller-supplied `search`/`resolve`) and visual controls in\n`@happyvertical/smrt-ui`. They do not require SvelteKit, a database, or the\ntop-level `smrt-svelte` integration package. Shared form controls live in\nsmrt-ui; build on them rather than re-implementing their logic locally.\n\n## Native forms and recovery\n\nThe application owns action URLs, HTTP methods, submitter names/values, field\nnames, request identity, tenant context and returned validation errors. Native\nforms use the base `Form` with `preventDefault={false}`. Enhancement is optional;\na handler that takes over submission is responsible for preventing navigation.\n\nA failed action returns the submitted strings, including invalid amounts and\nrepeated fields, alongside errors. Components must not reconstruct these values\nfrom saved models or generate a replacement request token. Unknown commit outcomes\nretain the same identity and payload until the application resolves the result.\nFile inputs cannot be repopulated after navigation: explain that the file must be\nselected again and retain the caller's recovery identity.\n\nThe existing [form retry contract](../../../docs/content/form-retry.md) provides\noptional browser enhancement. It does not replace server-side tenant/actor checks\nor idempotency. Permission props control presentation only; applications must\nsupply already-authorized data and reauthorize every submitted action.\n\n## Amounts and public compatibility\n\nNew commercial editors use explicitly documented integer minor-unit amounts.\nEditable values remain strings until the owning server validates them; blank,\ninvalid and zero are distinct. Currency and its display precision are explicit.\nDo not mix currencies in totals or infer an unknown allocation as zero.\n\nThe original invoice display DTOs in `src/svelte/types.ts` and their components\naccept decimal major-unit amounts. Preserve that published behavior. Applications\nadapting model amounts into those major-unit components must explicitly convert\nunits; do not pass a model's integer amount directly or silently change a prop's\nmeaning. New editor DTOs are separate contracts.\n\nApproved or otherwise retained invoice facts use the explicit `retained` display\nbranches and DTOs from `src/svelte/invoice-display.ts`. Their amounts are safe\nintegers in currency minor units, formatted through the package ISO exponent\ntable without floating-point division. Retained line subtotals, discounts, tax\nrows, holdbacks, totals, paid amounts and payable amounts are caller-authoritative;\nthe components never derive one from another, and an omitted fact stays omitted.\nApplication-specific evidence belongs in the typed detail snippets.\n\n## Domain boundaries\n\nCustomer and Vendor reference Profiles identity data. A consumer's Client record\nis not automatically a Commerce Customer; UI adoption performs no identity merge\nor persistence migration. Domain-specific contact fields, trades, project/package\nallocations and approval policies enter through typed adapters and extension slots.\n\nQuote/estimate preparation, customer acceptance, vendor award, expense review and\ninvoice approval are separate decisions. A UI status or selection must never\nimplicitly perform another transition. The server supplies current decision and\nsource availability, including restricted-reader presentation.\n\n## Backend capability assessment\n\nAssessment baseline: SMRT `98c872be4` and reviewed DomaCraft epic-138\n`b03b322f75fbfdcb9193b18e73d3ec818ef18811`.\n\n| Responsibility | Existing capability | Release decision |\n| --- | --- | --- |\n| Commitment versus actual | `ExpenseCollection.commitmentPosition()` sums reviewed, non-duplicate expenses in the commitment currency, optionally per line. | Reuse Expenses; keep DomaCraft award lineage and package-cap constraints in its adapter. |\n| Receipt identity and duplicate review | `ExpenseReceiptCollection` owns receipt joins and duplicate detection; `Expense` owns guarded review transitions. | Reuse these contracts. Private upload authorization, draft capacity and storage compensation stay in application composition. |\n| Idempotent submission and uncertain outcomes | Core `runOnce()` and UI `createFormRetry()` provide shared server/browser primitives. | Preserve the caller's request identity. Do not replace DomaCraft's transaction-bound financial submission logic without proving equivalent transaction and recovery semantics. |\n| Invoice source reservations | DomaCraft `finance.ts` reserves its own cost allocations under project transaction locks; Commerce Invoice has no equivalent source-allocation contract. | Keep the reservation service local: extracting it changes a published financial contract and requires an agreed cross-domain source model. UI accepts supplied source availability. |\n| Source-change approval invalidation | DomaCraft `withdrawInvoiceApprovalForSource()` and invoice fingerprint checks bind project policy, actor decisions and retained sources. | Keep this policy local. Shared presentation renders server-supplied approval state and cannot authorize execution. |\n\nNo generic backend redesign is required by these UI slices. A future extraction\nneeds evidence that a contract works beyond the application-specific project and\npolicy model; UI reuse alone does not establish that requirement.\n\n## Validation and consumer adoption\n\nEach slice covers its public exports, SSR/native payload, retained errors,\nread-only and empty states, keyboard operation and a 390px viewport. The release\nalso validates the complete touched packages and integrated workbench. A browser\nviewport test does not establish physical-device camera behavior.\n\nDomaCraft adopts a published release through normal dependency admission. Its\nnative financial, authorization, tenant and uncertain-outcome tests remain required;\nshared component tests do not establish those server-side properties.\n"
22509
21930
  },
22510
21931
  {
22511
21932
  "path": "agents/quote-ui.md",
@@ -22520,7 +21941,7 @@
22520
21941
  {
22521
21942
  "path": "agents/cost-billing.md",
22522
21943
  "module": "cost-billing",
22523
- "content": "# Exact cost billing\n\n`@happyvertical/smrt-commerce/cost-billing` is an independent, transport-neutral\nentry point without model, server or Svelte dependencies. It does not persist,\npost, authorize, select a currency/jurisdiction, or implement credit-note lifecycle.\nLegacy Invoice calculations and editor defaults are unchanged by this entry\npoint. In particular,\n`calculateInvoiceMinorLine` retains negative ties toward positive infinity; the\nnew opt-in helpers use symmetric half-away-from-zero ties. Positive-only invoice\neditor validation remains distinct from these generic signed calculations. The\n`roundingScope: 'invoice'` behavior in this guide is a standalone cost-billing\nconvention; v2 document arithmetic uses `roundingScope: 'document'` and stores\nallocated tax facts. See the [commercial money model](money-model.md) and\n[money calculator](money-calculator.md).\n\nCall `resolveCostMarkup(overrides)` with caller-approved precedence, most specific\nfirst. Each item has `source`, optional `policy` and `version`, and optional\n`percent`. The first defined percent wins, including `\"0\"`. At least one is\nrequired; even shadowed inputs are validated. The result contains the percent,\nselected index, and detached ordered provenance. Persist accepted snapshots in\nthe owning application; these helpers never reprice them automatically.\n\nCall `calculateCostBilling({ basisMinor, mode, markup })` with that result. Money\nis signed safe-integer minor units in a single caller-selected currency. The\nbasis is explicitly chosen by the caller (for example excluding recoverable tax).\n`reimbursement-plus-fee` returns principal once plus rounded fee; `fee-only`\nreturns only the fee; `nonbillable` and `fixed-contract` return zero charge.\nThe result includes `principalMinor`, `feeMinor`, `totalMinor`, and provenance.\nPercent strings use plain nonnegative decimal notation, range 0–100000 percent,\nat most six fractional places, without whitespace, exponent or leading zeroes.\nThe finite range and scale bound input parsing and arithmetic resource use; they\nare API limits, not tax or industry policy. Arithmetic uses BigInt intermediates and half-away-from-zero rounding. Invalid\ninputs, unknown modes, inconsistent provenance and unsafe outputs throw.\n\n`calculateCostBillingTax({ lines, rules, roundingScope })` accepts line\n`id`, signed `amountMinor`, and `category`; rules explicitly map every used\ncategory to `{ id, percent }` components. An empty component array explicitly\nmeans exempt. Components are additive, never compounded. Shared component IDs\nmust use equal rates across categories; distinct rate regimes need distinct IDs.\nThe caller owns taxable basis and jurisdiction mapping. Duplicate category,\ncomponent-within-category or line IDs fail closed. `line` scope rounds each\nline/component pair; `invoice` scope aggregates exact amounts per component and\nrounds once. Two 10-unit lines at 5 percent yield tax 2 versus 1 respectively.\nReturns `taxMinor` and component totals, deliberately without allocating rounded\ninvoice tax back to lines. Signed reversals use symmetric rounding, but mixed\npositive/negative lines may differ between rounding scopes.\n\n## Ordered compound tax with explicit allocation\n\n`calculateOrderedCostBillingTax` is a separate opt-in calculator on the same\nentry point. The additive calculator above, existing model/editor rounding and\nhistorical snapshots are unchanged. This helper neither infers regional rules\nnor persists policy. Applications must explicitly select approved tenant/regional\nsettings, preserve their accepted policy and result snapshots, and decide when a\nnew calculation is appropriate.\n\nSupply `lines` (`id`, signed safe-integer `amountMinor`, `category`), `categories`\n(`category`, `componentIds`) and ordered `components` (`id`, exact `percent`,\n`priorComponentIds`). Every component must explicitly supply `priorComponentIds`:\n`[]` means net-only; otherwise each ID must name a distinct earlier component.\nThe component basis on each eligible line is its net plus that line's allocated\ntax for exactly those prior components. A prior tax not applicable to that line\ncontributes zero. Category membership order does not control execution; the\nglobal component array does. A category's empty `componentIds` means exempt.\n\nChoose `roundingScope: 'line'` to round each eligible line/component pair\nhalf-away-from-zero. Invoice allocation options are forbidden in this mode,\nincluding an explicitly present `invoiceAllocation: undefined` property.\nChoose `roundingScope: 'invoice'` with the mandatory explicit\n`invoiceAllocation: 'signed-largest-remainder'` to round each component once\nover its eligible lines and distribute that rounded amount as follows:\n\n1. Calculate each eligible line's exact rational tax from its compound basis.\n2. Truncate each exact tax toward zero, retaining its signed fractional remainder.\n3. Round the exact component aggregate half-away-from-zero. Subtract the sum of\n truncated line taxes to obtain the signed residual.\n4. For positive residual, assign one extra minor unit to each of the lines with\n the largest signed remainders; for negative residual, subtract one unit from\n each line with the smallest (most negative) remainders. Stop when conserved.\n Ties use ascending JavaScript UTF-16 code-unit line ID order, never locale\n collation or input order. A zero residual leaves every truncated tax unchanged.\n\nThis allocation is a caller-selected arithmetic convention, not a jurisdiction\ndefault or independent per-line rounding. It supports mixed signs and exact full\nreversal: keeping the same IDs/policy and negating all amounts negates every\nreturned basis and allocation. An individual line's allocation can change when\nother lines change; independently recalculating a subset or a partial reversal\ndoes not preserve the original invoice allocation. Consumers needing historical\npartial adjustments must use their accepted snapshot and explicit adjustment\npolicy. Mixed exact taxes `+0.8` and `-0.8` have zero aggregate and allocate\n`0, 0` under this convention.\n\nFor example, lines `a` and `b`, each net 10, both receive `first` at 5 percent.\nThe invoice tax is 1, allocated `a: 1, b: 0`. If only `a` receives `second` at\n100 percent with `priorComponentIds: ['first']`, its basis is 11 and its tax 11;\nthe total tax is 12. If only `b` receives `second`, its basis and tax are 10.\nNegating both original amounts allocates `first` as `a: -1, b: 0`; the second\ntax on `a` has basis and tax -11, total tax -12. At line scope the positive\nexample instead yields first tax 2 and total tax 13.\n\nThe result is `{ taxMinor, components }`. Components follow the supplied global\norder and each returns `{ id, taxMinor, lines }`, where eligible lines are\n`{ id, basisMinor, taxMinor }` in ascending code-unit ID order. Each component's\nline allocations sum exactly to its total. Components with no eligible lines\nreturn zero and `lines: []`; well-formed unused categories and components remain\nvalid for reusable policy. Exempt lines appear in no component allocation.\nRepeated calculations and input line/category/membership permutations produce\nidentical output. All arithmetic intermediates use BigInt; every returned basis,\nallocation, component tax and final tax must fit a safe integer, even when a\nlarger value would later cancel or a component has a zero rate.\n\nAll configuration is validated, including unused entries. Missing category\nmapping, unknown IDs, duplicate line/category/component/membership/prior IDs,\nforward/self references, missing arrays, malformed rates and unknown modes fail\nclosed. Rates use the same bounded decimal-string contract as the additive API.\nThe input is never mutated and the result holds no aliases to input objects.\n"
21944
+ "content": "# Exact cost billing\n\n`@happyvertical/smrt-commerce/cost-billing` is an independent, transport-neutral\nentry point without model, server or Svelte dependencies. It does not persist,\npost, authorize, select a currency/jurisdiction, or implement credit-note lifecycle.\nIt does not change Invoice calculations or the invoice editor; both use the\ncommerce money calculator, whose ties also round half away from zero. The\n`roundingScope: 'invoice'` behavior in this guide is a standalone cost-billing\nconvention; document arithmetic uses `roundingScope: 'document'` and stores\nallocated tax facts. See the [commercial money model](money-model.md) and\n[money calculator](money-calculator.md).\n\nCall `resolveCostMarkup(overrides)` with caller-approved precedence, most specific\nfirst. Each item has `source`, optional `policy` and `version`, and optional\n`percent`. The first defined percent wins, including `\"0\"`. At least one is\nrequired; even shadowed inputs are validated. The result contains the percent,\nselected index, and detached ordered provenance. Persist accepted snapshots in\nthe owning application; these helpers never reprice them automatically.\n\nCall `calculateCostBilling({ basisMinor, mode, markup })` with that result. Money\nis signed safe-integer minor units in a single caller-selected currency. The\nbasis is explicitly chosen by the caller (for example excluding recoverable tax).\n`reimbursement-plus-fee` returns principal once plus rounded fee; `fee-only`\nreturns only the fee; `nonbillable` and `fixed-contract` return zero charge.\nThe result includes `principalMinor`, `feeMinor`, `totalMinor`, and provenance.\nPercent strings use plain nonnegative decimal notation, range 0–100000 percent,\nat most six fractional places, without whitespace, exponent or leading zeroes.\nThe finite range and scale bound input parsing and arithmetic resource use; they\nare API limits, not tax or industry policy. Arithmetic uses BigInt intermediates and half-away-from-zero rounding. Invalid\ninputs, unknown modes, inconsistent provenance and unsafe outputs throw.\n\n`calculateCostBillingTax({ lines, rules, roundingScope })` accepts line\n`id`, signed `amountMinor`, and `category`; rules explicitly map every used\ncategory to `{ id, percent }` components. An empty component array explicitly\nmeans exempt. Components are additive, never compounded. Shared component IDs\nmust use equal rates across categories; distinct rate regimes need distinct IDs.\nThe caller owns taxable basis and jurisdiction mapping. Duplicate category,\ncomponent-within-category or line IDs fail closed. `line` scope rounds each\nline/component pair; `invoice` scope aggregates exact amounts per component and\nrounds once. Two 10-unit lines at 5 percent yield tax 2 versus 1 respectively.\nReturns `taxMinor` and component totals, deliberately without allocating rounded\ninvoice tax back to lines. Signed reversals use symmetric rounding, but mixed\npositive/negative lines may differ between rounding scopes.\n\n## Ordered compound tax with explicit allocation\n\n`calculateOrderedCostBillingTax` is a separate opt-in calculator on the same\nentry point. The additive calculator above, existing model/editor rounding and\nhistorical snapshots are unchanged. This helper neither infers regional rules\nnor persists policy. Applications must explicitly select approved tenant/regional\nsettings, preserve their accepted policy and result snapshots, and decide when a\nnew calculation is appropriate.\n\nSupply `lines` (`id`, signed safe-integer `amountMinor`, `category`), `categories`\n(`category`, `componentIds`) and ordered `components` (`id`, exact `percent`,\n`priorComponentIds`). Every component must explicitly supply `priorComponentIds`:\n`[]` means net-only; otherwise each ID must name a distinct earlier component.\nThe component basis on each eligible line is its net plus that line's allocated\ntax for exactly those prior components. A prior tax not applicable to that line\ncontributes zero. Category membership order does not control execution; the\nglobal component array does. A category's empty `componentIds` means exempt.\n\nChoose `roundingScope: 'line'` to round each eligible line/component pair\nhalf-away-from-zero. Invoice allocation options are forbidden in this mode,\nincluding an explicitly present `invoiceAllocation: undefined` property.\nChoose `roundingScope: 'invoice'` with the mandatory explicit\n`invoiceAllocation: 'signed-largest-remainder'` to round each component once\nover its eligible lines and distribute that rounded amount as follows:\n\n1. Calculate each eligible line's exact rational tax from its compound basis.\n2. Truncate each exact tax toward zero, retaining its signed fractional remainder.\n3. Round the exact component aggregate half-away-from-zero. Subtract the sum of\n truncated line taxes to obtain the signed residual.\n4. For positive residual, assign one extra minor unit to each of the lines with\n the largest signed remainders; for negative residual, subtract one unit from\n each line with the smallest (most negative) remainders. Stop when conserved.\n Ties use ascending JavaScript UTF-16 code-unit line ID order, never locale\n collation or input order. A zero residual leaves every truncated tax unchanged.\n\nThis allocation is a caller-selected arithmetic convention, not a jurisdiction\ndefault or independent per-line rounding. It supports mixed signs and exact full\nreversal: keeping the same IDs/policy and negating all amounts negates every\nreturned basis and allocation. An individual line's allocation can change when\nother lines change; independently recalculating a subset or a partial reversal\ndoes not preserve the original invoice allocation. Consumers needing historical\npartial adjustments must use their accepted snapshot and explicit adjustment\npolicy. Mixed exact taxes `+0.8` and `-0.8` have zero aggregate and allocate\n`0, 0` under this convention.\n\nFor example, lines `a` and `b`, each net 10, both receive `first` at 5 percent.\nThe invoice tax is 1, allocated `a: 1, b: 0`. If only `a` receives `second` at\n100 percent with `priorComponentIds: ['first']`, its basis is 11 and its tax 11;\nthe total tax is 12. If only `b` receives `second`, its basis and tax are 10.\nNegating both original amounts allocates `first` as `a: -1, b: 0`; the second\ntax on `a` has basis and tax -11, total tax -12. At line scope the positive\nexample instead yields first tax 2 and total tax 13.\n\nThe result is `{ taxMinor, components }`. Components follow the supplied global\norder and each returns `{ id, taxMinor, lines }`, where eligible lines are\n`{ id, basisMinor, taxMinor }` in ascending code-unit ID order. Each component's\nline allocations sum exactly to its total. Components with no eligible lines\nreturn zero and `lines: []`; well-formed unused categories and components remain\nvalid for reusable policy. Exempt lines appear in no component allocation.\nRepeated calculations and input line/category/membership permutations produce\nidentical output. All arithmetic intermediates use BigInt; every returned basis,\nallocation, component tax and final tax must fit a safe integer, even when a\nlarger value would later cancel or a component has a zero rate.\n\nAll configuration is validated, including unused entries. Missing category\nmapping, unknown IDs, duplicate line/category/component/membership/prior IDs,\nforward/self references, missing arrays, malformed rates and unknown modes fail\nclosed. Rates use the same bounded decimal-string contract as the additive API.\nThe input is never mutated and the result holds no aliases to input objects.\n"
22524
21945
  }
22525
21946
  ],
22526
21947
  "recipes": [
@@ -22611,7 +22032,7 @@
22611
22032
  "visibility": "hidden"
22612
22033
  },
22613
22034
  "taxRate": {
22614
- "help": "Stored fraction: 0.05 means 5%."
22035
+ "visibility": "hidden"
22615
22036
  }
22616
22037
  }
22617
22038
  },
@@ -22768,7 +22189,7 @@
22768
22189
  "visibility": "hidden"
22769
22190
  },
22770
22191
  "taxRate": {
22771
- "help": "Stored fraction: 0.05 means 5%."
22192
+ "visibility": "hidden"
22772
22193
  }
22773
22194
  }
22774
22195
  }
@@ -22963,9 +22384,6 @@
22963
22384
  },
22964
22385
  "@happyvertical/smrt-commerce:InvoiceLineItem": {
22965
22386
  "fields": {
22966
- "invoiceEditorStateJson": {
22967
- "visibility": "hidden"
22968
- },
22969
22387
  "sourceType": {
22970
22388
  "visibility": "hidden"
22971
22389
  },
@@ -22991,7 +22409,7 @@
22991
22409
  "visibility": "hidden"
22992
22410
  },
22993
22411
  "taxRate": {
22994
- "help": "Stored fraction: 0.05 means 5%."
22412
+ "visibility": "hidden"
22995
22413
  }
22996
22414
  }
22997
22415
  },
@@ -23139,7 +22557,7 @@
23139
22557
  "visibility": "hidden"
23140
22558
  },
23141
22559
  "taxRate": {
23142
- "help": "Stored fraction: 0.05 means 5%."
22560
+ "visibility": "hidden"
23143
22561
  }
23144
22562
  }
23145
22563
  }
@@ -23240,7 +22658,7 @@
23240
22658
  "visibility": "hidden"
23241
22659
  },
23242
22660
  "taxRate": {
23243
- "help": "Stored fraction: 0.05 means 5%."
22661
+ "visibility": "hidden"
23244
22662
  }
23245
22663
  }
23246
22664
  }
@@ -23333,7 +22751,7 @@
23333
22751
  "visibility": "hidden"
23334
22752
  },
23335
22753
  "taxRate": {
23336
- "help": "Stored fraction: 0.05 means 5%."
22754
+ "visibility": "hidden"
23337
22755
  }
23338
22756
  }
23339
22757
  }
@@ -23435,7 +22853,7 @@
23435
22853
  "visibility": "hidden"
23436
22854
  },
23437
22855
  "taxRate": {
23438
- "help": "Stored fraction: 0.05 means 5%."
22856
+ "visibility": "hidden"
23439
22857
  }
23440
22858
  }
23441
22859
  }
@@ -23592,7 +23010,7 @@
23592
23010
  "visibility": "hidden"
23593
23011
  },
23594
23012
  "taxRate": {
23595
- "help": "Stored fraction: 0.05 means 5%."
23013
+ "visibility": "hidden"
23596
23014
  }
23597
23015
  }
23598
23016
  }