@happyvertical/smrt-commerce 0.56.1 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/AGENTS.md +24 -11
  2. package/README.md +159 -93
  3. package/agents/accounting-snapshot-mapper.md +3 -3
  4. package/agents/commercial-ui.md +2 -2
  5. package/agents/contract-money.md +9 -6
  6. package/agents/cost-billing.md +3 -6
  7. package/agents/customer-vendor-ui.md +6 -1
  8. package/agents/document-money.md +59 -31
  9. package/agents/invoice-calculations.md +31 -56
  10. package/agents/invoice-preparation.md +62 -73
  11. package/agents/invoice-quantity.md +9 -21
  12. package/agents/minor-unit-migration.md +4 -4
  13. package/agents/money-calculator.md +7 -8
  14. package/agents/money-model.md +18 -22
  15. package/agents/money-upgrade.md +92 -0
  16. package/agents/revenue-journal.md +2 -2
  17. package/agents/tax-codes.md +17 -14
  18. package/agents/tax-summary.md +12 -8
  19. package/dist/billing/period-close.d.ts.map +1 -1
  20. package/dist/billing/runtime.d.ts +5 -2
  21. package/dist/billing/runtime.d.ts.map +1 -1
  22. package/dist/chunks/{ContractLineItemCollection-DYjhj_pU.js → ContractLineItemCollection-BZcrlrrY.js} +22 -27
  23. package/dist/chunks/ContractLineItemCollection-BZcrlrrY.js.map +1 -0
  24. package/dist/chunks/{CustomerCollection-DQIujn4f.js → CustomerCollection-BIPH2ZUx.js} +3 -3
  25. package/dist/chunks/{CustomerCollection-DQIujn4f.js.map → CustomerCollection-BIPH2ZUx.js.map} +1 -1
  26. package/dist/chunks/{DocumentTaxCollection-BEpoqXAQ.js → DocumentTaxCollection-DDzlywMW.js} +41 -14
  27. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +1 -0
  28. package/dist/chunks/{InvoiceCollection-CKQflFjc.js → InvoiceCollection-cinzAqt6.js} +53 -164
  29. package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +1 -0
  30. package/dist/chunks/{TaxCodeCollection-DOpzW4y8.js → TaxCodeCollection-DaBOPG9c.js} +11 -4
  31. package/dist/chunks/{TaxCodeCollection-DOpzW4y8.js.map → TaxCodeCollection-DaBOPG9c.js.map} +1 -1
  32. package/dist/chunks/{TaxComponentCollection-Cs2uXwEl.js → TaxComponentCollection-C4zxnyVu.js} +3 -3
  33. package/dist/chunks/{TaxComponentCollection-Cs2uXwEl.js.map → TaxComponentCollection-C4zxnyVu.js.map} +1 -1
  34. package/dist/chunks/VendorCollection-D6ZcUYy8.js +100 -0
  35. package/dist/chunks/VendorCollection-D6ZcUYy8.js.map +1 -0
  36. package/dist/chunks/{allocation-integrity-DI3G16BN.js → allocation-integrity-WmWyXhtG.js} +6 -6
  37. package/dist/chunks/{allocation-integrity-DI3G16BN.js.map → allocation-integrity-WmWyXhtG.js.map} +1 -1
  38. package/dist/chunks/calculations-C9daQLsh.js +82 -0
  39. package/dist/chunks/calculations-C9daQLsh.js.map +1 -0
  40. package/dist/chunks/contract-money-EuToOghh.js +301 -0
  41. package/dist/chunks/contract-money-EuToOghh.js.map +1 -0
  42. package/dist/chunks/detect-CY9ufaFb.js +328 -0
  43. package/dist/chunks/detect-CY9ufaFb.js.map +1 -0
  44. package/dist/chunks/{document-money-DNVhDJyC.js → document-money-DV5t4vlK.js} +389 -63
  45. package/dist/chunks/document-money-DV5t4vlK.js.map +1 -0
  46. package/dist/chunks/{invoice-posting-BwV8g7z0.js → invoice-posting-C2i5dOVZ.js} +21 -23
  47. package/dist/chunks/invoice-posting-C2i5dOVZ.js.map +1 -0
  48. package/dist/chunks/{revenue-journal-DY9fro8E.js → revenue-journal-LLA9Kh_i.js} +3 -14
  49. package/dist/chunks/revenue-journal-LLA9Kh_i.js.map +1 -0
  50. package/dist/chunks/tenant-money-barrier-6TlqUeVj.js +255 -0
  51. package/dist/chunks/tenant-money-barrier-6TlqUeVj.js.map +1 -0
  52. package/dist/chunks/{types-LDETvQ1C.js → types-Bbm0Xflc.js} +2 -1
  53. package/dist/chunks/types-Bbm0Xflc.js.map +1 -0
  54. package/dist/chunks/{units-CNcjH_eE.js → units-8ElbjVZK.js} +18 -3
  55. package/dist/chunks/units-8ElbjVZK.js.map +1 -0
  56. package/dist/collections/ContractCollection.d.ts.map +1 -1
  57. package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
  58. package/dist/collections/InvoiceCollection.d.ts.map +1 -1
  59. package/dist/collections/InvoiceLineItemCollection.d.ts +0 -30
  60. package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
  61. package/dist/collections/TaxCodeCollection.d.ts +5 -1
  62. package/dist/collections/TaxCodeCollection.d.ts.map +1 -1
  63. package/dist/collections/TaxSettingsCollection.d.ts +15 -8
  64. package/dist/collections/TaxSettingsCollection.d.ts.map +1 -1
  65. package/dist/collections/TaxSummaryReportCollection.d.ts +3 -3
  66. package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -1
  67. package/dist/collections/index.d.ts +0 -1
  68. package/dist/collections/index.d.ts.map +1 -1
  69. package/dist/commerce-money-tools.d.ts.map +1 -1
  70. package/dist/contract-money.d.ts +1 -13
  71. package/dist/contract-money.d.ts.map +1 -1
  72. package/dist/document-money-internal.d.ts +7 -3
  73. package/dist/document-money-internal.d.ts.map +1 -1
  74. package/dist/document-money.d.ts +1 -11
  75. package/dist/document-money.d.ts.map +1 -1
  76. package/dist/index.d.ts +6 -5
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +686 -1656
  79. package/dist/index.js.map +1 -1
  80. package/dist/invoice-accounting-sync.d.ts.map +1 -1
  81. package/dist/invoice-mcp.d.ts +3 -3
  82. package/dist/invoice-mcp.d.ts.map +1 -1
  83. package/dist/invoice-posting.d.ts +11 -5
  84. package/dist/invoice-posting.d.ts.map +1 -1
  85. package/dist/invoice-record-screen.d.ts +1 -1
  86. package/dist/invoice-record-screen.d.ts.map +1 -1
  87. package/dist/invoices.d.ts +6 -1
  88. package/dist/invoices.d.ts.map +1 -1
  89. package/dist/invoices.js +2 -2
  90. package/dist/manifest.json +1041 -863
  91. package/dist/migrations/invoiceEditorStorage.d.ts +2 -4
  92. package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -1
  93. package/dist/migrations/partyLinks.d.ts.map +1 -1
  94. package/dist/models/Contract.d.ts +10 -4
  95. package/dist/models/Contract.d.ts.map +1 -1
  96. package/dist/models/ContractLineItem.d.ts +9 -17
  97. package/dist/models/ContractLineItem.d.ts.map +1 -1
  98. package/dist/models/DocumentTax.d.ts.map +1 -1
  99. package/dist/models/Invoice.d.ts +18 -48
  100. package/dist/models/Invoice.d.ts.map +1 -1
  101. package/dist/models/InvoiceLineItem.d.ts +13 -32
  102. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  103. package/dist/models/TaxSettings.d.ts +0 -10
  104. package/dist/models/TaxSettings.d.ts.map +1 -1
  105. package/dist/models/index.d.ts +1 -2
  106. package/dist/models/index.d.ts.map +1 -1
  107. package/dist/playground.js +1 -1
  108. package/dist/playground.js.map +1 -1
  109. package/dist/recipes.d.ts +8 -11
  110. package/dist/recipes.d.ts.map +1 -1
  111. package/dist/revenue-journal.d.ts +0 -8
  112. package/dist/revenue-journal.d.ts.map +1 -1
  113. package/dist/smrt-knowledge.json +2886 -6121
  114. package/dist/svelte/components/InvoiceEditor.svelte +7 -148
  115. package/dist/svelte/components/InvoiceEditor.svelte.d.ts +4 -1
  116. package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
  117. package/dist/svelte/components/InvoiceTotals.svelte +14 -118
  118. package/dist/svelte/components/InvoiceTotals.svelte.d.ts +4 -27
  119. package/dist/svelte/components/InvoiceTotals.svelte.d.ts.map +1 -1
  120. package/dist/svelte/components/MoneyInvoiceEditor.svelte +3 -23
  121. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +3 -26
  122. package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -1
  123. package/dist/svelte/index.d.ts +2 -3
  124. package/dist/svelte/index.d.ts.map +1 -1
  125. package/dist/svelte/index.js +1 -2
  126. package/dist/svelte/invoices/calculations.d.ts +3 -108
  127. package/dist/svelte/invoices/calculations.d.ts.map +1 -1
  128. package/dist/svelte/invoices/calculations.js +12 -309
  129. package/dist/svelte/invoices/messages.d.ts +1 -37
  130. package/dist/svelte/invoices/messages.d.ts.map +1 -1
  131. package/dist/svelte/invoices/messages.js +1 -37
  132. package/dist/svelte/invoices/money-editor.d.ts +1 -1
  133. package/dist/svelte/invoices/money-editor.d.ts.map +1 -1
  134. package/dist/svelte/invoices/types.d.ts +14 -114
  135. package/dist/svelte/invoices/types.d.ts.map +1 -1
  136. package/dist/svelte/playground/InvoicePreparationPreview.svelte +85 -94
  137. package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -1
  138. package/dist/svelte/playground.js +1 -1
  139. package/dist/tax/index.d.ts +2 -1
  140. package/dist/tax/index.d.ts.map +1 -1
  141. package/dist/tax/standard-codes.d.ts +12 -0
  142. package/dist/tax/standard-codes.d.ts.map +1 -0
  143. package/dist/tax/tenant-bootstrap.d.ts +29 -0
  144. package/dist/tax/tenant-bootstrap.d.ts.map +1 -0
  145. package/dist/tenant-money-barrier.d.ts +12 -44
  146. package/dist/tenant-money-barrier.d.ts.map +1 -1
  147. package/dist/types/index.d.ts +11 -23
  148. package/dist/types/index.d.ts.map +1 -1
  149. package/dist/ui.js +1 -1
  150. package/dist/ui.js.map +1 -1
  151. package/dist/upgrade/detect.d.ts +129 -0
  152. package/dist/upgrade/detect.d.ts.map +1 -0
  153. package/dist/upgrade/money-v2.d.ts +81 -0
  154. package/dist/upgrade/money-v2.d.ts.map +1 -0
  155. package/package.json +14 -13
  156. package/agents/line-amount-migration.md +0 -50
  157. package/dist/chunks/ContractLineItemCollection-DYjhj_pU.js.map +0 -1
  158. package/dist/chunks/DocumentTaxCollection-BEpoqXAQ.js.map +0 -1
  159. package/dist/chunks/InvoiceCollection-CKQflFjc.js.map +0 -1
  160. package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js +0 -467
  161. package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js.map +0 -1
  162. package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js +0 -319
  163. package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js.map +0 -1
  164. package/dist/chunks/calculations-DaHR17il.js +0 -333
  165. package/dist/chunks/calculations-DaHR17il.js.map +0 -1
  166. package/dist/chunks/document-money-DNVhDJyC.js.map +0 -1
  167. package/dist/chunks/invoice-posting-BwV8g7z0.js.map +0 -1
  168. package/dist/chunks/revenue-journal-DY9fro8E.js.map +0 -1
  169. package/dist/chunks/tenant-money-barrier-DjRXgMnX.js +0 -135
  170. package/dist/chunks/tenant-money-barrier-DjRXgMnX.js.map +0 -1
  171. package/dist/chunks/types-LDETvQ1C.js.map +0 -1
  172. package/dist/chunks/units-CNcjH_eE.js.map +0 -1
  173. package/dist/collections/MoneyMigrationDocumentCollection.d.ts +0 -12
  174. package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +0 -1
  175. package/dist/migrations/commerceLineAmounts.d.ts +0 -84
  176. package/dist/migrations/commerceLineAmounts.d.ts.map +0 -1
  177. package/dist/models/MoneyMigrationDocument.d.ts +0 -28
  178. package/dist/models/MoneyMigrationDocument.d.ts.map +0 -1
  179. package/dist/svelte/components/InvoiceLineEditor.svelte +0 -92
  180. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +0 -8
  181. package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +0 -1
  182. package/dist/svelte/invoices/fields.d.ts +0 -6
  183. package/dist/svelte/invoices/fields.d.ts.map +0 -1
  184. package/dist/svelte/invoices/fields.js +0 -22
  185. package/dist/tax/legacy.d.ts +0 -48
  186. package/dist/tax/legacy.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TaxComponentCollection-Cs2uXwEl.js","names":[],"sources":["../../src/models/TaxComponent.ts","../../src/collections/TaxComponentCollection.ts"],"sourcesContent":["import {\n crossPackageRef,\n field,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { AccountCollection } from '@happyvertical/smrt-ledgers';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport { MoneyValidationError } from '../money.js';\nimport type { TaxComponentOptions } from '../types/index.js';\n\nconst PERCENT = /^(\\d+)(?:\\.(\\d{1,6}))?$/;\n\nfunction isPercent(value: string): boolean {\n const match = PERCENT.exec(value);\n if (!match) return false;\n const scaled =\n BigInt(match[1]) * 1_000_000n + BigInt((match[2] ?? '').padEnd(6, '0'));\n return scaled <= 100_000_000n;\n}\n\nfunction canonicalPercent(value: string): string {\n const [whole, fraction = ''] = String(value).split('.');\n const normalizedWhole = whole.replace(/^0+(?=\\d)/, '') || '0';\n const normalizedFraction = fraction.replace(/0+$/, '');\n return normalizedFraction\n ? `${normalizedWhole}.${normalizedFraction}`\n : normalizedWhole;\n}\n\n/**\n * One reusable tax rate component. Codes may combine components, but the\n * component's percent is immutable after it has been saved: archive it and\n * repoint editable codes to a replacement rate instead of rewriting history.\n */\n@TenantScoped({ mode: 'required' })\n@smrt({\n api: {\n include: ['list', 'get', 'create', 'update'],\n writable: [\n 'name',\n 'percent',\n 'liabilityAccountId',\n 'recoverable',\n 'active',\n ],\n },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n})\nexport class TaxComponent extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-commerce\";\n @tenantId()\n tenantId = '';\n\n @field({ description: 'The name of this tax component, for example GST.' })\n name = '';\n\n /** Percent, not a fraction: 7.125 means 7.125%. Stored as DECIMAL. */\n @field({\n type: 'decimal',\n description: 'The tax percentage, for example 7.125 for 7.125 percent.',\n })\n percent = '0.0';\n\n @crossPackageRef('@happyvertical/smrt-ledgers:Account', {\n description: 'The liability account that receives this tax component.',\n })\n liabilityAccountId = '';\n\n @field({\n description:\n 'Whether purchases using this component may recover the full input tax. Partial recovery is not supported.',\n })\n recoverable = false;\n\n @field({ description: 'Whether new tax codes and overrides may select it.' })\n active = true;\n\n constructor(options: TaxComponentOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId ?? '';\n if (options.name !== undefined) this.name = options.name;\n if (options.percent !== undefined) this.percent = options.percent;\n if (options.liabilityAccountId !== undefined)\n this.liabilityAccountId = options.liabilityAccountId;\n if (options.recoverable !== undefined)\n this.recoverable = options.recoverable;\n if (options.active !== undefined) this.active = options.active;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n this.percent = canonicalPercent(String(this.percent));\n return this;\n }\n\n override async save(): Promise<this> {\n if (!this.name.trim())\n throw new Error('Tax component requires a nonempty name');\n if (!isPercent(this.percent)) {\n throw new MoneyValidationError(\n 'PERCENT_OUT_OF_RANGE',\n 'Tax component percent must be between 0 and 100 with at most six decimal places.',\n );\n }\n this.percent = canonicalPercent(this.percent);\n return this.withTransaction(async () => {\n const { TaxComponentCollection } = await import(\n '../collections/TaxComponentCollection.js'\n );\n const components = await TaxComponentCollection.create({ db: this.db });\n const persisted = this.id\n ? await components.get({ id: this.id }, { cache: false })\n : null;\n if (persisted && canonicalPercent(persisted.percent) !== this.percent) {\n const referenced = await this.isReferenced(persisted);\n // claimRevision advanced the row's version; the pending save must use\n // that version rather than its stale caller snapshot.\n this.updated_at = persisted.updated_at;\n if (referenced) {\n throw new MoneyValidationError(\n 'PERCENT_OUT_OF_RANGE',\n 'A referenced tax component rate is immutable; archive it and repoint its tax codes.',\n );\n }\n }\n if (this.liabilityAccountId) {\n const accounts = await AccountCollection.create({ db: this.db });\n const account = await accounts.get(\n { id: this.liabilityAccountId },\n { cache: false },\n );\n if (!account || account.tenantId !== this.tenantId) {\n throw new Error(\n 'Tax component liability account must belong to its tenant',\n );\n }\n }\n return (await super.save()) as this;\n });\n }\n\n private async isReferenced(persisted: TaxComponent): Promise<boolean> {\n if (!this.id) return false;\n if (!persisted.updated_at)\n throw new Error('Referenced tax component is missing its revision');\n // Claim before inspecting references. TaxCode.save() claims the same row\n // before attaching it, so a concurrent rate edit must reload and retry.\n await persisted.claimRevision(persisted.updated_at);\n const { TaxCodeCollection } = await import(\n '../collections/TaxCodeCollection.js'\n );\n const { DocumentTaxCollection } = await import(\n '../collections/DocumentTaxCollection.js'\n );\n const codes = await TaxCodeCollection.create({ db: this.db });\n const usedByCode = (\n await codes.list({ where: { tenantId: this.tenantId } })\n ).some((code) =>\n code.components.some((component) => component.taxComponentId === this.id),\n );\n if (usedByCode) return true;\n const documentTaxes = await DocumentTaxCollection.create({ db: this.db });\n return (\n (\n await documentTaxes.list({\n where: { tenantId: this.tenantId, taxComponentId: this.id },\n limit: 1,\n })\n ).length > 0\n );\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { TaxComponent } from '../models/TaxComponent.js';\n\nexport class TaxComponentCollection extends SmrtCollection<TaxComponent> {\n static readonly _itemClass = TaxComponent;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TaxComponentCollection-C4zxnyVu.js","names":[],"sources":["../../src/models/TaxComponent.ts","../../src/collections/TaxComponentCollection.ts"],"sourcesContent":["import {\n crossPackageRef,\n field,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { AccountCollection } from '@happyvertical/smrt-ledgers';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport { MoneyValidationError } from '../money.js';\nimport type { TaxComponentOptions } from '../types/index.js';\n\nconst PERCENT = /^(\\d+)(?:\\.(\\d{1,6}))?$/;\n\nfunction isPercent(value: string): boolean {\n const match = PERCENT.exec(value);\n if (!match) return false;\n const scaled =\n BigInt(match[1]) * 1_000_000n + BigInt((match[2] ?? '').padEnd(6, '0'));\n return scaled <= 100_000_000n;\n}\n\nfunction canonicalPercent(value: string): string {\n const [whole, fraction = ''] = String(value).split('.');\n const normalizedWhole = whole.replace(/^0+(?=\\d)/, '') || '0';\n const normalizedFraction = fraction.replace(/0+$/, '');\n return normalizedFraction\n ? `${normalizedWhole}.${normalizedFraction}`\n : normalizedWhole;\n}\n\n/**\n * One reusable tax rate component. Codes may combine components, but the\n * component's percent is immutable after it has been saved: archive it and\n * repoint editable codes to a replacement rate instead of rewriting history.\n */\n@TenantScoped({ mode: 'required' })\n@smrt({\n api: {\n include: ['list', 'get', 'create', 'update'],\n writable: [\n 'name',\n 'percent',\n 'liabilityAccountId',\n 'recoverable',\n 'active',\n ],\n },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n})\nexport class TaxComponent extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-commerce\";\n @tenantId()\n tenantId = '';\n\n @field({ description: 'The name of this tax component, for example GST.' })\n name = '';\n\n /** Percent, not a fraction: 7.125 means 7.125%. Stored as DECIMAL. */\n @field({\n type: 'decimal',\n description: 'The tax percentage, for example 7.125 for 7.125 percent.',\n })\n percent = '0.0';\n\n @crossPackageRef('@happyvertical/smrt-ledgers:Account', {\n description: 'The liability account that receives this tax component.',\n })\n liabilityAccountId = '';\n\n @field({\n description:\n 'Whether purchases using this component may recover the full input tax. Partial recovery is not supported.',\n })\n recoverable = false;\n\n @field({ description: 'Whether new tax codes and overrides may select it.' })\n active = true;\n\n constructor(options: TaxComponentOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId ?? '';\n if (options.name !== undefined) this.name = options.name;\n if (options.percent !== undefined) this.percent = options.percent;\n if (options.liabilityAccountId !== undefined)\n this.liabilityAccountId = options.liabilityAccountId;\n if (options.recoverable !== undefined)\n this.recoverable = options.recoverable;\n if (options.active !== undefined) this.active = options.active;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n this.percent = canonicalPercent(String(this.percent));\n return this;\n }\n\n override async save(): Promise<this> {\n if (!this.name.trim())\n throw new Error('Tax component requires a nonempty name');\n if (!isPercent(this.percent)) {\n throw new MoneyValidationError(\n 'PERCENT_OUT_OF_RANGE',\n 'Tax component percent must be between 0 and 100 with at most six decimal places.',\n );\n }\n this.percent = canonicalPercent(this.percent);\n return this.withTransaction(async () => {\n const { TaxComponentCollection } = await import(\n '../collections/TaxComponentCollection.js'\n );\n const components = await TaxComponentCollection.create({ db: this.db });\n const persisted = this.id\n ? await components.get({ id: this.id }, { cache: false })\n : null;\n if (persisted && canonicalPercent(persisted.percent) !== this.percent) {\n const referenced = await this.isReferenced(persisted);\n // claimRevision advanced the row's version; the pending save must use\n // that version rather than its stale caller snapshot.\n this.updated_at = persisted.updated_at;\n if (referenced) {\n throw new MoneyValidationError(\n 'PERCENT_OUT_OF_RANGE',\n 'A referenced tax component rate is immutable; archive it and repoint its tax codes.',\n );\n }\n }\n if (this.liabilityAccountId) {\n const accounts = await AccountCollection.create({ db: this.db });\n const account = await accounts.get(\n { id: this.liabilityAccountId },\n { cache: false },\n );\n if (!account || account.tenantId !== this.tenantId) {\n throw new Error(\n 'Tax component liability account must belong to its tenant',\n );\n }\n }\n return (await super.save()) as this;\n });\n }\n\n private async isReferenced(persisted: TaxComponent): Promise<boolean> {\n if (!this.id) return false;\n if (!persisted.updated_at)\n throw new Error('Referenced tax component is missing its revision');\n // Claim before inspecting references. TaxCode.save() claims the same row\n // before attaching it, so a concurrent rate edit must reload and retry.\n await persisted.claimRevision(persisted.updated_at);\n const { TaxCodeCollection } = await import(\n '../collections/TaxCodeCollection.js'\n );\n const { DocumentTaxCollection } = await import(\n '../collections/DocumentTaxCollection.js'\n );\n const codes = await TaxCodeCollection.create({ db: this.db });\n const usedByCode = (\n await codes.list({ where: { tenantId: this.tenantId } })\n ).some((code) =>\n code.components.some((component) => component.taxComponentId === this.id),\n );\n if (usedByCode) return true;\n const documentTaxes = await DocumentTaxCollection.create({ db: this.db });\n return (\n (\n await documentTaxes.list({\n where: { tenantId: this.tenantId, taxComponentId: this.id },\n limit: 1,\n })\n ).length > 0\n );\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { TaxComponent } from '../models/TaxComponent.js';\n\nexport class TaxComponentCollection extends SmrtCollection<TaxComponent> {\n static readonly _itemClass = TaxComponent;\n}\n"],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ import { S as Vendor } from "./DocumentTaxCollection-DDzlywMW.js";
2
+ import { p as VendorStatus } from "./types-Bbm0Xflc.js";
3
+ import { SmrtCollection } from "@happyvertical/smrt-core";
4
+ import { queryGlobal, queryWithGlobals } from "@happyvertical/smrt-tenancy";
5
+ //#region src/collections/VendorCollection.ts
6
+ var VendorCollection = class extends SmrtCollection {
7
+ static _itemClass = Vendor;
8
+ /**
9
+ * Find vendors by profile ID
10
+ *
11
+ * @param profileId - Profile ID from smrt-profiles
12
+ * @returns Array of vendors linked to this profile
13
+ */
14
+ async findByProfile(profileId) {
15
+ return await this.list({
16
+ where: { profileId },
17
+ orderBy: "created_at DESC"
18
+ });
19
+ }
20
+ /**
21
+ * Find all active vendors
22
+ *
23
+ * @returns Array of active vendors
24
+ */
25
+ async findActive() {
26
+ return await this.list({
27
+ where: { status: VendorStatus.ACTIVE },
28
+ orderBy: "created_at DESC"
29
+ });
30
+ }
31
+ /**
32
+ * Find vendors by status
33
+ *
34
+ * @param status - Vendor status
35
+ * @returns Array of vendors
36
+ */
37
+ async findByStatus(status) {
38
+ return await this.list({
39
+ where: { status },
40
+ orderBy: "created_at DESC"
41
+ });
42
+ }
43
+ /**
44
+ * Get or create a vendor for a profile
45
+ *
46
+ * @param profileId - Profile ID
47
+ * @param defaults - Default values if creating
48
+ * @returns Vendor
49
+ */
50
+ async getOrCreateForProfile(profileId, defaults = {}) {
51
+ const existing = await this.findByProfile(profileId);
52
+ if (existing.length > 0) return existing[0];
53
+ const vendor = await this.create({
54
+ profileId,
55
+ leadTimeDays: defaults.leadTimeDays ?? 0,
56
+ minimumOrderAmount: defaults.minimumOrderAmount ?? 0,
57
+ paymentTerms: defaults.paymentTerms ?? "",
58
+ status: VendorStatus.ACTIVE
59
+ });
60
+ await vendor.save();
61
+ return vendor;
62
+ }
63
+ /**
64
+ * Find all vendors belonging to a specific tenant
65
+ *
66
+ * @param tenantId - Tenant ID
67
+ * @returns Array of vendors for the tenant
68
+ */
69
+ async findByTenant(tenantId) {
70
+ return this.list({ where: { tenantId } });
71
+ }
72
+ /**
73
+ * Find all global vendors (not associated with any tenant).
74
+ *
75
+ * Routes through the shared tenant-global helper so it does not throw under
76
+ * an active tenant context (an explicit `tenant_id IS NULL` filter would be
77
+ * flagged as an isolation violation). (#1600)
78
+ *
79
+ * @returns Array of global vendors
80
+ */
81
+ async findGlobal() {
82
+ return queryGlobal(this);
83
+ }
84
+ /**
85
+ * Find vendors for a tenant including global vendors.
86
+ *
87
+ * Fails closed if an active tenant context requests a different tenant's
88
+ * rows; the admin/system path keeps the cross-tenant capability. (#1600)
89
+ *
90
+ * @param tenantId - Tenant ID
91
+ * @returns Array of tenant-specific and global vendors
92
+ */
93
+ async findWithGlobals(tenantId) {
94
+ return queryWithGlobals(this, tenantId, "Vendor.findWithGlobals");
95
+ }
96
+ };
97
+ //#endregion
98
+ export { VendorCollection as t };
99
+
100
+ //# sourceMappingURL=VendorCollection-D6ZcUYy8.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VendorCollection-D6ZcUYy8.js","names":[],"sources":["../../src/collections/VendorCollection.ts"],"sourcesContent":["/**\n * VendorCollection - Collection manager for Vendor objects\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { queryGlobal, queryWithGlobals } from '@happyvertical/smrt-tenancy';\nimport { Vendor } from '../models/Vendor.js';\nimport { VendorStatus } from '../types/index.js';\n\nexport class VendorCollection extends SmrtCollection<Vendor> {\n static readonly _itemClass = Vendor;\n\n /**\n * Find vendors by profile ID\n *\n * @param profileId - Profile ID from smrt-profiles\n * @returns Array of vendors linked to this profile\n */\n async findByProfile(profileId: string): Promise<Vendor[]> {\n return await this.list({\n where: { profileId },\n orderBy: 'created_at DESC',\n });\n }\n\n /**\n * Find all active vendors\n *\n * @returns Array of active vendors\n */\n async findActive(): Promise<Vendor[]> {\n return await this.list({\n where: { status: VendorStatus.ACTIVE },\n orderBy: 'created_at DESC',\n });\n }\n\n /**\n * Find vendors by status\n *\n * @param status - Vendor status\n * @returns Array of vendors\n */\n async findByStatus(status: VendorStatus): Promise<Vendor[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /**\n * Get or create a vendor for a profile\n *\n * @param profileId - Profile ID\n * @param defaults - Default values if creating\n * @returns Vendor\n */\n async getOrCreateForProfile(\n profileId: string,\n defaults: Partial<{\n leadTimeDays: number;\n minimumOrderAmount: number;\n paymentTerms: string;\n }> = {},\n ): Promise<Vendor> {\n const existing = await this.findByProfile(profileId);\n if (existing.length > 0) {\n return existing[0];\n }\n\n const vendor = await this.create({\n profileId,\n leadTimeDays: defaults.leadTimeDays ?? 0,\n minimumOrderAmount: defaults.minimumOrderAmount ?? 0,\n paymentTerms: defaults.paymentTerms ?? '',\n status: VendorStatus.ACTIVE,\n });\n await vendor.save();\n return vendor;\n }\n\n // ============================================================================\n // Tenant Helper Methods\n // ============================================================================\n\n /**\n * Find all vendors belonging to a specific tenant\n *\n * @param tenantId - Tenant ID\n * @returns Array of vendors for the tenant\n */\n async findByTenant(tenantId: string): Promise<Vendor[]> {\n return this.list({ where: { tenantId } });\n }\n\n /**\n * Find all global vendors (not associated with any tenant).\n *\n * Routes through the shared tenant-global helper so it does not throw under\n * an active tenant context (an explicit `tenant_id IS NULL` filter would be\n * flagged as an isolation violation). (#1600)\n *\n * @returns Array of global vendors\n */\n async findGlobal(): Promise<Vendor[]> {\n return queryGlobal<Vendor>(this);\n }\n\n /**\n * Find vendors for a tenant including global vendors.\n *\n * Fails closed if an active tenant context requests a different tenant's\n * rows; the admin/system path keeps the cross-tenant capability. (#1600)\n *\n * @param tenantId - Tenant ID\n * @returns Array of tenant-specific and global vendors\n */\n async findWithGlobals(tenantId: string): Promise<Vendor[]> {\n return queryWithGlobals<Vendor>(this, tenantId, 'Vendor.findWithGlobals');\n }\n}\n"],"mappings":";;;;;AAUO,IAAM,mBAAN,cAA+B,eAAuB;CAC3D,OAAgB,aAAa;;;;;;;CAQ7B,MAAM,cAAc,WAAsC;EACxD,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO,EAAE,UAAU;GACnB,SAAS;EACX,CAAC;CACH;;;;;;CAOA,MAAM,aAAgC;EACpC,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO,EAAE,QAAQ,aAAa,OAAO;GACrC,SAAS;EACX,CAAC;CACH;;;;;;;CAQA,MAAM,aAAa,QAAyC;EAC1D,OAAO,MAAM,KAAK,KAAK;GACrB,OAAO,EAAE,OAAO;GAChB,SAAS;EACX,CAAC;CACH;;;;;;;;CASA,MAAM,sBACJ,WACA,WAIK,CAAC,GACW;EACjB,MAAM,WAAW,MAAM,KAAK,cAAc,SAAS;EACnD,IAAI,SAAS,SAAS,GACpB,OAAO,SAAS;EAGlB,MAAM,SAAS,MAAM,KAAK,OAAO;GAC/B;GACA,cAAc,SAAS,gBAAgB;GACvC,oBAAoB,SAAS,sBAAsB;GACnD,cAAc,SAAS,gBAAgB;GACvC,QAAQ,aAAa;EACvB,CAAC;EACD,MAAM,OAAO,KAAK;EAClB,OAAO;CACT;;;;;;;CAYA,MAAM,aAAa,UAAqC;EACtD,OAAO,KAAK,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;CAC1C;;;;;;;;;;CAWA,MAAM,aAAgC;EACpC,OAAO,YAAoB,IAAI;CACjC;;;;;;;;;;CAWA,MAAM,gBAAgB,UAAqC;EACzD,OAAO,iBAAyB,MAAM,UAAU,wBAAwB;CAC1E;AACF"}
@@ -6,8 +6,8 @@ function allocationOwner(value) {
6
6
  }
7
7
  async function claimAllocationParent(parent, kind) {
8
8
  if (!parent.id) return;
9
- const { PaymentCollection } = await import("../index.js").then((n) => n.i);
10
- const { InvoiceCollection } = await import("./InvoiceCollection-CKQflFjc.js").then((n) => n.n);
9
+ const { PaymentCollection } = await import("../index.js").then((n) => n.r);
10
+ const { InvoiceCollection } = await import("./InvoiceCollection-cinzAqt6.js").then((n) => n.n);
11
11
  const persisted = await (kind === "payment" ? await PaymentCollection.create({ db: parent.db }) : await InvoiceCollection.create({ db: parent.db })).get({ id: parent.id }, { cache: false });
12
12
  if (!persisted) return;
13
13
  const expected = parent.isPersisted ? parent.updated_at : persisted.updated_at;
@@ -24,13 +24,13 @@ function allocationTotal(rows, excluding) {
24
24
  }
25
25
  async function assertAllocationParent(parent, kind) {
26
26
  if (!parent.id) return;
27
- const { PaymentAllocationCollection } = await import("../index.js").then((n) => n.r);
27
+ const { PaymentAllocationCollection } = await import("../index.js").then((n) => n.n);
28
28
  const allocations = await PaymentAllocationCollection.create({ db: parent.db });
29
29
  if (!(isPostgresDatabase(parent.db) ? (await parent.db.query("SELECT to_regclass(?) AS relation", [allocations.tableName])).rows[0]?.relation != null : await parent.db.tableExists(allocations.tableName))) return;
30
30
  const rows = kind === "payment" ? await allocations.findByPayment(parent.id) : await allocations.findByInvoice(parent.id);
31
31
  if (!rows.length) return;
32
- const { PaymentCollection } = await import("../index.js").then((n) => n.i);
33
- const { InvoiceCollection } = await import("./InvoiceCollection-CKQflFjc.js").then((n) => n.n);
32
+ const { PaymentCollection } = await import("../index.js").then((n) => n.r);
33
+ const { InvoiceCollection } = await import("./InvoiceCollection-cinzAqt6.js").then((n) => n.n);
34
34
  const opposite = kind === "payment" ? await InvoiceCollection.create({ db: parent.db }) : await PaymentCollection.create({ db: parent.db });
35
35
  const cap = kind === "payment" ? parent.amount : parent.totalAmount;
36
36
  if (allocationTotal(rows) > BigInt(cap)) throw new Error("Parent amount cannot be less than committed allocations");
@@ -42,4 +42,4 @@ async function assertAllocationParent(parent, kind) {
42
42
  //#endregion
43
43
  export { claimAllocationParent as i, allocationTotal as n, assertAllocationParent as r, allocationOwner as t };
44
44
 
45
- //# sourceMappingURL=allocation-integrity-DI3G16BN.js.map
45
+ //# sourceMappingURL=allocation-integrity-WmWyXhtG.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"allocation-integrity-DI3G16BN.js","names":[],"sources":["../../src/allocation-integrity.ts"],"sourcesContent":["/** Transactional allocation invariants shared by the owning models. */\nimport { isPostgresDatabase } from '@happyvertical/smrt-core';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport type { Invoice } from './models/Invoice.js';\nimport type { Payment } from './models/Payment.js';\n\nexport function allocationOwner(value: string | null): string | null {\n return value || getTenantId() || null;\n}\n\n/** Claim the persisted parent before reading dependants, including at repeatable read. */\nexport async function claimAllocationParent(\n parent: Payment | Invoice,\n kind: 'payment' | 'invoice',\n): Promise<void> {\n if (!parent.id) return;\n const { PaymentCollection } = await import(\n './collections/PaymentCollection.js'\n );\n const { InvoiceCollection } = await import(\n './collections/InvoiceCollection.js'\n );\n const collection =\n kind === 'payment'\n ? await PaymentCollection.create({ db: parent.db })\n : await InvoiceCollection.create({ db: parent.db });\n const persisted = await collection.get({ id: parent.id }, { cache: false });\n if (!persisted) return;\n const expected = parent.isPersisted\n ? parent.updated_at\n : persisted.updated_at;\n if (!expected) throw new Error('Allocation parent has no persisted revision');\n await persisted.claimRevision(expected);\n parent.updated_at = persisted.updated_at;\n}\n\n/** Exact totals must never silently exceed JavaScript's integer range. */\nexport function allocationTotal(\n rows: { amount: number; id?: string | null }[],\n excluding?: string | null,\n): bigint {\n return rows.reduce((total, row) => {\n if (row.id === excluding) return total;\n if (!Number.isSafeInteger(row.amount) || row.amount <= 0) {\n throw new Error('Invalid persisted payment allocation amount');\n }\n return total + BigInt(row.amount);\n }, 0n);\n}\n\n/** Parent corrections cannot invalidate an already committed allocation. */\nexport async function assertAllocationParent(\n parent: Payment | Invoice,\n kind: 'payment' | 'invoice',\n): Promise<void> {\n if (!parent.id) return;\n const { PaymentAllocationCollection } = await import(\n './collections/PaymentAllocationCollection.js'\n );\n const allocations = await PaymentAllocationCollection.create({\n db: parent.db,\n });\n // Payment-only deployments need not provision allocation storage. PostgreSQL's\n // information_schema hides inaccessible tables, so use its catalog lookup and\n // let the subsequent read report permission errors for an existing table.\n const exists = isPostgresDatabase(parent.db)\n ? (\n await parent.db.query('SELECT to_regclass(?) AS relation', [\n allocations.tableName,\n ])\n ).rows[0]?.relation != null\n : await parent.db.tableExists(allocations.tableName);\n if (!exists) return;\n const rows =\n kind === 'payment'\n ? await allocations.findByPayment(parent.id)\n : await allocations.findByInvoice(parent.id);\n if (!rows.length) return;\n const { PaymentCollection } = await import(\n './collections/PaymentCollection.js'\n );\n const { InvoiceCollection } = await import(\n './collections/InvoiceCollection.js'\n );\n const opposite =\n kind === 'payment'\n ? await InvoiceCollection.create({ db: parent.db })\n : await PaymentCollection.create({ db: parent.db });\n const cap =\n kind === 'payment'\n ? (parent as Payment).amount\n : (parent as Invoice).totalAmount;\n if (allocationTotal(rows) > BigInt(cap))\n throw new Error('Parent amount cannot be less than committed allocations');\n for (const row of rows) {\n const other = await opposite.get(\n { id: kind === 'payment' ? row.invoiceId : row.paymentId },\n { cache: false },\n );\n if (\n !other ||\n row.tenantId !== allocationOwner(parent.tenantId) ||\n other.tenantId !== row.tenantId ||\n (other.customerId || null) !== (parent.customerId || null) ||\n other.currency !== parent.currency\n ) {\n throw new Error(\n 'Parent correction would invalidate payment allocation tenant, customer or currency',\n );\n }\n }\n}\n"],"mappings":";;;AAMO,SAAS,gBAAgB,OAAqC;CACnE,OAAO,SAAS,YAAY,KAAK;AACnC;AAGA,eAAsB,sBACpB,QACA,MACe;CACf,IAAI,CAAC,OAAO,IAAI;CAChB,MAAM,EAAE,sBAAsB,MAAM,OAClC,cAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAEF,MAAM,EAAE,sBAAsB,MAAM,OAClC,kCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAMF,MAAM,YAAY,OAHhB,SAAS,YACL,MAAM,kBAAkB,OAAO,EAAE,IAAI,OAAO,GAAG,CAAC,IAChD,MAAM,kBAAkB,OAAO,EAAE,IAAI,OAAO,GAAG,CAAC,EAAA,CACnB,IAAI,EAAE,IAAI,OAAO,GAAG,GAAG,EAAE,OAAO,MAAM,CAAC;CAC1E,IAAI,CAAC,WAAW;CAChB,MAAM,WAAW,OAAO,cACpB,OAAO,aACP,UAAU;CACd,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,6CAA6C;CAC5E,MAAM,UAAU,cAAc,QAAQ;CACtC,OAAO,aAAa,UAAU;AAChC;AAGO,SAAS,gBACd,MACA,WACQ;CACR,OAAO,KAAK,QAAQ,OAAO,QAAQ;EACjC,IAAI,IAAI,OAAO,WAAW,OAAO;EACjC,IAAI,CAAC,OAAO,cAAc,IAAI,MAAM,KAAK,IAAI,UAAU,GACrD,MAAM,IAAI,MAAM,6CAA6C;EAE/D,OAAO,QAAQ,OAAO,IAAI,MAAM;CAClC,GAAG,EAAE;AACP;AAGA,eAAsB,uBACpB,QACA,MACe;CACf,IAAI,CAAC,OAAO,IAAI;CAChB,MAAM,EAAE,gCAAgC,MAAM,OAC5C,cAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAEF,MAAM,cAAc,MAAM,4BAA4B,OAAO,EAC3D,IAAI,OAAO,GACb,CAAC;CAWD,IAAI,EAPW,mBAAmB,OAAO,EAAE,KAErC,MAAM,OAAO,GAAG,MAAM,qCAAqC,CACzD,YAAY,SACd,CAAC,EAAA,CACD,KAAK,EAAC,EAAG,YAAY,OACvB,MAAM,OAAO,GAAG,YAAY,YAAY,SAAS,IACxC;CACb,MAAM,OACJ,SAAS,YACL,MAAM,YAAY,cAAc,OAAO,EAAE,IACzC,MAAM,YAAY,cAAc,OAAO,EAAE;CAC/C,IAAI,CAAC,KAAK,QAAQ;CAClB,MAAM,EAAE,sBAAsB,MAAM,OAClC,cAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAEF,MAAM,EAAE,sBAAsB,MAAM,OAClC,kCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAEF,MAAM,WACJ,SAAS,YACL,MAAM,kBAAkB,OAAO,EAAE,IAAI,OAAO,GAAG,CAAC,IAChD,MAAM,kBAAkB,OAAO,EAAE,IAAI,OAAO,GAAG,CAAC;CACtD,MAAM,MACJ,SAAS,YACJ,OAAmB,SACnB,OAAmB;CAC1B,IAAI,gBAAgB,IAAI,IAAI,OAAO,GAAG,GACpC,MAAM,IAAI,MAAM,yDAAyD;CAC3E,KAAA,MAAW,OAAO,MAAM;EACtB,MAAM,QAAQ,MAAM,SAAS,IAC3B,EAAE,IAAI,SAAS,YAAY,IAAI,YAAY,IAAI,UAAU,GACzD,EAAE,OAAO,MAAM,CACjB;EACA,IACE,CAAC,SACD,IAAI,aAAa,gBAAgB,OAAO,QAAQ,KAChD,MAAM,aAAa,IAAI,aACtB,MAAM,cAAc,WAAW,OAAO,cAAc,SACrD,MAAM,aAAa,OAAO,UAE1B,MAAM,IAAI,MACR,oFACF;CAEJ;AACF"}
1
+ {"version":3,"file":"allocation-integrity-WmWyXhtG.js","names":[],"sources":["../../src/allocation-integrity.ts"],"sourcesContent":["/** Transactional allocation invariants shared by the owning models. */\nimport { isPostgresDatabase } from '@happyvertical/smrt-core';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport type { Invoice } from './models/Invoice.js';\nimport type { Payment } from './models/Payment.js';\n\nexport function allocationOwner(value: string | null): string | null {\n return value || getTenantId() || null;\n}\n\n/** Claim the persisted parent before reading dependants, including at repeatable read. */\nexport async function claimAllocationParent(\n parent: Payment | Invoice,\n kind: 'payment' | 'invoice',\n): Promise<void> {\n if (!parent.id) return;\n const { PaymentCollection } = await import(\n './collections/PaymentCollection.js'\n );\n const { InvoiceCollection } = await import(\n './collections/InvoiceCollection.js'\n );\n const collection =\n kind === 'payment'\n ? await PaymentCollection.create({ db: parent.db })\n : await InvoiceCollection.create({ db: parent.db });\n const persisted = await collection.get({ id: parent.id }, { cache: false });\n if (!persisted) return;\n const expected = parent.isPersisted\n ? parent.updated_at\n : persisted.updated_at;\n if (!expected) throw new Error('Allocation parent has no persisted revision');\n await persisted.claimRevision(expected);\n parent.updated_at = persisted.updated_at;\n}\n\n/** Exact totals must never silently exceed JavaScript's integer range. */\nexport function allocationTotal(\n rows: { amount: number; id?: string | null }[],\n excluding?: string | null,\n): bigint {\n return rows.reduce((total, row) => {\n if (row.id === excluding) return total;\n if (!Number.isSafeInteger(row.amount) || row.amount <= 0) {\n throw new Error('Invalid persisted payment allocation amount');\n }\n return total + BigInt(row.amount);\n }, 0n);\n}\n\n/** Parent corrections cannot invalidate an already committed allocation. */\nexport async function assertAllocationParent(\n parent: Payment | Invoice,\n kind: 'payment' | 'invoice',\n): Promise<void> {\n if (!parent.id) return;\n const { PaymentAllocationCollection } = await import(\n './collections/PaymentAllocationCollection.js'\n );\n const allocations = await PaymentAllocationCollection.create({\n db: parent.db,\n });\n // Payment-only deployments need not provision allocation storage. PostgreSQL's\n // information_schema hides inaccessible tables, so use its catalog lookup and\n // let the subsequent read report permission errors for an existing table.\n const exists = isPostgresDatabase(parent.db)\n ? (\n await parent.db.query('SELECT to_regclass(?) AS relation', [\n allocations.tableName,\n ])\n ).rows[0]?.relation != null\n : await parent.db.tableExists(allocations.tableName);\n if (!exists) return;\n const rows =\n kind === 'payment'\n ? await allocations.findByPayment(parent.id)\n : await allocations.findByInvoice(parent.id);\n if (!rows.length) return;\n const { PaymentCollection } = await import(\n './collections/PaymentCollection.js'\n );\n const { InvoiceCollection } = await import(\n './collections/InvoiceCollection.js'\n );\n const opposite =\n kind === 'payment'\n ? await InvoiceCollection.create({ db: parent.db })\n : await PaymentCollection.create({ db: parent.db });\n const cap =\n kind === 'payment'\n ? (parent as Payment).amount\n : (parent as Invoice).totalAmount;\n if (allocationTotal(rows) > BigInt(cap))\n throw new Error('Parent amount cannot be less than committed allocations');\n for (const row of rows) {\n const other = await opposite.get(\n { id: kind === 'payment' ? row.invoiceId : row.paymentId },\n { cache: false },\n );\n if (\n !other ||\n row.tenantId !== allocationOwner(parent.tenantId) ||\n other.tenantId !== row.tenantId ||\n (other.customerId || null) !== (parent.customerId || null) ||\n other.currency !== parent.currency\n ) {\n throw new Error(\n 'Parent correction would invalidate payment allocation tenant, customer or currency',\n );\n }\n }\n}\n"],"mappings":";;;AAMO,SAAS,gBAAgB,OAAqC;CACnE,OAAO,SAAS,YAAY,KAAK;AACnC;AAGA,eAAsB,sBACpB,QACA,MACe;CACf,IAAI,CAAC,OAAO,IAAI;CAChB,MAAM,EAAE,sBAAsB,MAAM,OAClC,cAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAEF,MAAM,EAAE,sBAAsB,MAAM,OAClC,kCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAMF,MAAM,YAAY,OAHhB,SAAS,YACL,MAAM,kBAAkB,OAAO,EAAE,IAAI,OAAO,GAAG,CAAC,IAChD,MAAM,kBAAkB,OAAO,EAAE,IAAI,OAAO,GAAG,CAAC,EAAA,CACnB,IAAI,EAAE,IAAI,OAAO,GAAG,GAAG,EAAE,OAAO,MAAM,CAAC;CAC1E,IAAI,CAAC,WAAW;CAChB,MAAM,WAAW,OAAO,cACpB,OAAO,aACP,UAAU;CACd,IAAI,CAAC,UAAU,MAAM,IAAI,MAAM,6CAA6C;CAC5E,MAAM,UAAU,cAAc,QAAQ;CACtC,OAAO,aAAa,UAAU;AAChC;AAGO,SAAS,gBACd,MACA,WACQ;CACR,OAAO,KAAK,QAAQ,OAAO,QAAQ;EACjC,IAAI,IAAI,OAAO,WAAW,OAAO;EACjC,IAAI,CAAC,OAAO,cAAc,IAAI,MAAM,KAAK,IAAI,UAAU,GACrD,MAAM,IAAI,MAAM,6CAA6C;EAE/D,OAAO,QAAQ,OAAO,IAAI,MAAM;CAClC,GAAG,EAAE;AACP;AAGA,eAAsB,uBACpB,QACA,MACe;CACf,IAAI,CAAC,OAAO,IAAI;CAChB,MAAM,EAAE,gCAAgC,MAAM,OAC5C,cAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAEF,MAAM,cAAc,MAAM,4BAA4B,OAAO,EAC3D,IAAI,OAAO,GACb,CAAC;CAWD,IAAI,EAPW,mBAAmB,OAAO,EAAE,KAErC,MAAM,OAAO,GAAG,MAAM,qCAAqC,CACzD,YAAY,SACd,CAAC,EAAA,CACD,KAAK,EAAC,EAAG,YAAY,OACvB,MAAM,OAAO,GAAG,YAAY,YAAY,SAAS,IACxC;CACb,MAAM,OACJ,SAAS,YACL,MAAM,YAAY,cAAc,OAAO,EAAE,IACzC,MAAM,YAAY,cAAc,OAAO,EAAE;CAC/C,IAAI,CAAC,KAAK,QAAQ;CAClB,MAAM,EAAE,sBAAsB,MAAM,OAClC,cAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAEF,MAAM,EAAE,sBAAsB,MAAM,OAClC,kCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;CAEF,MAAM,WACJ,SAAS,YACL,MAAM,kBAAkB,OAAO,EAAE,IAAI,OAAO,GAAG,CAAC,IAChD,MAAM,kBAAkB,OAAO,EAAE,IAAI,OAAO,GAAG,CAAC;CACtD,MAAM,MACJ,SAAS,YACJ,OAAmB,SACnB,OAAmB;CAC1B,IAAI,gBAAgB,IAAI,IAAI,OAAO,GAAG,GACpC,MAAM,IAAI,MAAM,yDAAyD;CAC3E,KAAA,MAAW,OAAO,MAAM;EACtB,MAAM,QAAQ,MAAM,SAAS,IAC3B,EAAE,IAAI,SAAS,YAAY,IAAI,YAAY,IAAI,UAAU,GACzD,EAAE,OAAO,MAAM,CACjB;EACA,IACE,CAAC,SACD,IAAI,aAAa,gBAAgB,OAAO,QAAQ,KAChD,MAAM,aAAa,IAAI,aACtB,MAAM,cAAc,WAAW,OAAO,cAAc,SACrD,MAAM,aAAa,OAAO,UAE1B,MAAM,IAAI,MACR,oFACF;CAEJ;AACF"}
@@ -0,0 +1,82 @@
1
+ import { MoneyValidationError, calculateMoneyDocument } from "../money.js";
2
+ //#region src/svelte/invoices/calculations.ts
3
+ function numberDecimal(value, precision) {
4
+ if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) throw new RangeError("Invoice rate or quantity is outside the supported range.");
5
+ const [mantissa, exponent = "0"] = value.toString().split("e");
6
+ const [whole, fraction = ""] = mantissa.split(".");
7
+ const places = fraction.length - Number(exponent);
8
+ if (places > precision) throw new RangeError("Invoice rate or quantity exceeds the supported decimal precision.");
9
+ const coefficient = BigInt(whole + fraction);
10
+ return places < 0 ? {
11
+ coefficient: coefficient * 10n ** BigInt(-places),
12
+ scale: 1n
13
+ } : {
14
+ coefficient,
15
+ scale: 10n ** BigInt(places)
16
+ };
17
+ }
18
+ function decimalText(value) {
19
+ if (value.scale === 1n) return value.coefficient.toString();
20
+ const places = value.scale.toString().length - 1;
21
+ const digits = value.coefficient.toString().padStart(places + 1, "0");
22
+ const split = digits.length - places;
23
+ return `${digits.slice(0, split)}.${digits.slice(split)}`.replace(/0+$/, "").replace(/\.$/, "");
24
+ }
25
+ function rateTaxCode(percent) {
26
+ const normalized = percent.replace(/^0+(?=\d)/, "").replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "") || "0";
27
+ return normalized === "0" ? {
28
+ id: "rate-zero",
29
+ kind: "zero-rated",
30
+ components: []
31
+ } : {
32
+ id: `rate-${normalized}`,
33
+ kind: "taxable",
34
+ components: [{
35
+ taxComponentId: `rate-${normalized}`,
36
+ percent: normalized,
37
+ compound: false
38
+ }]
39
+ };
40
+ }
41
+ function calculateInvoiceMinorLine(input) {
42
+ for (const amount of [input.unitPrice, input.discount]) if (!Number.isSafeInteger(amount)) throw new RangeError("Invoice money must be safe integer minor units.");
43
+ if (input.taxRate > 1) throw new RangeError("Invoice tax rate is outside the supported range.");
44
+ numberDecimal(input.quantity, 6);
45
+ const tax = numberDecimal(input.taxRate, 8);
46
+ const percent = decimalText({
47
+ coefficient: tax.coefficient * 100n,
48
+ scale: tax.scale
49
+ });
50
+ let result;
51
+ try {
52
+ result = calculateMoneyDocument({
53
+ currency: "USD",
54
+ lineAmountType: "exclusive",
55
+ roundingScope: "line",
56
+ lines: [{
57
+ id: "line",
58
+ quantity: input.quantity,
59
+ unitPrice: input.unitPrice,
60
+ discount: input.discount === 0 ? { kind: "none" } : {
61
+ kind: "amount",
62
+ minor: input.discount
63
+ },
64
+ taxCode: rateTaxCode(percent)
65
+ }]
66
+ }).lines[0];
67
+ } catch (cause) {
68
+ if (cause instanceof MoneyValidationError) throw new RangeError(cause.message, { cause });
69
+ throw cause;
70
+ }
71
+ return {
72
+ grossMinor: result.grossMinor,
73
+ discountMinor: result.discountMinor,
74
+ subtotalMinor: result.netMinor,
75
+ taxMinor: result.taxMinor,
76
+ totalMinor: result.netMinor + result.taxMinor
77
+ };
78
+ }
79
+ //#endregion
80
+ export { calculateInvoiceMinorLine as t };
81
+
82
+ //# sourceMappingURL=calculations-C9daQLsh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculations-C9daQLsh.js","names":[],"sources":["../../src/svelte/invoices/calculations.ts"],"sourcesContent":["/** Exact, transport-neutral invoice line calculation shared by browser and server. */\nimport {\n calculateMoneyDocument,\n type MoneyLineResult,\n type MoneyTaxCodeInput,\n MoneyValidationError,\n} from '../../money.js';\n\n/** All monetary outputs are safe integer minor units. */\nexport interface InvoiceCalculatedAmounts {\n /** Rounded quantity times unit price before discounts. */\n grossMinor: number;\n /** Discount in minor units before tax; it carries the gross sign. */\n discountMinor: number;\n /** Gross less discount. */\n subtotalMinor: number;\n /** Rounded tax on the discounted subtotal. */\n taxMinor: number;\n /** Subtotal plus tax. */\n totalMinor: number;\n}\n\ninterface Decimal {\n coefficient: bigint;\n scale: bigint;\n}\n\n/** Existing model monetary inputs, independent of currency display precision. */\nexport interface InvoiceMinorLineInput {\n /** Nonnegative quantity with at most six fractional digits. */\n quantity: number;\n /** Signed integer minor units; negative prices support existing credit lines. */\n unitPrice: number;\n /** Flat discount in signed integer minor units; it carries the gross sign. */\n discount: number;\n /** Resolved tax fraction, at most eight fractional digits, in [0, 1]. */\n taxRate: number;\n}\n\nfunction numberDecimal(value: number, precision: number): Decimal {\n if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER)\n throw new RangeError(\n 'Invoice rate or quantity is outside the supported range.',\n );\n // The shortest canonical decimal preserves the caller's Number. toFixed can\n // invent different decimal digits that round back to that same binary value.\n const [mantissa, exponent = '0'] = value.toString().split('e');\n const [whole, fraction = ''] = mantissa.split('.');\n const places = fraction.length - Number(exponent);\n if (places > precision)\n throw new RangeError(\n 'Invoice rate or quantity exceeds the supported decimal precision.',\n );\n const coefficient = BigInt(whole + fraction);\n return places < 0\n ? { coefficient: coefficient * 10n ** BigInt(-places), scale: 1n }\n : { coefficient, scale: 10n ** BigInt(places) };\n}\n\nfunction decimalText(value: Decimal): string {\n if (value.scale === 1n) return value.coefficient.toString();\n const places = value.scale.toString().length - 1;\n const digits = value.coefficient.toString().padStart(places + 1, '0');\n const split = digits.length - places;\n return `${digits.slice(0, split)}.${digits.slice(split)}`\n .replace(/0+$/, '')\n .replace(/\\.$/, '');\n}\n\n/** An in-memory single-rate tax code for editor previews of a percent rate. */\nfunction rateTaxCode(percent: string): MoneyTaxCodeInput {\n const normalized =\n percent\n .replace(/^0+(?=\\d)/, '')\n .replace(/(\\.\\d*?)0+$/, '$1')\n .replace(/\\.$/, '') || '0';\n return normalized === '0'\n ? { id: 'rate-zero', kind: 'zero-rated', components: [] }\n : {\n id: `rate-${normalized}`,\n kind: 'taxable',\n components: [\n {\n taxComponentId: `rate-${normalized}`,\n percent: normalized,\n compound: false,\n },\n ],\n };\n}\n\n/**\n * Exact model calculation, including existing negative-price credit lines.\n * Throws before persistence for unsafe money, invalid quantity, or invalid tax.\n */\nexport function calculateInvoiceMinorLine(\n input: InvoiceMinorLineInput,\n): InvoiceCalculatedAmounts {\n for (const amount of [input.unitPrice, input.discount])\n if (!Number.isSafeInteger(amount))\n throw new RangeError('Invoice money must be safe integer minor units.');\n if (input.taxRate > 1)\n throw new RangeError('Invoice tax rate is outside the supported range.');\n numberDecimal(input.quantity, 6);\n const tax = numberDecimal(input.taxRate, 8);\n const percent = decimalText({\n coefficient: tax.coefficient * 100n,\n scale: tax.scale,\n });\n let result: MoneyLineResult;\n try {\n result = calculateMoneyDocument({\n currency: 'USD',\n lineAmountType: 'exclusive',\n roundingScope: 'line',\n lines: [\n {\n id: 'line',\n quantity: input.quantity,\n unitPrice: input.unitPrice,\n discount:\n input.discount === 0\n ? { kind: 'none' }\n : { kind: 'amount', minor: input.discount },\n taxCode: rateTaxCode(percent),\n },\n ],\n }).lines[0];\n } catch (cause) {\n if (cause instanceof MoneyValidationError)\n throw new RangeError(cause.message, { cause });\n throw cause;\n }\n return {\n grossMinor: result.grossMinor,\n discountMinor: result.discountMinor,\n subtotalMinor: result.netMinor,\n taxMinor: result.taxMinor,\n totalMinor: result.netMinor + result.taxMinor,\n };\n}\n"],"mappings":";;AAuCA,SAAS,cAAc,OAAe,WAA4B;CAChE,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,KAAK,QAAQ,OAAO,kBACzD,MAAM,IAAI,WACR,0DACF;CAGF,MAAM,CAAC,UAAU,WAAW,OAAO,MAAM,SAAS,CAAA,CAAE,MAAM,GAAG;CAC7D,MAAM,CAAC,OAAO,WAAW,MAAM,SAAS,MAAM,GAAG;CACjD,MAAM,SAAS,SAAS,SAAS,OAAO,QAAQ;CAChD,IAAI,SAAS,WACX,MAAM,IAAI,WACR,mEACF;CACF,MAAM,cAAc,OAAO,QAAQ,QAAQ;CAC3C,OAAO,SAAS,IACZ;EAAE,aAAa,cAAc,OAAO,OAAO,CAAC,MAAM;EAAG,OAAO;CAAG,IAC/D;EAAE;EAAa,OAAO,OAAO,OAAO,MAAM;CAAE;AAClD;AAEA,SAAS,YAAY,OAAwB;CAC3C,IAAI,MAAM,UAAU,IAAI,OAAO,MAAM,YAAY,SAAS;CAC1D,MAAM,SAAS,MAAM,MAAM,SAAS,CAAA,CAAE,SAAS;CAC/C,MAAM,SAAS,MAAM,YAAY,SAAS,CAAA,CAAE,SAAS,SAAS,GAAG,GAAG;CACpE,MAAM,QAAQ,OAAO,SAAS;CAC9B,OAAO,GAAG,OAAO,MAAM,GAAG,KAAK,EAAC,GAAI,OAAO,MAAM,KAAK,IACnD,QAAQ,OAAO,EAAE,CAAA,CACjB,QAAQ,OAAO,EAAE;AACtB;AAGA,SAAS,YAAY,SAAoC;CACvD,MAAM,aACJ,QACG,QAAQ,aAAa,EAAE,CAAA,CACvB,QAAQ,eAAe,IAAI,CAAA,CAC3B,QAAQ,OAAO,EAAE,KAAK;CAC3B,OAAO,eAAe,MAClB;EAAE,IAAI;EAAa,MAAM;EAAc,YAAY,CAAC;CAAE,IACtD;EACE,IAAI,QAAQ;EACZ,MAAM;EACN,YAAY,CACV;GACE,gBAAgB,QAAQ;GACxB,SAAS;GACT,UAAU;EACZ,CACF;CACF;AACN;AAMO,SAAS,0BACd,OAC0B;CAC1B,KAAA,MAAW,UAAU,CAAC,MAAM,WAAW,MAAM,QAAQ,GACnD,IAAI,CAAC,OAAO,cAAc,MAAM,GAC9B,MAAM,IAAI,WAAW,iDAAiD;CAC1E,IAAI,MAAM,UAAU,GAClB,MAAM,IAAI,WAAW,kDAAkD;CACzE,cAAc,MAAM,UAAU,CAAC;CAC/B,MAAM,MAAM,cAAc,MAAM,SAAS,CAAC;CAC1C,MAAM,UAAU,YAAY;EAC1B,aAAa,IAAI,cAAc;EAC/B,OAAO,IAAI;CACb,CAAC;CACD,IAAI;CACJ,IAAI;EACF,SAAS,uBAAuB;GAC9B,UAAU;GACV,gBAAgB;GAChB,eAAe;GACf,OAAO,CACL;IACE,IAAI;IACJ,UAAU,MAAM;IAChB,WAAW,MAAM;IACjB,UACE,MAAM,aAAa,IACf,EAAE,MAAM,OAAO,IACf;KAAE,MAAM;KAAU,OAAO,MAAM;IAAS;IAC9C,SAAS,YAAY,OAAO;GAC9B,CACF;EACF,CAAC,CAAA,CAAE,MAAM;CACX,SAAS,OAAO;EACd,IAAI,iBAAiB,sBACnB,MAAM,IAAI,WAAW,MAAM,SAAS,EAAE,MAAM,CAAC;EAC/C,MAAM;CACR;CACA,OAAO;EACL,YAAY,OAAO;EACnB,eAAe,OAAO;EACtB,eAAe,OAAO;EACtB,UAAU,OAAO;EACjB,YAAY,OAAO,WAAW,OAAO;CACvC;AACF"}
@@ -0,0 +1,301 @@
1
+ import { b as classifyContractPurchase, i as ContractCollection, t as DocumentTaxCollection } from "./DocumentTaxCollection-DDzlywMW.js";
2
+ import { t as ContractStatus } from "./types-Bbm0Xflc.js";
3
+ import { t as CustomerCollection } from "./CustomerCollection-BIPH2ZUx.js";
4
+ import { MoneyValidationError, calculateMoneyDocument, resolveLineTaxCode } from "../money.js";
5
+ import { m as lockDocumentMoneyParents, p as DOCUMENT_MONEY_WRITE } from "./detect-CY9ufaFb.js";
6
+ import { r as withTenantMoneyRuntime } from "./tenant-money-barrier-6TlqUeVj.js";
7
+ import { t as TaxCodeCollection } from "./TaxCodeCollection-DaBOPG9c.js";
8
+ import { t as ContractLineItemCollection } from "./ContractLineItemCollection-BZcrlrrY.js";
9
+ import { t as TaxComponentCollection } from "./TaxComponentCollection-C4zxnyVu.js";
10
+ import { t as VendorCollection } from "./VendorCollection-D6ZcUYy8.js";
11
+ import { getTenantId } from "@happyvertical/smrt-tenancy";
12
+ //#region src/contract-money.ts
13
+ var FROZEN_CONTRACT_FIELDS = [
14
+ "tenantId",
15
+ "subtotal",
16
+ "discountTotal",
17
+ "taxAmount",
18
+ "totalAmount",
19
+ "currency",
20
+ "lineAmountType",
21
+ "taxRoundingScope",
22
+ "sellerTaxRegistrationsJson",
23
+ "partyTaxId",
24
+ "moneySnapshotId",
25
+ "reference",
26
+ "issueDate"
27
+ ];
28
+ var FROZEN_LINE_FIELDS = [
29
+ "tenantId",
30
+ "contractId",
31
+ "description",
32
+ "productId",
33
+ "sku",
34
+ "quantity",
35
+ "unitPrice",
36
+ "discountType",
37
+ "discountRate",
38
+ "discount",
39
+ "taxMode",
40
+ "taxCodeId",
41
+ "taxCodeCode",
42
+ "taxCodeName",
43
+ "taxComponentsJson",
44
+ "taxAmount",
45
+ "taxRate",
46
+ "amount",
47
+ "startDate",
48
+ "endDate",
49
+ "billingPeriod",
50
+ "sortOrder",
51
+ "moneySnapshotId"
52
+ ];
53
+ function required(value, label) {
54
+ if (!value) throw new MoneyValidationError("INVALID_MONEY_INPUT", `${label} requires an ID`);
55
+ return value;
56
+ }
57
+ function same(a, b) {
58
+ const value = (item) => item instanceof Date ? item.toISOString() : item === "" || item === void 0 ? null : item;
59
+ return value(a) === value(b);
60
+ }
61
+ function assertUnchanged(proposed, stored, fields, label) {
62
+ for (const field of fields) if (!same(proposed[field], stored[field])) throw new MoneyValidationError("FROZEN_DOCUMENT", `${label} ${String(field)} is frozen`);
63
+ }
64
+ function isFrozen(contract) {
65
+ return contract.status !== ContractStatus.DRAFT || !!contract.moneySnapshotId;
66
+ }
67
+ function direction(contract) {
68
+ return classifyContractPurchase(contract) ? "purchases" : "sales";
69
+ }
70
+ function discount(line) {
71
+ if (line.discountType === "percent") return {
72
+ kind: "percent",
73
+ rate: String(line.discountRate)
74
+ };
75
+ if (line.discountType === "amount") return {
76
+ kind: "amount",
77
+ minor: line.discount
78
+ };
79
+ return { kind: "none" };
80
+ }
81
+ async function loadContract(db, id) {
82
+ return (await ContractCollection.create({ db })).get({ id }, { cache: false });
83
+ }
84
+ async function withLockedContract(db, tenantId, contractId, operation) {
85
+ const contracts = await ContractCollection.create({ db });
86
+ await lockDocumentMoneyParents([{
87
+ id: contractId,
88
+ tenantId,
89
+ collection: contracts
90
+ }]);
91
+ const contract = await contracts.get({ id: contractId }, { cache: false });
92
+ if (!contract || contract.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Contract was not found in its tenant");
93
+ return operation(contract, await (await ContractLineItemCollection.create({ db })).findByContract(contractId));
94
+ }
95
+ async function recompute(db, contract, lines, settings, persist, snapshotId = "") {
96
+ const tenantId = required(contract.tenantId, "Contract tenant");
97
+ const codes = await (await TaxCodeCollection.create({ db })).list({ where: { tenantId } });
98
+ const components = await (await TaxComponentCollection.create({ db })).list({ where: { tenantId } });
99
+ const byCode = Object.fromEntries(codes.flatMap((code) => {
100
+ if (!code.id) return [];
101
+ return [[code.id, {
102
+ id: code.id,
103
+ kind: code.kind,
104
+ active: code.active,
105
+ appliesTo: code.appliesTo,
106
+ components: code.components.map((part) => ({
107
+ taxComponentId: part.taxComponentId,
108
+ percent: components.find((component) => component.id === part.taxComponentId)?.percent ?? "0",
109
+ compound: part.compound
110
+ }))
111
+ }]];
112
+ }));
113
+ const codeById = new Map(codes.flatMap((code) => code.id ? [[code.id, code]] : []));
114
+ const productIds = [...new Set(lines.map((line) => line.productId).filter((productId) => !!productId))];
115
+ const productById = /* @__PURE__ */ new Map();
116
+ if (productIds.length > 0) {
117
+ const { ProductCollection } = await import("@happyvertical/smrt-products/collections");
118
+ const products = await ProductCollection.create({ db });
119
+ for (const productId of productIds) {
120
+ const product = await products.get({ id: productId }, { cache: false });
121
+ if (!product) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Contract product ${productId} was not found`);
122
+ if (product.tenantId !== null && product.tenantId !== tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Contract product ${productId} must be global or belong to its tenant`);
123
+ productById.set(productId, product);
124
+ }
125
+ }
126
+ const customer = "customerId" in contract && contract.customerId ? await (await CustomerCollection.create({ db })).get({ id: contract.customerId }, { cache: false }) : null;
127
+ const vendor = "vendorId" in contract && contract.vendorId ? await (await VendorCollection.create({ db })).get({ id: contract.vendorId }, { cache: false }) : null;
128
+ const resolved = /* @__PURE__ */ new Map();
129
+ const result = calculateMoneyDocument({
130
+ currency: contract.currency,
131
+ lineAmountType: contract.lineAmountType,
132
+ roundingScope: contract.taxRoundingScope,
133
+ lines: lines.map((line) => {
134
+ const selected = resolveLineTaxCode({
135
+ lineAmountType: contract.lineAmountType,
136
+ lineOverride: line.taxMode === "override" ? line.taxCodeId : null
137
+ }, {
138
+ direction: direction(contract),
139
+ codes: byCode,
140
+ customerTaxExempt: customer?.taxExempt ?? false,
141
+ exemptCodeId: codes.find((code) => code.kind === "exempt")?.id ?? null,
142
+ productCodeId: line.productId ? direction(contract) === "sales" ? productById.get(line.productId)?.defaultSalesTaxCodeId || null : productById.get(line.productId)?.defaultPurchaseTaxCodeId || null : null,
143
+ partyCodeId: customer?.defaultTaxCodeId ?? vendor?.defaultTaxCodeId ?? null,
144
+ appDefaultCodeId: direction(contract) === "sales" ? settings.defaultSalesTaxCodeId : settings.defaultPurchaseTaxCodeId,
145
+ outOfScopeCodeId: codes.find((code) => code.kind === "out-of-scope")?.id ?? null
146
+ });
147
+ resolved.set(required(line.id, "Contract line"), selected.taxCode?.id ?? null);
148
+ return {
149
+ id: required(line.id, "Contract line"),
150
+ quantity: line.quantity,
151
+ unitPrice: line.unitPrice,
152
+ discount: discount(line),
153
+ taxCode: selected.taxCode
154
+ };
155
+ })
156
+ });
157
+ const resultById = new Map(result.lines.map((row) => [row.id, row]));
158
+ for (const line of lines) {
159
+ const lineId = required(line.id, "Contract line");
160
+ const row = resultById.get(lineId);
161
+ if (!row) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Calculated Contract line ${lineId} was not loaded`);
162
+ const codeId = resolved.get(lineId) ?? null;
163
+ const code = codeId ? codeById.get(codeId) ?? null : null;
164
+ line.discount = row.discountMinor;
165
+ line.amount = row.amountMinor;
166
+ line.taxAmount = row.taxMinor;
167
+ line.taxCodeId = code?.id ?? null;
168
+ line.taxCodeCode = code?.code ?? null;
169
+ line.taxCodeName = code?.name ?? null;
170
+ line.taxComponents = row.components.map((part) => {
171
+ const component = components.find((item) => item.id === part.taxComponentId);
172
+ if (!component) throw new MoneyValidationError("TAX_CODE_NOT_FOUND", `Tax component ${part.taxComponentId} was not loaded`);
173
+ return {
174
+ taxComponentId: part.taxComponentId,
175
+ name: component.name,
176
+ percent: component.percent,
177
+ taxMinor: part.taxMinor
178
+ };
179
+ });
180
+ line.taxRate = line.taxComponents.reduce((sum, part) => sum + Number(part.percent) / 100, 0);
181
+ if (snapshotId) line.moneySnapshotId = snapshotId;
182
+ if (persist) await line.saveDerived(DOCUMENT_MONEY_WRITE);
183
+ }
184
+ const taxRows = result.taxes.map((tax) => {
185
+ const code = tax.taxCodeId ? codeById.get(tax.taxCodeId) ?? null : null;
186
+ const component = tax.taxComponentId ? components.find((item) => item.id === tax.taxComponentId) ?? null : null;
187
+ return {
188
+ tenantId,
189
+ documentType: "contract",
190
+ documentId: required(contract.id, "Contract"),
191
+ currency: contract.currency,
192
+ taxCodeId: code?.id ?? null,
193
+ taxCodeCode: code?.code ?? null,
194
+ kind: code?.kind ?? null,
195
+ taxComponentId: component?.id ?? null,
196
+ taxComponentName: component?.name ?? null,
197
+ percent: component?.percent ?? null,
198
+ liabilityAccountId: component?.liabilityAccountId ?? null,
199
+ taxableMinor: tax.taxableMinor,
200
+ taxMinor: tax.taxMinor,
201
+ recoverableMinor: direction(contract) === "purchases" && component?.recoverable ? tax.taxMinor : 0
202
+ };
203
+ });
204
+ contract.subtotal = result.subtotalMinor;
205
+ contract.discountTotal = result.discountTotalMinor;
206
+ contract.taxAmount = result.taxMinor;
207
+ contract.totalAmount = result.totalMinor;
208
+ if (persist) await (await DocumentTaxCollection.create({ db })).replaceDerivedForDocument(DOCUMENT_MONEY_WRITE, {
209
+ tenantId,
210
+ documentType: "contract",
211
+ documentId: required(contract.id, "Contract"),
212
+ rows: taxRows
213
+ });
214
+ return taxRows;
215
+ }
216
+ function requireContractTenant(tenantId) {
217
+ if (!tenantId) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Commerce money documents require a tenant");
218
+ return tenantId;
219
+ }
220
+ async function saveContract(input) {
221
+ const tenantId = requireContractTenant(input.tenantId ?? getTenantId() ?? null);
222
+ return withTenantMoneyRuntime(input.db, tenantId, async (scope) => input.withDatabase(scope.db, async (contract) => {
223
+ const settings = scope.settings;
224
+ const existing = contract.id ? await loadContract(scope.db, contract.id) : null;
225
+ if (existing && existing.tenantId !== scope.tenantId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Contract tenant identity cannot change");
226
+ if (!existing) {
227
+ contract.tenantId = tenantId;
228
+ contract.lineAmountType = settings.defaultLineAmountType;
229
+ contract.taxRoundingScope = settings.taxRoundingScope;
230
+ contract.subtotal = 0;
231
+ contract.discountTotal = 0;
232
+ contract.taxAmount = 0;
233
+ contract.totalAmount = 0;
234
+ if (contract.status !== ContractStatus.DRAFT) {
235
+ contract.moneySnapshotId = crypto.randomUUID();
236
+ contract.sellerTaxRegistrations = structuredClone(settings.taxRegistrations);
237
+ }
238
+ return contract.saveDerived(DOCUMENT_MONEY_WRITE);
239
+ }
240
+ return withLockedContract(scope.db, tenantId, required(existing.id, "Contract"), async (stored, lines) => {
241
+ if (isFrozen(stored)) {
242
+ assertUnchanged(contract, stored, FROZEN_CONTRACT_FIELDS, "Contract");
243
+ return contract.saveDerived(DOCUMENT_MONEY_WRITE);
244
+ }
245
+ const freezing = contract.status !== ContractStatus.DRAFT;
246
+ const snapshotId = freezing ? stored.moneySnapshotId || crypto.randomUUID() : "";
247
+ await recompute(scope.db, contract, lines, settings, true, snapshotId);
248
+ if (freezing) {
249
+ const party = "customerId" in contract && contract.customerId ? await (await CustomerCollection.create({ db: scope.db })).get({ id: contract.customerId }, { cache: false }) : "vendorId" in contract && contract.vendorId ? await (await VendorCollection.create({ db: scope.db })).get({ id: contract.vendorId }, { cache: false }) : null;
250
+ contract.sellerTaxRegistrations = structuredClone(settings.taxRegistrations);
251
+ contract.partyTaxId = party?.taxId ?? "";
252
+ contract.moneySnapshotId = snapshotId;
253
+ }
254
+ return contract.saveDerived(DOCUMENT_MONEY_WRITE);
255
+ });
256
+ }));
257
+ }
258
+ async function saveContractLineItem(input) {
259
+ const tenantId = requireContractTenant(input.tenantId ?? getTenantId() ?? null);
260
+ return withTenantMoneyRuntime(input.db, tenantId, async (scope) => input.withDatabase(scope.db, async (line) => {
261
+ const persisted = line.id ? await (await ContractLineItemCollection.create({ db: scope.db })).get({ id: line.id }, { cache: false }) : null;
262
+ const contractId = persisted?.contractId ?? line.contractId;
263
+ const settings = scope.settings;
264
+ return withLockedContract(scope.db, scope.tenantId, contractId, async (contract, lines) => {
265
+ if (persisted && line.contractId !== persisted.contractId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Contract line cannot move between parents");
266
+ if (isFrozen(contract)) {
267
+ if (!persisted) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot add a line to a frozen Contract");
268
+ assertUnchanged(line, persisted, FROZEN_LINE_FIELDS, "Contract line");
269
+ return line.saveDerived(DOCUMENT_MONEY_WRITE);
270
+ }
271
+ line.contractId = contractId;
272
+ line.tenantId = scope.tenantId;
273
+ await line.saveDerived(DOCUMENT_MONEY_WRITE);
274
+ const all = [...lines.filter((candidate) => candidate.id !== line.id), line];
275
+ await recompute(scope.db, contract, all, settings, true);
276
+ await contract.saveDerived(DOCUMENT_MONEY_WRITE);
277
+ return line;
278
+ });
279
+ }));
280
+ }
281
+ async function deleteContractLineItem(input, options) {
282
+ const tenantId = requireContractTenant(input.tenantId ?? getTenantId() ?? null);
283
+ await withTenantMoneyRuntime(input.db, tenantId, async (scope) => input.withDatabase(scope.db, async (line) => {
284
+ const persisted = line.id ? await (await ContractLineItemCollection.create({ db: scope.db })).get({ id: line.id }, { cache: false }) : null;
285
+ if (!persisted) return;
286
+ const settings = scope.settings;
287
+ await withLockedContract(scope.db, scope.tenantId, persisted.contractId, async (contract, lines) => {
288
+ if (isFrozen(contract)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Cannot delete a line from a frozen Contract");
289
+ await line.deleteDerived(DOCUMENT_MONEY_WRITE, options);
290
+ await recompute(scope.db, contract, lines.filter((candidate) => candidate.id !== persisted.id), settings, true);
291
+ await contract.saveDerived(DOCUMENT_MONEY_WRITE);
292
+ });
293
+ }));
294
+ }
295
+ async function recalculateContractTotals(contract) {
296
+ await saveContract(contract);
297
+ }
298
+ //#endregion
299
+ export { deleteContractLineItem, recalculateContractTotals, saveContract, saveContractLineItem };
300
+
301
+ //# sourceMappingURL=contract-money-EuToOghh.js.map