@happyvertical/smrt-commerce 0.57.2 → 0.57.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +130 -0
  3. package/agents/accounting-snapshot-mapper.md +11 -0
  4. package/agents/contract-billing-schedules.md +67 -0
  5. package/agents/contract-money.md +81 -0
  6. package/agents/customer-funds.md +96 -0
  7. package/agents/historical-sales.md +132 -0
  8. package/agents/money-calculator.md +36 -0
  9. package/agents/money-model.md +6 -3
  10. package/agents/observed-purchase-prices.md +37 -0
  11. package/agents/payment-allocations.md +59 -0
  12. package/agents/received-sales-contract-money.md +38 -0
  13. package/dist/accounting-snapshot.d.ts +8 -1
  14. package/dist/accounting-snapshot.d.ts.map +1 -1
  15. package/dist/allocation-integrity.d.ts.map +1 -1
  16. package/dist/billing/period-close.d.ts.map +1 -1
  17. package/dist/chunks/{ContractLineItemCollection-BZcrlrrY.js → ContractLineItemCollection-J4xtRtzE.js} +6 -6
  18. package/dist/chunks/{ContractLineItemCollection-BZcrlrrY.js.map → ContractLineItemCollection-J4xtRtzE.js.map} +1 -1
  19. package/dist/chunks/DocumentTaxCollection-C42x0YLW.js +4385 -0
  20. package/dist/chunks/DocumentTaxCollection-C42x0YLW.js.map +1 -0
  21. package/dist/chunks/HistoricalCustomerFundsCollections-B5x8gTW3.js +170 -0
  22. package/dist/chunks/HistoricalCustomerFundsCollections-B5x8gTW3.js.map +1 -0
  23. package/dist/chunks/{InvoiceCollection-cinzAqt6.js → InvoiceCollection-BIcge9Wl.js} +131 -18
  24. package/dist/chunks/InvoiceCollection-BIcge9Wl.js.map +1 -0
  25. package/dist/chunks/{TaxCodeCollection-DaBOPG9c.js → TaxCodeCollection-RWdVaheF.js} +3 -4
  26. package/dist/chunks/TaxCodeCollection-RWdVaheF.js.map +1 -0
  27. package/dist/chunks/{TaxComponentCollection-C4zxnyVu.js → TaxComponentCollection-Dt2jeWiC.js} +3 -3
  28. package/dist/chunks/{TaxComponentCollection-C4zxnyVu.js.map → TaxComponentCollection-Dt2jeWiC.js.map} +1 -1
  29. package/dist/chunks/{allocation-integrity-WmWyXhtG.js → allocation-integrity-DJ7-LY1j.js} +7 -7
  30. package/dist/chunks/{allocation-integrity-WmWyXhtG.js.map → allocation-integrity-DJ7-LY1j.js.map} +1 -1
  31. package/dist/chunks/{contract-money-EuToOghh.js → contract-money-DSRTMoXm.js} +45 -12
  32. package/dist/chunks/contract-money-DSRTMoXm.js.map +1 -0
  33. package/dist/chunks/customer-funds-internal-BQzoJXK5.js +24 -0
  34. package/dist/chunks/customer-funds-internal-BQzoJXK5.js.map +1 -0
  35. package/dist/chunks/{detect-CY9ufaFb.js → detect-DAE-_NAk.js} +5 -2
  36. package/dist/chunks/{detect-CY9ufaFb.js.map → detect-DAE-_NAk.js.map} +1 -1
  37. package/dist/chunks/historical-commerce-internal-C9ButfuH.js +18 -0
  38. package/dist/chunks/historical-commerce-internal-C9ButfuH.js.map +1 -0
  39. package/dist/chunks/{invoice-posting-C2i5dOVZ.js → invoice-posting-BeTPw3h9.js} +5 -5
  40. package/dist/chunks/invoice-posting-BeTPw3h9.js.map +1 -0
  41. package/dist/chunks/retention-accounting-Brdj9x-V.js +2323 -0
  42. package/dist/chunks/retention-accounting-Brdj9x-V.js.map +1 -0
  43. package/dist/chunks/retention-accounting-internal-CBQRvd74.js +17 -0
  44. package/dist/chunks/retention-accounting-internal-CBQRvd74.js.map +1 -0
  45. package/dist/chunks/{tenant-money-barrier-6TlqUeVj.js → tenant-money-barrier-DoF6lR8h.js} +5 -5
  46. package/dist/chunks/{tenant-money-barrier-6TlqUeVj.js.map → tenant-money-barrier-DoF6lR8h.js.map} +1 -1
  47. package/dist/chunks/types-Bbm0Xflc.js.map +1 -1
  48. package/dist/collections/DocumentTaxCollection.d.ts +4 -1
  49. package/dist/collections/DocumentTaxCollection.d.ts.map +1 -1
  50. package/dist/collections/HistoricalCustomerFundsCollections.d.ts +10 -0
  51. package/dist/collections/HistoricalCustomerFundsCollections.d.ts.map +1 -0
  52. package/dist/collections/HistoricalSalesLineCollection.d.ts +6 -0
  53. package/dist/collections/HistoricalSalesLineCollection.d.ts.map +1 -0
  54. package/dist/collections/PaymentAllocationCollection.d.ts +2 -6
  55. package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
  56. package/dist/collections/PurchasePriceObservationCollection.d.ts +6 -0
  57. package/dist/collections/PurchasePriceObservationCollection.d.ts.map +1 -0
  58. package/dist/collections/index.d.ts +1 -0
  59. package/dist/collections/index.d.ts.map +1 -1
  60. package/dist/contract-billing-schedule.d.ts +83 -0
  61. package/dist/contract-billing-schedule.d.ts.map +1 -0
  62. package/dist/contract-money.d.ts +2 -0
  63. package/dist/contract-money.d.ts.map +1 -1
  64. package/dist/customer-funds-internal.d.ts +6 -0
  65. package/dist/customer-funds-internal.d.ts.map +1 -0
  66. package/dist/customer-funds.d.ts +108 -0
  67. package/dist/customer-funds.d.ts.map +1 -0
  68. package/dist/document-money.d.ts +11 -0
  69. package/dist/document-money.d.ts.map +1 -1
  70. package/dist/historical-commerce-internal.d.ts +3 -0
  71. package/dist/historical-commerce-internal.d.ts.map +1 -0
  72. package/dist/historical-customer-funds-types.d.ts +113 -0
  73. package/dist/historical-customer-funds-types.d.ts.map +1 -0
  74. package/dist/historical-customer-funds.d.ts +23 -0
  75. package/dist/historical-customer-funds.d.ts.map +1 -0
  76. package/dist/historical-funds-internal.d.ts +5 -0
  77. package/dist/historical-funds-internal.d.ts.map +1 -0
  78. package/dist/historical-sales-money.d.ts +22 -0
  79. package/dist/historical-sales-money.d.ts.map +1 -0
  80. package/dist/historical-sales-reports.d.ts +31 -0
  81. package/dist/historical-sales-reports.d.ts.map +1 -0
  82. package/dist/historical-sales-types.d.ts +106 -0
  83. package/dist/historical-sales-types.d.ts.map +1 -0
  84. package/dist/historical-sales-validation.d.ts +12 -0
  85. package/dist/historical-sales-validation.d.ts.map +1 -0
  86. package/dist/index.d.ts +21 -0
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +1685 -1640
  89. package/dist/index.js.map +1 -1
  90. package/dist/invoice-accounting-sync.d.ts +0 -1
  91. package/dist/invoice-accounting-sync.d.ts.map +1 -1
  92. package/dist/invoice-mcp.d.ts.map +1 -1
  93. package/dist/invoice-posting.d.ts.map +1 -1
  94. package/dist/invoice-retention.d.ts +16 -0
  95. package/dist/invoice-retention.d.ts.map +1 -0
  96. package/dist/invoices.d.ts +2 -0
  97. package/dist/invoices.d.ts.map +1 -1
  98. package/dist/invoices.js +2 -2
  99. package/dist/manifest.json +7563 -4419
  100. package/dist/models/Contract.d.ts +14 -0
  101. package/dist/models/Contract.d.ts.map +1 -1
  102. package/dist/models/DocumentTax.d.ts +2 -0
  103. package/dist/models/DocumentTax.d.ts.map +1 -1
  104. package/dist/models/HistoricalCustomerFunds.d.ts +36 -0
  105. package/dist/models/HistoricalCustomerFunds.d.ts.map +1 -0
  106. package/dist/models/HistoricalCustomerFundsAllocation.d.ts +32 -0
  107. package/dist/models/HistoricalCustomerFundsAllocation.d.ts.map +1 -0
  108. package/dist/models/HistoricalSalesLine.d.ts +29 -0
  109. package/dist/models/HistoricalSalesLine.d.ts.map +1 -0
  110. package/dist/models/Invoice.d.ts +10 -1
  111. package/dist/models/Invoice.d.ts.map +1 -1
  112. package/dist/models/Payment.d.ts +7 -1
  113. package/dist/models/Payment.d.ts.map +1 -1
  114. package/dist/models/PaymentAllocation.d.ts +3 -1
  115. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  116. package/dist/models/PaymentIntent.d.ts.map +1 -1
  117. package/dist/models/Payout.d.ts.map +1 -1
  118. package/dist/models/PurchasePriceObservation.d.ts +15 -0
  119. package/dist/models/PurchasePriceObservation.d.ts.map +1 -0
  120. package/dist/models/TaxCode.d.ts +1 -1
  121. package/dist/models/TaxCode.d.ts.map +1 -1
  122. package/dist/models/index.d.ts +1 -0
  123. package/dist/models/index.d.ts.map +1 -1
  124. package/dist/money.d.ts +5 -1
  125. package/dist/money.d.ts.map +1 -1
  126. package/dist/money.js +62 -15
  127. package/dist/money.js.map +1 -1
  128. package/dist/observed-purchase-price-internal.d.ts +3 -0
  129. package/dist/observed-purchase-price-internal.d.ts.map +1 -0
  130. package/dist/observed-purchase-price.d.ts +71 -0
  131. package/dist/observed-purchase-price.d.ts.map +1 -0
  132. package/dist/received-purchase-money.d.ts +87 -0
  133. package/dist/received-purchase-money.d.ts.map +1 -0
  134. package/dist/recipes.d.ts +4 -1
  135. package/dist/recipes.d.ts.map +1 -1
  136. package/dist/retention-accounting-internal.d.ts +3 -0
  137. package/dist/retention-accounting-internal.d.ts.map +1 -0
  138. package/dist/retention-accounting.d.ts +164 -0
  139. package/dist/retention-accounting.d.ts.map +1 -0
  140. package/dist/retention-money.d.ts +43 -0
  141. package/dist/retention-money.d.ts.map +1 -0
  142. package/dist/smrt-knowledge.json +1866 -113
  143. package/dist/tax/cookbook.d.ts.map +1 -1
  144. package/dist/tax-summary.d.ts +1 -0
  145. package/dist/tax-summary.d.ts.map +1 -1
  146. package/dist/types/index.d.ts +20 -1
  147. package/dist/types/index.d.ts.map +1 -1
  148. package/package.json +16 -16
  149. package/dist/chunks/CustomerCollection-BIPH2ZUx.js +0 -99
  150. package/dist/chunks/CustomerCollection-BIPH2ZUx.js.map +0 -1
  151. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js +0 -1666
  152. package/dist/chunks/DocumentTaxCollection-DDzlywMW.js.map +0 -1
  153. package/dist/chunks/InvoiceCollection-cinzAqt6.js.map +0 -1
  154. package/dist/chunks/TaxCodeCollection-DaBOPG9c.js.map +0 -1
  155. package/dist/chunks/VendorCollection-D6ZcUYy8.js +0 -100
  156. package/dist/chunks/VendorCollection-D6ZcUYy8.js.map +0 -1
  157. package/dist/chunks/calculations-C9daQLsh.js +0 -82
  158. package/dist/chunks/calculations-C9daQLsh.js.map +0 -1
  159. package/dist/chunks/contract-money-EuToOghh.js.map +0 -1
  160. package/dist/chunks/document-money-DV5t4vlK.js +0 -1186
  161. package/dist/chunks/document-money-DV5t4vlK.js.map +0 -1
  162. package/dist/chunks/invoice-posting-C2i5dOVZ.js.map +0 -1
