@happyvertical/smrt-commerce 0.58.0 → 0.59.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/accounting-snapshot-mapper.md +3 -0
- package/agents/quickbooks-pilot.md +56 -0
- package/dist/chunks/{ContractLineItemCollection-D74LbDOI.js → ContractLineItemCollection-BFWJ2e6a.js} +4 -4
- package/dist/chunks/{ContractLineItemCollection-D74LbDOI.js.map → ContractLineItemCollection-BFWJ2e6a.js.map} +1 -1
- package/dist/chunks/{DocumentTaxCollection-BmKyJGZi.js → DocumentTaxCollection-MFfprLH-.js} +9 -9
- package/dist/chunks/{DocumentTaxCollection-BmKyJGZi.js.map → DocumentTaxCollection-MFfprLH-.js.map} +1 -1
- package/dist/chunks/{InvoiceCollection-BPEzXaKm.js → InvoiceCollection-D_8NXuaw.js} +9 -9
- package/dist/chunks/{InvoiceCollection-BPEzXaKm.js.map → InvoiceCollection-D_8NXuaw.js.map} +1 -1
- package/dist/chunks/{TaxCodeCollection-DpQeIqP4.js → TaxCodeCollection-Bc-Z8alx.js} +2 -2
- package/dist/chunks/{TaxCodeCollection-DpQeIqP4.js.map → TaxCodeCollection-Bc-Z8alx.js.map} +1 -1
- package/dist/chunks/{TaxComponentCollection-wFGWOaxy.js → TaxComponentCollection-DUY7A2sU.js} +3 -3
- package/dist/chunks/{TaxComponentCollection-wFGWOaxy.js.map → TaxComponentCollection-DUY7A2sU.js.map} +1 -1
- package/dist/chunks/{allocation-integrity-DgnPec2a.js → allocation-integrity-ri5cUYXB.js} +6 -6
- package/dist/chunks/{allocation-integrity-DgnPec2a.js.map → allocation-integrity-ri5cUYXB.js.map} +1 -1
- package/dist/chunks/{contract-money-BxvY2Kcp.js → contract-money-By1XIzp_.js} +6 -6
- package/dist/chunks/{contract-money-BxvY2Kcp.js.map → contract-money-By1XIzp_.js.map} +1 -1
- package/dist/chunks/{invoice-posting-C1q3CIYz.js → invoice-posting-BfnOyqBR.js} +3 -3
- package/dist/chunks/{invoice-posting-C1q3CIYz.js.map → invoice-posting-BfnOyqBR.js.map} +1 -1
- package/dist/chunks/{retention-accounting-BAKsmbwJ.js → retention-accounting-CASwH8OH.js} +239 -74
- package/dist/chunks/retention-accounting-CASwH8OH.js.map +1 -0
- package/dist/chunks/{tenant-money-barrier-j1m0r6pe.js → tenant-money-barrier-BbbInDGP.js} +3 -3
- package/dist/chunks/{tenant-money-barrier-j1m0r6pe.js.map → tenant-money-barrier-BbbInDGP.js.map} +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/dist/invoice-accounting-sync.d.ts +33 -1
- package/dist/invoice-accounting-sync.d.ts.map +1 -1
- package/dist/invoices.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/quickbooks-pilot-evidence.d.ts +47 -0
- package/dist/quickbooks-pilot-evidence.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +3 -3
- package/package.json +13 -13
- package/dist/chunks/retention-accounting-BAKsmbwJ.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TaxComponentCollection-wFGWOaxy.js","names":[],"sources":["../../src/models/TaxComponent.ts","../../src/collections/TaxComponentCollection.ts"],"sourcesContent":["import {\n crossPackageRef,\n field,\n SmrtObject,\n type SmrtSaveOptions,\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(options?: SmrtSaveOptions): 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 callerRevision = options?.expectedUpdatedAt;\n const referenced = await this.isReferenced(persisted, callerRevision);\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 (callerRevision !== undefined && persisted.updated_at)\n options = { ...options, expectedUpdatedAt: 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(options)) as this;\n });\n }\n\n private async isReferenced(\n persisted: TaxComponent,\n expectedUpdatedAt?: Date | string,\n ): 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(expectedUpdatedAt ?? persisted.updated_at);\n const { TaxCodeCollection } = await import(\n '../collections/TaxCodeCollection.js'\n );\n const { DocumentTaxCollection } = await import(\n '../collections/DocumentTaxCollection.js'\n );\n const codes = await TaxCodeCollection.create({ db: this.db });\n const usedByCode = (\n await codes.list({ where: { tenantId: this.tenantId } })\n ).some((code) =>\n code.components.some((component) => component.taxComponentId === this.id),\n );\n if (usedByCode) return true;\n const documentTaxes = await DocumentTaxCollection.create({ db: this.db });\n return (\n (\n await documentTaxes.list({\n where: { tenantId: this.tenantId, taxComponentId: this.id },\n limit: 1,\n })\n ).length > 0\n );\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { TaxComponent } from '../models/TaxComponent.js';\n\nexport class TaxComponentCollection extends SmrtCollection<TaxComponent> {\n static readonly _itemClass = TaxComponent;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TaxComponentCollection-DUY7A2sU.js","names":[],"sources":["../../src/models/TaxComponent.ts","../../src/collections/TaxComponentCollection.ts"],"sourcesContent":["import {\n crossPackageRef,\n field,\n SmrtObject,\n type SmrtSaveOptions,\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(options?: SmrtSaveOptions): 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 callerRevision = options?.expectedUpdatedAt;\n const referenced = await this.isReferenced(persisted, callerRevision);\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 (callerRevision !== undefined && persisted.updated_at)\n options = { ...options, expectedUpdatedAt: 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(options)) as this;\n });\n }\n\n private async isReferenced(\n persisted: TaxComponent,\n expectedUpdatedAt?: Date | string,\n ): 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(expectedUpdatedAt ?? persisted.updated_at);\n const { TaxCodeCollection } = await import(\n '../collections/TaxCodeCollection.js'\n );\n const { DocumentTaxCollection } = await import(\n '../collections/DocumentTaxCollection.js'\n );\n const codes = await TaxCodeCollection.create({ db: this.db });\n const usedByCode = (\n await codes.list({ where: { tenantId: this.tenantId } })\n ).some((code) =>\n code.components.some((component) => component.taxComponentId === this.id),\n );\n if (usedByCode) return true;\n const documentTaxes = await DocumentTaxCollection.create({ db: this.db });\n return (\n (\n await documentTaxes.list({\n where: { tenantId: this.tenantId, taxComponentId: this.id },\n limit: 1,\n })\n ).length > 0\n );\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { TaxComponent } from '../models/TaxComponent.js';\n\nexport class TaxComponentCollection extends SmrtCollection<TaxComponent> {\n static readonly _itemClass = TaxComponent;\n}\n"],"mappings":"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"}
|
|
@@ -6,8 +6,8 @@ function allocationOwner(value) {
|
|
|
6
6
|
}
|
|
7
7
|
async function claimAllocationParent(parent, kind, expectedUpdatedAt) {
|
|
8
8
|
if (!parent.id) return;
|
|
9
|
-
const { PaymentCollection } = await import("./retention-accounting-
|
|
10
|
-
const { InvoiceCollection } = await import("./InvoiceCollection-
|
|
9
|
+
const { PaymentCollection } = await import("./retention-accounting-CASwH8OH.js").then((n) => n.I);
|
|
10
|
+
const { InvoiceCollection } = await import("./InvoiceCollection-D_8NXuaw.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 ? expectedUpdatedAt ?? 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("./retention-accounting-
|
|
27
|
+
const { PaymentAllocationCollection } = await import("./retention-accounting-CASwH8OH.js").then((n) => n.M);
|
|
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("./retention-accounting-
|
|
33
|
-
const { InvoiceCollection } = await import("./InvoiceCollection-
|
|
32
|
+
const { PaymentCollection } = await import("./retention-accounting-CASwH8OH.js").then((n) => n.I);
|
|
33
|
+
const { InvoiceCollection } = await import("./InvoiceCollection-D_8NXuaw.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 - (parent.getPayableFacts().retainedMinor - parent.getPayableFacts().releasedMinor);
|
|
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-ri5cUYXB.js.map
|
package/dist/chunks/{allocation-integrity-DgnPec2a.js.map → allocation-integrity-ri5cUYXB.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"allocation-integrity-
|
|
1
|
+
{"version":3,"file":"allocation-integrity-ri5cUYXB.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 expectedUpdatedAt?: Date | string,\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 ? (expectedUpdatedAt ?? 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 ((parent as Invoice).getPayableFacts().retainedMinor -\n (parent as Invoice).getPayableFacts().releasedMinor);\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,MACA,mBACe;CACf,IAAI,CAAC,OAAO,IAAI;CAChB,MAAM,EAAE,sBAAsB,MAAM,OAClC,qCAAA,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,cACnB,qBAAqB,OAAO,aAC7B,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,qCAAA,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,qCAAA,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,eAClB,OAAmB,gBAAgB,CAAA,CAAE,gBACpC,OAAmB,gBAAgB,CAAA,CAAE;CAC9C,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,11 +1,11 @@
|
|
|
1
|
-
import { O as ContractCollection, W as classifyContractPurchase, Z as CustomerCollection, i as getReceivedPurchaseMoney, l as VendorCollection, t as DocumentTaxCollection } from "./DocumentTaxCollection-
|
|
1
|
+
import { O as ContractCollection, W as classifyContractPurchase, Z as CustomerCollection, i as getReceivedPurchaseMoney, l as VendorCollection, t as DocumentTaxCollection } from "./DocumentTaxCollection-MFfprLH-.js";
|
|
2
2
|
import { t as ContractStatus } from "./types-Bbm0Xflc.js";
|
|
3
3
|
import { MoneyValidationError, calculateMoneyDocument, resolveLineTaxCode } from "../money.js";
|
|
4
4
|
import { h as lockDocumentMoneyParents, p as DOCUMENT_MONEY_WRITE } from "./detect-DAE-_NAk.js";
|
|
5
|
-
import { r as withTenantMoneyRuntime } from "./tenant-money-barrier-
|
|
6
|
-
import { t as TaxCodeCollection } from "./TaxCodeCollection-
|
|
7
|
-
import { t as ContractLineItemCollection } from "./ContractLineItemCollection-
|
|
8
|
-
import { t as TaxComponentCollection } from "./TaxComponentCollection-
|
|
5
|
+
import { r as withTenantMoneyRuntime } from "./tenant-money-barrier-BbbInDGP.js";
|
|
6
|
+
import { t as TaxCodeCollection } from "./TaxCodeCollection-Bc-Z8alx.js";
|
|
7
|
+
import { t as ContractLineItemCollection } from "./ContractLineItemCollection-BFWJ2e6a.js";
|
|
8
|
+
import { t as TaxComponentCollection } from "./TaxComponentCollection-DUY7A2sU.js";
|
|
9
9
|
import { getTenantId } from "@happyvertical/smrt-tenancy";
|
|
10
10
|
//#region src/contract-money.ts
|
|
11
11
|
var FROZEN_CONTRACT_FIELDS = [
|
|
@@ -332,4 +332,4 @@ async function deleteContract(input, options) {
|
|
|
332
332
|
//#endregion
|
|
333
333
|
export { deleteContract, deleteContractLineItem, recalculateContractTotals, saveContract, saveContractLineItem };
|
|
334
334
|
|
|
335
|
-
//# sourceMappingURL=contract-money-
|
|
335
|
+
//# sourceMappingURL=contract-money-By1XIzp_.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract-money-BxvY2Kcp.js","names":[],"sources":["../../src/contract-money.ts"],"sourcesContent":["/** Transactional Contract money aggregate persistence. */\n\nimport type { SmrtSaveOptions } from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/smrt-core/migrations';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport { ContractCollection } from './collections/ContractCollection.js';\nimport { ContractLineItemCollection } from './collections/ContractLineItemCollection.js';\nimport { CustomerCollection } from './collections/CustomerCollection.js';\nimport {\n DocumentTaxCollection,\n type DocumentTaxWriteInput,\n} from './collections/DocumentTaxCollection.js';\nimport { TaxCodeCollection } from './collections/TaxCodeCollection.js';\nimport { TaxComponentCollection } from './collections/TaxComponentCollection.js';\nimport { VendorCollection } from './collections/VendorCollection.js';\nimport {\n DOCUMENT_MONEY_WRITE,\n lockDocumentMoneyParents,\n} from './document-money-internal.js';\nimport {\n type Contract,\n classifyContractPurchase,\n isContractSnapshotReportEligible,\n} from './models/Contract.js';\nimport type { ContractLineItem } from './models/ContractLineItem.js';\nimport type { TaxSettings } from './models/TaxSettings.js';\nimport {\n calculateMoneyDocument,\n MoneyValidationError,\n resolveLineTaxCode,\n} from './money.js';\nimport { getReceivedPurchaseMoney } from './received-purchase-money.js';\nimport { withTenantMoneyRuntime } from './tenant-money-barrier.js';\nimport {\n ContractStatus,\n type ContractTaxComponentSnapshot,\n} from './types/index.js';\n\ntype DeleteOptions = { expectedUpdatedAt?: Date | string };\ntype ProductTaxDefaults = {\n tenantId: string | null;\n defaultSalesTaxCodeId: string;\n defaultPurchaseTaxCodeId: string;\n};\n\nconst FROZEN_CONTRACT_FIELDS = [\n 'tenantId',\n 'moneyAuthority',\n 'receivedPurchaseMoneyJson',\n 'subtotal',\n 'discountTotal',\n 'taxAmount',\n 'totalAmount',\n 'currency',\n 'lineAmountType',\n 'taxRoundingScope',\n 'sellerTaxRegistrationsJson',\n 'partyTaxId',\n 'moneySnapshotId',\n 'reference',\n 'issueDate',\n] as const;\nconst FROZEN_LINE_FIELDS = [\n 'tenantId',\n 'contractId',\n 'description',\n 'productId',\n 'sku',\n 'quantity',\n 'unitPrice',\n 'discountType',\n 'discountRate',\n 'discount',\n 'taxMode',\n 'taxCodeId',\n 'taxCodeCode',\n 'taxCodeName',\n 'taxComponentsJson',\n 'taxAmount',\n 'taxRate',\n 'amount',\n 'startDate',\n 'endDate',\n 'billingPeriod',\n 'sortOrder',\n 'moneySnapshotId',\n] as const;\n\nfunction required(value: string | null | undefined, label: string): string {\n if (!value)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `${label} requires an ID`,\n );\n return value;\n}\n/** Frozen-field identity: an absent optional reference (`''`) equals a stored NULL. */\nfunction same(a: unknown, b: unknown): boolean {\n const value = (item: unknown) =>\n item instanceof Date\n ? item.toISOString()\n : item === '' || item === undefined\n ? null\n : item;\n return value(a) === value(b);\n}\nfunction assertUnchanged<T extends object>(\n proposed: T,\n stored: T,\n fields: readonly (keyof T)[],\n label: string,\n): void {\n for (const field of fields)\n if (!same(proposed[field], stored[field]))\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n `${label} ${String(field)} is frozen`,\n );\n}\nfunction isFrozen(contract: Contract): boolean {\n return contract.status !== ContractStatus.DRAFT || !!contract.moneySnapshotId;\n}\n\nexport { classifyContractPurchase, isContractSnapshotReportEligible };\n\nfunction direction(contract: Contract): 'sales' | 'purchases' {\n return classifyContractPurchase(contract) ? 'purchases' : 'sales';\n}\nfunction discount(line: ContractLineItem) {\n if (line.discountType === 'percent')\n return { kind: 'percent' as const, rate: String(line.discountRate) };\n if (line.discountType === 'amount')\n return { kind: 'amount' as const, minor: line.discount };\n return { kind: 'none' as const };\n}\n\nasync function loadContract(\n db: DatabaseInterface,\n id: string,\n): Promise<Contract | null> {\n return (await ContractCollection.create({ db })).get(\n { id },\n { cache: false },\n );\n}\nasync function withLockedContract<T>(\n db: DatabaseInterface,\n tenantId: string,\n contractId: string,\n operation: (contract: Contract, lines: ContractLineItem[]) => Promise<T>,\n): Promise<T> {\n const contracts = await ContractCollection.create({ db });\n await lockDocumentMoneyParents([\n { id: contractId, tenantId, collection: contracts },\n ]);\n const contract = await contracts.get({ id: contractId }, { cache: false });\n if (!contract || contract.tenantId !== tenantId)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Contract was not found in its tenant',\n );\n const lines = await (\n await ContractLineItemCollection.create({ db })\n ).findByContract(contractId);\n return operation(contract, lines);\n}\n\nasync function recompute(\n db: DatabaseInterface,\n contract: Contract,\n lines: ContractLineItem[],\n settings: TaxSettings,\n persist: boolean,\n snapshotId = '',\n): Promise<DocumentTaxWriteInput[]> {\n const tenantId = required(contract.tenantId, 'Contract tenant');\n const codes = await (await TaxCodeCollection.create({ db })).list({\n where: { tenantId },\n });\n const components = await (await TaxComponentCollection.create({ db })).list({\n where: { tenantId },\n });\n const byCode = Object.fromEntries(\n codes.flatMap((code) => {\n if (!code.id) return [];\n return [\n [\n code.id,\n {\n id: code.id,\n kind: code.kind,\n active: code.active,\n appliesTo: code.appliesTo,\n components: code.components.map((part) => ({\n taxComponentId: part.taxComponentId,\n percent:\n components.find(\n (component) => component.id === part.taxComponentId,\n )?.percent ?? '0',\n compound: part.compound,\n })),\n },\n ],\n ] as const;\n }),\n );\n const codeById = new Map(\n codes.flatMap((code) => (code.id ? ([[code.id, code]] as const) : [])),\n );\n // Product owns catalog reads. Load defaults on the aggregate transaction so\n // Contract follows the same line > product > party > tenant precedence as\n // Invoice without adding a static package edge.\n const productIds = [\n ...new Set(\n lines\n .map((line) => line.productId)\n .filter((productId): productId is string => !!productId),\n ),\n ];\n const productById = new Map<string, ProductTaxDefaults>();\n if (productIds.length > 0) {\n const { ProductCollection } = await import(\n '@happyvertical/smrt-products/collections'\n );\n const products = await ProductCollection.create({ db });\n for (const productId of productIds) {\n const product = await products.get({ id: productId }, { cache: false });\n if (!product)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Contract product ${productId} was not found`,\n );\n if (product.tenantId !== null && product.tenantId !== tenantId)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Contract product ${productId} must be global or belong to its tenant`,\n );\n productById.set(productId, product);\n }\n }\n const customer =\n 'customerId' in contract && contract.customerId\n ? await (await CustomerCollection.create({ db })).get(\n { id: contract.customerId },\n { cache: false },\n )\n : null;\n const vendor =\n 'vendorId' in contract && contract.vendorId\n ? await (await VendorCollection.create({ db })).get(\n { id: contract.vendorId },\n { cache: false },\n )\n : null;\n const resolved = new Map<string, string | null>();\n const result = calculateMoneyDocument({\n currency: contract.currency,\n lineAmountType: contract.lineAmountType,\n roundingScope: contract.taxRoundingScope,\n lines: lines.map((line) => {\n const selected = resolveLineTaxCode(\n {\n lineAmountType: contract.lineAmountType,\n lineOverride: line.taxMode === 'override' ? line.taxCodeId : null,\n },\n {\n direction: direction(contract),\n codes: byCode,\n customerTaxExempt: customer?.taxExempt ?? false,\n exemptCodeId:\n codes.find((code) => code.kind === 'exempt')?.id ?? null,\n productCodeId: line.productId\n ? direction(contract) === 'sales'\n ? productById.get(line.productId)?.defaultSalesTaxCodeId || null\n : productById.get(line.productId)?.defaultPurchaseTaxCodeId ||\n null\n : null,\n partyCodeId:\n customer?.defaultTaxCodeId ?? vendor?.defaultTaxCodeId ?? null,\n appDefaultCodeId:\n direction(contract) === 'sales'\n ? settings.defaultSalesTaxCodeId\n : settings.defaultPurchaseTaxCodeId,\n outOfScopeCodeId:\n codes.find((code) => code.kind === 'out-of-scope')?.id ?? null,\n },\n );\n resolved.set(\n required(line.id, 'Contract line'),\n selected.taxCode?.id ?? null,\n );\n return {\n id: required(line.id, 'Contract line'),\n quantity: line.quantity,\n unitPrice: line.unitPrice,\n discount: discount(line),\n taxCode: selected.taxCode,\n };\n }),\n });\n const resultById = new Map(result.lines.map((row) => [row.id, row]));\n for (const line of lines) {\n const lineId = required(line.id, 'Contract line');\n const row = resultById.get(lineId);\n if (!row)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Calculated Contract line ${lineId} was not loaded`,\n );\n const codeId = resolved.get(lineId) ?? null;\n const code = codeId ? (codeById.get(codeId) ?? null) : null;\n line.discount = row.discountMinor;\n line.amount = row.amountMinor;\n line.taxAmount = row.taxMinor;\n line.taxCodeId = code?.id ?? null;\n line.taxCodeCode = code?.code ?? null;\n line.taxCodeName = code?.name ?? null;\n line.taxComponents = row.components.map(\n (part): ContractTaxComponentSnapshot => {\n const component = components.find(\n (item) => item.id === part.taxComponentId,\n );\n if (!component)\n throw new MoneyValidationError(\n 'TAX_CODE_NOT_FOUND',\n `Tax component ${part.taxComponentId} was not loaded`,\n );\n return {\n taxComponentId: part.taxComponentId,\n name: component.name,\n percent: component.percent,\n taxMinor: part.taxMinor,\n ...(part.compound\n ? { compound: true, taxableMinor: part.taxableMinor }\n : {}),\n };\n },\n );\n line.taxRate = row.components.some((part) => part.compound)\n ? row.netMinor === 0\n ? 0\n : row.taxMinor / row.netMinor\n : line.taxComponents.reduce(\n (sum, part) => sum + Number(part.percent) / 100,\n 0,\n );\n if (snapshotId) line.moneySnapshotId = snapshotId;\n if (persist) await line.saveDerived(DOCUMENT_MONEY_WRITE);\n }\n const taxRows = result.taxes.map((tax): DocumentTaxWriteInput => {\n const code = tax.taxCodeId ? (codeById.get(tax.taxCodeId) ?? null) : null;\n const component = tax.taxComponentId\n ? (components.find((item) => item.id === tax.taxComponentId) ?? null)\n : null;\n return {\n tenantId,\n documentType: 'contract',\n documentId: required(contract.id, 'Contract'),\n currency: contract.currency,\n taxCodeId: code?.id ?? null,\n taxCodeCode: code?.code ?? null,\n kind: code?.kind ?? null,\n taxComponentId: component?.id ?? null,\n taxComponentName: component?.name ?? null,\n percent: component?.percent ?? null,\n liabilityAccountId: component?.liabilityAccountId ?? null,\n taxableMinor: tax.taxableMinor,\n netMinor: tax.netMinor ?? null,\n taxMinor: tax.taxMinor,\n recoverableMinor:\n direction(contract) === 'purchases' && component?.recoverable\n ? tax.taxMinor\n : 0,\n };\n });\n contract.subtotal = result.subtotalMinor;\n contract.discountTotal = result.discountTotalMinor;\n contract.taxAmount = result.taxMinor;\n contract.totalAmount = result.totalMinor;\n if (persist)\n await (\n await DocumentTaxCollection.create({ db })\n ).replaceDerivedForDocument(DOCUMENT_MONEY_WRITE, {\n tenantId,\n documentType: 'contract',\n documentId: required(contract.id, 'Contract'),\n rows: taxRows,\n });\n return taxRows;\n}\n\n/** Every money document belongs to a tenant. */\nfunction requireContractTenant(tenantId: string | null): string {\n if (!tenantId)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Commerce money documents require a tenant',\n );\n return tenantId;\n}\n\n/** Save a Contract and recompute its money aggregate. */\nexport async function saveContract(\n input: Contract,\n options?: SmrtSaveOptions,\n): Promise<Contract> {\n const tenantId = requireContractTenant(\n input.tenantId ?? getTenantId() ?? null,\n );\n return withTenantMoneyRuntime(input.db, tenantId, async (scope) =>\n input.withDatabase(scope.db, async (contract) => {\n const settings = scope.settings;\n const existing = contract.id\n ? await loadContract(scope.db, contract.id)\n : null;\n // NULL (a row written before the column had a default) and `''` both\n // mean no schedule (#4155); any other difference is a schedule change.\n contract.billingScheduleJson ??= '';\n if (\n contract.billingScheduleJson !== (existing?.billingScheduleJson ?? '')\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Billing schedule is managed by schedule commands',\n );\n if (existing && existing.tenantId !== scope.tenantId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Contract tenant identity cannot change',\n );\n if (!existing) {\n if (\n contract.moneyAuthority !== 'calculated' ||\n contract.receivedPurchaseMoneyJson\n )\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Use recordReceivedPurchaseMoney or recordReceivedSalesContractMoney to record received evidence',\n );\n contract.tenantId = tenantId;\n contract.lineAmountType = settings.defaultLineAmountType;\n contract.taxRoundingScope = settings.taxRoundingScope;\n // A new Contract has no lines yet: its totals start at zero and are\n // derived from lines on every later save, never taken from the caller.\n contract.subtotal = 0;\n contract.discountTotal = 0;\n contract.taxAmount = 0;\n contract.totalAmount = 0;\n if (contract.status !== ContractStatus.DRAFT) {\n contract.moneySnapshotId = crypto.randomUUID();\n contract.sellerTaxRegistrations = structuredClone(\n settings.taxRegistrations,\n );\n }\n return contract.saveDerived(DOCUMENT_MONEY_WRITE, options);\n }\n return withLockedContract(\n scope.db,\n tenantId,\n required(existing.id, 'Contract'),\n async (stored, lines) => {\n assertUnchanged(\n contract,\n stored,\n ['moneyAuthority', 'receivedPurchaseMoneyJson'],\n 'Contract money authority',\n );\n const received = getReceivedPurchaseMoney(stored);\n if (received) {\n if (\n contract.constructor !== stored.constructor ||\n contract.contractType !== stored.contractType\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Received purchase document type is frozen',\n );\n assertUnchanged(\n contract,\n stored,\n [\n ...FROZEN_CONTRACT_FIELDS,\n 'vendorId' as keyof Contract,\n 'customerId' as keyof Contract,\n ],\n 'Received purchase money',\n );\n if (lines.length)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Received money cannot mix with calculated lines',\n );\n if (\n contract.status !== ContractStatus.DRAFT &&\n !stored.moneySnapshotId\n )\n contract.moneySnapshotId = crypto.randomUUID();\n return contract.saveDerived(DOCUMENT_MONEY_WRITE, options);\n }\n if (isFrozen(stored)) {\n assertUnchanged(\n contract,\n stored,\n FROZEN_CONTRACT_FIELDS,\n 'Contract',\n );\n return contract.saveDerived(DOCUMENT_MONEY_WRITE, options);\n }\n const freezing = contract.status !== ContractStatus.DRAFT;\n const snapshotId = freezing\n ? stored.moneySnapshotId || crypto.randomUUID()\n : '';\n await recompute(\n scope.db,\n contract,\n lines,\n settings,\n true,\n snapshotId,\n );\n if (freezing) {\n const party =\n 'customerId' in contract && contract.customerId\n ? await (await CustomerCollection.create({ db: scope.db })).get(\n { id: contract.customerId },\n { cache: false },\n )\n : 'vendorId' in contract && contract.vendorId\n ? await (await VendorCollection.create({ db: scope.db })).get(\n { id: contract.vendorId },\n { cache: false },\n )\n : null;\n contract.sellerTaxRegistrations = structuredClone(\n settings.taxRegistrations,\n );\n contract.partyTaxId = party?.taxId ?? '';\n contract.moneySnapshotId = snapshotId;\n }\n return contract.saveDerived(DOCUMENT_MONEY_WRITE, options);\n },\n );\n }),\n );\n}\n\n/** Save a line and atomically recompute its Contract aggregate. */\nexport async function saveContractLineItem(\n input: ContractLineItem,\n options?: SmrtSaveOptions,\n): Promise<ContractLineItem> {\n const tenantId = requireContractTenant(\n input.tenantId ?? getTenantId() ?? null,\n );\n return withTenantMoneyRuntime(input.db, tenantId, async (scope) =>\n input.withDatabase(scope.db, async (line) => {\n const persisted = line.id\n ? await (await ContractLineItemCollection.create({ db: scope.db })).get(\n { id: line.id },\n { cache: false },\n )\n : null;\n const contractId = persisted?.contractId ?? line.contractId;\n const settings = scope.settings;\n return withLockedContract(\n scope.db,\n scope.tenantId,\n contractId,\n async (contract, lines) => {\n if (getReceivedPurchaseMoney(contract))\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Received purchase evidence cannot contain calculated lines',\n );\n if (persisted && line.contractId !== persisted.contractId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Contract line cannot move between parents',\n );\n if (isFrozen(contract)) {\n if (!persisted)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Cannot add a line to a frozen Contract',\n );\n assertUnchanged(\n line,\n persisted,\n FROZEN_LINE_FIELDS,\n 'Contract line',\n );\n return line.saveDerived(DOCUMENT_MONEY_WRITE, options);\n }\n line.contractId = contractId;\n line.tenantId = scope.tenantId;\n await line.saveDerived(DOCUMENT_MONEY_WRITE, options);\n const all = [\n ...lines.filter((candidate) => candidate.id !== line.id),\n line,\n ];\n await recompute(scope.db, contract, all, settings, true);\n await contract.saveDerived(DOCUMENT_MONEY_WRITE);\n return line;\n },\n );\n }),\n );\n}\n\nexport async function deleteContractLineItem(\n input: ContractLineItem,\n options: DeleteOptions,\n): Promise<void> {\n const tenantId = requireContractTenant(\n input.tenantId ?? getTenantId() ?? null,\n );\n await withTenantMoneyRuntime(input.db, tenantId, async (scope) =>\n input.withDatabase(scope.db, async (line) => {\n const persisted = line.id\n ? await (await ContractLineItemCollection.create({ db: scope.db })).get(\n { id: line.id },\n { cache: false },\n )\n : null;\n if (!persisted) return;\n const settings = scope.settings;\n await withLockedContract(\n scope.db,\n scope.tenantId,\n persisted.contractId,\n async (contract, lines) => {\n if (isFrozen(contract))\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Cannot delete a line from a frozen Contract',\n );\n await line.deleteDerived(DOCUMENT_MONEY_WRITE, options);\n await recompute(\n scope.db,\n contract,\n lines.filter((candidate) => candidate.id !== persisted.id),\n settings,\n true,\n );\n await contract.saveDerived(DOCUMENT_MONEY_WRITE);\n },\n );\n }),\n );\n}\n\n/** Recompute a Contract through its normal runtime gate. */\nexport async function recalculateContractTotals(\n contract: Contract,\n): Promise<void> {\n await saveContract(contract);\n}\n\n/** Delete only a calculated Contract under the same lock as received-fact recording. */\nexport async function deleteContract(\n input: Contract,\n options: DeleteOptions,\n): Promise<void> {\n const contractId = input.id;\n if (!contractId) return;\n const tenantId = requireContractTenant(\n input.tenantId ?? getTenantId() ?? null,\n );\n await withTenantMoneyRuntime(input.db, tenantId, async ({ db }) => {\n const existing = await loadContract(db, contractId);\n if (!existing) return;\n await withLockedContract(db, tenantId, contractId, async (stored) => {\n if (stored.billingScheduleJson)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Contract billing schedule history cannot be deleted',\n );\n if (getReceivedPurchaseMoney(stored))\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Received purchase monetary history cannot be deleted',\n );\n await input.withDatabase(db, (bound) =>\n bound.deleteDerived(DOCUMENT_MONEY_WRITE, options),\n );\n });\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contract-money-By1XIzp_.js","names":[],"sources":["../../src/contract-money.ts"],"sourcesContent":["/** Transactional Contract money aggregate persistence. */\n\nimport type { SmrtSaveOptions } from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/smrt-core/migrations';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport { ContractCollection } from './collections/ContractCollection.js';\nimport { ContractLineItemCollection } from './collections/ContractLineItemCollection.js';\nimport { CustomerCollection } from './collections/CustomerCollection.js';\nimport {\n DocumentTaxCollection,\n type DocumentTaxWriteInput,\n} from './collections/DocumentTaxCollection.js';\nimport { TaxCodeCollection } from './collections/TaxCodeCollection.js';\nimport { TaxComponentCollection } from './collections/TaxComponentCollection.js';\nimport { VendorCollection } from './collections/VendorCollection.js';\nimport {\n DOCUMENT_MONEY_WRITE,\n lockDocumentMoneyParents,\n} from './document-money-internal.js';\nimport {\n type Contract,\n classifyContractPurchase,\n isContractSnapshotReportEligible,\n} from './models/Contract.js';\nimport type { ContractLineItem } from './models/ContractLineItem.js';\nimport type { TaxSettings } from './models/TaxSettings.js';\nimport {\n calculateMoneyDocument,\n MoneyValidationError,\n resolveLineTaxCode,\n} from './money.js';\nimport { getReceivedPurchaseMoney } from './received-purchase-money.js';\nimport { withTenantMoneyRuntime } from './tenant-money-barrier.js';\nimport {\n ContractStatus,\n type ContractTaxComponentSnapshot,\n} from './types/index.js';\n\ntype DeleteOptions = { expectedUpdatedAt?: Date | string };\ntype ProductTaxDefaults = {\n tenantId: string | null;\n defaultSalesTaxCodeId: string;\n defaultPurchaseTaxCodeId: string;\n};\n\nconst FROZEN_CONTRACT_FIELDS = [\n 'tenantId',\n 'moneyAuthority',\n 'receivedPurchaseMoneyJson',\n 'subtotal',\n 'discountTotal',\n 'taxAmount',\n 'totalAmount',\n 'currency',\n 'lineAmountType',\n 'taxRoundingScope',\n 'sellerTaxRegistrationsJson',\n 'partyTaxId',\n 'moneySnapshotId',\n 'reference',\n 'issueDate',\n] as const;\nconst FROZEN_LINE_FIELDS = [\n 'tenantId',\n 'contractId',\n 'description',\n 'productId',\n 'sku',\n 'quantity',\n 'unitPrice',\n 'discountType',\n 'discountRate',\n 'discount',\n 'taxMode',\n 'taxCodeId',\n 'taxCodeCode',\n 'taxCodeName',\n 'taxComponentsJson',\n 'taxAmount',\n 'taxRate',\n 'amount',\n 'startDate',\n 'endDate',\n 'billingPeriod',\n 'sortOrder',\n 'moneySnapshotId',\n] as const;\n\nfunction required(value: string | null | undefined, label: string): string {\n if (!value)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `${label} requires an ID`,\n );\n return value;\n}\n/** Frozen-field identity: an absent optional reference (`''`) equals a stored NULL. */\nfunction same(a: unknown, b: unknown): boolean {\n const value = (item: unknown) =>\n item instanceof Date\n ? item.toISOString()\n : item === '' || item === undefined\n ? null\n : item;\n return value(a) === value(b);\n}\nfunction assertUnchanged<T extends object>(\n proposed: T,\n stored: T,\n fields: readonly (keyof T)[],\n label: string,\n): void {\n for (const field of fields)\n if (!same(proposed[field], stored[field]))\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n `${label} ${String(field)} is frozen`,\n );\n}\nfunction isFrozen(contract: Contract): boolean {\n return contract.status !== ContractStatus.DRAFT || !!contract.moneySnapshotId;\n}\n\nexport { classifyContractPurchase, isContractSnapshotReportEligible };\n\nfunction direction(contract: Contract): 'sales' | 'purchases' {\n return classifyContractPurchase(contract) ? 'purchases' : 'sales';\n}\nfunction discount(line: ContractLineItem) {\n if (line.discountType === 'percent')\n return { kind: 'percent' as const, rate: String(line.discountRate) };\n if (line.discountType === 'amount')\n return { kind: 'amount' as const, minor: line.discount };\n return { kind: 'none' as const };\n}\n\nasync function loadContract(\n db: DatabaseInterface,\n id: string,\n): Promise<Contract | null> {\n return (await ContractCollection.create({ db })).get(\n { id },\n { cache: false },\n );\n}\nasync function withLockedContract<T>(\n db: DatabaseInterface,\n tenantId: string,\n contractId: string,\n operation: (contract: Contract, lines: ContractLineItem[]) => Promise<T>,\n): Promise<T> {\n const contracts = await ContractCollection.create({ db });\n await lockDocumentMoneyParents([\n { id: contractId, tenantId, collection: contracts },\n ]);\n const contract = await contracts.get({ id: contractId }, { cache: false });\n if (!contract || contract.tenantId !== tenantId)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Contract was not found in its tenant',\n );\n const lines = await (\n await ContractLineItemCollection.create({ db })\n ).findByContract(contractId);\n return operation(contract, lines);\n}\n\nasync function recompute(\n db: DatabaseInterface,\n contract: Contract,\n lines: ContractLineItem[],\n settings: TaxSettings,\n persist: boolean,\n snapshotId = '',\n): Promise<DocumentTaxWriteInput[]> {\n const tenantId = required(contract.tenantId, 'Contract tenant');\n const codes = await (await TaxCodeCollection.create({ db })).list({\n where: { tenantId },\n });\n const components = await (await TaxComponentCollection.create({ db })).list({\n where: { tenantId },\n });\n const byCode = Object.fromEntries(\n codes.flatMap((code) => {\n if (!code.id) return [];\n return [\n [\n code.id,\n {\n id: code.id,\n kind: code.kind,\n active: code.active,\n appliesTo: code.appliesTo,\n components: code.components.map((part) => ({\n taxComponentId: part.taxComponentId,\n percent:\n components.find(\n (component) => component.id === part.taxComponentId,\n )?.percent ?? '0',\n compound: part.compound,\n })),\n },\n ],\n ] as const;\n }),\n );\n const codeById = new Map(\n codes.flatMap((code) => (code.id ? ([[code.id, code]] as const) : [])),\n );\n // Product owns catalog reads. Load defaults on the aggregate transaction so\n // Contract follows the same line > product > party > tenant precedence as\n // Invoice without adding a static package edge.\n const productIds = [\n ...new Set(\n lines\n .map((line) => line.productId)\n .filter((productId): productId is string => !!productId),\n ),\n ];\n const productById = new Map<string, ProductTaxDefaults>();\n if (productIds.length > 0) {\n const { ProductCollection } = await import(\n '@happyvertical/smrt-products/collections'\n );\n const products = await ProductCollection.create({ db });\n for (const productId of productIds) {\n const product = await products.get({ id: productId }, { cache: false });\n if (!product)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Contract product ${productId} was not found`,\n );\n if (product.tenantId !== null && product.tenantId !== tenantId)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Contract product ${productId} must be global or belong to its tenant`,\n );\n productById.set(productId, product);\n }\n }\n const customer =\n 'customerId' in contract && contract.customerId\n ? await (await CustomerCollection.create({ db })).get(\n { id: contract.customerId },\n { cache: false },\n )\n : null;\n const vendor =\n 'vendorId' in contract && contract.vendorId\n ? await (await VendorCollection.create({ db })).get(\n { id: contract.vendorId },\n { cache: false },\n )\n : null;\n const resolved = new Map<string, string | null>();\n const result = calculateMoneyDocument({\n currency: contract.currency,\n lineAmountType: contract.lineAmountType,\n roundingScope: contract.taxRoundingScope,\n lines: lines.map((line) => {\n const selected = resolveLineTaxCode(\n {\n lineAmountType: contract.lineAmountType,\n lineOverride: line.taxMode === 'override' ? line.taxCodeId : null,\n },\n {\n direction: direction(contract),\n codes: byCode,\n customerTaxExempt: customer?.taxExempt ?? false,\n exemptCodeId:\n codes.find((code) => code.kind === 'exempt')?.id ?? null,\n productCodeId: line.productId\n ? direction(contract) === 'sales'\n ? productById.get(line.productId)?.defaultSalesTaxCodeId || null\n : productById.get(line.productId)?.defaultPurchaseTaxCodeId ||\n null\n : null,\n partyCodeId:\n customer?.defaultTaxCodeId ?? vendor?.defaultTaxCodeId ?? null,\n appDefaultCodeId:\n direction(contract) === 'sales'\n ? settings.defaultSalesTaxCodeId\n : settings.defaultPurchaseTaxCodeId,\n outOfScopeCodeId:\n codes.find((code) => code.kind === 'out-of-scope')?.id ?? null,\n },\n );\n resolved.set(\n required(line.id, 'Contract line'),\n selected.taxCode?.id ?? null,\n );\n return {\n id: required(line.id, 'Contract line'),\n quantity: line.quantity,\n unitPrice: line.unitPrice,\n discount: discount(line),\n taxCode: selected.taxCode,\n };\n }),\n });\n const resultById = new Map(result.lines.map((row) => [row.id, row]));\n for (const line of lines) {\n const lineId = required(line.id, 'Contract line');\n const row = resultById.get(lineId);\n if (!row)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Calculated Contract line ${lineId} was not loaded`,\n );\n const codeId = resolved.get(lineId) ?? null;\n const code = codeId ? (codeById.get(codeId) ?? null) : null;\n line.discount = row.discountMinor;\n line.amount = row.amountMinor;\n line.taxAmount = row.taxMinor;\n line.taxCodeId = code?.id ?? null;\n line.taxCodeCode = code?.code ?? null;\n line.taxCodeName = code?.name ?? null;\n line.taxComponents = row.components.map(\n (part): ContractTaxComponentSnapshot => {\n const component = components.find(\n (item) => item.id === part.taxComponentId,\n );\n if (!component)\n throw new MoneyValidationError(\n 'TAX_CODE_NOT_FOUND',\n `Tax component ${part.taxComponentId} was not loaded`,\n );\n return {\n taxComponentId: part.taxComponentId,\n name: component.name,\n percent: component.percent,\n taxMinor: part.taxMinor,\n ...(part.compound\n ? { compound: true, taxableMinor: part.taxableMinor }\n : {}),\n };\n },\n );\n line.taxRate = row.components.some((part) => part.compound)\n ? row.netMinor === 0\n ? 0\n : row.taxMinor / row.netMinor\n : line.taxComponents.reduce(\n (sum, part) => sum + Number(part.percent) / 100,\n 0,\n );\n if (snapshotId) line.moneySnapshotId = snapshotId;\n if (persist) await line.saveDerived(DOCUMENT_MONEY_WRITE);\n }\n const taxRows = result.taxes.map((tax): DocumentTaxWriteInput => {\n const code = tax.taxCodeId ? (codeById.get(tax.taxCodeId) ?? null) : null;\n const component = tax.taxComponentId\n ? (components.find((item) => item.id === tax.taxComponentId) ?? null)\n : null;\n return {\n tenantId,\n documentType: 'contract',\n documentId: required(contract.id, 'Contract'),\n currency: contract.currency,\n taxCodeId: code?.id ?? null,\n taxCodeCode: code?.code ?? null,\n kind: code?.kind ?? null,\n taxComponentId: component?.id ?? null,\n taxComponentName: component?.name ?? null,\n percent: component?.percent ?? null,\n liabilityAccountId: component?.liabilityAccountId ?? null,\n taxableMinor: tax.taxableMinor,\n netMinor: tax.netMinor ?? null,\n taxMinor: tax.taxMinor,\n recoverableMinor:\n direction(contract) === 'purchases' && component?.recoverable\n ? tax.taxMinor\n : 0,\n };\n });\n contract.subtotal = result.subtotalMinor;\n contract.discountTotal = result.discountTotalMinor;\n contract.taxAmount = result.taxMinor;\n contract.totalAmount = result.totalMinor;\n if (persist)\n await (\n await DocumentTaxCollection.create({ db })\n ).replaceDerivedForDocument(DOCUMENT_MONEY_WRITE, {\n tenantId,\n documentType: 'contract',\n documentId: required(contract.id, 'Contract'),\n rows: taxRows,\n });\n return taxRows;\n}\n\n/** Every money document belongs to a tenant. */\nfunction requireContractTenant(tenantId: string | null): string {\n if (!tenantId)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Commerce money documents require a tenant',\n );\n return tenantId;\n}\n\n/** Save a Contract and recompute its money aggregate. */\nexport async function saveContract(\n input: Contract,\n options?: SmrtSaveOptions,\n): Promise<Contract> {\n const tenantId = requireContractTenant(\n input.tenantId ?? getTenantId() ?? null,\n );\n return withTenantMoneyRuntime(input.db, tenantId, async (scope) =>\n input.withDatabase(scope.db, async (contract) => {\n const settings = scope.settings;\n const existing = contract.id\n ? await loadContract(scope.db, contract.id)\n : null;\n // NULL (a row written before the column had a default) and `''` both\n // mean no schedule (#4155); any other difference is a schedule change.\n contract.billingScheduleJson ??= '';\n if (\n contract.billingScheduleJson !== (existing?.billingScheduleJson ?? '')\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Billing schedule is managed by schedule commands',\n );\n if (existing && existing.tenantId !== scope.tenantId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Contract tenant identity cannot change',\n );\n if (!existing) {\n if (\n contract.moneyAuthority !== 'calculated' ||\n contract.receivedPurchaseMoneyJson\n )\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Use recordReceivedPurchaseMoney or recordReceivedSalesContractMoney to record received evidence',\n );\n contract.tenantId = tenantId;\n contract.lineAmountType = settings.defaultLineAmountType;\n contract.taxRoundingScope = settings.taxRoundingScope;\n // A new Contract has no lines yet: its totals start at zero and are\n // derived from lines on every later save, never taken from the caller.\n contract.subtotal = 0;\n contract.discountTotal = 0;\n contract.taxAmount = 0;\n contract.totalAmount = 0;\n if (contract.status !== ContractStatus.DRAFT) {\n contract.moneySnapshotId = crypto.randomUUID();\n contract.sellerTaxRegistrations = structuredClone(\n settings.taxRegistrations,\n );\n }\n return contract.saveDerived(DOCUMENT_MONEY_WRITE, options);\n }\n return withLockedContract(\n scope.db,\n tenantId,\n required(existing.id, 'Contract'),\n async (stored, lines) => {\n assertUnchanged(\n contract,\n stored,\n ['moneyAuthority', 'receivedPurchaseMoneyJson'],\n 'Contract money authority',\n );\n const received = getReceivedPurchaseMoney(stored);\n if (received) {\n if (\n contract.constructor !== stored.constructor ||\n contract.contractType !== stored.contractType\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Received purchase document type is frozen',\n );\n assertUnchanged(\n contract,\n stored,\n [\n ...FROZEN_CONTRACT_FIELDS,\n 'vendorId' as keyof Contract,\n 'customerId' as keyof Contract,\n ],\n 'Received purchase money',\n );\n if (lines.length)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Received money cannot mix with calculated lines',\n );\n if (\n contract.status !== ContractStatus.DRAFT &&\n !stored.moneySnapshotId\n )\n contract.moneySnapshotId = crypto.randomUUID();\n return contract.saveDerived(DOCUMENT_MONEY_WRITE, options);\n }\n if (isFrozen(stored)) {\n assertUnchanged(\n contract,\n stored,\n FROZEN_CONTRACT_FIELDS,\n 'Contract',\n );\n return contract.saveDerived(DOCUMENT_MONEY_WRITE, options);\n }\n const freezing = contract.status !== ContractStatus.DRAFT;\n const snapshotId = freezing\n ? stored.moneySnapshotId || crypto.randomUUID()\n : '';\n await recompute(\n scope.db,\n contract,\n lines,\n settings,\n true,\n snapshotId,\n );\n if (freezing) {\n const party =\n 'customerId' in contract && contract.customerId\n ? await (await CustomerCollection.create({ db: scope.db })).get(\n { id: contract.customerId },\n { cache: false },\n )\n : 'vendorId' in contract && contract.vendorId\n ? await (await VendorCollection.create({ db: scope.db })).get(\n { id: contract.vendorId },\n { cache: false },\n )\n : null;\n contract.sellerTaxRegistrations = structuredClone(\n settings.taxRegistrations,\n );\n contract.partyTaxId = party?.taxId ?? '';\n contract.moneySnapshotId = snapshotId;\n }\n return contract.saveDerived(DOCUMENT_MONEY_WRITE, options);\n },\n );\n }),\n );\n}\n\n/** Save a line and atomically recompute its Contract aggregate. */\nexport async function saveContractLineItem(\n input: ContractLineItem,\n options?: SmrtSaveOptions,\n): Promise<ContractLineItem> {\n const tenantId = requireContractTenant(\n input.tenantId ?? getTenantId() ?? null,\n );\n return withTenantMoneyRuntime(input.db, tenantId, async (scope) =>\n input.withDatabase(scope.db, async (line) => {\n const persisted = line.id\n ? await (await ContractLineItemCollection.create({ db: scope.db })).get(\n { id: line.id },\n { cache: false },\n )\n : null;\n const contractId = persisted?.contractId ?? line.contractId;\n const settings = scope.settings;\n return withLockedContract(\n scope.db,\n scope.tenantId,\n contractId,\n async (contract, lines) => {\n if (getReceivedPurchaseMoney(contract))\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Received purchase evidence cannot contain calculated lines',\n );\n if (persisted && line.contractId !== persisted.contractId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Contract line cannot move between parents',\n );\n if (isFrozen(contract)) {\n if (!persisted)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Cannot add a line to a frozen Contract',\n );\n assertUnchanged(\n line,\n persisted,\n FROZEN_LINE_FIELDS,\n 'Contract line',\n );\n return line.saveDerived(DOCUMENT_MONEY_WRITE, options);\n }\n line.contractId = contractId;\n line.tenantId = scope.tenantId;\n await line.saveDerived(DOCUMENT_MONEY_WRITE, options);\n const all = [\n ...lines.filter((candidate) => candidate.id !== line.id),\n line,\n ];\n await recompute(scope.db, contract, all, settings, true);\n await contract.saveDerived(DOCUMENT_MONEY_WRITE);\n return line;\n },\n );\n }),\n );\n}\n\nexport async function deleteContractLineItem(\n input: ContractLineItem,\n options: DeleteOptions,\n): Promise<void> {\n const tenantId = requireContractTenant(\n input.tenantId ?? getTenantId() ?? null,\n );\n await withTenantMoneyRuntime(input.db, tenantId, async (scope) =>\n input.withDatabase(scope.db, async (line) => {\n const persisted = line.id\n ? await (await ContractLineItemCollection.create({ db: scope.db })).get(\n { id: line.id },\n { cache: false },\n )\n : null;\n if (!persisted) return;\n const settings = scope.settings;\n await withLockedContract(\n scope.db,\n scope.tenantId,\n persisted.contractId,\n async (contract, lines) => {\n if (isFrozen(contract))\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Cannot delete a line from a frozen Contract',\n );\n await line.deleteDerived(DOCUMENT_MONEY_WRITE, options);\n await recompute(\n scope.db,\n contract,\n lines.filter((candidate) => candidate.id !== persisted.id),\n settings,\n true,\n );\n await contract.saveDerived(DOCUMENT_MONEY_WRITE);\n },\n );\n }),\n );\n}\n\n/** Recompute a Contract through its normal runtime gate. */\nexport async function recalculateContractTotals(\n contract: Contract,\n): Promise<void> {\n await saveContract(contract);\n}\n\n/** Delete only a calculated Contract under the same lock as received-fact recording. */\nexport async function deleteContract(\n input: Contract,\n options: DeleteOptions,\n): Promise<void> {\n const contractId = input.id;\n if (!contractId) return;\n const tenantId = requireContractTenant(\n input.tenantId ?? getTenantId() ?? null,\n );\n await withTenantMoneyRuntime(input.db, tenantId, async ({ db }) => {\n const existing = await loadContract(db, contractId);\n if (!existing) return;\n await withLockedContract(db, tenantId, contractId, async (stored) => {\n if (stored.billingScheduleJson)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Contract billing schedule history cannot be deleted',\n );\n if (getReceivedPurchaseMoney(stored))\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Received purchase monetary history cannot be deleted',\n );\n await input.withDatabase(db, (bound) =>\n bound.deleteDerived(DOCUMENT_MONEY_WRITE, options),\n );\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Y as InvoiceLineItemCollection, t as DocumentTaxCollection, x as lockAndReloadInvoice } from "./DocumentTaxCollection-
|
|
1
|
+
import { Y as InvoiceLineItemCollection, t as DocumentTaxCollection, x as lockAndReloadInvoice } from "./DocumentTaxCollection-MFfprLH-.js";
|
|
2
2
|
import { s as InvoiceStatus } from "./types-Bbm0Xflc.js";
|
|
3
3
|
import { n as deterministicId } from "./units-8ElbjVZK.js";
|
|
4
4
|
import { MoneyValidationError } from "../money.js";
|
|
5
5
|
import { _ as withDocumentMoneyTransaction, p as DOCUMENT_MONEY_WRITE } from "./detect-DAE-_NAk.js";
|
|
6
|
-
import { t as lockTenantMoneyRuntime } from "./tenant-money-barrier-
|
|
6
|
+
import { t as lockTenantMoneyRuntime } from "./tenant-money-barrier-BbbInDGP.js";
|
|
7
7
|
import { t as buildRevenueJournalEntries } from "./revenue-journal-LLA9Kh_i.js";
|
|
8
8
|
import { getTenantId } from "@happyvertical/smrt-tenancy";
|
|
9
9
|
import { ISO_4217_MINOR_UNITS } from "@happyvertical/smrt-ui/currency";
|
|
@@ -224,4 +224,4 @@ async function cancelInvoiceWithRevenueReversal(input, options) {
|
|
|
224
224
|
//#endregion
|
|
225
225
|
export { cancelInvoiceWithRevenueReversal, rebindJournal, recognizeInvoiceRevenue };
|
|
226
226
|
|
|
227
|
-
//# sourceMappingURL=invoice-posting-
|
|
227
|
+
//# sourceMappingURL=invoice-posting-BfnOyqBR.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoice-posting-C1q3CIYz.js","names":[],"sources":["../../src/invoice-posting.ts"],"sourcesContent":["/** Atomic revenue posting and cancellation reversal. */\n\nimport type { DatabaseInterface } from '@happyvertical/smrt-core/migrations';\nimport type { Journal, JournalEntryData } from '@happyvertical/smrt-ledgers';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport { ISO_4217_MINOR_UNITS } from '@happyvertical/smrt-ui/currency';\nimport { deterministicId } from './billing/units.js';\nimport { DocumentTaxCollection } from './collections/DocumentTaxCollection.js';\nimport { InvoiceLineItemCollection } from './collections/InvoiceLineItemCollection.js';\nimport { lockAndReloadInvoice } from './document-money.js';\nimport {\n DOCUMENT_MONEY_WRITE,\n withDocumentMoneyTransaction,\n} from './document-money-internal.js';\nimport type { Invoice } from './models/Invoice.js';\nimport { MoneyValidationError } from './money.js';\nimport { buildRevenueJournalEntries } from './revenue-journal.js';\nimport { lockTenantMoneyRuntime } from './tenant-money-barrier.js';\nimport {\n type CancelRevenueOptions,\n InvoiceStatus,\n type RecognizeRevenueOptions,\n} from './types/index.js';\n\ntype PostingMetadata = {\n kind:\n | 'invoice-revenue'\n | 'credit-note-revenue'\n | 'invoice-cancellation-reversal'\n | 'credit-note-cancellation-reversal';\n invoiceId: string;\n moneySnapshotId: string;\n originalJournalId?: string;\n};\n\nfunction required(value: string | null | undefined, label: string): string {\n if (!value || value.trim() !== value)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `${label} must be a nonempty value`,\n );\n return value;\n}\n\nfunction bookCurrency(value: string | undefined): string {\n if (\n !value ||\n value !== value.toUpperCase() ||\n !ISO_4217_MINOR_UNITS.has(value) ||\n ISO_4217_MINOR_UNITS.get(value) === null\n )\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Revenue posting requires a supported ISO 4217 bookCurrency',\n );\n return value;\n}\n\nfunction assertTenantAuthority(tenantId: string): void {\n if (getTenantId() !== tenantId)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Invoice posting requires its active tenant context',\n );\n}\n\nfunction canonicalEntries(entries: readonly JournalEntryData[]): string {\n return JSON.stringify(\n entries\n .map((entry) => ({\n accountId: entry.accountId,\n debit: entry.debit ?? 0,\n credit: entry.credit ?? 0,\n currency: entry.currency ?? '',\n memo: entry.memo ?? '',\n }))\n .sort((left, right) => {\n const leftKey = JSON.stringify(left);\n const rightKey = JSON.stringify(right);\n return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;\n }),\n );\n}\n\nasync function validateAccounts(\n db: DatabaseInterface,\n tenantId: string,\n accountIds: readonly string[],\n): Promise<void> {\n const { AccountCollection } = await import('@happyvertical/smrt-ledgers');\n const accounts = await AccountCollection.create({ db });\n for (const accountId of [...new Set(accountIds)]) {\n const account = await accounts.get({ id: accountId }, { cache: false });\n if (!account || account.tenantId !== tenantId || !account.active)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Posting account ${accountId} must be active and owned by the invoice tenant`,\n );\n }\n}\n\nasync function entriesForJournal(\n db: DatabaseInterface,\n journalId: string,\n): Promise<JournalEntryData[]> {\n const { JournalEntryCollection } = await import(\n '@happyvertical/smrt-ledgers'\n );\n const entries = await (\n await JournalEntryCollection.create({ db })\n ).findByJournal(journalId);\n return entries.map((entry) => ({\n accountId: entry.accountId,\n debit: entry.debit,\n credit: entry.credit,\n currency: entry.currency,\n memo: entry.memo,\n }));\n}\n\nfunction metadataMatches(journal: Journal, expected: PostingMetadata): boolean {\n const actual = journal.metadata as Partial<PostingMetadata>;\n return (\n actual.kind === expected.kind &&\n actual.invoiceId === expected.invoiceId &&\n actual.moneySnapshotId === expected.moneySnapshotId &&\n actual.originalJournalId === expected.originalJournalId\n );\n}\n\nasync function createOrReadPostedJournal(\n db: DatabaseInterface,\n input: {\n id: string;\n tenantId: string;\n sourceRef: string;\n description: string;\n date: Date;\n metadata: PostingMetadata;\n entries: readonly JournalEntryData[];\n },\n): Promise<Journal> {\n const { JournalCollection } = await import('@happyvertical/smrt-ledgers');\n const journals = await JournalCollection.create({ db });\n const existing = await journals.get({ id: input.id }, { cache: false });\n if (existing) {\n const storedEntries = await entriesForJournal(db, input.id);\n if (\n existing.status !== 'posted' ||\n existing.tenantId !== input.tenantId ||\n existing.sourceModule !== 'smrt-commerce' ||\n existing.sourceRef !== input.sourceRef ||\n !metadataMatches(existing, input.metadata) ||\n canonicalEntries(storedEntries) !== canonicalEntries(input.entries)\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n `Deterministic journal ${input.id} conflicts with the frozen invoice snapshot`,\n );\n return existing;\n }\n const journal = await journals.create({\n id: input.id,\n tenantId: input.tenantId,\n date: input.date,\n description: input.description,\n sourceModule: 'smrt-commerce',\n sourceRef: input.sourceRef,\n metadata: input.metadata,\n _insertOnly: true,\n });\n for (const entry of input.entries) await journal.addEntry(entry);\n await journal.post();\n if (!journal.id)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Posted revenue journal is missing its identity',\n );\n return journal;\n}\n\n/**\n * Reload a journal posted inside a money transaction on `db`, so the caller\n * gets an instance whose reads do not run on the finished transaction.\n */\nexport async function rebindJournal(\n db: DatabaseInterface,\n journal: Journal,\n): Promise<Journal> {\n const { JournalCollection } = await import('@happyvertical/smrt-ledgers');\n return (\n (await (\n await JournalCollection.create({ db })\n ).get({ id: required(journal.id, 'Journal ID') }, { cache: false })) ??\n journal\n );\n}\n\n/** Post an issued invoice's revenue journal from its frozen money facts. */\nexport async function recognizeInvoiceRevenue(\n input: Invoice,\n options: RecognizeRevenueOptions,\n): Promise<Journal> {\n return withDocumentMoneyTransaction(input.db, async (tx) =>\n input.withDatabase(tx, async (callerInvoice) => {\n const candidateTenantId = getTenantId() ?? callerInvoice.tenantId;\n await lockTenantMoneyRuntime(\n tx,\n required(candidateTenantId, 'Invoice tenant ID'),\n );\n const invoiceId = required(callerInvoice.id, 'Invoice ID');\n const tenantId = required(candidateTenantId, 'Invoice tenant ID');\n assertTenantAuthority(tenantId);\n const currency = bookCurrency(options.bookCurrency);\n const { invoice } = await lockAndReloadInvoice(tx, invoiceId, tenantId);\n if (invoice.moneyAuthority === 'historical')\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Original historical revenue is retained evidence; a new revenue posting requires an explicit reviewed historical accounting contract',\n );\n if (invoice.status === InvoiceStatus.DRAFT || !invoice.moneySnapshotId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Revenue recognition requires an issued frozen invoice',\n );\n if (invoice.currency !== currency)\n throw new MoneyValidationError(\n 'CURRENCY_MISMATCH',\n `Invoice currency ${invoice.currency} does not match book currency ${currency}`,\n );\n\n const lines = await (\n await InvoiceLineItemCollection.create({ db: tx })\n ).findByInvoice(invoiceId);\n const taxes = await (\n await DocumentTaxCollection.create({ db: tx })\n ).findByDocument(tenantId, invoice.kind, invoiceId);\n const built = buildRevenueJournalEntries(\n {\n id: invoiceId,\n number: invoice.invoiceNumber,\n currency: invoice.currency,\n lineAmountType: invoice.lineAmountType,\n subtotalMinor: invoice.subtotal,\n taxMinor: invoice.taxAmount,\n totalMinor: invoice.totalAmount,\n providerTaxMinor: invoice.providerTaxAmount,\n },\n lines.map((line) => ({\n id: required(line.id, 'Invoice line ID'),\n amountMinor: line.amount,\n taxMinor: line.taxAmount,\n revenueAccountId: line.revenueAccountId,\n })),\n taxes.map((tax) => ({\n currency: tax.currency,\n taxCodeId: tax.taxCodeId,\n taxCodeCode: tax.taxCodeCode,\n taxComponentId: tax.taxComponentId,\n liabilityAccountId: tax.liabilityAccountId,\n taxMinor: tax.taxMinor,\n })),\n {\n arAccountId: options.arAccountId,\n revenueAccountId: options.revenueAccountId,\n taxAccountId: options.taxAccountId,\n bookCurrency: currency,\n direction: invoice.kind === 'credit_note' ? -1 : 1,\n },\n );\n await validateAccounts(\n tx,\n tenantId,\n built.entries.map((entry) => entry.accountId),\n );\n const postingKind =\n invoice.kind === 'credit_note'\n ? ('credit-note-revenue' as const)\n : ('invoice-revenue' as const);\n const journalId = await deterministicId([\n postingKind,\n tenantId,\n invoiceId,\n invoice.moneySnapshotId,\n ]);\n const metadata: PostingMetadata = {\n kind: postingKind,\n invoiceId,\n moneySnapshotId: invoice.moneySnapshotId,\n };\n const journal = await createOrReadPostedJournal(tx, {\n id: journalId,\n tenantId,\n sourceRef: `${postingKind}:${invoiceId}:${invoice.moneySnapshotId}`,\n description: `${invoice.kind === 'credit_note' ? 'Credit note' : 'Revenue'}: Invoice ${invoice.invoiceNumber}`,\n date: invoice.issueDate,\n metadata,\n entries: built.entries,\n });\n if (invoice.arJournalId && invoice.arJournalId !== journalId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Invoice already links a different revenue journal',\n );\n invoice.arJournalId = journalId;\n await invoice.saveDerived(DOCUMENT_MONEY_WRITE);\n callerInvoice.arJournalId = journalId;\n return journal;\n }),\n );\n}\n\nfunction reverseEntries(\n entries: readonly JournalEntryData[],\n currency: string,\n): JournalEntryData[] {\n return entries.map((entry, index) => {\n if (\n entry.currency !== currency ||\n !Number.isSafeInteger(entry.debit ?? 0) ||\n !Number.isSafeInteger(entry.credit ?? 0) ||\n (entry.debit ?? 0) < 0 ||\n (entry.credit ?? 0) < 0 ||\n (entry.debit ?? 0) > 0 === (entry.credit ?? 0) > 0\n )\n throw new MoneyValidationError(\n entry.currency !== currency ? 'CURRENCY_MISMATCH' : 'UNSAFE_MONEY',\n `Original journal entry ${index + 1} is not an exact ${currency} movement`,\n );\n return {\n accountId: required(entry.accountId, 'Original journal account ID'),\n debit: entry.credit ?? 0,\n credit: entry.debit ?? 0,\n currency,\n memo: `Reversal: ${entry.memo ?? ''}`,\n };\n });\n}\n\n/** Atomically reverse a posted invoice and move it to CANCELLED. */\nexport async function cancelInvoiceWithRevenueReversal(\n input: Invoice,\n options: CancelRevenueOptions,\n): Promise<Journal> {\n return withDocumentMoneyTransaction(input.db, async (tx) =>\n input.withDatabase(tx, async (callerInvoice) => {\n const candidateTenantId = getTenantId() ?? callerInvoice.tenantId;\n await lockTenantMoneyRuntime(\n tx,\n required(candidateTenantId, 'Invoice tenant ID'),\n );\n const invoiceId = required(callerInvoice.id, 'Invoice ID');\n const tenantId = required(candidateTenantId, 'Invoice tenant ID');\n assertTenantAuthority(tenantId);\n const currency = bookCurrency(options.bookCurrency);\n const { invoice } = await lockAndReloadInvoice(tx, invoiceId, tenantId);\n if (invoice.currency !== currency)\n throw new MoneyValidationError(\n 'CURRENCY_MISMATCH',\n `Invoice currency ${invoice.currency} does not match book currency ${currency}`,\n );\n if (\n (invoice.kind === 'invoice' && invoice.isPaid()) ||\n (invoice.kind === 'credit_note' && invoice.amountPaid > 0)\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Paid invoices and refunded credit notes cannot be cancelled',\n );\n if (!invoice.arJournalId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Revenue must be posted before using cancellation reversal',\n );\n const snapshotId = required(\n invoice.moneySnapshotId,\n 'Invoice money snapshot ID',\n );\n const originalJournalId = invoice.arJournalId;\n const originalKind =\n invoice.kind === 'credit_note'\n ? ('credit-note-revenue' as const)\n : ('invoice-revenue' as const);\n const { JournalCollection } = await import('@happyvertical/smrt-ledgers');\n const journals = await JournalCollection.create({ db: tx });\n const original = await journals.get(\n { id: originalJournalId },\n { cache: false },\n );\n if (\n original?.status !== 'posted' ||\n original.tenantId !== tenantId ||\n original.sourceModule !== 'smrt-commerce' ||\n original.sourceRef !== `${originalKind}:${invoiceId}:${snapshotId}` ||\n !metadataMatches(original, {\n kind: originalKind,\n invoiceId,\n moneySnapshotId: snapshotId,\n })\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Invoice revenue journal must be posted and tenant-owned before reversal',\n );\n const originalEntries = await entriesForJournal(tx, originalJournalId);\n const reversalEntries = reverseEntries(originalEntries, currency);\n await validateAccounts(\n tx,\n tenantId,\n reversalEntries.map((entry) => entry.accountId),\n );\n const reversalKind =\n invoice.kind === 'credit_note'\n ? ('credit-note-cancellation-reversal' as const)\n : ('invoice-cancellation-reversal' as const);\n const reversalId = await deterministicId([\n reversalKind,\n tenantId,\n invoiceId,\n snapshotId,\n originalJournalId,\n ]);\n const metadata: PostingMetadata = {\n kind: reversalKind,\n invoiceId,\n moneySnapshotId: snapshotId,\n originalJournalId,\n };\n const reversal = await createOrReadPostedJournal(tx, {\n id: reversalId,\n tenantId,\n sourceRef: `invoice-cancellation:${invoiceId}:${snapshotId}`,\n description: `Cancellation reversal: Invoice ${invoice.invoiceNumber}`,\n date: new Date(),\n metadata,\n entries: reversalEntries,\n });\n if (\n invoice.arReversalJournalId &&\n invoice.arReversalJournalId !== reversalId\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Invoice already links a different cancellation reversal',\n );\n invoice.arReversalJournalId = reversalId;\n if (invoice.status !== InvoiceStatus.CANCELLED) invoice.cancel();\n await invoice.saveDerived(DOCUMENT_MONEY_WRITE);\n callerInvoice.arReversalJournalId = reversalId;\n callerInvoice.status = InvoiceStatus.CANCELLED;\n return reversal;\n }),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"invoice-posting-BfnOyqBR.js","names":[],"sources":["../../src/invoice-posting.ts"],"sourcesContent":["/** Atomic revenue posting and cancellation reversal. */\n\nimport type { DatabaseInterface } from '@happyvertical/smrt-core/migrations';\nimport type { Journal, JournalEntryData } from '@happyvertical/smrt-ledgers';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport { ISO_4217_MINOR_UNITS } from '@happyvertical/smrt-ui/currency';\nimport { deterministicId } from './billing/units.js';\nimport { DocumentTaxCollection } from './collections/DocumentTaxCollection.js';\nimport { InvoiceLineItemCollection } from './collections/InvoiceLineItemCollection.js';\nimport { lockAndReloadInvoice } from './document-money.js';\nimport {\n DOCUMENT_MONEY_WRITE,\n withDocumentMoneyTransaction,\n} from './document-money-internal.js';\nimport type { Invoice } from './models/Invoice.js';\nimport { MoneyValidationError } from './money.js';\nimport { buildRevenueJournalEntries } from './revenue-journal.js';\nimport { lockTenantMoneyRuntime } from './tenant-money-barrier.js';\nimport {\n type CancelRevenueOptions,\n InvoiceStatus,\n type RecognizeRevenueOptions,\n} from './types/index.js';\n\ntype PostingMetadata = {\n kind:\n | 'invoice-revenue'\n | 'credit-note-revenue'\n | 'invoice-cancellation-reversal'\n | 'credit-note-cancellation-reversal';\n invoiceId: string;\n moneySnapshotId: string;\n originalJournalId?: string;\n};\n\nfunction required(value: string | null | undefined, label: string): string {\n if (!value || value.trim() !== value)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `${label} must be a nonempty value`,\n );\n return value;\n}\n\nfunction bookCurrency(value: string | undefined): string {\n if (\n !value ||\n value !== value.toUpperCase() ||\n !ISO_4217_MINOR_UNITS.has(value) ||\n ISO_4217_MINOR_UNITS.get(value) === null\n )\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Revenue posting requires a supported ISO 4217 bookCurrency',\n );\n return value;\n}\n\nfunction assertTenantAuthority(tenantId: string): void {\n if (getTenantId() !== tenantId)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Invoice posting requires its active tenant context',\n );\n}\n\nfunction canonicalEntries(entries: readonly JournalEntryData[]): string {\n return JSON.stringify(\n entries\n .map((entry) => ({\n accountId: entry.accountId,\n debit: entry.debit ?? 0,\n credit: entry.credit ?? 0,\n currency: entry.currency ?? '',\n memo: entry.memo ?? '',\n }))\n .sort((left, right) => {\n const leftKey = JSON.stringify(left);\n const rightKey = JSON.stringify(right);\n return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;\n }),\n );\n}\n\nasync function validateAccounts(\n db: DatabaseInterface,\n tenantId: string,\n accountIds: readonly string[],\n): Promise<void> {\n const { AccountCollection } = await import('@happyvertical/smrt-ledgers');\n const accounts = await AccountCollection.create({ db });\n for (const accountId of [...new Set(accountIds)]) {\n const account = await accounts.get({ id: accountId }, { cache: false });\n if (!account || account.tenantId !== tenantId || !account.active)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Posting account ${accountId} must be active and owned by the invoice tenant`,\n );\n }\n}\n\nasync function entriesForJournal(\n db: DatabaseInterface,\n journalId: string,\n): Promise<JournalEntryData[]> {\n const { JournalEntryCollection } = await import(\n '@happyvertical/smrt-ledgers'\n );\n const entries = await (\n await JournalEntryCollection.create({ db })\n ).findByJournal(journalId);\n return entries.map((entry) => ({\n accountId: entry.accountId,\n debit: entry.debit,\n credit: entry.credit,\n currency: entry.currency,\n memo: entry.memo,\n }));\n}\n\nfunction metadataMatches(journal: Journal, expected: PostingMetadata): boolean {\n const actual = journal.metadata as Partial<PostingMetadata>;\n return (\n actual.kind === expected.kind &&\n actual.invoiceId === expected.invoiceId &&\n actual.moneySnapshotId === expected.moneySnapshotId &&\n actual.originalJournalId === expected.originalJournalId\n );\n}\n\nasync function createOrReadPostedJournal(\n db: DatabaseInterface,\n input: {\n id: string;\n tenantId: string;\n sourceRef: string;\n description: string;\n date: Date;\n metadata: PostingMetadata;\n entries: readonly JournalEntryData[];\n },\n): Promise<Journal> {\n const { JournalCollection } = await import('@happyvertical/smrt-ledgers');\n const journals = await JournalCollection.create({ db });\n const existing = await journals.get({ id: input.id }, { cache: false });\n if (existing) {\n const storedEntries = await entriesForJournal(db, input.id);\n if (\n existing.status !== 'posted' ||\n existing.tenantId !== input.tenantId ||\n existing.sourceModule !== 'smrt-commerce' ||\n existing.sourceRef !== input.sourceRef ||\n !metadataMatches(existing, input.metadata) ||\n canonicalEntries(storedEntries) !== canonicalEntries(input.entries)\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n `Deterministic journal ${input.id} conflicts with the frozen invoice snapshot`,\n );\n return existing;\n }\n const journal = await journals.create({\n id: input.id,\n tenantId: input.tenantId,\n date: input.date,\n description: input.description,\n sourceModule: 'smrt-commerce',\n sourceRef: input.sourceRef,\n metadata: input.metadata,\n _insertOnly: true,\n });\n for (const entry of input.entries) await journal.addEntry(entry);\n await journal.post();\n if (!journal.id)\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Posted revenue journal is missing its identity',\n );\n return journal;\n}\n\n/**\n * Reload a journal posted inside a money transaction on `db`, so the caller\n * gets an instance whose reads do not run on the finished transaction.\n */\nexport async function rebindJournal(\n db: DatabaseInterface,\n journal: Journal,\n): Promise<Journal> {\n const { JournalCollection } = await import('@happyvertical/smrt-ledgers');\n return (\n (await (\n await JournalCollection.create({ db })\n ).get({ id: required(journal.id, 'Journal ID') }, { cache: false })) ??\n journal\n );\n}\n\n/** Post an issued invoice's revenue journal from its frozen money facts. */\nexport async function recognizeInvoiceRevenue(\n input: Invoice,\n options: RecognizeRevenueOptions,\n): Promise<Journal> {\n return withDocumentMoneyTransaction(input.db, async (tx) =>\n input.withDatabase(tx, async (callerInvoice) => {\n const candidateTenantId = getTenantId() ?? callerInvoice.tenantId;\n await lockTenantMoneyRuntime(\n tx,\n required(candidateTenantId, 'Invoice tenant ID'),\n );\n const invoiceId = required(callerInvoice.id, 'Invoice ID');\n const tenantId = required(candidateTenantId, 'Invoice tenant ID');\n assertTenantAuthority(tenantId);\n const currency = bookCurrency(options.bookCurrency);\n const { invoice } = await lockAndReloadInvoice(tx, invoiceId, tenantId);\n if (invoice.moneyAuthority === 'historical')\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Original historical revenue is retained evidence; a new revenue posting requires an explicit reviewed historical accounting contract',\n );\n if (invoice.status === InvoiceStatus.DRAFT || !invoice.moneySnapshotId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Revenue recognition requires an issued frozen invoice',\n );\n if (invoice.currency !== currency)\n throw new MoneyValidationError(\n 'CURRENCY_MISMATCH',\n `Invoice currency ${invoice.currency} does not match book currency ${currency}`,\n );\n\n const lines = await (\n await InvoiceLineItemCollection.create({ db: tx })\n ).findByInvoice(invoiceId);\n const taxes = await (\n await DocumentTaxCollection.create({ db: tx })\n ).findByDocument(tenantId, invoice.kind, invoiceId);\n const built = buildRevenueJournalEntries(\n {\n id: invoiceId,\n number: invoice.invoiceNumber,\n currency: invoice.currency,\n lineAmountType: invoice.lineAmountType,\n subtotalMinor: invoice.subtotal,\n taxMinor: invoice.taxAmount,\n totalMinor: invoice.totalAmount,\n providerTaxMinor: invoice.providerTaxAmount,\n },\n lines.map((line) => ({\n id: required(line.id, 'Invoice line ID'),\n amountMinor: line.amount,\n taxMinor: line.taxAmount,\n revenueAccountId: line.revenueAccountId,\n })),\n taxes.map((tax) => ({\n currency: tax.currency,\n taxCodeId: tax.taxCodeId,\n taxCodeCode: tax.taxCodeCode,\n taxComponentId: tax.taxComponentId,\n liabilityAccountId: tax.liabilityAccountId,\n taxMinor: tax.taxMinor,\n })),\n {\n arAccountId: options.arAccountId,\n revenueAccountId: options.revenueAccountId,\n taxAccountId: options.taxAccountId,\n bookCurrency: currency,\n direction: invoice.kind === 'credit_note' ? -1 : 1,\n },\n );\n await validateAccounts(\n tx,\n tenantId,\n built.entries.map((entry) => entry.accountId),\n );\n const postingKind =\n invoice.kind === 'credit_note'\n ? ('credit-note-revenue' as const)\n : ('invoice-revenue' as const);\n const journalId = await deterministicId([\n postingKind,\n tenantId,\n invoiceId,\n invoice.moneySnapshotId,\n ]);\n const metadata: PostingMetadata = {\n kind: postingKind,\n invoiceId,\n moneySnapshotId: invoice.moneySnapshotId,\n };\n const journal = await createOrReadPostedJournal(tx, {\n id: journalId,\n tenantId,\n sourceRef: `${postingKind}:${invoiceId}:${invoice.moneySnapshotId}`,\n description: `${invoice.kind === 'credit_note' ? 'Credit note' : 'Revenue'}: Invoice ${invoice.invoiceNumber}`,\n date: invoice.issueDate,\n metadata,\n entries: built.entries,\n });\n if (invoice.arJournalId && invoice.arJournalId !== journalId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Invoice already links a different revenue journal',\n );\n invoice.arJournalId = journalId;\n await invoice.saveDerived(DOCUMENT_MONEY_WRITE);\n callerInvoice.arJournalId = journalId;\n return journal;\n }),\n );\n}\n\nfunction reverseEntries(\n entries: readonly JournalEntryData[],\n currency: string,\n): JournalEntryData[] {\n return entries.map((entry, index) => {\n if (\n entry.currency !== currency ||\n !Number.isSafeInteger(entry.debit ?? 0) ||\n !Number.isSafeInteger(entry.credit ?? 0) ||\n (entry.debit ?? 0) < 0 ||\n (entry.credit ?? 0) < 0 ||\n (entry.debit ?? 0) > 0 === (entry.credit ?? 0) > 0\n )\n throw new MoneyValidationError(\n entry.currency !== currency ? 'CURRENCY_MISMATCH' : 'UNSAFE_MONEY',\n `Original journal entry ${index + 1} is not an exact ${currency} movement`,\n );\n return {\n accountId: required(entry.accountId, 'Original journal account ID'),\n debit: entry.credit ?? 0,\n credit: entry.debit ?? 0,\n currency,\n memo: `Reversal: ${entry.memo ?? ''}`,\n };\n });\n}\n\n/** Atomically reverse a posted invoice and move it to CANCELLED. */\nexport async function cancelInvoiceWithRevenueReversal(\n input: Invoice,\n options: CancelRevenueOptions,\n): Promise<Journal> {\n return withDocumentMoneyTransaction(input.db, async (tx) =>\n input.withDatabase(tx, async (callerInvoice) => {\n const candidateTenantId = getTenantId() ?? callerInvoice.tenantId;\n await lockTenantMoneyRuntime(\n tx,\n required(candidateTenantId, 'Invoice tenant ID'),\n );\n const invoiceId = required(callerInvoice.id, 'Invoice ID');\n const tenantId = required(candidateTenantId, 'Invoice tenant ID');\n assertTenantAuthority(tenantId);\n const currency = bookCurrency(options.bookCurrency);\n const { invoice } = await lockAndReloadInvoice(tx, invoiceId, tenantId);\n if (invoice.currency !== currency)\n throw new MoneyValidationError(\n 'CURRENCY_MISMATCH',\n `Invoice currency ${invoice.currency} does not match book currency ${currency}`,\n );\n if (\n (invoice.kind === 'invoice' && invoice.isPaid()) ||\n (invoice.kind === 'credit_note' && invoice.amountPaid > 0)\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Paid invoices and refunded credit notes cannot be cancelled',\n );\n if (!invoice.arJournalId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Revenue must be posted before using cancellation reversal',\n );\n const snapshotId = required(\n invoice.moneySnapshotId,\n 'Invoice money snapshot ID',\n );\n const originalJournalId = invoice.arJournalId;\n const originalKind =\n invoice.kind === 'credit_note'\n ? ('credit-note-revenue' as const)\n : ('invoice-revenue' as const);\n const { JournalCollection } = await import('@happyvertical/smrt-ledgers');\n const journals = await JournalCollection.create({ db: tx });\n const original = await journals.get(\n { id: originalJournalId },\n { cache: false },\n );\n if (\n original?.status !== 'posted' ||\n original.tenantId !== tenantId ||\n original.sourceModule !== 'smrt-commerce' ||\n original.sourceRef !== `${originalKind}:${invoiceId}:${snapshotId}` ||\n !metadataMatches(original, {\n kind: originalKind,\n invoiceId,\n moneySnapshotId: snapshotId,\n })\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Invoice revenue journal must be posted and tenant-owned before reversal',\n );\n const originalEntries = await entriesForJournal(tx, originalJournalId);\n const reversalEntries = reverseEntries(originalEntries, currency);\n await validateAccounts(\n tx,\n tenantId,\n reversalEntries.map((entry) => entry.accountId),\n );\n const reversalKind =\n invoice.kind === 'credit_note'\n ? ('credit-note-cancellation-reversal' as const)\n : ('invoice-cancellation-reversal' as const);\n const reversalId = await deterministicId([\n reversalKind,\n tenantId,\n invoiceId,\n snapshotId,\n originalJournalId,\n ]);\n const metadata: PostingMetadata = {\n kind: reversalKind,\n invoiceId,\n moneySnapshotId: snapshotId,\n originalJournalId,\n };\n const reversal = await createOrReadPostedJournal(tx, {\n id: reversalId,\n tenantId,\n sourceRef: `invoice-cancellation:${invoiceId}:${snapshotId}`,\n description: `Cancellation reversal: Invoice ${invoice.invoiceNumber}`,\n date: new Date(),\n metadata,\n entries: reversalEntries,\n });\n if (\n invoice.arReversalJournalId &&\n invoice.arReversalJournalId !== reversalId\n )\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Invoice already links a different cancellation reversal',\n );\n invoice.arReversalJournalId = reversalId;\n if (invoice.status !== InvoiceStatus.CANCELLED) invoice.cancel();\n await invoice.saveDerived(DOCUMENT_MONEY_WRITE);\n callerInvoice.arReversalJournalId = reversalId;\n callerInvoice.status = InvoiceStatus.CANCELLED;\n return reversal;\n }),\n );\n}\n"],"mappings":"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"}
|