@happyvertical/smrt-commerce 0.56.1 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +24 -11
- package/README.md +159 -93
- package/agents/accounting-snapshot-mapper.md +3 -3
- package/agents/commercial-ui.md +2 -2
- package/agents/contract-money.md +9 -6
- package/agents/cost-billing.md +3 -6
- package/agents/customer-vendor-ui.md +6 -1
- package/agents/document-money.md +59 -31
- package/agents/invoice-calculations.md +31 -56
- package/agents/invoice-preparation.md +62 -73
- package/agents/invoice-quantity.md +9 -21
- package/agents/minor-unit-migration.md +4 -4
- package/agents/money-calculator.md +7 -8
- package/agents/money-model.md +18 -22
- package/agents/money-upgrade.md +92 -0
- package/agents/revenue-journal.md +2 -2
- package/agents/tax-codes.md +17 -14
- package/agents/tax-summary.md +12 -8
- package/dist/billing/period-close.d.ts.map +1 -1
- package/dist/billing/runtime.d.ts +5 -2
- package/dist/billing/runtime.d.ts.map +1 -1
- package/dist/chunks/{ContractLineItemCollection-DYjhj_pU.js → ContractLineItemCollection-BZcrlrrY.js} +22 -27
- package/dist/chunks/ContractLineItemCollection-BZcrlrrY.js.map +1 -0
- package/dist/chunks/{CustomerCollection-DQIujn4f.js → CustomerCollection-BIPH2ZUx.js} +3 -3
- package/dist/chunks/{CustomerCollection-DQIujn4f.js.map → CustomerCollection-BIPH2ZUx.js.map} +1 -1
- package/dist/chunks/{DocumentTaxCollection-BEpoqXAQ.js → DocumentTaxCollection-DDzlywMW.js} +41 -14
- package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +1 -0
- package/dist/chunks/{InvoiceCollection-CKQflFjc.js → InvoiceCollection-cinzAqt6.js} +53 -164
- package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +1 -0
- package/dist/chunks/{TaxCodeCollection-DOpzW4y8.js → TaxCodeCollection-DaBOPG9c.js} +11 -4
- package/dist/chunks/{TaxCodeCollection-DOpzW4y8.js.map → TaxCodeCollection-DaBOPG9c.js.map} +1 -1
- package/dist/chunks/{TaxComponentCollection-Cs2uXwEl.js → TaxComponentCollection-C4zxnyVu.js} +3 -3
- package/dist/chunks/{TaxComponentCollection-Cs2uXwEl.js.map → TaxComponentCollection-C4zxnyVu.js.map} +1 -1
- package/dist/chunks/VendorCollection-D6ZcUYy8.js +100 -0
- package/dist/chunks/VendorCollection-D6ZcUYy8.js.map +1 -0
- package/dist/chunks/{allocation-integrity-DI3G16BN.js → allocation-integrity-WmWyXhtG.js} +6 -6
- package/dist/chunks/{allocation-integrity-DI3G16BN.js.map → allocation-integrity-WmWyXhtG.js.map} +1 -1
- package/dist/chunks/calculations-C9daQLsh.js +82 -0
- package/dist/chunks/calculations-C9daQLsh.js.map +1 -0
- package/dist/chunks/contract-money-EuToOghh.js +301 -0
- package/dist/chunks/contract-money-EuToOghh.js.map +1 -0
- package/dist/chunks/detect-CY9ufaFb.js +328 -0
- package/dist/chunks/detect-CY9ufaFb.js.map +1 -0
- package/dist/chunks/{document-money-DNVhDJyC.js → document-money-DV5t4vlK.js} +389 -63
- package/dist/chunks/document-money-DV5t4vlK.js.map +1 -0
- package/dist/chunks/{invoice-posting-BwV8g7z0.js → invoice-posting-C2i5dOVZ.js} +21 -23
- package/dist/chunks/invoice-posting-C2i5dOVZ.js.map +1 -0
- package/dist/chunks/{revenue-journal-DY9fro8E.js → revenue-journal-LLA9Kh_i.js} +3 -14
- package/dist/chunks/revenue-journal-LLA9Kh_i.js.map +1 -0
- package/dist/chunks/tenant-money-barrier-6TlqUeVj.js +255 -0
- package/dist/chunks/tenant-money-barrier-6TlqUeVj.js.map +1 -0
- package/dist/chunks/{types-LDETvQ1C.js → types-Bbm0Xflc.js} +2 -1
- package/dist/chunks/types-Bbm0Xflc.js.map +1 -0
- package/dist/chunks/{units-CNcjH_eE.js → units-8ElbjVZK.js} +18 -3
- package/dist/chunks/units-8ElbjVZK.js.map +1 -0
- package/dist/collections/ContractCollection.d.ts.map +1 -1
- package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
- package/dist/collections/InvoiceCollection.d.ts.map +1 -1
- package/dist/collections/InvoiceLineItemCollection.d.ts +0 -30
- package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
- package/dist/collections/TaxCodeCollection.d.ts +5 -1
- package/dist/collections/TaxCodeCollection.d.ts.map +1 -1
- package/dist/collections/TaxSettingsCollection.d.ts +15 -8
- package/dist/collections/TaxSettingsCollection.d.ts.map +1 -1
- package/dist/collections/TaxSummaryReportCollection.d.ts +3 -3
- package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -1
- package/dist/collections/index.d.ts +0 -1
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/commerce-money-tools.d.ts.map +1 -1
- package/dist/contract-money.d.ts +1 -13
- package/dist/contract-money.d.ts.map +1 -1
- package/dist/document-money-internal.d.ts +7 -3
- package/dist/document-money-internal.d.ts.map +1 -1
- package/dist/document-money.d.ts +1 -11
- package/dist/document-money.d.ts.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +686 -1656
- package/dist/index.js.map +1 -1
- package/dist/invoice-accounting-sync.d.ts.map +1 -1
- package/dist/invoice-mcp.d.ts +3 -3
- package/dist/invoice-mcp.d.ts.map +1 -1
- package/dist/invoice-posting.d.ts +11 -5
- package/dist/invoice-posting.d.ts.map +1 -1
- package/dist/invoice-record-screen.d.ts +1 -1
- package/dist/invoice-record-screen.d.ts.map +1 -1
- package/dist/invoices.d.ts +6 -1
- package/dist/invoices.d.ts.map +1 -1
- package/dist/invoices.js +2 -2
- package/dist/manifest.json +1041 -863
- package/dist/migrations/invoiceEditorStorage.d.ts +2 -4
- package/dist/migrations/invoiceEditorStorage.d.ts.map +1 -1
- package/dist/migrations/partyLinks.d.ts.map +1 -1
- package/dist/models/Contract.d.ts +10 -4
- package/dist/models/Contract.d.ts.map +1 -1
- package/dist/models/ContractLineItem.d.ts +9 -17
- package/dist/models/ContractLineItem.d.ts.map +1 -1
- package/dist/models/DocumentTax.d.ts.map +1 -1
- package/dist/models/Invoice.d.ts +18 -48
- package/dist/models/Invoice.d.ts.map +1 -1
- package/dist/models/InvoiceLineItem.d.ts +13 -32
- package/dist/models/InvoiceLineItem.d.ts.map +1 -1
- package/dist/models/TaxSettings.d.ts +0 -10
- package/dist/models/TaxSettings.d.ts.map +1 -1
- package/dist/models/index.d.ts +1 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/playground.js +1 -1
- package/dist/playground.js.map +1 -1
- package/dist/recipes.d.ts +8 -11
- package/dist/recipes.d.ts.map +1 -1
- package/dist/revenue-journal.d.ts +0 -8
- package/dist/revenue-journal.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +2886 -6121
- package/dist/svelte/components/InvoiceEditor.svelte +7 -148
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts +4 -1
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InvoiceTotals.svelte +14 -118
- package/dist/svelte/components/InvoiceTotals.svelte.d.ts +4 -27
- package/dist/svelte/components/InvoiceTotals.svelte.d.ts.map +1 -1
- package/dist/svelte/components/MoneyInvoiceEditor.svelte +3 -23
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +3 -26
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/index.d.ts +2 -3
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +1 -2
- package/dist/svelte/invoices/calculations.d.ts +3 -108
- package/dist/svelte/invoices/calculations.d.ts.map +1 -1
- package/dist/svelte/invoices/calculations.js +12 -309
- package/dist/svelte/invoices/messages.d.ts +1 -37
- package/dist/svelte/invoices/messages.d.ts.map +1 -1
- package/dist/svelte/invoices/messages.js +1 -37
- package/dist/svelte/invoices/money-editor.d.ts +1 -1
- package/dist/svelte/invoices/money-editor.d.ts.map +1 -1
- package/dist/svelte/invoices/types.d.ts +14 -114
- package/dist/svelte/invoices/types.d.ts.map +1 -1
- package/dist/svelte/playground/InvoicePreparationPreview.svelte +85 -94
- package/dist/svelte/playground/InvoicePreparationPreview.svelte.d.ts.map +1 -1
- package/dist/svelte/playground.js +1 -1
- package/dist/tax/index.d.ts +2 -1
- package/dist/tax/index.d.ts.map +1 -1
- package/dist/tax/standard-codes.d.ts +12 -0
- package/dist/tax/standard-codes.d.ts.map +1 -0
- package/dist/tax/tenant-bootstrap.d.ts +29 -0
- package/dist/tax/tenant-bootstrap.d.ts.map +1 -0
- package/dist/tenant-money-barrier.d.ts +12 -44
- package/dist/tenant-money-barrier.d.ts.map +1 -1
- package/dist/types/index.d.ts +11 -23
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui.js +1 -1
- package/dist/ui.js.map +1 -1
- package/dist/upgrade/detect.d.ts +129 -0
- package/dist/upgrade/detect.d.ts.map +1 -0
- package/dist/upgrade/money-v2.d.ts +81 -0
- package/dist/upgrade/money-v2.d.ts.map +1 -0
- package/package.json +14 -13
- package/agents/line-amount-migration.md +0 -50
- package/dist/chunks/ContractLineItemCollection-DYjhj_pU.js.map +0 -1
- package/dist/chunks/DocumentTaxCollection-BEpoqXAQ.js.map +0 -1
- package/dist/chunks/InvoiceCollection-CKQflFjc.js.map +0 -1
- package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js +0 -467
- package/dist/chunks/InvoiceLineItemCollection-CHm9zdqh.js.map +0 -1
- package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js +0 -319
- package/dist/chunks/TaxSettingsCollection-BBTZDH1S.js.map +0 -1
- package/dist/chunks/calculations-DaHR17il.js +0 -333
- package/dist/chunks/calculations-DaHR17il.js.map +0 -1
- package/dist/chunks/document-money-DNVhDJyC.js.map +0 -1
- package/dist/chunks/invoice-posting-BwV8g7z0.js.map +0 -1
- package/dist/chunks/revenue-journal-DY9fro8E.js.map +0 -1
- package/dist/chunks/tenant-money-barrier-DjRXgMnX.js +0 -135
- package/dist/chunks/tenant-money-barrier-DjRXgMnX.js.map +0 -1
- package/dist/chunks/types-LDETvQ1C.js.map +0 -1
- package/dist/chunks/units-CNcjH_eE.js.map +0 -1
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts +0 -12
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +0 -1
- package/dist/migrations/commerceLineAmounts.d.ts +0 -84
- package/dist/migrations/commerceLineAmounts.d.ts.map +0 -1
- package/dist/models/MoneyMigrationDocument.d.ts +0 -28
- package/dist/models/MoneyMigrationDocument.d.ts.map +0 -1
- package/dist/svelte/components/InvoiceLineEditor.svelte +0 -92
- package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts +0 -8
- package/dist/svelte/components/InvoiceLineEditor.svelte.d.ts.map +0 -1
- package/dist/svelte/invoices/fields.d.ts +0 -6
- package/dist/svelte/invoices/fields.d.ts.map +0 -1
- package/dist/svelte/invoices/fields.js +0 -22
- package/dist/tax/legacy.d.ts +0 -48
- package/dist/tax/legacy.d.ts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-money-EuToOghh.js","names":[],"sources":["../../src/contract-money.ts"],"sourcesContent":["/** Transactional Contract money aggregate persistence. */\n\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 { 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 '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 };\n },\n );\n line.taxRate = 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 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(input: Contract): 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 if (existing && existing.tenantId !== scope.tenantId)\n throw new MoneyValidationError(\n 'FROZEN_DOCUMENT',\n 'Contract tenant identity cannot change',\n );\n if (!existing) {\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);\n }\n return withLockedContract(\n scope.db,\n tenantId,\n required(existing.id, 'Contract'),\n async (stored, lines) => {\n if (isFrozen(stored)) {\n assertUnchanged(\n contract,\n stored,\n FROZEN_CONTRACT_FIELDS,\n 'Contract',\n );\n return contract.saveDerived(DOCUMENT_MONEY_WRITE);\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);\n },\n );\n }),\n );\n}\n\n/** Save a line and atomically recompute its Contract aggregate. */\nexport async function saveContractLineItem(\n input: ContractLineItem,\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 (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);\n }\n line.contractId = contractId;\n line.tenantId = scope.tenantId;\n await line.saveDerived(DOCUMENT_MONEY_WRITE);\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"],"mappings":"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"}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { MoneyValidationError } from "../money.js";
|
|
3
|
+
import { isPostgresDatabase } from "@happyvertical/smrt-core";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
//#region src/document-money-internal.ts
|
|
6
|
+
var activeMoneyTransactions = /* @__PURE__ */ new WeakSet();
|
|
7
|
+
var transactionRoots = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
function rootMoneyDatabase(db) {
|
|
9
|
+
return transactionRoots.get(db) ?? db;
|
|
10
|
+
}
|
|
11
|
+
var DOCUMENT_MONEY_WRITE = /* @__PURE__ */ Symbol("commerce.documentMoneyWrite");
|
|
12
|
+
var DOCUMENT_MONEY_READ_REBIND = /* @__PURE__ */ Symbol("commerce.documentMoneyReadRebind");
|
|
13
|
+
function compareCodeUnits(left, right) {
|
|
14
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
15
|
+
}
|
|
16
|
+
function quoteIdentifier(identifier) {
|
|
17
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Unsafe money parent table identifier: ${identifier}`);
|
|
18
|
+
return `"${identifier}"`;
|
|
19
|
+
}
|
|
20
|
+
function assertIdentifier(value, label) {
|
|
21
|
+
if (typeof value !== "string" || value.trim() !== value || value.length === 0) throw new MoneyValidationError("INVALID_MONEY_INPUT", `${label} must be a nonempty identifier`);
|
|
22
|
+
}
|
|
23
|
+
async function withDocumentMoneyTransaction(db, operation) {
|
|
24
|
+
if (activeMoneyTransactions.has(db)) return operation(db);
|
|
25
|
+
if (!db.transaction) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Document money persistence requires transaction support");
|
|
26
|
+
return db.transaction(async (tx) => {
|
|
27
|
+
activeMoneyTransactions.add(tx);
|
|
28
|
+
transactionRoots.set(tx, rootMoneyDatabase(db));
|
|
29
|
+
try {
|
|
30
|
+
return await operation(tx);
|
|
31
|
+
} finally {
|
|
32
|
+
activeMoneyTransactions.delete(tx);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function withDocumentMoneyObjectTransaction(input, operation) {
|
|
37
|
+
if (activeMoneyTransactions.has(input.db)) return operation(input, input.db);
|
|
38
|
+
const root = rootMoneyDatabase(input.db);
|
|
39
|
+
return input.withTransaction(async (bound) => {
|
|
40
|
+
const tx = bound.db;
|
|
41
|
+
activeMoneyTransactions.add(tx);
|
|
42
|
+
if (tx !== root) transactionRoots.set(tx, root);
|
|
43
|
+
try {
|
|
44
|
+
return await operation(bound, tx);
|
|
45
|
+
} finally {
|
|
46
|
+
activeMoneyTransactions.delete(tx);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function lockDocumentMoneyParents(targets) {
|
|
51
|
+
const sorted = [...targets].sort((left, right) => compareCodeUnits(left.id, right.id) || compareCodeUnits(left.collection.tableName, right.collection.tableName));
|
|
52
|
+
const identities = /* @__PURE__ */ new Set();
|
|
53
|
+
for (const target of sorted) {
|
|
54
|
+
assertIdentifier(target.id, "Money parent id");
|
|
55
|
+
assertIdentifier(target.tenantId, "Money parent tenant id");
|
|
56
|
+
const identity = `${target.collection.tableName}\0${target.id}`;
|
|
57
|
+
if (identities.has(identity)) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Duplicate money parent lock target: ${target.id}`);
|
|
58
|
+
identities.add(identity);
|
|
59
|
+
const table = quoteIdentifier(target.collection.tableName);
|
|
60
|
+
if ((await target.collection.query(`UPDATE ${table}
|
|
61
|
+
SET "updated_at" = "updated_at"
|
|
62
|
+
WHERE "id" = ? AND "tenant_id" = ?
|
|
63
|
+
RETURNING "id"`, [target.id, target.tenantId], { allowRawOnTenantScoped: true })).length !== 1) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Money parent ${target.id} was not found in tenant ${target.tenantId}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/upgrade/detect.ts
|
|
68
|
+
var detect_exports = /* @__PURE__ */ __exportAll({
|
|
69
|
+
COMMERCE_DATA_NEEDS_UPGRADE: () => COMMERCE_DATA_NEEDS_UPGRADE,
|
|
70
|
+
CommerceDataNeedsUpgradeError: () => CommerceDataNeedsUpgradeError,
|
|
71
|
+
MONEY_DOCUMENT_TABLES: () => MONEY_DOCUMENT_TABLES,
|
|
72
|
+
OLD_MONEY_COLUMNS: () => OLD_MONEY_COLUMNS,
|
|
73
|
+
OLD_MONEY_TABLES: () => OLD_MONEY_TABLES,
|
|
74
|
+
assertCommerceMoneySchemaCurrent: () => assertCommerceMoneySchemaCurrent,
|
|
75
|
+
assertHydratedMoneyRowCurrent: () => assertHydratedMoneyRowCurrent,
|
|
76
|
+
assertTenantMoneyCurrent: () => assertTenantMoneyCurrent,
|
|
77
|
+
commerceMoneyUpgradeCommand: () => commerceMoneyUpgradeCommand,
|
|
78
|
+
findCommerceMoneyUpgradeNeeds: () => findCommerceMoneyUpgradeNeeds,
|
|
79
|
+
findOldMoneySchema: () => findOldMoneySchema,
|
|
80
|
+
findTenantlessMoneyDocuments: () => findTenantlessMoneyDocuments,
|
|
81
|
+
withCommerceMoneyUpgradeExecutor: () => withCommerceMoneyUpgradeExecutor
|
|
82
|
+
});
|
|
83
|
+
var COMMERCE_DATA_NEEDS_UPGRADE = "COMMERCE_DATA_NEEDS_UPGRADE";
|
|
84
|
+
function commerceMoneyUpgradeCommand(tenantId) {
|
|
85
|
+
return tenantId ? `smrt db:upgrade-commerce-money --tenant-id ${tenantId}` : "smrt db:upgrade-commerce-money";
|
|
86
|
+
}
|
|
87
|
+
var CommerceDataNeedsUpgradeError = class extends Error {
|
|
88
|
+
constructor(detail, tenantId) {
|
|
89
|
+
super(`${COMMERCE_DATA_NEEDS_UPGRADE}: ${detail} Run ${commerceMoneyUpgradeCommand(tenantId)} to upgrade it.`);
|
|
90
|
+
this.detail = detail;
|
|
91
|
+
this.tenantId = tenantId;
|
|
92
|
+
this.name = "CommerceDataNeedsUpgradeError";
|
|
93
|
+
}
|
|
94
|
+
detail;
|
|
95
|
+
tenantId;
|
|
96
|
+
code = COMMERCE_DATA_NEEDS_UPGRADE;
|
|
97
|
+
};
|
|
98
|
+
var MONEY_DOCUMENT_TABLES = [
|
|
99
|
+
"invoices",
|
|
100
|
+
"invoice_line_items",
|
|
101
|
+
"contracts",
|
|
102
|
+
"contract_line_items"
|
|
103
|
+
];
|
|
104
|
+
var OLD_MONEY_COLUMNS = [
|
|
105
|
+
["tax_settings", "money_model"],
|
|
106
|
+
["tax_settings", "money_migration_id"],
|
|
107
|
+
["invoice_line_items", "legacy_amount"],
|
|
108
|
+
["contract_line_items", "legacy_amount"]
|
|
109
|
+
];
|
|
110
|
+
var OLD_MONEY_TABLES = ["commerce_money_migration_documents"];
|
|
111
|
+
function rowsOf(result) {
|
|
112
|
+
return result.rows;
|
|
113
|
+
}
|
|
114
|
+
async function tableExists(db, table) {
|
|
115
|
+
return !db.tableExists || await db.tableExists(table);
|
|
116
|
+
}
|
|
117
|
+
async function findOldMoneySchema(db) {
|
|
118
|
+
const found = [];
|
|
119
|
+
if (isPostgresDatabase(db)) {
|
|
120
|
+
const result = await db.query(`SELECT table_name AS "table", column_name AS "column"
|
|
121
|
+
FROM information_schema.columns
|
|
122
|
+
WHERE table_schema = ANY (current_schemas(false))
|
|
123
|
+
AND ((table_name = 'tax_settings'
|
|
124
|
+
AND column_name IN ('money_model', 'money_migration_id'))
|
|
125
|
+
OR (table_name IN ('invoice_line_items', 'contract_line_items')
|
|
126
|
+
AND column_name = 'legacy_amount'))
|
|
127
|
+
UNION ALL
|
|
128
|
+
SELECT table_name AS "table", '' AS "column"
|
|
129
|
+
FROM information_schema.tables
|
|
130
|
+
WHERE table_schema = ANY (current_schemas(false))
|
|
131
|
+
AND table_name = 'commerce_money_migration_documents'`);
|
|
132
|
+
for (const row of rowsOf(result)) found.push(row.column ? `${row.table}.${row.column}` : String(row.table));
|
|
133
|
+
} else {
|
|
134
|
+
for (const [table, column] of OLD_MONEY_COLUMNS) {
|
|
135
|
+
if (!await tableExists(db, table)) continue;
|
|
136
|
+
if ((await db.query(`SELECT name FROM pragma_table_info('${table}') WHERE name = ?`, column)).rows.length > 0) found.push(`${table}.${column}`);
|
|
137
|
+
}
|
|
138
|
+
for (const table of OLD_MONEY_TABLES) if ((await db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", table)).rows.length > 0) found.push(table);
|
|
139
|
+
}
|
|
140
|
+
return found.sort();
|
|
141
|
+
}
|
|
142
|
+
async function withPermissionIndependentScan(db, scan) {
|
|
143
|
+
if (!isPostgresDatabase(db)) return scan(db);
|
|
144
|
+
const url = rootMoneyDatabase(db).url;
|
|
145
|
+
if (typeof url !== "string" || !url) throw new Error("Commerce money detection on PostgreSQL needs a database with a connection URL, to scan on a connection of its own.");
|
|
146
|
+
const { getDatabase } = await import("@happyvertical/sql");
|
|
147
|
+
const own = await getDatabase({
|
|
148
|
+
type: "postgres",
|
|
149
|
+
url,
|
|
150
|
+
cache: false,
|
|
151
|
+
max: 1
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
if (!own.transaction) throw new Error("Commerce money detection needs transaction support.");
|
|
155
|
+
return await own.transaction(async (tx) => {
|
|
156
|
+
await tx.query("SELECT set_config('smrt.system_context', 'true', true)");
|
|
157
|
+
return scan(tx);
|
|
158
|
+
});
|
|
159
|
+
} finally {
|
|
160
|
+
await own.close?.();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function oldFormatDocumentQueries(db, tenantFilter) {
|
|
164
|
+
const queries = [];
|
|
165
|
+
const issued = (table, extra) => `SELECT DISTINCT CAST(tenant_id AS TEXT) AS "tenantId" FROM ${table}
|
|
166
|
+
WHERE tenant_id IS NOT NULL
|
|
167
|
+
AND (status <> 'draft'${extra})
|
|
168
|
+
AND COALESCE(CAST(money_snapshot_id AS TEXT), '') = ''${tenantFilter}`;
|
|
169
|
+
const taxed = (table) => `SELECT DISTINCT CAST(tenant_id AS TEXT) AS "tenantId" FROM ${table}
|
|
170
|
+
WHERE tenant_id IS NOT NULL
|
|
171
|
+
AND tax_code_id IS NULL
|
|
172
|
+
AND tax_components_json IS NULL${tenantFilter}`;
|
|
173
|
+
if (await tableExists(db, "invoices")) queries.push(issued("invoices", " OR COALESCE(CAST(ar_journal_id AS TEXT), '') <> ''"));
|
|
174
|
+
if (await tableExists(db, "contracts")) queries.push(issued("contracts", ""));
|
|
175
|
+
for (const table of ["invoice_line_items", "contract_line_items"]) if (await tableExists(db, table)) queries.push(taxed(table));
|
|
176
|
+
return queries;
|
|
177
|
+
}
|
|
178
|
+
async function findTenantlessMoneyDocuments(db, limit) {
|
|
179
|
+
return withPermissionIndependentScan(db, async (scanDb) => {
|
|
180
|
+
const found = [];
|
|
181
|
+
for (const table of MONEY_DOCUMENT_TABLES) {
|
|
182
|
+
if (!await tableExists(scanDb, table)) continue;
|
|
183
|
+
const result = await scanDb.query(`SELECT CAST(id AS TEXT) AS id FROM ${table} WHERE tenant_id IS NULL ORDER BY id${limit ? ` LIMIT ${Math.trunc(limit)}` : ""}`);
|
|
184
|
+
for (const row of rowsOf(result)) found.push({
|
|
185
|
+
table,
|
|
186
|
+
id: String(row.id)
|
|
187
|
+
});
|
|
188
|
+
if (limit && found.length >= limit) return found.slice(0, limit);
|
|
189
|
+
}
|
|
190
|
+
return found;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function findCommerceMoneyUpgradeNeeds(db) {
|
|
194
|
+
const oldSchema = await findOldMoneySchema(db);
|
|
195
|
+
const tenants = await withPermissionIndependentScan(db, async (scanDb) => {
|
|
196
|
+
const needs = /* @__PURE__ */ new Map();
|
|
197
|
+
const hasSettings = await tableExists(scanDb, "tax_settings");
|
|
198
|
+
if (hasSettings && oldSchema.includes("tax_settings.money_model")) {
|
|
199
|
+
const markers = await scanDb.query(`SELECT CAST(tenant_id AS TEXT) AS "tenantId", money_model AS "model"
|
|
200
|
+
FROM tax_settings
|
|
201
|
+
WHERE money_model <> 'v2'`);
|
|
202
|
+
for (const row of rowsOf(markers)) needs.set(String(row.tenantId), row.model === "migrating" ? "interrupted-migration" : "legacy-marker");
|
|
203
|
+
}
|
|
204
|
+
for (const table of ["invoices", "contracts"]) {
|
|
205
|
+
if (!await tableExists(scanDb, table)) continue;
|
|
206
|
+
const orphans = await scanDb.query(hasSettings ? `SELECT DISTINCT CAST(d.tenant_id AS TEXT) AS "tenantId"
|
|
207
|
+
FROM ${table} d
|
|
208
|
+
WHERE d.tenant_id IS NOT NULL
|
|
209
|
+
AND NOT EXISTS (
|
|
210
|
+
SELECT 1 FROM tax_settings s
|
|
211
|
+
WHERE CAST(s.tenant_id AS TEXT) = CAST(d.tenant_id AS TEXT))` : `SELECT DISTINCT CAST(tenant_id AS TEXT) AS "tenantId"
|
|
212
|
+
FROM ${table} WHERE tenant_id IS NOT NULL`);
|
|
213
|
+
for (const row of rowsOf(orphans)) {
|
|
214
|
+
const tenantId = String(row.tenantId);
|
|
215
|
+
if (!needs.has(tenantId)) needs.set(tenantId, "missing-settings");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const sql of await oldFormatDocumentQueries(scanDb, "")) for (const row of rowsOf(await scanDb.query(sql))) {
|
|
219
|
+
const tenantId = String(row.tenantId);
|
|
220
|
+
if (!needs.has(tenantId)) needs.set(tenantId, "old-format-documents");
|
|
221
|
+
}
|
|
222
|
+
return [...needs].map(([tenantId, reason]) => ({
|
|
223
|
+
tenantId,
|
|
224
|
+
reason
|
|
225
|
+
})).sort((left, right) => left.tenantId < right.tenantId ? -1 : left.tenantId > right.tenantId ? 1 : 0);
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
oldSchema,
|
|
229
|
+
tenantlessDocuments: await findTenantlessMoneyDocuments(db),
|
|
230
|
+
tenants
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
var currentByUrl = /* @__PURE__ */ new Map();
|
|
234
|
+
var currentByObject = /* @__PURE__ */ new WeakMap();
|
|
235
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
236
|
+
var objectKeys = /* @__PURE__ */ new WeakMap();
|
|
237
|
+
var nextObjectKey = 0;
|
|
238
|
+
function inFlightKey(db, tenantId) {
|
|
239
|
+
const root = rootMoneyDatabase(db);
|
|
240
|
+
const url = root.url;
|
|
241
|
+
let base;
|
|
242
|
+
if (typeof url === "string" && url && !url.includes(":memory:")) base = `url:${url}`;
|
|
243
|
+
else {
|
|
244
|
+
base = objectKeys.get(root) ?? `object:${nextObjectKey++}`;
|
|
245
|
+
objectKeys.set(root, base);
|
|
246
|
+
}
|
|
247
|
+
return `${base}\0${tenantId}`;
|
|
248
|
+
}
|
|
249
|
+
async function shareInFlight(key, check) {
|
|
250
|
+
const running = inFlight.get(key);
|
|
251
|
+
if (running) return running;
|
|
252
|
+
const started = check().finally(() => inFlight.delete(key));
|
|
253
|
+
inFlight.set(key, started);
|
|
254
|
+
return started;
|
|
255
|
+
}
|
|
256
|
+
function currentState(db) {
|
|
257
|
+
const root = rootMoneyDatabase(db);
|
|
258
|
+
const url = root.url;
|
|
259
|
+
if (typeof url === "string" && url && !url.includes(":memory:")) {
|
|
260
|
+
let state2 = currentByUrl.get(url);
|
|
261
|
+
if (!state2) {
|
|
262
|
+
state2 = {
|
|
263
|
+
schema: false,
|
|
264
|
+
tenants: /* @__PURE__ */ new Set()
|
|
265
|
+
};
|
|
266
|
+
currentByUrl.set(url, state2);
|
|
267
|
+
}
|
|
268
|
+
return state2;
|
|
269
|
+
}
|
|
270
|
+
let state = currentByObject.get(root);
|
|
271
|
+
if (!state) {
|
|
272
|
+
state = {
|
|
273
|
+
schema: false,
|
|
274
|
+
tenants: /* @__PURE__ */ new Set()
|
|
275
|
+
};
|
|
276
|
+
currentByObject.set(root, state);
|
|
277
|
+
}
|
|
278
|
+
return state;
|
|
279
|
+
}
|
|
280
|
+
var upgradeScope = new AsyncLocalStorage();
|
|
281
|
+
async function withCommerceMoneyUpgradeExecutor(_tx, operation) {
|
|
282
|
+
return upgradeScope.run(true, operation);
|
|
283
|
+
}
|
|
284
|
+
async function assertCommerceMoneySchemaCurrent(db) {
|
|
285
|
+
if (upgradeScope.getStore()) return;
|
|
286
|
+
const state = currentState(db);
|
|
287
|
+
if (state.schema) return;
|
|
288
|
+
await shareInFlight(inFlightKey(db, ""), () => checkCommerceMoneySchema(db, state));
|
|
289
|
+
}
|
|
290
|
+
async function checkCommerceMoneySchema(db, state) {
|
|
291
|
+
const old = await findOldMoneySchema(db);
|
|
292
|
+
if (old.length > 0) throw new CommerceDataNeedsUpgradeError(`The database holds commerce money data from an earlier version (${old.join(", ")}).`);
|
|
293
|
+
if ((await findTenantlessMoneyDocuments(db, 1)).length > 0) throw new CommerceDataNeedsUpgradeError("The database holds commerce money documents without a tenant; the upgrade lists them to assign to a tenant or delete.");
|
|
294
|
+
state.schema = true;
|
|
295
|
+
}
|
|
296
|
+
async function assertTenantMoneyCurrent(db, tenantId) {
|
|
297
|
+
if (upgradeScope.getStore()) return;
|
|
298
|
+
const state = currentState(db);
|
|
299
|
+
if (state.tenants.has(tenantId)) return;
|
|
300
|
+
await assertCommerceMoneySchemaCurrent(db);
|
|
301
|
+
await shareInFlight(inFlightKey(db, tenantId), () => checkTenantMoney(db, tenantId, state));
|
|
302
|
+
}
|
|
303
|
+
async function checkTenantMoney(db, tenantId, state) {
|
|
304
|
+
if (state.tenants.has(tenantId)) return;
|
|
305
|
+
const stale = await withPermissionIndependentScan(db, async (scanDb) => {
|
|
306
|
+
if ((await tableExists(scanDb, "tax_settings") ? await scanDb.query("SELECT 1 AS found FROM tax_settings WHERE CAST(tenant_id AS TEXT) = ? LIMIT 1", tenantId) : { rows: [] }).rows.length === 0) {
|
|
307
|
+
for (const table of MONEY_DOCUMENT_TABLES) {
|
|
308
|
+
if (!await tableExists(scanDb, table)) continue;
|
|
309
|
+
if ((await scanDb.query(`SELECT 1 AS found FROM ${table} WHERE CAST(tenant_id AS TEXT) = ? LIMIT 1`, tenantId)).rows.length > 0) return "missing-settings";
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
for (const sql of await oldFormatDocumentQueries(scanDb, " AND CAST(tenant_id AS TEXT) = ?")) if ((await scanDb.query(`${sql} LIMIT 1`, tenantId)).rows.length > 0) return "old-format-documents";
|
|
314
|
+
return null;
|
|
315
|
+
});
|
|
316
|
+
if (stale === "old-format-documents") throw new CommerceDataNeedsUpgradeError(`Tenant ${tenantId} has commerce documents in an earlier version's money format.`, tenantId);
|
|
317
|
+
if (stale === "missing-settings") throw new CommerceDataNeedsUpgradeError(`Tenant ${tenantId} has commerce documents but no tenant tax settings.`, tenantId);
|
|
318
|
+
state.tenants.add(tenantId);
|
|
319
|
+
}
|
|
320
|
+
async function assertHydratedMoneyRowCurrent(row) {
|
|
321
|
+
if (upgradeScope.getStore()) return;
|
|
322
|
+
if (row.tenantId) await assertTenantMoneyCurrent(row.db, row.tenantId);
|
|
323
|
+
else await assertCommerceMoneySchemaCurrent(row.db);
|
|
324
|
+
}
|
|
325
|
+
//#endregion
|
|
326
|
+
export { assertCommerceMoneySchemaCurrent as a, detect_exports as c, withCommerceMoneyUpgradeExecutor as d, DOCUMENT_MONEY_READ_REBIND as f, withDocumentMoneyTransaction as g, withDocumentMoneyObjectTransaction as h, OLD_MONEY_TABLES as i, findCommerceMoneyUpgradeNeeds as l, lockDocumentMoneyParents as m, CommerceDataNeedsUpgradeError as n, assertTenantMoneyCurrent as o, DOCUMENT_MONEY_WRITE as p, OLD_MONEY_COLUMNS as r, commerceMoneyUpgradeCommand as s, COMMERCE_DATA_NEEDS_UPGRADE as t, findOldMoneySchema as u };
|
|
327
|
+
|
|
328
|
+
//# sourceMappingURL=detect-CY9ufaFb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect-CY9ufaFb.js","names":["state"],"sources":["../../src/document-money-internal.ts","../../src/upgrade/detect.ts"],"sourcesContent":["/** Internal capabilities shared by commerce document-money persistence. */\n\nimport type { DatabaseInterface } from '@happyvertical/smrt-core/migrations';\nimport { MoneyValidationError } from './money.js';\n\nconst activeMoneyTransactions = new WeakSet<object>();\n/** Each open money transaction executor and the root database it came from. */\nconst transactionRoots = new WeakMap<object, DatabaseInterface>();\n\n/** @internal Whether `db` is an executor opened by a money transaction. */\nexport function isMoneyTransaction(db: DatabaseInterface): boolean {\n return activeMoneyTransactions.has(db as object);\n}\n\n/**\n * @internal The root database behind a money transaction executor, or `db`\n * itself. Lets per-database caches survive the per-transaction executor.\n */\nexport function rootMoneyDatabase(db: DatabaseInterface): DatabaseInterface {\n return transactionRoots.get(db as object) ?? db;\n}\n\n/**\n * Capability required by derived money rows. This module is intentionally not\n * exported from the package root; aggregate persistence owns every mutation.\n */\nexport const DOCUMENT_MONEY_WRITE = Symbol('commerce.documentMoneyWrite');\n\nexport type DocumentMoneyWriteCapability = typeof DOCUMENT_MONEY_WRITE;\n\n/** Internal capability for returning a locked snapshot on its root executor. */\nexport const DOCUMENT_MONEY_READ_REBIND = Symbol(\n 'commerce.documentMoneyReadRebind',\n);\n\nexport type DocumentMoneyReadRebindCapability =\n typeof DOCUMENT_MONEY_READ_REBIND;\n\nexport interface MoneyParentLockCollection {\n readonly tableName: string;\n query(\n sql: string,\n params: unknown[],\n options: { allowRawOnTenantScoped: true },\n ): Promise<unknown[]>;\n}\nexport interface MoneyParentLockTarget {\n id: string;\n tenantId: string;\n collection: MoneyParentLockCollection;\n}\n\nfunction compareCodeUnits(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\nfunction quoteIdentifier(identifier: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Unsafe money parent table identifier: ${identifier}`,\n );\n }\n return `\"${identifier}\"`;\n}\n\nfunction assertIdentifier(value: string, label: string): void {\n if (\n typeof value !== 'string' ||\n value.trim() !== value ||\n value.length === 0\n ) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `${label} must be a nonempty identifier`,\n );\n }\n}\n/** Run aggregate work on one transaction-bound database executor. */\nexport async function withDocumentMoneyTransaction<T>(\n db: DatabaseInterface,\n operation: (tx: DatabaseInterface) => Promise<T>,\n): Promise<T> {\n if (activeMoneyTransactions.has(db as object)) return operation(db);\n if (!db.transaction) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n 'Document money persistence requires transaction support',\n );\n }\n return db.transaction(async (tx) => {\n activeMoneyTransactions.add(tx as object);\n transactionRoots.set(tx as object, rootMoneyDatabase(db));\n try {\n return await operation(tx);\n } finally {\n activeMoneyTransactions.delete(tx as object);\n }\n });\n}\n\ninterface DocumentMoneyTransactionalObject {\n readonly db: DatabaseInterface;\n withTransaction<T>(operation: (bound: this) => Promise<T>): Promise<T>;\n}\n\n/**\n * Run aggregate work through the model's root transaction so rejected writes\n * restore the instance's persistence revision before callers retry it.\n */\nexport async function withDocumentMoneyObjectTransaction<\n TObject extends DocumentMoneyTransactionalObject,\n TResult,\n>(\n input: TObject,\n operation: (bound: TObject, tx: DatabaseInterface) => Promise<TResult>,\n): Promise<TResult> {\n if (activeMoneyTransactions.has(input.db as object)) {\n return operation(input, input.db);\n }\n const root = rootMoneyDatabase(input.db);\n return input.withTransaction(async (bound) => {\n const tx = bound.db;\n activeMoneyTransactions.add(tx as object);\n if (tx !== root) transactionRoots.set(tx as object, root);\n try {\n return await operation(bound as TObject, tx);\n } finally {\n activeMoneyTransactions.delete(tx as object);\n }\n });\n}\n\n/**\n * Serialize aggregate writers by touching each tenant-owned parent first.\n * Parents are locked sequentially in stable code-unit ID order. Callers must\n * re-read authoritative parent and child state on the same executor afterward.\n */\nexport async function lockDocumentMoneyParents(\n targets: readonly MoneyParentLockTarget[],\n): Promise<void> {\n const sorted = [...targets].sort(\n (left, right) =>\n compareCodeUnits(left.id, right.id) ||\n compareCodeUnits(left.collection.tableName, right.collection.tableName),\n );\n const identities = new Set<string>();\n for (const target of sorted) {\n assertIdentifier(target.id, 'Money parent id');\n assertIdentifier(target.tenantId, 'Money parent tenant id');\n const identity = `${target.collection.tableName}\\u0000${target.id}`;\n if (identities.has(identity)) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Duplicate money parent lock target: ${target.id}`,\n );\n }\n identities.add(identity);\n const table = quoteIdentifier(target.collection.tableName);\n const locked = await target.collection.query(\n `UPDATE ${table}\n SET \"updated_at\" = \"updated_at\"\n WHERE \"id\" = ? AND \"tenant_id\" = ?\n RETURNING \"id\"`,\n [target.id, target.tenantId],\n { allowRawOnTenantScoped: true },\n );\n if (locked.length !== 1) {\n throw new MoneyValidationError(\n 'INVALID_MONEY_INPUT',\n `Money parent ${target.id} was not found in tenant ${target.tenantId}`,\n );\n }\n }\n}\n","/**\n * Old-format commerce money data detection (#4021).\n *\n * The runtime knows one money model. Data written by an earlier version is\n * refused here, never read or computed: this module only answers \"does this\n * database or tenant still hold old-format money data?\" and raises\n * {@link CommerceDataNeedsUpgradeError} with the upgrade command.\n *\n * Old-format data has one of two shapes:\n *\n * - **Pre-#4021 schema.** A database last migrated by a version with the\n * per-tenant money switch still has its columns (`tax_settings.money_model`,\n * `tax_settings.money_migration_id`, `*_line_items.legacy_amount`) or the\n * `commerce_money_migration_documents` table. The upgrade converts every tenant and\n * drops them, so their presence means the database has not been upgraded.\n * - **Pre-settings tenant.** A tenant with Invoice or Contract rows but no\n * `tax_settings` row. Its documents predate tenant tax settings.\n * - **Tenantless documents.** An Invoice or Contract row (header or line)\n * without a tenant. The current model writes none; such rows must be\n * assigned to a tenant or deleted. Like the old schema, they refuse the\n * whole database.\n * - **Old-format documents.** An issued Invoice or Contract without a money\n * snapshot, or a line with neither a tax code nor a component snapshot.\n * The current model never writes either, so they remain detectable after a\n * `db:migrate --drop-columns` has removed the old columns.\n *\n * Delete this module, the `upgrade/` directory and its call sites together\n * once no supported version can have written old-format data.\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { isPostgresDatabase } from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/smrt-core/migrations';\nimport { rootMoneyDatabase } from '../document-money-internal.js';\n\n/** The stable code on {@link CommerceDataNeedsUpgradeError}. */\nexport const COMMERCE_DATA_NEEDS_UPGRADE = 'COMMERCE_DATA_NEEDS_UPGRADE';\n\n/** The CLI command that converts old-format data, optionally for one tenant. */\nexport function commerceMoneyUpgradeCommand(tenantId?: string): string {\n return tenantId\n ? `smrt db:upgrade-commerce-money --tenant-id ${tenantId}`\n : 'smrt db:upgrade-commerce-money';\n}\n\n/** Commerce found money data written by an earlier version. */\nexport class CommerceDataNeedsUpgradeError extends Error {\n readonly code = COMMERCE_DATA_NEEDS_UPGRADE;\n\n constructor(\n readonly detail: string,\n readonly tenantId?: string,\n ) {\n super(\n `${COMMERCE_DATA_NEEDS_UPGRADE}: ${detail} Run ${commerceMoneyUpgradeCommand(tenantId)} to upgrade it.`,\n );\n this.name = 'CommerceDataNeedsUpgradeError';\n }\n}\n\n/** Header and line tables whose rows carry tenant money. */\nexport const MONEY_DOCUMENT_TABLES = [\n 'invoices',\n 'invoice_line_items',\n 'contracts',\n 'contract_line_items',\n] as const;\n\n/** Columns only a pre-#4021 schema has. The upgrade drops each one. */\nexport const OLD_MONEY_COLUMNS = [\n ['tax_settings', 'money_model'],\n ['tax_settings', 'money_migration_id'],\n ['invoice_line_items', 'legacy_amount'],\n ['contract_line_items', 'legacy_amount'],\n] as const;\n\n/** Tables only a pre-#4021 schema has. The upgrade drops each one. */\nexport const OLD_MONEY_TABLES = ['commerce_money_migration_documents'] as const;\n\nfunction rowsOf(result: { rows: unknown[] }): Array<Record<string, unknown>> {\n return result.rows as Array<Record<string, unknown>>;\n}\n\nasync function tableExists(\n db: DatabaseInterface,\n table: string,\n): Promise<boolean> {\n return !db.tableExists || (await db.tableExists(table));\n}\n\n/**\n * Every pre-#4021 column (`table.column`) and table still in the schema, in\n * one catalog query. Empty means the schema carries no old money storage.\n */\nexport async function findOldMoneySchema(\n db: DatabaseInterface,\n): Promise<string[]> {\n const found: string[] = [];\n if (isPostgresDatabase(db)) {\n const result = await db.query(\n `SELECT table_name AS \"table\", column_name AS \"column\"\n FROM information_schema.columns\n WHERE table_schema = ANY (current_schemas(false))\n AND ((table_name = 'tax_settings'\n AND column_name IN ('money_model', 'money_migration_id'))\n OR (table_name IN ('invoice_line_items', 'contract_line_items')\n AND column_name = 'legacy_amount'))\n UNION ALL\n SELECT table_name AS \"table\", '' AS \"column\"\n FROM information_schema.tables\n WHERE table_schema = ANY (current_schemas(false))\n AND table_name = 'commerce_money_migration_documents'`,\n );\n for (const row of rowsOf(result))\n found.push(row.column ? `${row.table}.${row.column}` : String(row.table));\n } else {\n for (const [table, column] of OLD_MONEY_COLUMNS) {\n if (!(await tableExists(db, table))) continue;\n const result = await db.query(\n `SELECT name FROM pragma_table_info('${table}') WHERE name = ?`,\n column,\n );\n if (result.rows.length > 0) found.push(`${table}.${column}`);\n }\n for (const table of OLD_MONEY_TABLES) {\n const result = await db.query(\n \"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?\",\n table,\n );\n if (result.rows.length > 0) found.push(table);\n }\n }\n return found.sort();\n}\n\n/**\n * Run a scan whose answer must not depend on the caller's table permissions.\n *\n * Under the PostgreSQL permission policies a principal sees only the rows it\n * may read, so a caller without invoice access would find no documents. On\n * PostgreSQL the scan therefore runs on a connection of its own, in a\n * transaction of its own that raises `smrt.system_context`: the caller's\n * connection and settings are never touched, so concurrent statements on the\n * caller's transaction keep their row policies. It sees committed rows only.\n * Callers return booleans or ids from the scan, never row data. SQLite and\n * other engines have no row policies and scan on `db` directly.\n */\nasync function withPermissionIndependentScan<T>(\n db: DatabaseInterface,\n scan: (db: DatabaseInterface) => Promise<T>,\n): Promise<T> {\n if (!isPostgresDatabase(db)) return scan(db);\n const url = (rootMoneyDatabase(db) as { url?: unknown }).url;\n if (typeof url !== 'string' || !url)\n throw new Error(\n 'Commerce money detection on PostgreSQL needs a database with a connection URL, to scan on a connection of its own.',\n );\n const { getDatabase } = await import('@happyvertical/sql');\n // Uncached: the adapter caches pools by URL, so a cached handle would be\n // the caller's own pool, and closing it would wait on the caller's open\n // transaction. A one-connection pool of our own closes on its own.\n const own = (await getDatabase({\n type: 'postgres',\n url,\n cache: false,\n max: 1,\n } as Parameters<typeof getDatabase>[0])) as unknown as DatabaseInterface & {\n close?: () => Promise<void>;\n };\n try {\n if (!own.transaction)\n throw new Error('Commerce money detection needs transaction support.');\n return await own.transaction(async (tx) => {\n await tx.query(\"SELECT set_config('smrt.system_context', 'true', true)\");\n return scan(tx);\n });\n } finally {\n await own.close?.();\n }\n}\n\n/** Whether any Invoice or Contract row, header or line, exists for a tenant. */\nexport async function tenantHasMoneyDocuments(\n db: DatabaseInterface,\n tenantId: string,\n): Promise<boolean> {\n return withPermissionIndependentScan(db, async (scanDb) => {\n for (const table of MONEY_DOCUMENT_TABLES) {\n if (!(await tableExists(scanDb, table))) continue;\n const result = await scanDb.query(\n `SELECT 1 AS found FROM ${table} WHERE CAST(tenant_id AS TEXT) = ? LIMIT 1`,\n tenantId,\n );\n if (result.rows.length > 0) return true;\n }\n return false;\n });\n}\n\n/**\n * SQL selecting the tenant ids that hold old-format documents: an issued\n * header without a money snapshot, or a line with neither a tax code nor a\n * component snapshot (the current model writes one on every line it\n * computes). `tenantFilter` is appended to each branch's WHERE clause.\n */\nasync function oldFormatDocumentQueries(\n db: DatabaseInterface,\n tenantFilter: string,\n): Promise<string[]> {\n const queries: string[] = [];\n const issued = (table: string, extra: string) =>\n `SELECT DISTINCT CAST(tenant_id AS TEXT) AS \"tenantId\" FROM ${table}\n WHERE tenant_id IS NOT NULL\n AND (status <> 'draft'${extra})\n AND COALESCE(CAST(money_snapshot_id AS TEXT), '') = ''${tenantFilter}`;\n const taxed = (table: string) =>\n `SELECT DISTINCT CAST(tenant_id AS TEXT) AS \"tenantId\" FROM ${table}\n WHERE tenant_id IS NOT NULL\n AND tax_code_id IS NULL\n AND tax_components_json IS NULL${tenantFilter}`;\n if (await tableExists(db, 'invoices'))\n queries.push(\n issued('invoices', \" OR COALESCE(CAST(ar_journal_id AS TEXT), '') <> ''\"),\n );\n if (await tableExists(db, 'contracts')) queries.push(issued('contracts', ''));\n for (const table of ['invoice_line_items', 'contract_line_items'])\n if (await tableExists(db, table)) queries.push(taxed(table));\n return queries;\n}\n\n/** A money row without a tenant: the current model never writes one. */\nexport interface TenantlessMoneyDocument {\n table: (typeof MONEY_DOCUMENT_TABLES)[number];\n id: string;\n}\n\n/**\n * @internal Operator-only: every Invoice or Contract row, header or line,\n * that has no tenant. It ignores the caller's row permissions and returns\n * document ids, so it is not exported from the package entry.\n */\nexport async function findTenantlessMoneyDocuments(\n db: DatabaseInterface,\n limit?: number,\n): Promise<TenantlessMoneyDocument[]> {\n return withPermissionIndependentScan(db, async (scanDb) => {\n const found: TenantlessMoneyDocument[] = [];\n for (const table of MONEY_DOCUMENT_TABLES) {\n if (!(await tableExists(scanDb, table))) continue;\n const result = await scanDb.query(\n `SELECT CAST(id AS TEXT) AS id FROM ${table} WHERE tenant_id IS NULL ORDER BY id${\n limit ? ` LIMIT ${Math.trunc(limit)}` : ''\n }`,\n );\n for (const row of rowsOf(result))\n found.push({ table, id: String(row.id) });\n if (limit && found.length >= limit) return found.slice(0, limit);\n }\n return found;\n });\n}\n\n/** Why one tenant needs the upgrade. */\nexport interface TenantMoneyUpgradeNeed {\n tenantId: string;\n /**\n * - `legacy-marker`: the pre-#4021 switch keeps the tenant's documents in\n * the old tax-inclusive line format.\n * - `interrupted-migration`: a pre-#4021 tenant migration never finished.\n * - `missing-settings`: documents but no tenant tax settings.\n * - `old-format-documents`: an issued document without a money snapshot,\n * or a line with neither a tax code nor a component snapshot.\n */\n reason:\n | 'legacy-marker'\n | 'interrupted-migration'\n | 'missing-settings'\n | 'old-format-documents';\n}\n\n/** The full database view: old schema storage and every tenant to convert. */\nexport interface CommerceMoneyUpgradeStatus {\n oldSchema: string[];\n /** Money rows without a tenant; assign or delete them (not converted). */\n tenantlessDocuments: TenantlessMoneyDocument[];\n tenants: TenantMoneyUpgradeNeed[];\n}\n\n/**\n * Operator-only: scan the whole database for old-format money data. For\n * `smrt db:status`, `smrt app doctor` and the upgrade command; never wire it\n * into a principal-facing surface, because it ignores the caller's row\n * permissions and returns tenant and document ids. The runtime uses the\n * cached per-tenant check {@link assertTenantMoneyCurrent} instead.\n */\nexport async function findCommerceMoneyUpgradeNeeds(\n db: DatabaseInterface,\n): Promise<CommerceMoneyUpgradeStatus> {\n const oldSchema = await findOldMoneySchema(db);\n const tenants = await withPermissionIndependentScan(db, async (scanDb) => {\n const needs = new Map<string, TenantMoneyUpgradeNeed['reason']>();\n const hasSettings = await tableExists(scanDb, 'tax_settings');\n if (hasSettings && oldSchema.includes('tax_settings.money_model')) {\n const markers = await scanDb.query(\n `SELECT CAST(tenant_id AS TEXT) AS \"tenantId\", money_model AS \"model\"\n FROM tax_settings\n WHERE money_model <> 'v2'`,\n );\n for (const row of rowsOf(markers))\n needs.set(\n String(row.tenantId),\n row.model === 'migrating' ? 'interrupted-migration' : 'legacy-marker',\n );\n }\n for (const table of ['invoices', 'contracts'] as const) {\n if (!(await tableExists(scanDb, table))) continue;\n const orphans = await scanDb.query(\n hasSettings\n ? `SELECT DISTINCT CAST(d.tenant_id AS TEXT) AS \"tenantId\"\n FROM ${table} d\n WHERE d.tenant_id IS NOT NULL\n AND NOT EXISTS (\n SELECT 1 FROM tax_settings s\n WHERE CAST(s.tenant_id AS TEXT) = CAST(d.tenant_id AS TEXT))`\n : `SELECT DISTINCT CAST(tenant_id AS TEXT) AS \"tenantId\"\n FROM ${table} WHERE tenant_id IS NOT NULL`,\n );\n for (const row of rowsOf(orphans)) {\n const tenantId = String(row.tenantId);\n if (!needs.has(tenantId)) needs.set(tenantId, 'missing-settings');\n }\n }\n for (const sql of await oldFormatDocumentQueries(scanDb, '')) {\n for (const row of rowsOf(await scanDb.query(sql))) {\n const tenantId = String(row.tenantId);\n if (!needs.has(tenantId)) needs.set(tenantId, 'old-format-documents');\n }\n }\n return [...needs]\n .map(([tenantId, reason]) => ({ tenantId, reason }))\n .sort((left, right) =>\n left.tenantId < right.tenantId\n ? -1\n : left.tenantId > right.tenantId\n ? 1\n : 0,\n );\n });\n const tenantlessDocuments = await findTenantlessMoneyDocuments(db);\n return { oldSchema, tenantlessDocuments, tenants };\n}\n\n/**\n * Databases and tenants already proven current. A database is keyed by its\n * connection URL when it has a durable one (so every request executor of a\n * PostgreSQL app shares one entry), else by its root executor object.\n */\nconst currentByUrl = new Map<\n string,\n { schema: boolean; tenants: Set<string> }\n>();\nconst currentByObject = new WeakMap<\n object,\n { schema: boolean; tenants: Set<string> }\n>();\n/**\n * Checks in flight, by database key and tenant (`''` for the schema check):\n * concurrent callers share one scan instead of racing their own.\n */\nconst inFlight = new Map<string, Promise<void>>();\nconst objectKeys = new WeakMap<object, string>();\nlet nextObjectKey = 0;\n\nfunction inFlightKey(db: DatabaseInterface, tenantId: string): string {\n const root = rootMoneyDatabase(db);\n const url = (root as { url?: unknown }).url;\n let base: string;\n if (typeof url === 'string' && url && !url.includes(':memory:'))\n base = `url:${url}`;\n else {\n base = objectKeys.get(root as object) ?? `object:${nextObjectKey++}`;\n objectKeys.set(root as object, base);\n }\n return `${base}\\0${tenantId}`;\n}\n\nasync function shareInFlight(\n key: string,\n check: () => Promise<void>,\n): Promise<void> {\n const running = inFlight.get(key);\n if (running) return running;\n const started = check().finally(() => inFlight.delete(key));\n inFlight.set(key, started);\n return started;\n}\n\nfunction currentState(db: DatabaseInterface): {\n schema: boolean;\n tenants: Set<string>;\n} {\n const root = rootMoneyDatabase(db);\n const url = (root as { url?: unknown }).url;\n if (typeof url === 'string' && url && !url.includes(':memory:')) {\n let state = currentByUrl.get(url);\n if (!state) {\n state = { schema: false, tenants: new Set() };\n currentByUrl.set(url, state);\n }\n return state;\n }\n let state = currentByObject.get(root as object);\n if (!state) {\n state = { schema: false, tenants: new Set() };\n currentByObject.set(root as object, state);\n }\n return state;\n}\n\n/**\n * @internal Forget every passing result, as a process restart would. For\n * tests that rewrite a database into an old format mid-process.\n */\nexport function forgetCommerceMoneyChecks(): void {\n currentByUrl.clear();\n}\n\n/** Set while an upgrade converts old-format data in this async context. */\nconst upgradeScope = new AsyncLocalStorage<true>();\n\n/**\n * @internal Run the upgrade's own work past the old-format checks, so it can\n * seed settings and recompute converted drafts through the runtime. Only the\n * async context of `operation` is exempt; `tx` documents the executor.\n */\nexport async function withCommerceMoneyUpgradeExecutor<T>(\n _tx: DatabaseInterface,\n operation: () => Promise<T>,\n): Promise<T> {\n return upgradeScope.run(true, operation);\n}\n\n/**\n * Refuse a database whose schema still carries pre-#4021 money storage, or\n * that holds money documents without a tenant.\n * Only a passing result is cached, so a database upgraded while the process\n * runs is accepted on the next call.\n */\nexport async function assertCommerceMoneySchemaCurrent(\n db: DatabaseInterface,\n): Promise<void> {\n if (upgradeScope.getStore()) return;\n const state = currentState(db);\n if (state.schema) return;\n await shareInFlight(inFlightKey(db, ''), () =>\n checkCommerceMoneySchema(db, state),\n );\n}\n\nasync function checkCommerceMoneySchema(\n db: DatabaseInterface,\n state: { schema: boolean },\n): Promise<void> {\n const old = await findOldMoneySchema(db);\n if (old.length > 0)\n throw new CommerceDataNeedsUpgradeError(\n `The database holds commerce money data from an earlier version (${old.join(', ')}).`,\n );\n // The refusal names no document: the scan ignores the caller's row\n // permissions, so only the upgrade report lists identities.\n if ((await findTenantlessMoneyDocuments(db, 1)).length > 0)\n throw new CommerceDataNeedsUpgradeError(\n 'The database holds commerce money documents without a tenant; the upgrade lists them to assign to a tenant or delete.',\n );\n state.schema = true;\n}\n\n/**\n * Refuse a tenant whose money data predates the current model: first the\n * database-wide schema check, then old-format documents and documents\n * without tenant tax settings. Each database and tenant is checked once per\n * process once it passes.\n */\nexport async function assertTenantMoneyCurrent(\n db: DatabaseInterface,\n tenantId: string,\n): Promise<void> {\n if (upgradeScope.getStore()) return;\n const state = currentState(db);\n if (state.tenants.has(tenantId)) return;\n await assertCommerceMoneySchemaCurrent(db);\n await shareInFlight(inFlightKey(db, tenantId), () =>\n checkTenantMoney(db, tenantId, state),\n );\n}\n\nasync function checkTenantMoney(\n db: DatabaseInterface,\n tenantId: string,\n state: { tenants: Set<string> },\n): Promise<void> {\n if (state.tenants.has(tenantId)) return;\n const stale = await withPermissionIndependentScan(db, async (scanDb) => {\n const settings = (await tableExists(scanDb, 'tax_settings'))\n ? await scanDb.query(\n 'SELECT 1 AS found FROM tax_settings WHERE CAST(tenant_id AS TEXT) = ? LIMIT 1',\n tenantId,\n )\n : { rows: [] };\n if (settings.rows.length === 0) {\n for (const table of MONEY_DOCUMENT_TABLES) {\n if (!(await tableExists(scanDb, table))) continue;\n const found = await scanDb.query(\n `SELECT 1 AS found FROM ${table} WHERE CAST(tenant_id AS TEXT) = ? LIMIT 1`,\n tenantId,\n );\n if (found.rows.length > 0) return 'missing-settings' as const;\n }\n return null;\n }\n for (const sql of await oldFormatDocumentQueries(\n scanDb,\n ' AND CAST(tenant_id AS TEXT) = ?',\n ))\n if ((await scanDb.query(`${sql} LIMIT 1`, tenantId)).rows.length > 0)\n return 'old-format-documents' as const;\n return null;\n });\n if (stale === 'old-format-documents')\n throw new CommerceDataNeedsUpgradeError(\n `Tenant ${tenantId} has commerce documents in an earlier version's money format.`,\n tenantId,\n );\n if (stale === 'missing-settings')\n throw new CommerceDataNeedsUpgradeError(\n `Tenant ${tenantId} has commerce documents but no tenant tax settings.`,\n tenantId,\n );\n state.tenants.add(tenantId);\n}\n\n/**\n * Refuse a money document row hydrated from the database while its tenant (or,\n * for a tenantless row, the database) still holds old-format money data.\n * Models call it from `initialize()`, so every collection read is covered,\n * including generated STI subtype collections and unscoped reads.\n */\nexport async function assertHydratedMoneyRowCurrent(row: {\n readonly db: DatabaseInterface;\n readonly tenantId: string | null;\n}): Promise<void> {\n if (upgradeScope.getStore()) return;\n if (row.tenantId) await assertTenantMoneyCurrent(row.db, row.tenantId);\n else await assertCommerceMoneySchemaCurrent(row.db);\n}\n"],"mappings":"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"}
|