@@ -0,0 +1,24 @@
1
+ //#region src/customer-funds-internal.ts
2
+ var writers = /* @__PURE__ */ new WeakSet();
3
+ async function withCustomerFundsWrite(object, action) {
4
+ if (writers.has(object)) return action();
5
+ writers.add(object);
6
+ try {
7
+ return await action();
8
+ } finally {
9
+ writers.delete(object);
10
+ }
11
+ }
12
+ function assertCustomerFundsWrite(object) {
13
+ if (!writers.has(object)) throw new Error("Customer funds require the public customer-funds command");
14
+ }
15
+ function customerFundsState(value) {
16
+ if (value === "" || value === null || value === void 0) return null;
17
+ const state = typeof value === "string" ? JSON.parse(value) : value;
18
+ if (!state || state.version !== 1 || !["receipt", "refund"].includes(state.kind)) throw new Error("Invalid persisted customer-funds evidence");
19
+ return state;
20
+ }
21
+ //#endregion
22
+ export { customerFundsState as n, withCustomerFundsWrite as r, assertCustomerFundsWrite as t };
23
+
24
+ //# sourceMappingURL=customer-funds-internal-BQzoJXK5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customer-funds-internal-BQzoJXK5.js","names":[],"sources":["../../src/customer-funds-internal.ts"],"sourcesContent":["/** Internal object capabilities for customer-funds authority, never exported by the package. */\nimport type { CustomerFundsState } from './customer-funds.js';\n\nconst writers = new WeakSet<object>();\nexport async function withCustomerFundsWrite<T>(\n object: object,\n action: () => Promise<T>,\n): Promise<T> {\n if (writers.has(object)) return action();\n writers.add(object);\n try {\n return await action();\n } finally {\n writers.delete(object);\n }\n}\nexport function assertCustomerFundsWrite(object: object): void {\n if (!writers.has(object))\n throw new Error('Customer funds require the public customer-funds command');\n}\nexport function customerFundsState(value: unknown): CustomerFundsState | null {\n if (value === '' || value === null || value === undefined) return null;\n const state = typeof value === 'string' ? JSON.parse(value) : value;\n if (\n !state ||\n state.version !== 1 ||\n !['receipt', 'refund'].includes(state.kind)\n )\n throw new Error('Invalid persisted customer-funds evidence');\n return state as CustomerFundsState;\n}\n"],"mappings":";AAGA,IAAM,0BAAU,IAAI,QAAgB;AACpC,eAAsB,uBACpB,QACA,QACY;CACZ,IAAI,QAAQ,IAAI,MAAM,GAAG,OAAO,OAAO;CACvC,QAAQ,IAAI,MAAM;CAClB,IAAI;EACF,OAAO,MAAM,OAAO;CACtB,UAAE;EACA,QAAQ,OAAO,MAAM;CACvB;AACF;AACO,SAAS,yBAAyB,QAAsB;CAC7D,IAAI,CAAC,QAAQ,IAAI,MAAM,GACrB,MAAM,IAAI,MAAM,0DAA0D;AAC9E;AACO,SAAS,mBAAmB,OAA2C;CAC5E,IAAI,UAAU,MAAM,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAClE,MAAM,QAAQ,OAAO,UAAU,WAAW,KAAK,MAAM,KAAK,IAAI;CAC9D,IACE,CAAC,SACD,MAAM,YAAY,KAClB,CAAC,CAAC,WAAW,QAAQ,CAAA,CAAE,SAAS,MAAM,IAAI,GAE1C,MAAM,IAAI,MAAM,2CAA2C;CAC7D,OAAO;AACT"}
@@ -5,6 +5,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
5
5
  //#region src/document-money-internal.ts
