@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.
- package/AGENTS.md +24 -11
- package/README.md +137 -89
- package/agents/accounting-snapshot-mapper.md +3 -3
- package/agents/commercial-ui.md +2 -2
- package/agents/contract-money.md +9 -12
- package/agents/cost-billing.md +3 -6
- package/agents/customer-vendor-ui.md +1 -2
- package/agents/document-money.md +58 -31
- package/agents/invoice-calculations.md +31 -56
- package/agents/invoice-preparation.md +62 -73
- package/agents/invoice-quantity.md +9 -21
- package/agents/minor-unit-migration.md +4 -4
- package/agents/money-calculator.md +7 -8
- package/agents/money-model.md +18 -22
- package/agents/money-upgrade.md +92 -0
- package/agents/revenue-journal.md +2 -2
- package/agents/tax-codes.md +17 -14
- package/agents/tax-summary.md +8 -6
- package/dist/billing/period-close.d.ts.map +1 -1
- package/dist/billing/runtime.d.ts +5 -2
- package/dist/billing/runtime.d.ts.map +1 -1
- package/dist/chunks/{ContractLineItemCollection--BrGMtNz.js → ContractLineItemCollection-BZcrlrrY.js} +19 -24
- package/dist/chunks/ContractLineItemCollection-BZcrlrrY.js.map +1 -0
- package/dist/chunks/{CustomerCollection-CjGaZSpf.js → CustomerCollection-BIPH2ZUx.js} +2 -2
- package/dist/chunks/{CustomerCollection-CjGaZSpf.js.map → CustomerCollection-BIPH2ZUx.js.map} +1 -1
- package/dist/chunks/{DocumentTaxCollection-KOkaQT2M.js → DocumentTaxCollection-DDzlywMW.js} +14 -7
- package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +1 -0
- package/dist/chunks/{InvoiceCollection-BUe_8FT1.js → InvoiceCollection-cinzAqt6.js} +50 -161
- package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +1 -0
- package/dist/chunks/{TaxCodeCollection-DF2eaJed.js → TaxCodeCollection-DaBOPG9c.js} +11 -4
- package/dist/chunks/{TaxCodeCollection-DF2eaJed.js.map → TaxCodeCollection-DaBOPG9c.js.map} +1 -1
- package/dist/chunks/{TaxComponentCollection-3x24xSNh.js → TaxComponentCollection-C4zxnyVu.js} +3 -3
- package/dist/chunks/{TaxComponentCollection-3x24xSNh.js.map → TaxComponentCollection-C4zxnyVu.js.map} +1 -1
- package/dist/chunks/VendorCollection-D6ZcUYy8.js +100 -0
- package/dist/chunks/VendorCollection-D6ZcUYy8.js.map +1 -0
- package/dist/chunks/{allocation-integrity-Cq0g6CfO.js → allocation-integrity-WmWyXhtG.js} +6 -6
- package/dist/chunks/{allocation-integrity-Cq0g6CfO.js.map → allocation-integrity-WmWyXhtG.js.map} +1 -1
- package/dist/chunks/calculations-C9daQLsh.js +82 -0
- package/dist/chunks/calculations-C9daQLsh.js.map +1 -0
- package/dist/chunks/contract-money-EuToOghh.js +301 -0
- package/dist/chunks/contract-money-EuToOghh.js.map +1 -0
- package/dist/chunks/detect-CY9ufaFb.js +328 -0
- package/dist/chunks/detect-CY9ufaFb.js.map +1 -0
- package/dist/chunks/{document-money-oUDvdjOf.js → document-money-DV5t4vlK.js} +388 -62
- package/dist/chunks/document-money-DV5t4vlK.js.map +1 -0
- package/dist/chunks/{invoice-posting-CF3fE_Ec.js → invoice-posting-C2i5dOVZ.js} +20 -22
- package/dist/chunks/invoice-posting-C2i5dOVZ.js.map +1 -0
- package/dist/chunks/{revenue-journal-DY9fro8E.js → revenue-journal-LLA9Kh_i.js} +3 -14
- package/dist/chunks/revenue-journal-LLA9Kh_i.js.map +1 -0
- package/dist/chunks/tenant-money-barrier-6TlqUeVj.js +255 -0
- package/dist/chunks/tenant-money-barrier-6TlqUeVj.js.map +1 -0
- package/dist/chunks/types-Bbm0Xflc.js.map +1 -1
- package/dist/chunks/{units-CNcjH_eE.js → units-8ElbjVZK.js} +18 -3
- package/dist/chunks/units-8ElbjVZK.js.map +1 -0
- package/dist/collections/ContractCollection.d.ts.map +1 -1
- package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
- package/dist/collections/InvoiceCollection.d.ts.map +1 -1
- package/dist/collections/InvoiceLineItemCollection.d.ts +0 -30
- package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
- package/dist/collections/TaxCodeCollection.d.ts +5 -1
- package/dist/collections/TaxCodeCollection.d.ts.map +1 -1
- package/dist/collections/TaxSettingsCollection.d.ts +15 -8
- package/dist/collections/TaxSettingsCollection.d.ts.map +1 -1
- package/dist/collections/TaxSummaryReportCollection.d.ts +3 -3
- package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -1
- package/dist/collections/index.d.ts +0 -1
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/commerce-money-tools.d.ts.map +1 -1
- package/dist/contract-money.d.ts +1 -13
- package/dist/contract-money.d.ts.map +1 -1
- package/dist/document-money-internal.d.ts +7 -3
- package/dist/document-money-internal.d.ts.map +1 -1
- package/dist/document-money.d.ts +1 -11
- package/dist/document-money.d.ts.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +667 -1650
- package/dist/index.js.map +1 -1
- package/dist/invoice-accounting-sync.d.ts.map +1 -1
- package/dist/invoice-mcp.d.ts +3 -3
- package/dist/invoice-mcp.d.ts.map +1 -1
- package/dist/invoice-posting.d.ts +11 -5
- package/dist/invoice-posting.d.ts.map +1 -1
- package/dist/invoice-record-screen.d.ts +1 -1
- package/dist/invoice-record-screen.d.ts.map +1 -1
- package/dist/invoices.d.ts +6 -1
- package/dist/invoices.d.ts.map +1 -1
- package/dist/invoices.js +2 -2
- package/dist/manifest.json +37 -765
- package/dist/migrations/invoiceEditorStorage.d.ts +2 -4
- package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -1
- package/dist/models/Contract.d.ts.map +1 -1
- package/dist/models/ContractLineItem.d.ts +6 -22
- package/dist/models/ContractLineItem.d.ts.map +1 -1
- package/dist/models/Invoice.d.ts +18 -48
- package/dist/models/Invoice.d.ts.map +1 -1
- package/dist/models/InvoiceLineItem.d.ts +8 -37
- package/dist/models/InvoiceLineItem.d.ts.map +1 -1
- package/dist/models/TaxSettings.d.ts +0 -10
- package/dist/models/TaxSettings.d.ts.map +1 -1
- package/dist/models/index.d.ts +0 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/playground.js +1 -1
- package/dist/playground.js.map +1 -1
- package/dist/recipes.d.ts +8 -11
- package/dist/recipes.d.ts.map +1 -1
- package/dist/revenue-journal.d.ts +0 -8
- package/dist/revenue-journal.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +79 -661
- package/dist/svelte/components/InvoiceEditor.svelte +7 -148
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts +4 -1
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InvoiceTotals.svelte +14 -118
- package/dist/svelte/components/InvoiceTotals.svelte.d.ts +4 -27
- package/dist/svelte/components/InvoiceTotals.svelte.d.ts.map +1 -1
- package/dist/svelte/components/MoneyInvoiceEditor.svelte +3 -23
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +3 -26
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/index.d.ts +2 -3
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -2
- package/dist/svelte/invoices/calculations.d.ts +3 -108
- package/dist/svelte/invoices/calculations.d.ts.map +1 -1
- package/dist/svelte/invoices/calculations.js +12 -309
- package/dist/svelte/invoices/messages.d.ts +1 -37
- package/dist/svelte/invoices/messages.d.ts.map +1 -1
- package/dist/svelte/invoices/messages.js +1 -37
- package/dist/svelte/invoices/money-editor.d.ts +1 -1
- package/dist/svelte/invoices/money-editor.d.ts.map +1 -1
- package/dist/svelte/invoices/types.d.ts +14 -114
- package/dist/svelte/invoices/types.d.ts.map +1 -1
- package/dist/svelte/playground/InvoicePreparationPreview.svelte +85 -94
- package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -1
- package/dist/svelte/playground.js +1 -1
- package/dist/tax/index.d.ts +2 -1
- package/dist/tax/index.d.ts.map +1 -1
- package/dist/tax/standard-codes.d.ts +12 -0
- package/dist/tax/standard-codes.d.ts.map +1 -0
- package/dist/tax/tenant-bootstrap.d.ts +29 -0
- package/dist/tax/tenant-bootstrap.d.ts.map +1 -0
- package/dist/tenant-money-barrier.d.ts +12 -44
- package/dist/tenant-money-barrier.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -23
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/dist/upgrade/detect.d.ts +129 -0
- package/dist/upgrade/detect.d.ts.map +1 -0
- package/dist/upgrade/money-v2.d.ts +81 -0
- package/dist/upgrade/money-v2.d.ts.map +1 -0
- package/package.json +14 -13
- package/agents/line-amount-migration.md +0 -50
- package/dist/chunks/ContractLineItemCollection--BrGMtNz.js.map +0 -1
- package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js.map +0 -1
- package/dist/chunks/InvoiceCollection-BUe_8FT1.js.map +0 -1
- package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js +0 -468
- package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js.map +0 -1
- package/dist/chunks/TaxSettingsCollection-DYYF-In-.js +0 -319
- package/dist/chunks/TaxSettingsCollection-DYYF-In-.js.map +0 -1
- package/dist/chunks/calculations-DaHR17il.js +0 -333
- package/dist/chunks/calculations-DaHR17il.js.map +0 -1
- package/dist/chunks/document-money-oUDvdjOf.js.map +0 -1
- package/dist/chunks/invoice-posting-CF3fE_Ec.js.map +0 -1
- package/dist/chunks/revenue-journal-DY9fro8E.js.map +0 -1
- package/dist/chunks/tenant-money-barrier-y6EAa6vY.js +0 -135
- package/dist/chunks/tenant-money-barrier-y6EAa6vY.js.map +0 -1
- package/dist/chunks/units-CNcjH_eE.js.map +0 -1
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts +0 -12
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +0 -1
- package/dist/migrations/commerceLineAmounts.d.ts +0 -84
- package/dist/migrations/commerceLineAmounts.d.ts.map +0 -1
- package/dist/models/MoneyMigrationDocument.d.ts +0 -28
- package/dist/models/MoneyMigrationDocument.d.ts.map +0 -1
- package/dist/svelte/components/InvoiceLineEditor.svelte +0 -92
- package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +0 -8
- package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +0 -1
- package/dist/svelte/invoices/fields.d.ts +0 -6
- package/dist/svelte/invoices/fields.d.ts.map +0 -1
- package/dist/svelte/invoices/fields.js +0 -22
- package/dist/tax/legacy.d.ts +0 -48
- package/dist/tax/legacy.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TaxComponentCollection-
|
|
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":";;;;;;;;;;;;;;;;;;;;;AAWA,IAAM,UAAU;AAEhB,SAAS,UAAU,OAAwB;CACzC,MAAM,QAAQ,QAAQ,KAAK,KAAK;CAChC,IAAI,CAAC,OAAO,OAAO;CAGnB,OADE,OAAO,MAAM,EAAE,IAAI,WAAa,QAAQ,MAAM,MAAM,GAAA,CAAI,OAAO,GAAG,GAAG,CAAC,KACvD;AACnB;AAEA,SAAS,iBAAiB,OAAuB;CAC/C,MAAM,CAAC,OAAO,WAAW,MAAM,OAAO,KAAK,CAAA,CAAE,MAAM,GAAG;CACtD,MAAM,kBAAkB,MAAM,QAAQ,aAAa,EAAE,KAAK;CAC1D,MAAM,qBAAqB,SAAS,QAAQ,OAAO,EAAE;CACrD,OAAO,qBACH,GAAG,gBAAe,GAAI,uBACtB;AACN;AAsBO,IAAM,eAAN,cAA2B,WAAW;CAE3C,WAAW;CAGX,OAAO;CAOP,UAAU;CAKV,qBAAqB;CAMrB,cAAc;CAGd,SAAS;CAET,YAAY,UAA+B,CAAC,GAAG;EAC7C,MAAM,OAAO;EACb,IAAI,QAAQ,aAAa,KAAA,GAAW,KAAK,WAAW,QAAQ,YAAY;EACxE,IAAI,QAAQ,SAAS,KAAA,GAAW,KAAK,OAAO,QAAQ;EACpD,IAAI,QAAQ,YAAY,KAAA,GAAW,KAAK,UAAU,QAAQ;EAC1D,IAAI,QAAQ,uBAAuB,KAAA,GACjC,KAAK,qBAAqB,QAAQ;EACpC,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,KAAK,cAAc,QAAQ;EAC7B,IAAI,QAAQ,WAAW,KAAA,GAAW,KAAK,SAAS,QAAQ;CAC1D;CAEA,MAAe,aAA4B;EACzC,MAAM,MAAM,WAAW;EACvB,KAAK,UAAU,iBAAiB,OAAO,KAAK,OAAO,CAAC;EACpD,OAAO;CACT;CAEA,MAAe,OAAsB;EACnC,IAAI,CAAC,KAAK,KAAK,KAAK,GAClB,MAAM,IAAI,MAAM,wCAAwC;EAC1D,IAAI,CAAC,UAAU,KAAK,OAAO,GACzB,MAAM,IAAI,qBACR,wBACA,kFACF;EAEF,KAAK,UAAU,iBAAiB,KAAK,OAAO;EAC5C,OAAO,KAAK,gBAAgB,YAAY;GACtC,MAAM,EAAE,2BAA2B,MAAA,QAAA,QAAA,CAAA,CAAA,WAAA,8BAAA;GAGnC,MAAM,aAAa,MAAM,uBAAuB,OAAO,EAAE,IAAI,KAAK,GAAG,CAAC;GACtE,MAAM,YAAY,KAAK,KACnB,MAAM,WAAW,IAAI,EAAE,IAAI,KAAK,GAAG,GAAG,EAAE,OAAO,MAAM,CAAC,IACtD;GACJ,IAAI,aAAa,iBAAiB,UAAU,OAAO,MAAM,KAAK,SAAS;IACrE,MAAM,aAAa,MAAM,KAAK,aAAa,SAAS;IAGpD,KAAK,aAAa,UAAU;IAC5B,IAAI,YACF,MAAM,IAAI,qBACR,wBACA,qFACF;GAEJ;GACA,IAAI,KAAK,oBAAoB;IAE3B,MAAM,UAAU,OAAM,MADC,kBAAkB,OAAO,EAAE,IAAI,KAAK,GAAG,CAAC,EAAA,CAChC,IAC7B,EAAE,IAAI,KAAK,mBAAmB,GAC9B,EAAE,OAAO,MAAM,CACjB;IACA,IAAI,CAAC,WAAW,QAAQ,aAAa,KAAK,UACxC,MAAM,IAAI,MACR,2DACF;GAEJ;GACA,OAAQ,MAAM,MAAM,KAAK;EAC3B,CAAC;CACH;CAEA,MAAc,aAAa,WAA2C;EACpE,IAAI,CAAC,KAAK,IAAI,OAAO;EACrB,IAAI,CAAC,UAAU,YACb,MAAM,IAAI,MAAM,kDAAkD;EAGpE,MAAM,UAAU,cAAc,UAAU,UAAU;EAClD,MAAM,EAAE,sBAAsB,MAAM,OAClC,kCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAEF,MAAM,EAAE,0BAA0B,MAAM,OACtC,sCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAQF,KAJE,OAAM,MAFY,kBAAkB,OAAO,EAAE,IAAI,KAAK,GAAG,CAAC,EAAA,CAE9C,KAAK,EAAE,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC,EAAA,CACvD,MAAM,SACN,KAAK,WAAW,MAAM,cAAc,UAAU,mBAAmB,KAAK,EAAE,CAEtE,GAAY,OAAO;EAEvB,QAEI,OAAM,MAHkB,sBAAsB,OAAO,EAAE,IAAI,KAAK,GAAG,CAAC,EAAA,CAGhD,KAAK;GACvB,OAAO;IAAE,UAAU,KAAK;IAAU,gBAAgB,KAAK;GAAG;GAC1D,OAAO;EACT,CAAC,EAAA,CACD,SAAS;CAEf;AACF;AA1H+C,cAAlC,cAAyC,mBAAkB,8BAAA;AAEtE,gBAAA,CADC,SAAS,CAAA,GADC,aAEX,WAAA,YAAA,CAAA;AAGA,gBAAA,CADC,MAAM,EAAE,aAAa,mDAAmD,CAAC,CAAA,GAJ/D,aAKX,WAAA,QAAA,CAAA;AAOA,gBAAA,CAJC,MAAM;CACL,MAAM;CACN,aAAa;AACf,CAAC,CAAA,GAXU,aAYX,WAAA,WAAA,CAAA;AAKA,gBAAA,CAHC,gBAAgB,uCAAuC,EACtD,aAAa,0DACf,CAAC,CAAA,GAhBU,aAiBX,WAAA,sBAAA,CAAA;AAMA,gBAAA,CAJC,MAAM,EACL,aACE,4GACJ,CAAC,CAAA,GAtBU,aAuBX,WAAA,eAAA,CAAA;AAGA,gBAAA,CADC,MAAM,EAAE,aAAa,qDAAqD,CAAC,CAAA,GAzBjE,aA0BX,WAAA,UAAA,CAAA;AA1BW,eAAN,gBAAA,CAfN,aAAa,EAAE,MAAM,WAAW,CAAC,GACjC,KAAK;CACJ,KAAK;EACH,SAAS;GAAC;GAAQ;GAAO;GAAU;EAAQ;EAC3C,UAAU;GACR;GACA;GACA;GACA;GACA;EACF;CACF;CACA,KAAK,EAAE,SAAS,CAAC,QAAQ,KAAK,EAAE;CAChC,KAAK,EAAE,cAAc,KAAK;AAC5B,CAAC,CAAA,GACY,YAAA;;;;AC/CN,IAAM,yBAAN,cAAqC,eAA6B;CACvE,OAAgB,aAAa;AAC/B"}
|
|
@@ -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.
|
|
10
|
-
const { InvoiceCollection } = await import("./InvoiceCollection-
|
|
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.
|
|
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.
|
|
33
|
-
const { InvoiceCollection } = await import("./InvoiceCollection-
|
|
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-
|
|
45
|
+
//# sourceMappingURL=allocation-integrity-WmWyXhtG.js.map
|
package/dist/chunks/{allocation-integrity-Cq0g6CfO.js.map → allocation-integrity-WmWyXhtG.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"allocation-integrity-
|
|
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
|