@happyvertical/smrt-commerce 0.56.0 → 0.56.2
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 +9 -8
- package/README.md +98 -4
- package/agents/accounting-snapshot-mapper.md +111 -0
- package/agents/contract-money.md +24 -0
- package/agents/cost-billing.md +7 -2
- package/agents/customer-vendor-ui.md +7 -1
- package/agents/document-money.md +127 -0
- package/agents/invoice-calculations.md +14 -2
- package/agents/line-amount-migration.md +50 -0
- package/agents/minor-unit-migration.md +20 -0
- package/agents/money-calculator.md +45 -0
- package/agents/money-model.md +113 -0
- package/agents/revenue-journal.md +23 -0
- package/agents/tax-codes.md +48 -0
- package/agents/tax-summary.md +128 -0
- package/dist/accounting-snapshot.d.ts +79 -0
- package/dist/accounting-snapshot.d.ts.map +1 -0
- package/dist/billing/runtime.d.ts +2 -0
- package/dist/billing/runtime.d.ts.map +1 -1
- package/dist/chunks/ContractLineItemCollection--BrGMtNz.js +265 -0
- package/dist/chunks/ContractLineItemCollection--BrGMtNz.js.map +1 -0
- package/dist/chunks/CustomerCollection-CjGaZSpf.js +99 -0
- package/dist/chunks/CustomerCollection-CjGaZSpf.js.map +1 -0
- package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js +1659 -0
- package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js.map +1 -0
- package/dist/chunks/InvoiceCollection-BUe_8FT1.js +1047 -0
- package/dist/chunks/InvoiceCollection-BUe_8FT1.js.map +1 -0
- package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js +468 -0
- package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js.map +1 -0
- package/dist/chunks/TaxCodeCollection-DF2eaJed.js +149 -0
- package/dist/chunks/TaxCodeCollection-DF2eaJed.js.map +1 -0
- package/dist/chunks/TaxComponentCollection-3x24xSNh.js +129 -0
- package/dist/chunks/TaxComponentCollection-3x24xSNh.js.map +1 -0
- package/dist/chunks/TaxSettingsCollection-DYYF-In-.js +319 -0
- package/dist/chunks/TaxSettingsCollection-DYYF-In-.js.map +1 -0
- package/dist/chunks/{money-DfrRxrel.js → allocation-integrity-Cq0g6CfO.js} +7 -12
- package/dist/chunks/allocation-integrity-Cq0g6CfO.js.map +1 -0
- package/dist/chunks/{calculations-Bz7RB3qK.js → calculations-DaHR17il.js} +134 -54
- package/dist/chunks/calculations-DaHR17il.js.map +1 -0
- package/dist/chunks/credit-allocation-yspz0Lab.js +121 -0
- package/dist/chunks/credit-allocation-yspz0Lab.js.map +1 -0
- package/dist/chunks/document-money-oUDvdjOf.js +860 -0
- package/dist/chunks/document-money-oUDvdjOf.js.map +1 -0
- package/dist/chunks/invoice-posting-CF3fE_Ec.js +229 -0
- package/dist/chunks/invoice-posting-CF3fE_Ec.js.map +1 -0
- package/dist/chunks/money-rational-CcW7gzG5.js +64 -0
- package/dist/chunks/money-rational-CcW7gzG5.js.map +1 -0
- package/dist/chunks/revenue-journal-DY9fro8E.js +140 -0
- package/dist/chunks/revenue-journal-DY9fro8E.js.map +1 -0
- package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/chunks/tenant-money-barrier-y6EAa6vY.js +135 -0
- package/dist/chunks/tenant-money-barrier-y6EAa6vY.js.map +1 -0
- package/dist/chunks/{types-zVnIjSOk.js → types-Bbm0Xflc.js} +4 -14
- package/dist/chunks/types-Bbm0Xflc.js.map +1 -0
- package/dist/chunks/{units-BpZ0_Dpk.js → units-CNcjH_eE.js} +2 -2
- package/dist/chunks/{units-BpZ0_Dpk.js.map → units-CNcjH_eE.js.map} +1 -1
- package/dist/collections/ContractCollection.d.ts +35 -1
- package/dist/collections/ContractCollection.d.ts.map +1 -1
- package/dist/collections/ContractLineItemCollection.d.ts +26 -1
- package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
- package/dist/collections/DocumentTaxCollection.d.ts +75 -0
- package/dist/collections/DocumentTaxCollection.d.ts.map +1 -0
- package/dist/collections/InvoiceCollection.d.ts +26 -1
- package/dist/collections/InvoiceCollection.d.ts.map +1 -1
- package/dist/collections/InvoiceLineItemCollection.d.ts +26 -1
- package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts +12 -0
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +1 -0
- package/dist/collections/PaymentAllocationCollection.d.ts +2 -0
- package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
- package/dist/collections/TaxCodeCollection.d.ts +7 -0
- package/dist/collections/TaxCodeCollection.d.ts.map +1 -0
- package/dist/collections/TaxComponentCollection.d.ts +6 -0
- package/dist/collections/TaxComponentCollection.d.ts.map +1 -0
- package/dist/collections/TaxSettingsCollection.d.ts +16 -0
- package/dist/collections/TaxSettingsCollection.d.ts.map +1 -0
- package/dist/collections/TaxSummaryReportCollection.d.ts +151 -0
- package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +6 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/commerce-money-tools.d.ts +9 -0
- package/dist/commerce-money-tools.d.ts.map +1 -0
- package/dist/contract-money.d.ts +27 -0
- package/dist/contract-money.d.ts.map +1 -0
- package/dist/cost-billing.d.ts +0 -1
- package/dist/cost-billing.d.ts.map +1 -1
- package/dist/cost-billing.js +10 -15
- package/dist/cost-billing.js.map +1 -1
- package/dist/credit-allocation.d.ts +48 -0
- package/dist/credit-allocation.d.ts.map +1 -0
- package/dist/document-money-internal.d.ts +44 -0
- package/dist/document-money-internal.d.ts.map +1 -0
- package/dist/document-money.d.ts +85 -0
- package/dist/document-money.d.ts.map +1 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5411 -4249
- package/dist/index.js.map +1 -1
- package/dist/invoice-accounting-sync.d.ts +96 -0
- package/dist/invoice-accounting-sync.d.ts.map +1 -0
- package/dist/invoice-mcp.d.ts +16 -0
- package/dist/invoice-mcp.d.ts.map +1 -0
- package/dist/invoice-posting.d.ts +9 -0
- package/dist/invoice-posting.d.ts.map +1 -0
- package/dist/invoice-record-screen.d.ts +60 -0
- package/dist/invoice-record-screen.d.ts.map +1 -0
- package/dist/invoices.d.ts +2 -0
- package/dist/invoices.d.ts.map +1 -1
- package/dist/invoices.js +3 -2
- package/dist/manifest.json +12804 -6095
- package/dist/migrations/commerceLineAmounts.d.ts +84 -0
- package/dist/migrations/commerceLineAmounts.d.ts.map +1 -0
- package/dist/migrations/partyLinks.d.ts.map +1 -1
- package/dist/models/Contract.d.ts +34 -4
- package/dist/models/Contract.d.ts.map +1 -1
- package/dist/models/ContractLineItem.d.ts +43 -6
- package/dist/models/ContractLineItem.d.ts.map +1 -1
- package/dist/models/Customer.d.ts +7 -0
- package/dist/models/Customer.d.ts.map +1 -1
- package/dist/models/DocumentTax.d.ts +50 -0
- package/dist/models/DocumentTax.d.ts.map +1 -0
- package/dist/models/Invoice.d.ts +46 -2
- package/dist/models/Invoice.d.ts.map +1 -1
- package/dist/models/InvoiceLineItem.d.ts +46 -5
- package/dist/models/InvoiceLineItem.d.ts.map +1 -1
- package/dist/models/MoneyMigrationDocument.d.ts +28 -0
- package/dist/models/MoneyMigrationDocument.d.ts.map +1 -0
- package/dist/models/Payment.d.ts +5 -1
- package/dist/models/Payment.d.ts.map +1 -1
- package/dist/models/PaymentAllocation.d.ts.map +1 -1
- package/dist/models/TaxCode.d.ts +22 -0
- package/dist/models/TaxCode.d.ts.map +1 -0
- package/dist/models/TaxComponent.d.ts +21 -0
- package/dist/models/TaxComponent.d.ts.map +1 -0
- package/dist/models/TaxSettings.d.ts +33 -0
- package/dist/models/TaxSettings.d.ts.map +1 -0
- package/dist/models/TaxSummaryReport.d.ts +35 -0
- package/dist/models/TaxSummaryReport.d.ts.map +1 -0
- package/dist/models/Vendor.d.ts +9 -0
- package/dist/models/Vendor.d.ts.map +1 -1
- package/dist/models/index.d.ts +7 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/money-rational.d.ts +15 -0
- package/dist/money-rational.d.ts.map +1 -0
- package/dist/money.d.ts +106 -25
- package/dist/money.d.ts.map +1 -1
- package/dist/money.js +274 -0
- package/dist/money.js.map +1 -0
- package/dist/recipes.d.ts +15 -15
- package/dist/revenue-journal.d.ts +61 -0
- package/dist/revenue-journal.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +9621 -7592
- package/dist/svelte/components/InvoiceActions.svelte +12 -0
- package/dist/svelte/components/InvoiceActions.svelte.d.ts +5 -0
- package/dist/svelte/components/InvoiceActions.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InvoiceEditor.svelte +15 -1
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InvoiceLineItems.svelte +3 -3
- package/dist/svelte/components/InvoiceRecordScreen.svelte +94 -0
- package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts +21 -0
- package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceTotals.svelte +2 -2
- package/dist/svelte/components/MoneyInvoiceEditor.svelte +282 -0
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +31 -0
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte +53 -1
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +5 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/QuoteEditor.svelte +54 -1
- package/dist/svelte/components/QuoteEditor.svelte.d.ts +5 -0
- package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/i18n.d.ts +16 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +16 -0
- package/dist/svelte/index.d.ts +7 -2
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +4 -1
- package/dist/svelte/invoices/calculations.d.ts +7 -2
- package/dist/svelte/invoices/calculations.d.ts.map +1 -1
- package/dist/svelte/invoices/calculations.js +141 -47
- package/dist/svelte/invoices/money-editor.d.ts +123 -0
- package/dist/svelte/invoices/money-editor.d.ts.map +1 -0
- package/dist/svelte/invoices/money-editor.js +101 -0
- package/dist/svelte/invoices/types.d.ts +5 -1
- package/dist/svelte/invoices/types.d.ts.map +1 -1
- package/dist/svelte/purchasing-i18n.d.ts +5 -0
- package/dist/svelte/purchasing-i18n.d.ts.map +1 -1
- package/dist/svelte/purchasing-i18n.js +5 -0
- package/dist/svelte/quote-i18n.d.ts +5 -0
- package/dist/svelte/quote-i18n.d.ts.map +1 -1
- package/dist/svelte/quote-i18n.js +5 -0
- package/dist/tax/cookbook.d.ts +22 -0
- package/dist/tax/cookbook.d.ts.map +1 -0
- package/dist/tax/example-pack.d.ts +25 -0
- package/dist/tax/example-pack.d.ts.map +1 -0
- package/dist/tax/identity.d.ts +5 -0
- package/dist/tax/identity.d.ts.map +1 -0
- package/dist/tax/index.d.ts +4 -0
- package/dist/tax/index.d.ts.map +1 -0
- package/dist/tax/legacy.d.ts +48 -0
- package/dist/tax/legacy.d.ts.map +1 -0
- package/dist/tax-summary-report-adapter.d.ts +37 -0
- package/dist/tax-summary-report-adapter.d.ts.map +1 -0
- package/dist/tax-summary.d.ts +129 -0
- package/dist/tax-summary.d.ts.map +1 -0
- package/dist/tenant-money-barrier.d.ts +66 -0
- package/dist/tenant-money-barrier.d.ts.map +1 -0
- package/dist/types/index.d.ts +138 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +19 -11
- package/dist/chunks/calculations-Bz7RB3qK.js.map +0 -1
- package/dist/chunks/money-DfrRxrel.js.map +0 -1
- package/dist/chunks/types-zVnIjSOk.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaxSettingsCollection-DYYF-In-.js","names":[],"sources":["../../src/document-money-internal.ts","../../src/models/TaxSettings.ts","../../src/tax/identity.ts","../../src/collections/TaxSettingsCollection.ts"],"sourcesContent":["/** Internal capabilities shared by commerce document-money persistence. */\n\nimport type { DatabaseInterface } from '@happyvertical/smrt-core/migrations';\nimport { MoneyValidationError } from './money.js';\n\nconst activeMoneyTransactions = new WeakSet<object>();\n\n/**\n * Capability required by derived money rows. This module is intentionally not\n * exported from the package root; aggregate persistence owns every mutation.\n */\nexport const DOCUMENT_MONEY_WRITE = Symbol('commerce.documentMoneyWrite');\n\nexport type DocumentMoneyWriteCapability = typeof DOCUMENT_MONEY_WRITE;\n\n/** Internal capability for the server-owned tenant money migration marker. */\nexport const MONEY_MIGRATION_WRITE = Symbol('commerce.moneyMigrationWrite');\n\nexport type MoneyMigrationWriteCapability = typeof MONEY_MIGRATION_WRITE;\n\n/** Internal capability for returning a locked snapshot on its root executor. */\nexport const DOCUMENT_MONEY_READ_REBIND = Symbol(\n 'commerce.documentMoneyReadRebind',\n);\n\nexport type DocumentMoneyReadRebindCapability =\n typeof DOCUMENT_MONEY_READ_REBIND;\n\nexport interface MoneyParentLockCollection {\n readonly tableName: string;\n query(\n sql: string,\n params: unknown[],\n options: { allowRawOnTenantScoped: true },\n ): Promise<unknown[]>;\n}\nexport interface MoneyParentLockTarget {\n id: string;\n tenantId: string;\n collection: MoneyParentLockCollection;\n}\n\nfunction compareCodeUnits(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\nfunction quoteIdentifier(identifier: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Unsafe money parent table identifier: ${identifier}`,\n );\n }\n return `\"${identifier}\"`;\n}\n\nfunction assertIdentifier(value: string, label: string): void {\n if (\n typeof value !== 'string' ||\n value.trim() !== value ||\n value.length === 0\n ) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `${label} must be a nonempty identifier`,\n );\n }\n}\n/** Run aggregate work on one transaction-bound database executor. */\nexport async function withDocumentMoneyTransaction<T>(\n db: DatabaseInterface,\n operation: (tx: DatabaseInterface) => Promise<T>,\n): Promise<T> {\n if (activeMoneyTransactions.has(db as object)) return operation(db);\n if (!db.transaction) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Document money persistence requires transaction support',\n );\n }\n return db.transaction(async (tx) => {\n activeMoneyTransactions.add(tx as object);\n try {\n return await operation(tx);\n } finally {\n activeMoneyTransactions.delete(tx as object);\n }\n });\n}\n\ninterface DocumentMoneyTransactionalObject {\n readonly db: DatabaseInterface;\n withTransaction<T>(operation: (bound: this) => Promise<T>): Promise<T>;\n}\n\n/**\n * Run aggregate work through the model's root transaction so rejected writes\n * restore the instance's persistence revision before callers retry it.\n */\nexport async function withDocumentMoneyObjectTransaction<\n TObject extends DocumentMoneyTransactionalObject,\n TResult,\n>(\n input: TObject,\n operation: (bound: TObject, tx: DatabaseInterface) => Promise<TResult>,\n): Promise<TResult> {\n if (activeMoneyTransactions.has(input.db as object)) {\n return operation(input, input.db);\n }\n return input.withTransaction(async (bound) => {\n const tx = bound.db;\n activeMoneyTransactions.add(tx as object);\n try {\n return await operation(bound as TObject, tx);\n } finally {\n activeMoneyTransactions.delete(tx as object);\n }\n });\n}\n\n/**\n * Serialize aggregate writers by touching each tenant-owned parent first.\n * Parents are locked sequentially in stable code-unit ID order. Callers must\n * re-read authoritative parent and child state on the same executor afterward.\n */\nexport async function lockDocumentMoneyParents(\n targets: readonly MoneyParentLockTarget[],\n): Promise<void> {\n const sorted = [...targets].sort(\n (left, right) =>\n compareCodeUnits(left.id, right.id) ||\n compareCodeUnits(left.collection.tableName, right.collection.tableName),\n );\n const identities = new Set<string>();\n for (const target of sorted) {\n assertIdentifier(target.id, 'Money parent id');\n assertIdentifier(target.tenantId, 'Money parent tenant id');\n const identity = `${target.collection.tableName}\\u0000${target.id}`;\n if (identities.has(identity)) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Duplicate money parent lock target: ${target.id}`,\n );\n }\n identities.add(identity);\n const table = quoteIdentifier(target.collection.tableName);\n const locked = await target.collection.query(\n `UPDATE ${table}\n SET \"updated_at\" = \"updated_at\"\n WHERE \"id\" = ? AND \"tenant_id\" = ?\n RETURNING \"id\"`,\n [target.id, target.tenantId],\n { allowRawOnTenantScoped: true },\n );\n if (locked.length !== 1) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Money parent ${target.id} was not found in tenant ${target.tenantId}`,\n );\n }\n }\n}\n","import { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport {\n getTenantId,\n TenantScoped,\n tenantId,\n} from '@happyvertical/smrt-tenancy';\nimport {\n MONEY_MIGRATION_WRITE,\n type MoneyMigrationWriteCapability,\n} from '../document-money-internal.js';\nimport { MoneyValidationError } from '../money.js';\nimport type { TaxSettingsOptions } from '../types/index.js';\n\nexport interface TaxRegistration {\n taxComponentId: string;\n number: string;\n label: string;\n}\n\n/** Tenant-level defaults and registration numbers for tax determination. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n conflictColumns: ['tenant_id'],\n api: {\n include: ['get', 'create', 'update'],\n writable: [\n 'defaultSalesTaxCodeId',\n 'defaultPurchaseTaxCodeId',\n 'defaultLineAmountType',\n 'taxRoundingScope',\n 'taxRegistrations',\n ],\n },\n mcp: { include: ['get'] },\n cli: { skipApiCheck: true },\n})\nexport class TaxSettings extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-commerce\";\n @tenantId()\n tenantId = '';\n\n @foreignKey('TaxCode', {\n description: 'The default tax code for sales documents.',\n })\n defaultSalesTaxCodeId = '';\n\n @foreignKey('TaxCode', {\n description: 'The default tax code for purchase documents.',\n })\n defaultPurchaseTaxCodeId = '';\n\n @field({\n description:\n 'Whether new document amounts are exclusive, inclusive or out of scope.',\n })\n defaultLineAmountType: 'exclusive' | 'inclusive' | 'none' = 'exclusive';\n\n @field({ description: 'Whether components round per line or per document.' })\n taxRoundingScope: 'line' | 'document' = 'line';\n\n /** Stored JSON is guarded through the typed accessor below. */\n taxRegistrationsJson = '[]';\n\n /** Server-owned runtime semantics gate. Never exposed as a generated write. */\n moneyModel: 'legacy' | 'migrating' | 'v2' = 'legacy';\n\n /** Opaque owner token while a tenant migration is in progress. */\n moneyMigrationId = '';\n\n @field({\n sensitive: true,\n transient: true,\n description:\n 'Registration numbers by tax component, for issued-document snapshots.',\n })\n get taxRegistrations(): TaxRegistration[] {\n try {\n const value: unknown = JSON.parse(this.taxRegistrationsJson);\n if (!Array.isArray(value)) return [];\n return value.filter(\n (entry): entry is TaxRegistration =>\n !!entry &&\n typeof entry === 'object' &&\n typeof (entry as TaxRegistration).taxComponentId === 'string' &&\n typeof (entry as TaxRegistration).number === 'string' &&\n typeof (entry as TaxRegistration).label === 'string',\n );\n } catch {\n return [];\n }\n }\n\n set taxRegistrations(value: TaxRegistration[] | string) {\n this.taxRegistrationsJson =\n typeof value === 'string' ? value : JSON.stringify(value);\n }\n\n constructor(options: TaxSettingsOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.defaultSalesTaxCodeId !== undefined)\n this.defaultSalesTaxCodeId = options.defaultSalesTaxCodeId;\n if (options.defaultPurchaseTaxCodeId !== undefined)\n this.defaultPurchaseTaxCodeId = options.defaultPurchaseTaxCodeId;\n if (options.defaultLineAmountType !== undefined)\n this.defaultLineAmountType = options.defaultLineAmountType;\n if (options.taxRoundingScope !== undefined)\n this.taxRoundingScope = options.taxRoundingScope;\n if (options.taxRegistrations !== undefined)\n this.taxRegistrations = structuredClone(options.taxRegistrations);\n if (options.moneyModel !== undefined) this.moneyModel = options.moneyModel;\n if (options.moneyMigrationId !== undefined)\n this.moneyMigrationId = options.moneyMigrationId;\n }\n\n override async save(): Promise<this> {\n return this.saveValidated(false);\n }\n\n /** @internal Money migration owns marker transitions. */\n async saveMoneyModel(\n capability: MoneyMigrationWriteCapability,\n ): Promise<this> {\n if (capability !== MONEY_MIGRATION_WRITE) {\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Tenant money model requires the migration capability',\n );\n }\n return this.saveValidated(true);\n }\n\n /** @internal Caller already owns the transaction-bound migration barrier. */\n async saveMoneyModelOnTransaction(\n capability: MoneyMigrationWriteCapability,\n ): Promise<this> {\n if (capability !== MONEY_MIGRATION_WRITE) {\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Tenant money model requires the migration capability',\n );\n }\n return this.saveValidated(true, true);\n }\n\n private async saveValidated(\n allowMoneyModelWrite: boolean,\n transactionBound = false,\n ): Promise<this> {\n if (\n !['exclusive', 'inclusive', 'none'].includes(this.defaultLineAmountType)\n )\n throw new Error('Tax settings defaultLineAmountType is invalid');\n if (!['line', 'document'].includes(this.taxRoundingScope))\n throw new Error('Tax settings taxRoundingScope is invalid');\n if (!['legacy', 'migrating', 'v2'].includes(this.moneyModel))\n throw new Error('Tax settings moneyModel is invalid');\n if ((this.moneyModel === 'migrating') !== !!this.moneyMigrationId)\n throw new Error(\n 'Tax settings migrating state requires exactly one migration owner',\n );\n const registrations = this.parsedRegistrations();\n if (!registrations)\n throw new Error('Tax registrations must be a valid JSON array');\n if (\n registrations.some(\n (entry) =>\n !entry.taxComponentId || !entry.number.trim() || !entry.label.trim(),\n )\n )\n throw new Error('Tax registrations require component, number and label');\n const persist = async (): Promise<this> => {\n const { TaxCodeCollection } = await import(\n '../collections/TaxCodeCollection.js'\n );\n const { TaxComponentCollection } = await import(\n '../collections/TaxComponentCollection.js'\n );\n const codes = await TaxCodeCollection.create({ db: this.db });\n const components = await TaxComponentCollection.create({ db: this.db });\n if (!allowMoneyModelWrite) {\n // A new instance upserts on the tenant_id natural key, so the marker\n // row may be found by tenant rather than by this instance's id. Lock\n // and re-read that row on this save transaction so a migration claim\n // cannot commit between this check and the natural-key upsert.\n const tenantKey = this.tenantId || getTenantId() || '';\n const { TaxSettingsCollection } = await import(\n '../collections/TaxSettingsCollection.js'\n );\n const persistedByTenant = tenantKey\n ? await (\n await TaxSettingsCollection.create({ db: this.db })\n ).lockForTenant(tenantKey, false)\n : null;\n const persistedById = this.id\n ? await this.db.get(this.tableName, { id: this.id })\n : null;\n if (\n (persistedByTenant &&\n (persistedByTenant.moneyModel !== this.moneyModel ||\n persistedByTenant.moneyMigrationId !== this.moneyMigrationId)) ||\n (persistedById &&\n (persistedById.money_model !== this.moneyModel ||\n (persistedById.money_migration_id ?? '') !==\n this.moneyMigrationId))\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Tenant money model is server-managed',\n );\n // With no marker row to lock, a normal natural-key upsert could race\n // the migration's first marker insert and overwrite its claimed state.\n // Keep the first ordinary save insert-only so that collision refuses\n // the stale save instead of updating the newly committed marker.\n if (!persistedByTenant && !this.isPersisted) this.requireInsertOnSave();\n }\n for (const id of [\n this.defaultSalesTaxCodeId,\n this.defaultPurchaseTaxCodeId,\n ].filter(Boolean)) {\n const code = await codes.get({ id }, { cache: false });\n if (!code || code.tenantId !== this.tenantId)\n throw new Error(\n 'Tax setting default codes must belong to its tenant',\n );\n }\n for (const registration of registrations) {\n const component = await components.get(\n { id: registration.taxComponentId },\n { cache: false },\n );\n if (!component || component.tenantId !== this.tenantId)\n throw new Error(\n 'Tax setting registrations must belong to its tenant',\n );\n }\n return (await super.save()) as this;\n };\n return transactionBound ? persist() : this.withTransaction(persist);\n }\n\n private parsedRegistrations(): TaxRegistration[] | null {\n try {\n const value: unknown = JSON.parse(this.taxRegistrationsJson);\n if (!Array.isArray(value)) return null;\n if (\n value.some(\n (entry) =>\n !entry ||\n typeof entry !== 'object' ||\n typeof (entry as TaxRegistration).taxComponentId !== 'string' ||\n typeof (entry as TaxRegistration).number !== 'string' ||\n typeof (entry as TaxRegistration).label !== 'string',\n )\n )\n return null;\n return value as TaxRegistration[];\n } catch {\n return null;\n }\n }\n}\n","/** Stable UUID identities make tenant tax bootstrap retry and race safe. */\nexport async function deterministicTaxId(\n tenantId: string,\n kind: 'code' | 'component' | 'settings' | 'report',\n key: string,\n): Promise<string> {\n if (!tenantId.trim() || !key.trim())\n throw new Error('Tax bootstrap identity requires tenant and key');\n const bytes = new TextEncoder().encode(\n JSON.stringify(['commerce-tax', tenantId, kind, key]),\n );\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));\n const uuid = digest.slice(0, 16);\n uuid[6] = (uuid[6] & 0x0f) | 0x50;\n uuid[8] = (uuid[8] & 0x3f) | 0x80;\n const hex = Array.from(uuid, (byte) => byte.toString(16).padStart(2, '0'));\n const joined = hex.join('');\n return `${joined.slice(0, 8)}-${joined.slice(8, 12)}-${joined.slice(12, 16)}-${joined.slice(16, 20)}-${joined.slice(20)}`;\n}\n\n/** A racing deterministic insert loses safely and reloads the committed row. */\nexport async function createOrLoadTaxSeed<T>(\n create: () => Promise<T>,\n load: () => Promise<T | null | undefined>,\n): Promise<T> {\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n return await create();\n } catch (error) {\n const code = (error as { code?: unknown } | null)?.code;\n if (code === 'RUNTIME_REVISION_CONFLICT' && attempt < 2) continue;\n if (code !== 'VALIDATION_UNIQUE_CONSTRAINT' && code !== '23505')\n throw error;\n const winner = await load();\n if (!winner) throw error;\n return winner;\n }\n }\n throw new Error('Tax seed retry exhausted');\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport {\n lockDocumentMoneyParents,\n MONEY_MIGRATION_WRITE,\n withDocumentMoneyTransaction,\n} from '../document-money-internal.js';\nimport { TaxSettings } from '../models/TaxSettings.js';\nimport { MoneyValidationError } from '../money.js';\nimport { createOrLoadTaxSeed, deterministicTaxId } from '../tax/identity.js';\n\nexport class TaxSettingsCollection extends SmrtCollection<TaxSettings> {\n static readonly _itemClass = TaxSettings;\n\n async getOrCreateForTenant(tenantId: string): Promise<TaxSettings> {\n const existing = await this.getForTenant(tenantId);\n if (existing) return existing;\n const id = await deterministicTaxId(tenantId, 'settings', 'default');\n return createOrLoadTaxSeed(\n () => this.create({ id, tenantId }),\n async () =>\n (await this.list({ where: { tenantId }, limit: 1 }))[0] ?? null,\n );\n }\n\n async getForTenant(tenantId: string): Promise<TaxSettings | null> {\n const listed = (await this.list({ where: { tenantId }, limit: 1 }))[0];\n if (!listed?.id) return listed ?? null;\n return (await this.get({ id: listed.id }, { cache: false })) ?? null;\n }\n\n /** @internal Lock and re-read the one tenant marker on this transaction. */\n async lockForTenant(\n tenantId: string,\n create = true,\n ): Promise<TaxSettings | null> {\n const first = create\n ? await this.getOrCreateForTenant(tenantId)\n : await this.getForTenant(tenantId);\n if (!first?.id) return null;\n await lockDocumentMoneyParents([\n { id: first.id, tenantId, collection: this },\n ]);\n const locked = await this.getForTenant(tenantId);\n if (!locked || locked.id !== first.id)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Tenant money settings changed while acquiring the barrier',\n );\n return locked;\n }\n\n /** Claim exclusive runtime maintenance before any per-document rewrite. */\n async beginMoneyMigration(\n tenantId: string,\n requestedMigrationId?: string,\n ): Promise<string> {\n if (\n requestedMigrationId !== undefined &&\n !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n requestedMigrationId,\n )\n )\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Migration owner identity must be a UUID',\n );\n return withDocumentMoneyTransaction(this.db, async (tx) => {\n const { lockTenantMoneyMigrationClaim } = await import(\n '../tenant-money-barrier.js'\n );\n const current = await lockTenantMoneyMigrationClaim(tx, tenantId);\n const migrationId = requestedMigrationId ?? crypto.randomUUID();\n current.moneyModel = 'migrating';\n current.moneyMigrationId = migrationId;\n await current.saveMoneyModelOnTransaction(MONEY_MIGRATION_WRITE);\n return migrationId;\n });\n }\n\n async activateMoneyV2(tenantId: string, migrationId: string): Promise<void> {\n await withDocumentMoneyTransaction(this.db, async (tx) => {\n const { lockTenantMoneyMigrationOwner } = await import(\n '../tenant-money-barrier.js'\n );\n const current = await lockTenantMoneyMigrationOwner(\n tx,\n tenantId,\n migrationId,\n );\n current.moneyModel = 'v2';\n current.moneyMigrationId = '';\n await current.saveMoneyModelOnTransaction(MONEY_MIGRATION_WRITE);\n });\n }\n\n async restoreLegacyMoneyModel(\n tenantId: string,\n migrationId: string,\n ): Promise<void> {\n await withDocumentMoneyTransaction(this.db, async (tx) => {\n const { lockTenantMoneyMigrationOwner } = await import(\n '../tenant-money-barrier.js'\n );\n const current = await lockTenantMoneyMigrationOwner(\n tx,\n tenantId,\n migrationId,\n );\n current.moneyModel = 'legacy';\n current.moneyMigrationId = '';\n await current.saveMoneyModelOnTransaction(MONEY_MIGRATION_WRITE);\n });\n }\n\n /** Claim a previously completed migration for checksum-guarded rollback. */\n async beginMoneyRollback(\n tenantId: string,\n migrationId: string,\n ): Promise<void> {\n await withDocumentMoneyTransaction(this.db, async (tx) => {\n const current = await (\n await TaxSettingsCollection.create({ db: tx })\n ).lockForTenant(tenantId, false);\n if (current?.moneyModel !== 'v2')\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Money rollback requires an active v2 tenant marker',\n );\n current.moneyModel = 'migrating';\n current.moneyMigrationId = migrationId;\n await current.saveMoneyModelOnTransaction(MONEY_MIGRATION_WRITE);\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -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("
|
|
9
|
+
const { PaymentCollection } = await import("../index.js").then((n) => n.i);
|
|
10
|
+
const { InvoiceCollection } = await import("./InvoiceCollection-BUe_8FT1.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.r);
|
|
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("
|
|
32
|
+
const { PaymentCollection } = await import("../index.js").then((n) => n.i);
|
|
33
|
+
const { InvoiceCollection } = await import("./InvoiceCollection-BUe_8FT1.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");
|
|
@@ -40,11 +40,6 @@ async function assertAllocationParent(parent, kind) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
//#endregion
|
|
43
|
-
|
|
44
|
-
function assertIntegerMinorUnits(context, label, entries) {
|
|
45
|
-
for (const [field, value] of entries) if (!Number.isFinite(value) || !Number.isInteger(value)) throw new Error(`${context} ${label}: ${field} must be an integer number of minor units (cents, satoshis) \u2014 got ${value}. Money is exact: $19.99 is 1999, not 19.99.`);
|
|
46
|
-
}
|
|
47
|
-
//#endregion
|
|
48
|
-
export { claimAllocationParent as a, assertAllocationParent as i, allocationOwner as n, allocationTotal as r, assertIntegerMinorUnits as t };
|
|
43
|
+
export { claimAllocationParent as i, allocationTotal as n, assertAllocationParent as r, allocationOwner as t };
|
|
49
44
|
|
|
50
|
-
//# sourceMappingURL=
|
|
45
|
+
//# sourceMappingURL=allocation-integrity-Cq0g6CfO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allocation-integrity-Cq0g6CfO.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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MoneyValidationError, calculateMoneyDocument } from "../money.js";
|
|
2
2
|
import { ISO_4217_MINOR_UNITS } from "@happyvertical/smrt-ui/currency";
|
|
3
3
|
//#region src/svelte/invoices/currency.ts
|
|
4
4
|
var CURRENCY_PATTERN = /^[A-Z]{3}$/;
|
|
@@ -58,30 +58,89 @@ function numberDecimal(value, precision) {
|
|
|
58
58
|
scale: 10n ** BigInt(places)
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
-
function
|
|
61
|
+
function roundSignedLegacy(numerator, denominator) {
|
|
62
62
|
return numerator >= 0n ? roundHalfUp(numerator, denominator) : -((-numerator * 2n + denominator - 1n) / (denominator * 2n));
|
|
63
63
|
}
|
|
64
|
-
function
|
|
64
|
+
function decimalText(value) {
|
|
65
|
+
if (value.scale === 1n) return value.coefficient.toString();
|
|
66
|
+
const places = value.scale.toString().length - 1;
|
|
67
|
+
const digits = value.coefficient.toString().padStart(places + 1, "0");
|
|
68
|
+
const split = digits.length - places;
|
|
69
|
+
return `${digits.slice(0, split)}.${digits.slice(split)}`.replace(/0+$/, "").replace(/\.$/, "");
|
|
70
|
+
}
|
|
71
|
+
function legacyTaxCode(percent) {
|
|
72
|
+
const normalized = percent.replace(/^0+(?=\d)/, "").replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "") || "0";
|
|
73
|
+
return normalized === "0" ? {
|
|
74
|
+
id: "legacy-zero",
|
|
75
|
+
kind: "zero-rated",
|
|
76
|
+
components: []
|
|
77
|
+
} : {
|
|
78
|
+
id: `legacy-${normalized}`,
|
|
79
|
+
kind: "taxable",
|
|
80
|
+
components: [{
|
|
81
|
+
taxComponentId: `legacy-${normalized}`,
|
|
82
|
+
percent: normalized,
|
|
83
|
+
compound: false
|
|
84
|
+
}]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function calculateInvoiceMinorLine(input, options = {}) {
|
|
65
88
|
for (const amount of [input.unitPrice, input.discount]) if (!Number.isSafeInteger(amount)) throw new RangeError("Invoice money must be safe integer minor units.");
|
|
66
|
-
|
|
89
|
+
const moneyModel = options.moneyModel ?? "legacy";
|
|
90
|
+
if (moneyModel === "legacy" && input.discount < 0 || input.taxRate > 1) throw new RangeError("Invoice discount or tax rate is outside the supported range.");
|
|
67
91
|
const quantity = numberDecimal(input.quantity, 6);
|
|
68
92
|
const tax = numberDecimal(input.taxRate, 8);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
if (moneyModel === "legacy") {
|
|
94
|
+
const gross = roundSignedLegacy(quantity.coefficient * BigInt(input.unitPrice), quantity.scale);
|
|
95
|
+
const subtotal = gross - BigInt(input.discount);
|
|
96
|
+
const taxMinor = roundSignedLegacy(subtotal * tax.coefficient, tax.scale);
|
|
97
|
+
const total = subtotal + taxMinor;
|
|
98
|
+
for (const amount of [
|
|
99
|
+
gross,
|
|
100
|
+
subtotal,
|
|
101
|
+
taxMinor,
|
|
102
|
+
total
|
|
103
|
+
]) if (amount > MAX_MINOR || amount < -MAX_MINOR) throw new RangeError("Invoice line amount exceeds safe integer minor units.");
|
|
104
|
+
return {
|
|
105
|
+
grossMinor: Number(gross),
|
|
106
|
+
discountMinor: input.discount,
|
|
107
|
+
subtotalMinor: Number(subtotal),
|
|
108
|
+
taxMinor: Number(taxMinor),
|
|
109
|
+
totalMinor: Number(total)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (moneyModel !== "v2") throw new TypeError("Unknown invoice money model");
|
|
113
|
+
const percent = decimalText({
|
|
114
|
+
coefficient: tax.coefficient * 100n,
|
|
115
|
+
scale: tax.scale
|
|
116
|
+
});
|
|
117
|
+
let result;
|
|
118
|
+
try {
|
|
119
|
+
result = calculateMoneyDocument({
|
|
120
|
+
currency: "USD",
|
|
121
|
+
lineAmountType: "exclusive",
|
|
122
|
+
roundingScope: "line",
|
|
123
|
+
lines: [{
|
|
124
|
+
id: "legacy-line",
|
|
125
|
+
quantity: input.quantity,
|
|
126
|
+
unitPrice: input.unitPrice,
|
|
127
|
+
discount: input.discount === 0 ? { kind: "none" } : {
|
|
128
|
+
kind: "amount",
|
|
129
|
+
minor: input.discount
|
|
130
|
+
},
|
|
131
|
+
taxCode: legacyTaxCode(percent)
|
|
132
|
+
}]
|
|
133
|
+
}).lines[0];
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
if (cause instanceof MoneyValidationError) throw new RangeError(cause.message, { cause });
|
|
136
|
+
throw cause;
|
|
137
|
+
}
|
|
79
138
|
return {
|
|
80
|
-
grossMinor:
|
|
81
|
-
discountMinor:
|
|
82
|
-
subtotalMinor:
|
|
83
|
-
taxMinor:
|
|
84
|
-
totalMinor:
|
|
139
|
+
grossMinor: result.grossMinor,
|
|
140
|
+
discountMinor: result.discountMinor,
|
|
141
|
+
subtotalMinor: result.netMinor,
|
|
142
|
+
taxMinor: result.taxMinor,
|
|
143
|
+
totalMinor: result.netMinor + result.taxMinor
|
|
85
144
|
};
|
|
86
145
|
}
|
|
87
146
|
function resolveInvoiceLineDraft(line, context) {
|
|
@@ -141,31 +200,44 @@ function calculateInvoiceLine(line, context) {
|
|
|
141
200
|
valid: false,
|
|
142
201
|
errors
|
|
143
202
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
203
|
+
let calculated;
|
|
204
|
+
try {
|
|
205
|
+
calculated = calculateMoneyDocument({
|
|
206
|
+
currency: context.currency,
|
|
207
|
+
lineAmountType: "exclusive",
|
|
208
|
+
roundingScope: "line",
|
|
209
|
+
lines: [{
|
|
210
|
+
id: line.key,
|
|
211
|
+
quantity: Number(line.quantity),
|
|
212
|
+
unitPrice: Number(price),
|
|
213
|
+
discount: line.discountType === "percent" ? {
|
|
214
|
+
kind: "percent",
|
|
215
|
+
rate: line.discountValue
|
|
216
|
+
} : discount === 0n ? { kind: "none" } : {
|
|
217
|
+
kind: "amount",
|
|
218
|
+
minor: Number(discount)
|
|
219
|
+
},
|
|
220
|
+
taxCode: legacyTaxCode(effectiveTaxRate)
|
|
221
|
+
}]
|
|
222
|
+
}).lines[0];
|
|
223
|
+
} catch (cause) {
|
|
224
|
+
if (cause instanceof Error && "code" in cause && cause.code === "DISCOUNT_EXCEEDS_GROSS") return {
|
|
225
|
+
valid: false,
|
|
226
|
+
errors: { discountValue: "discount-exceeds-gross" }
|
|
227
|
+
};
|
|
228
|
+
return {
|
|
229
|
+
valid: false,
|
|
230
|
+
errors: { taxRate: "range" }
|
|
231
|
+
};
|
|
232
|
+
}
|
|
161
233
|
return {
|
|
162
234
|
valid: true,
|
|
163
235
|
effectiveTaxRate,
|
|
164
|
-
grossMinor:
|
|
165
|
-
discountMinor:
|
|
166
|
-
subtotalMinor:
|
|
167
|
-
taxMinor:
|
|
168
|
-
totalMinor:
|
|
236
|
+
grossMinor: calculated.grossMinor,
|
|
237
|
+
discountMinor: calculated.discountMinor,
|
|
238
|
+
subtotalMinor: calculated.netMinor,
|
|
239
|
+
taxMinor: calculated.taxMinor,
|
|
240
|
+
totalMinor: calculated.netMinor + calculated.taxMinor
|
|
169
241
|
};
|
|
170
242
|
}
|
|
171
243
|
function calendarDate(value) {
|
|
@@ -213,21 +285,29 @@ function calculateInvoiceDraft(values, options = {}) {
|
|
|
213
285
|
};
|
|
214
286
|
});
|
|
215
287
|
try {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
288
|
+
const calculated = calculateMoneyDocument({
|
|
289
|
+
currency: values.currency,
|
|
290
|
+
lineAmountType: "exclusive",
|
|
291
|
+
roundingScope: "document",
|
|
292
|
+
lines: taxable.map((row, index) => {
|
|
293
|
+
const result = lineResults[index];
|
|
294
|
+
if (!result.valid) throw new Error("Validated invoice line expected");
|
|
295
|
+
return {
|
|
296
|
+
id: row.id,
|
|
297
|
+
quantity: 1,
|
|
298
|
+
unitPrice: result.grossMinor,
|
|
299
|
+
discount: result.discountMinor === 0 ? { kind: "none" } : {
|
|
300
|
+
kind: "amount",
|
|
301
|
+
minor: result.discountMinor
|
|
302
|
+
},
|
|
303
|
+
taxCode: legacyTaxCode(row.category)
|
|
304
|
+
};
|
|
305
|
+
})
|
|
226
306
|
});
|
|
227
|
-
sums.taxMinor = BigInt(
|
|
307
|
+
sums.taxMinor = BigInt(calculated.taxMinor);
|
|
228
308
|
sums.totalMinor = sums.subtotalMinor + sums.taxMinor;
|
|
229
309
|
} catch (cause) {
|
|
230
|
-
if (!(cause instanceof RangeError)) throw cause;
|
|
310
|
+
if (!(cause instanceof RangeError || cause instanceof MoneyValidationError)) throw cause;
|
|
231
311
|
errors.total = "range";
|
|
232
312
|
}
|
|
233
313
|
}
|
|
@@ -250,4 +330,4 @@ function calculateInvoiceDraft(values, options = {}) {
|
|
|
250
330
|
//#endregion
|
|
251
331
|
export { currencyMinorUnitExponent as a, resolveInvoiceLineDraft as i, calculateInvoiceLine as n, normalizeCurrency as o, calculateInvoiceMinorLine as r, calculateInvoiceDraft as t };
|
|
252
332
|
|
|
253
|
-
//# sourceMappingURL=calculations-
|
|
333
|
+
//# sourceMappingURL=calculations-DaHR17il.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculations-DaHR17il.js","names":[],"sources":["../../src/svelte/invoices/currency.ts","../../src/svelte/invoices/calculations.ts"],"sourcesContent":["/** Svelte-free currency precision shared by invoice previews and billing. */\n\nimport { ISO_4217_MINOR_UNITS } from '@happyvertical/smrt-ui/currency';\n\nconst CURRENCY_PATTERN = /^[A-Z]{3}$/;\n\n/** Upper-case, validate, and return an ISO 4217-shaped currency code. */\nexport function normalizeCurrency(currency: string): string {\n const code = String(currency ?? '')\n .trim()\n .toUpperCase();\n if (!CURRENCY_PATTERN.test(code)) {\n throw new Error(`Currency '${currency}' must be a three-letter code.`);\n }\n return code;\n}\n\n/**\n * The ISO 4217 minor-unit exponent of a currency (2 for USD and RSD, 0 for\n * JPY, 3 for IQD). Unknown codes and currencies without a minor unit fail\n * closed because assigning either an inferred or invented exponent changes\n * the represented amount.\n */\nexport function currencyMinorUnitExponent(currency: string): number {\n const code = normalizeCurrency(currency);\n const exponent = ISO_4217_MINOR_UNITS.get(code);\n if (exponent === undefined)\n throw new Error(`Unsupported currency '${code}'.`);\n if (exponent === null)\n throw new Error(`Currency '${code}' does not define a minor unit.`);\n return exponent;\n}\n","/** Exact, transport-neutral invoice draft validation shared by browser and server. */\nimport {\n calculateMoneyDocument,\n type MoneyLineResult,\n type MoneyTaxCodeInput,\n MoneyValidationError,\n} from '../../money.js';\nimport { currencyMinorUnitExponent } from './currency.js';\n\n/** Unparsed editable invoice line; invalid values remain caller-owned. */\nexport interface InvoiceLineDraft {\n /** Stable caller-owned row identity. */\n key: string;\n /** Human-readable item description. */\n description: string;\n /** Optional stock or service code. */\n sku: string;\n /** Nonnegative decimal quantity, at most six decimal places. */\n quantity: string;\n /** Unit price in currency major units. */\n unitPrice: string;\n /** Flat major-unit discount or percentage; unknown drafts remain representable. */\n discountType: string;\n /** Discount in major units or percent according to discountType. */\n discountValue: string;\n /** Inherit the authorized default or use an explicit override. */\n taxMode: string;\n /** Override percent; zero is an explicit exemption, not inheritance. */\n taxRate: string;\n}\n\n/** Editable header and lines; clients never supply authoritative calculated totals. */\nexport interface InvoiceDraftValues {\n /** Customer reference; authorization/existence must be checked by the server. */\n customerId: string;\n /** Calendar issue date, YYYY-MM-DD. */\n issuedOn: string;\n /** Calendar due date, YYYY-MM-DD. */\n dueOn: string;\n /** Uppercase three-letter currency, with Commerce's explicit minor-unit table. */\n currency: string;\n /** Free-form payment terms. */\n paymentTerms: string;\n /** Default tax percent; the server must substitute its authorized rate. */\n taxRate: string;\n /** Caller-owned ordered lines. */\n lines: readonly InvoiceLineDraft[];\n}\n\n/** Fields on which line validation may report an error. */\nexport type InvoiceLineField = keyof InvoiceLineDraft;\n/** Stable validation reasons for caller-localized error messages. */\nexport type InvoiceCalculationError =\n | 'required'\n | 'invalid-decimal'\n | 'precision'\n | 'range'\n | 'invalid-mode'\n | 'discount-exceeds-gross'\n | 'invalid-date'\n | 'duplicate-key'\n | 'invalid-currency';\n/** Currency and server-authorized inherited percentage for a line. */\nexport interface InvoiceCalculationContext {\n /** Uppercase currency code. */\n currency: string;\n /** Default tax percent, for example \"5\". */\n inheritedTaxRate: string;\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 rounded to minor units before tax. */\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/** Success or field errors, with no plausible monetary output for an invalid line. */\nexport type InvoiceLineCalculation =\n | ({ valid: true; effectiveTaxRate: string } & InvoiceCalculatedAmounts)\n | {\n valid: false;\n errors: Partial<Record<InvoiceLineField, InvoiceCalculationError>>;\n };\n/** Header error keys include whole-invoice totals and line membership. */\nexport type InvoiceDraftField =\n | Exclude<keyof InvoiceDraftValues, 'lines'>\n | 'lines'\n | 'total'\n | 'roundingScope';\n/** Server-authorized tax rounding; absent means the historical per-line behavior. */\nexport interface InvoiceDraftCalculationOptions {\n /** Invoice scope groups equal effective percentages and rounds each group once. */\n roundingScope?: 'line' | 'invoice';\n}\n/**\n * Aggregate result retains standalone line calculations for validation/audit.\n * With invoice rounding, line tax/total are estimates, NOT allocated invoice\n * amounts, and must not be summed or persisted as authoritative invoice tax.\n */\nexport type InvoiceDraftCalculation =\n | ({\n valid: true;\n lineResults: readonly InvoiceLineCalculation[];\n } & InvoiceCalculatedAmounts)\n | {\n valid: false;\n errors: Partial<Record<InvoiceDraftField, InvoiceCalculationError>>;\n lineResults: readonly InvoiceLineCalculation[];\n };\n\ninterface Decimal {\n coefficient: bigint;\n scale: bigint;\n}\nconst MAX_MINOR = BigInt(Number.MAX_SAFE_INTEGER);\nconst MAX_TEXT = 128;\n\nfunction decimal(\n value: string,\n precision: number,\n): Decimal | InvoiceCalculationError {\n if (\n typeof value !== 'string' ||\n value.length > MAX_TEXT ||\n !/^\\d+(?:\\.\\d+)?$/.test(value)\n )\n return 'invalid-decimal';\n const [whole, fraction = ''] = value.split('.');\n if (fraction.length > precision) return 'precision';\n return {\n coefficient: BigInt(whole + fraction),\n scale: 10n ** BigInt(fraction.length),\n };\n}\n\nfunction roundHalfUp(numerator: bigint, denominator: bigint): bigint {\n return (numerator * 2n + denominator) / (denominator * 2n);\n}\n\nfunction minor(\n value: string,\n digits: number,\n): bigint | InvoiceCalculationError {\n const parsed = decimal(value, MAX_TEXT);\n if (typeof parsed === 'string') return parsed;\n const scaled = parsed.coefficient * 10n ** BigInt(digits);\n if (scaled % parsed.scale !== 0n) return 'precision';\n const result = scaled / parsed.scale;\n return result > MAX_MINOR ? 'range' : result;\n}\n\nfunction rate(value: string): Decimal | InvoiceCalculationError {\n const parsed = decimal(value, 6);\n if (typeof parsed === 'string') return parsed;\n return parsed.coefficient > 100n * parsed.scale ? 'range' : parsed;\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 /** Legacy uses a nonnegative flat discount; v2 stores the gross sign. */\n discount: number;\n /** Resolved tax fraction, at most eight fractional digits, in [0, 1]. */\n taxRate: number;\n}\n\n/** Explicit migration boundary for persisted legacy invoice-line arithmetic. */\nexport interface InvoiceMinorLineCalculationOptions {\n /** Existing callers remain legacy until #3933's per-tenant marker selects v2. */\n moneyModel?: 'legacy' | 'v2';\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 roundSignedLegacy(numerator: bigint, denominator: bigint): bigint {\n return numerator >= 0n\n ? roundHalfUp(numerator, denominator)\n : -((-numerator * 2n + denominator - 1n) / (denominator * 2n));\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\nfunction legacyTaxCode(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: 'legacy-zero', kind: 'zero-rated', components: [] }\n : {\n id: `legacy-${normalized}`,\n kind: 'taxable',\n components: [\n {\n taxComponentId: `legacy-${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 options: InvoiceMinorLineCalculationOptions = {},\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 const moneyModel = options.moneyModel ?? 'legacy';\n if ((moneyModel === 'legacy' && input.discount < 0) || input.taxRate > 1)\n throw new RangeError(\n 'Invoice discount or tax rate is outside the supported range.',\n );\n const quantity = numberDecimal(input.quantity, 6);\n const tax = numberDecimal(input.taxRate, 8);\n if (moneyModel === 'legacy') {\n const gross = roundSignedLegacy(\n quantity.coefficient * BigInt(input.unitPrice),\n quantity.scale,\n );\n const discount = BigInt(input.discount);\n const subtotal = gross - discount;\n const taxMinor = roundSignedLegacy(subtotal * tax.coefficient, tax.scale);\n const total = subtotal + taxMinor;\n for (const amount of [gross, subtotal, taxMinor, total])\n if (amount > MAX_MINOR || amount < -MAX_MINOR)\n throw new RangeError(\n 'Invoice line amount exceeds safe integer minor units.',\n );\n return {\n grossMinor: Number(gross),\n discountMinor: input.discount,\n subtotalMinor: Number(subtotal),\n taxMinor: Number(taxMinor),\n totalMinor: Number(total),\n };\n }\n if (moneyModel !== 'v2') throw new TypeError('Unknown invoice money model');\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: 'legacy-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: legacyTaxCode(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\n/** Validated line fields suitable for the existing model's resolved money columns. */\nexport interface InvoiceResolvedLine extends InvoiceMinorLineInput {\n /** Validated item description. */\n description: string;\n /** Caller stock code. */\n sku: string;\n /** Recomputed total; never copied from a browser total field. */\n amount: number;\n}\n\n/**\n * Resolve a validated editor draft to model fields. Percentage discounts become\n * exact flat minor units; inherited tax becomes the authorized effective rate.\n * Keep the raw draft alongside these fields when reopening its editing modes.\n */\nexport function resolveInvoiceLineDraft(\n line: InvoiceLineDraft,\n context: InvoiceCalculationContext,\n): InvoiceResolvedLine {\n const result = calculateInvoiceLine(line, context);\n if (!result.valid)\n throw new RangeError(\n `Invalid invoice line: ${JSON.stringify(result.errors)}`,\n );\n const quantity = Number(line.quantity);\n const sourceQuantity = decimal(line.quantity, 6);\n const storedQuantity = numberDecimal(quantity, 6);\n if (\n typeof sourceQuantity === 'string' ||\n sourceQuantity.coefficient * storedQuantity.scale !==\n storedQuantity.coefficient * sourceQuantity.scale\n )\n throw new RangeError(\n 'Invoice quantity cannot round-trip through numeric persistence.',\n );\n const [whole, fraction = ''] = result.effectiveTaxRate.split('.');\n const rateDigits = BigInt(whole + fraction)\n .toString()\n .padStart(fraction.length + 3, '0');\n const split = rateDigits.length - fraction.length - 2;\n const taxRate = Number(\n `${rateDigits.slice(0, split)}.${rateDigits.slice(split)}`,\n );\n const unitPrice = minor(\n line.unitPrice,\n currencyMinorUnitExponent(context.currency),\n );\n if (typeof unitPrice !== 'bigint')\n throw new RangeError('Invalid invoice unit price.');\n const resolved = {\n description: line.description,\n sku: line.sku,\n quantity,\n unitPrice: Number(unitPrice),\n discount: result.discountMinor,\n taxRate,\n amount: result.totalMinor,\n };\n const model = calculateInvoiceMinorLine(resolved);\n if (\n model.totalMinor !== result.totalMinor ||\n model.subtotalMinor !== result.subtotalMinor ||\n model.taxMinor !== result.taxMinor\n )\n throw new RangeError('Invoice draft and persisted calculation disagree.');\n return resolved;\n}\n\n/**\n * Validate and calculate one line using decimal integer arithmetic. Round half\n * up separately at gross, percentage discount, and tax boundaries. No rounding\n * is permitted when parsing a price or flat discount into currency minor units.\n */\nexport function calculateInvoiceLine(\n line: InvoiceLineDraft,\n context: InvoiceCalculationContext,\n): InvoiceLineCalculation {\n const errors: Partial<Record<InvoiceLineField, InvoiceCalculationError>> = {};\n if (!line.key?.trim()) errors.key = 'required';\n if (!line.description?.trim()) errors.description = 'required';\n if (!/^[A-Z]{3}$/.test(context.currency))\n return { valid: false, errors: { unitPrice: 'invalid-currency' } };\n const digits = currencyMinorUnitExponent(context.currency);\n const quantity = decimal(line.quantity, 6);\n const price = minor(line.unitPrice, digits);\n if (typeof quantity === 'string') errors.quantity = quantity;\n else if (quantity.coefficient > MAX_MINOR * quantity.scale)\n errors.quantity = 'range';\n else {\n try {\n const persisted = numberDecimal(Number(line.quantity), 6);\n if (\n quantity.coefficient * persisted.scale !==\n persisted.coefficient * quantity.scale\n )\n errors.quantity = 'precision';\n } catch {\n errors.quantity = 'precision';\n }\n }\n if (typeof price === 'string') errors.unitPrice = price;\n if (line.discountType !== 'flat' && line.discountType !== 'percent')\n errors.discountType = 'invalid-mode';\n if (line.taxMode !== 'inherit' && line.taxMode !== 'override')\n errors.taxMode = 'invalid-mode';\n const discount =\n line.discountType === 'percent'\n ? rate(line.discountValue)\n : minor(line.discountValue, digits);\n if (typeof discount === 'string') errors.discountValue = discount;\n const effectiveTaxRate =\n line.taxMode === 'inherit' ? context.inheritedTaxRate : line.taxRate;\n const tax = rate(effectiveTaxRate);\n if (typeof tax === 'string') errors.taxRate = tax;\n if (\n Object.keys(errors).length ||\n typeof quantity === 'string' ||\n typeof price === 'string' ||\n typeof discount === 'string' ||\n typeof tax === 'string'\n )\n return { valid: false, errors };\n let calculated: MoneyLineResult;\n try {\n calculated = calculateMoneyDocument({\n currency: context.currency,\n lineAmountType: 'exclusive',\n roundingScope: 'line',\n lines: [\n {\n id: line.key,\n quantity: Number(line.quantity),\n unitPrice: Number(price),\n discount:\n line.discountType === 'percent'\n ? { kind: 'percent', rate: line.discountValue }\n : discount === 0n\n ? { kind: 'none' }\n : { kind: 'amount', minor: Number(discount) },\n taxCode: legacyTaxCode(effectiveTaxRate),\n },\n ],\n }).lines[0];\n } catch (cause) {\n if (\n cause instanceof Error &&\n 'code' in cause &&\n cause.code === 'DISCOUNT_EXCEEDS_GROSS'\n )\n return {\n valid: false,\n errors: { discountValue: 'discount-exceeds-gross' },\n };\n return { valid: false, errors: { taxRate: 'range' } };\n }\n return {\n valid: true,\n effectiveTaxRate,\n grossMinor: calculated.grossMinor,\n discountMinor: calculated.discountMinor,\n subtotalMinor: calculated.netMinor,\n taxMinor: calculated.taxMinor,\n totalMinor: calculated.netMinor + calculated.taxMinor,\n };\n}\n\nfunction calendarDate(value: string): boolean {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value) || value.startsWith('0000'))\n return false;\n const date = new Date(`${value}T00:00:00.000Z`);\n return (\n Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value\n );\n}\n\n/** Validate the header and sum validated lines without floating-point arithmetic. */\nexport function calculateInvoiceDraft(\n values: InvoiceDraftValues,\n options: InvoiceDraftCalculationOptions = {},\n): InvoiceDraftCalculation {\n const errors: Partial<Record<InvoiceDraftField, InvoiceCalculationError>> =\n {};\n const roundingScope =\n options.roundingScope === undefined ? 'line' : options.roundingScope;\n if (roundingScope !== 'line' && roundingScope !== 'invoice')\n errors.roundingScope = 'invalid-mode';\n if (!values.customerId?.trim()) errors.customerId = 'required';\n if (!calendarDate(values.issuedOn)) errors.issuedOn = 'invalid-date';\n if (!calendarDate(values.dueOn) || values.dueOn < values.issuedOn)\n errors.dueOn = 'invalid-date';\n if (!/^[A-Z]{3}$/.test(values.currency)) errors.currency = 'invalid-currency';\n const inheritedRate = rate(values.taxRate);\n if (typeof inheritedRate === 'string') errors.taxRate = inheritedRate;\n if (values.lines.length === 0) errors.lines = 'required';\n if (\n new Set(values.lines.map((line) => line.key)).size !== values.lines.length\n )\n errors.lines = 'duplicate-key';\n const lineResults = values.lines.map((line) =>\n calculateInvoiceLine(line, {\n currency: values.currency,\n inheritedTaxRate: values.taxRate,\n }),\n );\n const sums = {\n grossMinor: 0n,\n discountMinor: 0n,\n subtotalMinor: 0n,\n taxMinor: 0n,\n totalMinor: 0n,\n };\n for (const result of lineResults) {\n if (!result.valid) continue;\n for (const key of Object.keys(sums) as Array<keyof typeof sums>)\n sums[key] += BigInt(result[key]);\n }\n if (\n roundingScope === 'invoice' &&\n !Object.keys(errors).length &&\n lineResults.every((result) => result.valid)\n ) {\n // Rates have already passed exact decimal validation. Normalize textual\n // equivalents without numeric conversion before assigning component IDs.\n const taxable = lineResults.map((result, index) => {\n if (!result.valid) throw new Error('Validated invoice line expected');\n const [whole, fraction = ''] = result.effectiveTaxRate.split('.');\n const digits = fraction.replace(/0+$/, '');\n const percent = `${BigInt(whole)}${digits ? `.${digits}` : ''}`;\n return {\n id: values.lines[index].key,\n amountMinor: result.subtotalMinor,\n category: percent,\n };\n });\n try {\n const calculated = calculateMoneyDocument({\n currency: values.currency,\n lineAmountType: 'exclusive',\n roundingScope: 'document',\n lines: taxable.map((row, index) => {\n const result = lineResults[index];\n if (!result.valid) throw new Error('Validated invoice line expected');\n return {\n id: row.id,\n quantity: 1,\n unitPrice: result.grossMinor,\n discount:\n result.discountMinor === 0\n ? ({ kind: 'none' } as const)\n : ({ kind: 'amount', minor: result.discountMinor } as const),\n taxCode: legacyTaxCode(row.category),\n };\n }),\n });\n sums.taxMinor = BigInt(calculated.taxMinor);\n sums.totalMinor = sums.subtotalMinor + sums.taxMinor;\n } catch (cause) {\n if (\n !(cause instanceof RangeError || cause instanceof MoneyValidationError)\n )\n throw cause;\n errors.total = 'range';\n }\n }\n if (Object.values(sums).some((value) => value > MAX_MINOR))\n errors.total = 'range';\n if (Object.keys(errors).length || lineResults.some((result) => !result.valid))\n return { valid: false, errors, lineResults };\n return {\n valid: true,\n lineResults,\n grossMinor: Number(sums.grossMinor),\n discountMinor: Number(sums.discountMinor),\n subtotalMinor: Number(sums.subtotalMinor),\n taxMinor: Number(sums.taxMinor),\n totalMinor: Number(sums.totalMinor),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { n as roundSignedRational } from "./money-rational-CcW7gzG5.js";
|
|
2
|
+
import { MoneyValidationError } from "../money.js";
|
|
3
|
+
//#region src/credit-allocation.ts
|
|
4
|
+
var QUANTITY_SCALE = 1000000n;
|
|
5
|
+
function quantityUnits(value, label) {
|
|
6
|
+
if (!/^(?:0|[1-9]\d*)(?:\.\d{1,6})?$/.test(value)) throw new MoneyValidationError("INVALID_MONEY_INPUT", `${label} must be a nonnegative decimal with at most six places`);
|
|
7
|
+
const [whole, fraction = ""] = value.split(".");
|
|
8
|
+
return BigInt(whole) * QUANTITY_SCALE + BigInt(fraction.padEnd(6, "0"));
|
|
9
|
+
}
|
|
10
|
+
function safeSigned(value, label) {
|
|
11
|
+
if (value < BigInt(Number.MIN_SAFE_INTEGER) || value > BigInt(Number.MAX_SAFE_INTEGER)) throw new MoneyValidationError("UNSAFE_MONEY", `${label} is unsafe`);
|
|
12
|
+
return Number(value);
|
|
13
|
+
}
|
|
14
|
+
function magnitude(value) {
|
|
15
|
+
return value < 0n ? -value : value;
|
|
16
|
+
}
|
|
17
|
+
function calculateAvailableRefund(input) {
|
|
18
|
+
for (const [label, value] of Object.entries(input)) if (!Number.isSafeInteger(value) || value < 0) throw new MoneyValidationError("UNSAFE_MONEY", `${label} must be a nonnegative safe integer`);
|
|
19
|
+
const outstanding = Math.max(0, input.sourceTotalMinor - input.sourcePaidMinor);
|
|
20
|
+
return Math.max(0, Math.max(0, input.issuedCreditMinor - outstanding) - input.settledRefundMinor);
|
|
21
|
+
}
|
|
22
|
+
function allocateFrozenCredit(sourceLines, issued, requests) {
|
|
23
|
+
for (const source of sourceLines) {
|
|
24
|
+
if (!source.id || !Number.isSafeInteger(source.netMinor) || !Number.isSafeInteger(source.discountMinor) || !Number.isSafeInteger(source.unitPrice)) throw new MoneyValidationError("UNSAFE_MONEY", "Frozen source money must use safe integer minor units");
|
|
25
|
+
if (quantityUnits(source.quantity, "Source quantity") <= 0n) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Frozen source quantity must be positive");
|
|
26
|
+
}
|
|
27
|
+
const sources = new Map(sourceLines.map((line) => [line.id, line]));
|
|
28
|
+
if (sources.size !== sourceLines.length || !requests.length) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credit allocation requires distinct source lines and requests");
|
|
29
|
+
const requestedIds = /* @__PURE__ */ new Set();
|
|
30
|
+
const consumed = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const row of issued) {
|
|
32
|
+
if (!row.creditedLineId || !Number.isInteger(row.netMinor) || !Number.isInteger(row.discountMinor)) throw new MoneyValidationError("UNSAFE_MONEY", "Issued credit money must use safe integer minor units");
|
|
33
|
+
if (!Number.isSafeInteger(row.netMinor) || !Number.isSafeInteger(row.discountMinor)) throw new MoneyValidationError("UNSAFE_MONEY", "Issued credit money must use safe integer minor units");
|
|
34
|
+
if (!sources.has(row.creditedLineId)) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Issued credit references unknown line ${row.creditedLineId}`);
|
|
35
|
+
const current = consumed.get(row.creditedLineId) ?? {
|
|
36
|
+
quantity: 0n,
|
|
37
|
+
net: 0n,
|
|
38
|
+
discount: 0n
|
|
39
|
+
};
|
|
40
|
+
const issuedQuantity = quantityUnits(row.quantity, "Issued credit quantity");
|
|
41
|
+
current.quantity += issuedQuantity;
|
|
42
|
+
current.net += BigInt(row.netMinor);
|
|
43
|
+
current.discount += BigInt(row.discountMinor);
|
|
44
|
+
consumed.set(row.creditedLineId, current);
|
|
45
|
+
}
|
|
46
|
+
const allocations = [...requests].sort((left, right) => left.creditedLineId < right.creditedLineId ? -1 : 1).map((request) => {
|
|
47
|
+
if (requestedIds.has(request.creditedLineId)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "A credit may reference a source line only once");
|
|
48
|
+
requestedIds.add(request.creditedLineId);
|
|
49
|
+
const source = sources.get(request.creditedLineId);
|
|
50
|
+
if (!source) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Unknown credited line ${request.creditedLineId}`);
|
|
51
|
+
const used = consumed.get(source.id) ?? {
|
|
52
|
+
quantity: 0n,
|
|
53
|
+
net: 0n,
|
|
54
|
+
discount: 0n
|
|
55
|
+
};
|
|
56
|
+
const sourceQuantity = quantityUnits(source.quantity, "Source quantity");
|
|
57
|
+
const remainingQuantity = sourceQuantity - used.quantity;
|
|
58
|
+
const sourceNet = BigInt(source.netMinor);
|
|
59
|
+
const sourceDiscount = BigInt(source.discountMinor);
|
|
60
|
+
const sign = sourceNet < 0n ? -1n : 1n;
|
|
61
|
+
if (used.net !== 0n && (used.net < 0n ? -1n : 1n) !== sign || used.discount !== 0n && sourceDiscount !== 0n && (used.discount < 0n ? -1n : 1n) !== (sourceDiscount < 0n ? -1n : 1n)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Issued credit signs must match the frozen source line");
|
|
62
|
+
const remainingNet = magnitude(sourceNet) - magnitude(used.net);
|
|
63
|
+
const remainingDiscount = magnitude(sourceDiscount) - magnitude(used.discount);
|
|
64
|
+
if (remainingQuantity < 0n || remainingNet < 0n || remainingDiscount < 0n) throw new MoneyValidationError("CREDIT_EXCEEDS_LINE", "Persisted credits already exceed the frozen source line");
|
|
65
|
+
const snapshot = {
|
|
66
|
+
sourceQuantity: source.quantity,
|
|
67
|
+
sourceUnitPrice: source.unitPrice,
|
|
68
|
+
sourceDiscountType: source.discountType,
|
|
69
|
+
sourceDiscountRate: String(source.discountRate),
|
|
70
|
+
sourceDiscountMinor: source.discountMinor
|
|
71
|
+
};
|
|
72
|
+
if ("quantity" in request) {
|
|
73
|
+
const quantity = quantityUnits(request.quantity, "Credit quantity");
|
|
74
|
+
if (quantity <= 0n || quantity > remainingQuantity) throw new MoneyValidationError("CREDIT_EXCEEDS_LINE", "Credit quantity exceeds the remaining frozen quantity");
|
|
75
|
+
const exhaustsLine = quantity === remainingQuantity;
|
|
76
|
+
const discountSign = sourceDiscount < 0n ? -1n : 1n;
|
|
77
|
+
const incrementalDiscount = (source.discountType === "none" ? 0n : roundSignedRational(sourceDiscount * (used.quantity + quantity), sourceQuantity)) - used.discount;
|
|
78
|
+
const boundedIncrementalDiscount = incrementalDiscount === 0n || (incrementalDiscount < 0n ? -1n : 1n) !== discountSign ? 0n : discountSign * (magnitude(incrementalDiscount) < remainingDiscount ? magnitude(incrementalDiscount) : remainingDiscount);
|
|
79
|
+
const discount = source.discountType === "none" ? 0n : exhaustsLine ? discountSign * remainingDiscount : boundedIncrementalDiscount;
|
|
80
|
+
return {
|
|
81
|
+
creditedLineId: source.id,
|
|
82
|
+
basis: {
|
|
83
|
+
kind: "quantity",
|
|
84
|
+
quantity: request.quantity,
|
|
85
|
+
...snapshot
|
|
86
|
+
},
|
|
87
|
+
netMinor: null,
|
|
88
|
+
discountMinor: safeSigned(discount, "Credit discount"),
|
|
89
|
+
remainingNetMinor: safeSigned(sign * remainingNet, "Remaining credit net amount"),
|
|
90
|
+
exhaustsLine
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (!Number.isSafeInteger(request.netMinor) || request.netMinor <= 0) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Credit net amount must be a positive safe integer");
|
|
94
|
+
const amount = BigInt(request.netMinor);
|
|
95
|
+
if (amount > remainingNet) throw new MoneyValidationError("CREDIT_EXCEEDS_LINE", "Credit net amount exceeds the remaining frozen net amount");
|
|
96
|
+
return {
|
|
97
|
+
creditedLineId: source.id,
|
|
98
|
+
basis: {
|
|
99
|
+
kind: "net_amount",
|
|
100
|
+
netMinor: request.netMinor,
|
|
101
|
+
...snapshot
|
|
102
|
+
},
|
|
103
|
+
netMinor: safeSigned(sign * amount, "Credit net amount"),
|
|
104
|
+
discountMinor: 0,
|
|
105
|
+
remainingNetMinor: safeSigned(sign * remainingNet, "Remaining credit net amount"),
|
|
106
|
+
exhaustsLine: amount === remainingNet
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const exhausted = new Set(allocations.filter((row) => row.exhaustsLine).map((row) => row.creditedLineId));
|
|
110
|
+
return {
|
|
111
|
+
allocations,
|
|
112
|
+
exhaustsDocument: sourceLines.every((line) => {
|
|
113
|
+
const used = consumed.get(line.id);
|
|
114
|
+
return magnitude(BigInt(line.netMinor)) - magnitude(used?.net ?? 0n) === 0n || exhausted.has(line.id);
|
|
115
|
+
})
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { calculateAvailableRefund as n, allocateFrozenCredit as t };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=credit-allocation-yspz0Lab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credit-allocation-yspz0Lab.js","names":[],"sources":["../../src/credit-allocation.ts"],"sourcesContent":["/** Pure allocation of positive credit requests against frozen invoice lines. */\n\nimport { MoneyValidationError } from './money.js';\nimport { roundSignedRational } from './money-rational.js';\nimport type { InvoiceCreditCalculationBasis } from './types/index.js';\n\nconst QUANTITY_SCALE = 1_000_000n;\n\nfunction quantityUnits(value: string, label: string): bigint {\n if (!/^(?:0|[1-9]\\d*)(?:\\.\\d{1,6})?$/.test(value))\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `${label} must be a nonnegative decimal with at most six places`,\n );\n const [whole, fraction = ''] = value.split('.');\n return BigInt(whole) * QUANTITY_SCALE + BigInt(fraction.padEnd(6, '0'));\n}\n\nfunction safeSigned(value: bigint, label: string): number {\n if (\n value < BigInt(Number.MIN_SAFE_INTEGER) ||\n value > BigInt(Number.MAX_SAFE_INTEGER)\n )\n throw new MoneyValidationError('UNSAFE_MONEY', `${label} is unsafe`);\n return Number(value);\n}\n\nfunction magnitude(value: bigint): bigint {\n return value < 0n ? -value : value;\n}\n\nexport interface FrozenCreditSourceLine {\n id: string;\n quantity: string;\n netMinor: number;\n discountMinor: number;\n unitPrice: number;\n discountType: 'none' | 'percent' | 'amount';\n discountRate: string;\n}\n\nexport interface IssuedCreditConsumption {\n creditedLineId: string;\n quantity: string;\n netMinor: number;\n discountMinor: number;\n}\n\nexport type FrozenCreditRequest =\n | { creditedLineId: string; quantity: string }\n | { creditedLineId: string; netMinor: number };\n\nexport interface FrozenCreditAllocation {\n creditedLineId: string;\n basis: InvoiceCreditCalculationBasis;\n netMinor: number | null;\n discountMinor: number;\n /** Signed tax-exclusive source capacity remaining before this allocation. */\n remainingNetMinor: number;\n exhaustsLine: boolean;\n}\n\nexport interface FrozenCreditAllocationResult {\n allocations: FrozenCreditAllocation[];\n exhaustsDocument: boolean;\n}\n\nexport interface AvailableRefundInput {\n sourceTotalMinor: number;\n sourcePaidMinor: number;\n issuedCreditMinor: number;\n settledRefundMinor: number;\n}\n\n/** Exact unapplied credit available to refund across one source invoice. */\nexport function calculateAvailableRefund(input: AvailableRefundInput): number {\n for (const [label, value] of Object.entries(input)) {\n if (!Number.isSafeInteger(value) || value < 0)\n throw new MoneyValidationError(\n 'UNSAFE_MONEY',\n `${label} must be a nonnegative safe integer`,\n );\n }\n const outstanding = Math.max(\n 0,\n input.sourceTotalMinor - input.sourcePaidMinor,\n );\n return Math.max(\n 0,\n Math.max(0, input.issuedCreditMinor - outstanding) -\n input.settledRefundMinor,\n );\n}\n\n/** Validate caps and normalize flat-discount/final-residual allocation. */\nexport function allocateFrozenCredit(\n sourceLines: readonly FrozenCreditSourceLine[],\n issued: readonly IssuedCreditConsumption[],\n requests: readonly FrozenCreditRequest[],\n): FrozenCreditAllocationResult {\n for (const source of sourceLines) {\n if (\n !source.id ||\n !Number.isSafeInteger(source.netMinor) ||\n !Number.isSafeInteger(source.discountMinor) ||\n !Number.isSafeInteger(source.unitPrice)\n )\n throw new MoneyValidationError(\n 'UNSAFE_MONEY',\n 'Frozen source money must use safe integer minor units',\n );\n if (quantityUnits(source.quantity, 'Source quantity') <= 0n)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Frozen source quantity must be positive',\n );\n }\n const sources = new Map(sourceLines.map((line) => [line.id, line]));\n if (sources.size !== sourceLines.length || !requests.length)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Credit allocation requires distinct source lines and requests',\n );\n const requestedIds = new Set<string>();\n const consumed = new Map<\n string,\n { quantity: bigint; net: bigint; discount: bigint }\n >();\n for (const row of issued) {\n if (\n !row.creditedLineId ||\n !Number.isInteger(row.netMinor) ||\n !Number.isInteger(row.discountMinor)\n )\n throw new MoneyValidationError(\n 'UNSAFE_MONEY',\n 'Issued credit money must use safe integer minor units',\n );\n if (\n !Number.isSafeInteger(row.netMinor) ||\n !Number.isSafeInteger(row.discountMinor)\n )\n throw new MoneyValidationError(\n 'UNSAFE_MONEY',\n 'Issued credit money must use safe integer minor units',\n );\n if (!sources.has(row.creditedLineId))\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Issued credit references unknown line ${row.creditedLineId}`,\n );\n const current = consumed.get(row.creditedLineId) ?? {\n quantity: 0n,\n net: 0n,\n discount: 0n,\n };\n const issuedQuantity = quantityUnits(\n row.quantity,\n 'Issued credit quantity',\n );\n current.quantity += issuedQuantity;\n current.net += BigInt(row.netMinor);\n current.discount += BigInt(row.discountMinor);\n consumed.set(row.creditedLineId, current);\n }\n const allocations = [...requests]\n .sort((left, right) =>\n left.creditedLineId < right.creditedLineId ? -1 : 1,\n )\n .map((request): FrozenCreditAllocation => {\n if (requestedIds.has(request.creditedLineId))\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'A credit may reference a source line only once',\n );\n requestedIds.add(request.creditedLineId);\n const source = sources.get(request.creditedLineId);\n if (!source)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Unknown credited line ${request.creditedLineId}`,\n );\n const used = consumed.get(source.id) ?? {\n quantity: 0n,\n net: 0n,\n discount: 0n,\n };\n const sourceQuantity = quantityUnits(source.quantity, 'Source quantity');\n const remainingQuantity = sourceQuantity - used.quantity;\n const sourceNet = BigInt(source.netMinor);\n const sourceDiscount = BigInt(source.discountMinor);\n const sign = sourceNet < 0n ? -1n : 1n;\n if (\n (used.net !== 0n && (used.net < 0n ? -1n : 1n) !== sign) ||\n (used.discount !== 0n &&\n sourceDiscount !== 0n &&\n (used.discount < 0n ? -1n : 1n) !== (sourceDiscount < 0n ? -1n : 1n))\n )\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Issued credit signs must match the frozen source line',\n );\n const remainingNet = magnitude(sourceNet) - magnitude(used.net);\n const remainingDiscount =\n magnitude(sourceDiscount) - magnitude(used.discount);\n if (remainingQuantity < 0n || remainingNet < 0n || remainingDiscount < 0n)\n throw new MoneyValidationError(\n 'CREDIT_EXCEEDS_LINE',\n 'Persisted credits already exceed the frozen source line',\n );\n const snapshot = {\n sourceQuantity: source.quantity,\n sourceUnitPrice: source.unitPrice,\n sourceDiscountType: source.discountType,\n sourceDiscountRate: String(source.discountRate),\n sourceDiscountMinor: source.discountMinor,\n } as const;\n if ('quantity' in request) {\n const quantity = quantityUnits(request.quantity, 'Credit quantity');\n if (quantity <= 0n || quantity > remainingQuantity)\n throw new MoneyValidationError(\n 'CREDIT_EXCEEDS_LINE',\n 'Credit quantity exceeds the remaining frozen quantity',\n );\n const exhaustsLine = quantity === remainingQuantity;\n const discountSign = sourceDiscount < 0n ? -1n : 1n;\n const cumulativeDiscount =\n source.discountType === 'none'\n ? 0n\n : roundSignedRational(\n sourceDiscount * (used.quantity + quantity),\n sourceQuantity,\n );\n const incrementalDiscount = cumulativeDiscount - used.discount;\n const boundedIncrementalDiscount =\n incrementalDiscount === 0n ||\n (incrementalDiscount < 0n ? -1n : 1n) !== discountSign\n ? 0n\n : discountSign *\n (magnitude(incrementalDiscount) < remainingDiscount\n ? magnitude(incrementalDiscount)\n : remainingDiscount);\n const discount =\n source.discountType === 'none'\n ? 0n\n : exhaustsLine\n ? discountSign * remainingDiscount\n : boundedIncrementalDiscount;\n return {\n creditedLineId: source.id,\n basis: { kind: 'quantity', quantity: request.quantity, ...snapshot },\n netMinor: null,\n discountMinor: safeSigned(discount, 'Credit discount'),\n remainingNetMinor: safeSigned(\n sign * remainingNet,\n 'Remaining credit net amount',\n ),\n exhaustsLine,\n };\n }\n if (!Number.isSafeInteger(request.netMinor) || request.netMinor <= 0)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Credit net amount must be a positive safe integer',\n );\n const amount = BigInt(request.netMinor);\n if (amount > remainingNet)\n throw new MoneyValidationError(\n 'CREDIT_EXCEEDS_LINE',\n 'Credit net amount exceeds the remaining frozen net amount',\n );\n return {\n creditedLineId: source.id,\n basis: { kind: 'net_amount', netMinor: request.netMinor, ...snapshot },\n netMinor: safeSigned(sign * amount, 'Credit net amount'),\n discountMinor: 0,\n remainingNetMinor: safeSigned(\n sign * remainingNet,\n 'Remaining credit net amount',\n ),\n exhaustsLine: amount === remainingNet,\n };\n });\n const exhausted = new Set(\n allocations\n .filter((row) => row.exhaustsLine)\n .map((row) => row.creditedLineId),\n );\n return {\n allocations,\n exhaustsDocument: sourceLines.every((line) => {\n const used = consumed.get(line.id);\n return (\n magnitude(BigInt(line.netMinor)) - magnitude(used?.net ?? 0n) === 0n ||\n exhausted.has(line.id)\n );\n }),\n };\n}\n"],"mappings":"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"}
|