6
6
  var activeMoneyTransactions = /* @__PURE__ */ new WeakSet();
7
7
  var transactionRoots = /* @__PURE__ */ new WeakMap();
8
+ function isMoneyTransaction(db) {
9
+ return activeMoneyTransactions.has(db);
10
+ }
8
11
  function rootMoneyDatabase(db) {
9
12
  return transactionRoots.get(db) ?? db;
10
13
  }
@@ -323,6 +326,6 @@ async function assertHydratedMoneyRowCurrent(row) {
323
326
  else await assertCommerceMoneySchemaCurrent(row.db);
324
327
  }
325
328
  //#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 };
329
+ export { withDocumentMoneyTransaction as _, assertCommerceMoneySchemaCurrent as a, detect_exports as c, withCommerceMoneyUpgradeExecutor as d, DOCUMENT_MONEY_READ_REBIND as f, withDocumentMoneyObjectTransaction as g, lockDocumentMoneyParents as h, OLD_MONEY_TABLES as i, findCommerceMoneyUpgradeNeeds as l, isMoneyTransaction 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
330
 
328
- //# sourceMappingURL=detect-CY9ufaFb.js.map
331
+ //# sourceMappingURL=detect-DAE-_NAk.js.map
@@ -1 +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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"}
1
+ {"version":3,"file":"detect-DAE-_NAk.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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"}
@@ -0,0 +1,18 @@
1
+ //#region src/historical-commerce-internal.ts
2
+ var writers = /* @__PURE__ */ new WeakSet();
3
+ async function withHistoricalCommerceWrite(object, apply) {
4
+ if (writers.has(object)) return apply();
5
+ writers.add(object);
6
+ try {
7
+ return await apply();
8
+ } finally {
9
+ writers.delete(object);
10
+ }
11
+ }
12
+ function assertHistoricalCommerceWrite(object) {
13
+ if (!writers.has(object)) throw new Error("Historical financial records require their public historical Commerce command");
14
+ }
15
+ //#endregion
16
+ export { withHistoricalCommerceWrite as n, assertHistoricalCommerceWrite as t };
17
+
18
+ //# sourceMappingURL=historical-commerce-internal-C9ButfuH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"historical-commerce-internal-C9ButfuH.js","names":[],"sources":["../../src/historical-commerce-internal.ts"],"sourcesContent":["/** Command-only authority for immutable original financial source children. */\nconst writers = new WeakSet<object>();\nexport async function withHistoricalCommerceWrite<T>(\n object: object,\n apply: () => Promise<T>,\n): Promise<T> {\n if (writers.has(object)) return apply();\n writers.add(object);\n try {\n return await apply();\n } finally {\n writers.delete(object);\n }\n}\nexport function assertHistoricalCommerceWrite(object: object): void {\n if (!writers.has(object))\n throw new Error(\n 'Historical financial records require their public historical Commerce command',\n );\n}\n"],"mappings":";AACA,IAAM,0BAAU,IAAI,QAAgB;AACpC,eAAsB,4BACpB,QACA,OACY;CACZ,IAAI,QAAQ,IAAI,MAAM,GAAG,OAAO,MAAM;CACtC,QAAQ,IAAI,MAAM;CAClB,IAAI;EACF,OAAO,MAAM,MAAM;CACrB,UAAE;EACA,QAAQ,OAAO,MAAM;CACvB;AACF;AACO,SAAS,8BAA8B,QAAsB;CAClE,IAAI,CAAC,QAAQ,IAAI,MAAM,GACrB,MAAM,IAAI,MACR,+EACF;AACJ"}
@@ -1,10 +1,9 @@
1
- import { t as DocumentTaxCollection } from "./DocumentTaxCollection-DDzlywMW.js";
1
+ import { Y as InvoiceLineItemCollection, t as DocumentTaxCollection, x as lockAndReloadInvoice } from "./DocumentTaxCollection-C42x0YLW.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
- import { g as withDocumentMoneyTransaction, p as DOCUMENT_MONEY_WRITE } from "./detect-CY9ufaFb.js";
6
- import { t as lockTenantMoneyRuntime } from "./tenant-money-barrier-6TlqUeVj.js";
7
- import { a as InvoiceLineItemCollection, r as lockAndReloadInvoice } from "./document-money-DV5t4vlK.js";
5
+ import { _ as withDocumentMoneyTransaction, p as DOCUMENT_MONEY_WRITE } from "./detect-DAE-_NAk.js";
6
+ import { t as lockTenantMoneyRuntime } from "./tenant-money-barrier-DoF6lR8h.js";
8
7
  import { t as buildRevenueJournalEntries } from "./revenue-journal-LLA9Kh_i.js";
9
8
  import { getTenantId } from "@happyvertical/smrt-tenancy";
10
9
  import { ISO_4217_MINOR_UNITS } from "@happyvertical/smrt-ui/currency";
@@ -92,6 +91,7 @@ async function recognizeInvoiceRevenue(input, options) {
92
91
  assertTenantAuthority(tenantId);
93
92
  const currency = bookCurrency(options.bookCurrency);
94
93
  const { invoice } = await lockAndReloadInvoice(tx, invoiceId, tenantId);
94
+ if (invoice.moneyAuthority === "historical") throw new MoneyValidationError("INVALID_MONEY_INPUT", "Original historical revenue is retained evidence; a new revenue posting requires an explicit reviewed historical accounting contract");
95
95
  if (invoice.status === InvoiceStatus.DRAFT || !invoice.moneySnapshotId) throw new MoneyValidationError("FROZEN_DOCUMENT", "Revenue recognition requires an issued frozen invoice");
96
96
  if (invoice.currency !== currency) throw new MoneyValidationError("CURRENCY_MISMATCH", `Invoice currency ${invoice.currency} does not match book currency ${currency}`);
97
97
  const lines = await (await InvoiceLineItemCollection.create({ db: tx })).findByInvoice(invoiceId);
@@ -224,4 +224,4 @@ async function cancelInvoiceWithRevenueReversal(input, options) {
224
224
  //#endregion
225
225
  export { cancelInvoiceWithRevenueReversal, rebindJournal, recognizeInvoiceRevenue };
226
226
 
227
- //# sourceMappingURL=invoice-posting-C2i5dOVZ.js.map
227
+ //# sourceMappingURL=invoice-posting-BeTPw3h9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invoice-posting-BeTPw3h9.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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"}