@voyant-travel/finance 0.119.5

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 (294) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/dist/action-ledger-drift.d.ts +29 -0
  4. package/dist/action-ledger-drift.d.ts.map +1 -0
  5. package/dist/action-ledger-drift.js +166 -0
  6. package/dist/booking-tax.d.ts +124 -0
  7. package/dist/booking-tax.d.ts.map +1 -0
  8. package/dist/booking-tax.js +264 -0
  9. package/dist/checkout-routes.d.ts +1154 -0
  10. package/dist/checkout-routes.d.ts.map +1 -0
  11. package/dist/checkout-routes.js +116 -0
  12. package/dist/checkout-service-plan.d.ts +137 -0
  13. package/dist/checkout-service-plan.d.ts.map +1 -0
  14. package/dist/checkout-service-plan.js +119 -0
  15. package/dist/checkout-service.d.ts +9 -0
  16. package/dist/checkout-service.d.ts.map +1 -0
  17. package/dist/checkout-service.js +324 -0
  18. package/dist/checkout-validation.d.ts +1682 -0
  19. package/dist/checkout-validation.d.ts.map +1 -0
  20. package/dist/checkout-validation.js +228 -0
  21. package/dist/document-download.d.ts +3 -0
  22. package/dist/document-download.d.ts.map +1 -0
  23. package/dist/document-download.js +1 -0
  24. package/dist/fx-money.d.ts +17 -0
  25. package/dist/fx-money.d.ts.map +1 -0
  26. package/dist/fx-money.js +194 -0
  27. package/dist/index.d.ts +65 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +108 -0
  30. package/dist/invoice-fx.d.ts +134 -0
  31. package/dist/invoice-fx.d.ts.map +1 -0
  32. package/dist/invoice-fx.js +240 -0
  33. package/dist/invoice-number-errors.d.ts +2 -0
  34. package/dist/invoice-number-errors.d.ts.map +1 -0
  35. package/dist/invoice-number-errors.js +58 -0
  36. package/dist/markets-ref.d.ts +149 -0
  37. package/dist/markets-ref.d.ts.map +1 -0
  38. package/dist/markets-ref.js +17 -0
  39. package/dist/payment-link.d.ts +23 -0
  40. package/dist/payment-link.d.ts.map +1 -0
  41. package/dist/payment-link.js +30 -0
  42. package/dist/payment-policy.d.ts +113 -0
  43. package/dist/payment-policy.d.ts.map +1 -0
  44. package/dist/payment-policy.js +193 -0
  45. package/dist/route-runtime.d.ts +22 -0
  46. package/dist/route-runtime.d.ts.map +1 -0
  47. package/dist/route-runtime.js +18 -0
  48. package/dist/routes-action-ledger.d.ts +181 -0
  49. package/dist/routes-action-ledger.d.ts.map +1 -0
  50. package/dist/routes-action-ledger.js +142 -0
  51. package/dist/routes-booking-billing.d.ts +852 -0
  52. package/dist/routes-booking-billing.d.ts.map +1 -0
  53. package/dist/routes-booking-billing.js +223 -0
  54. package/dist/routes-booking-create.d.ts +3 -0
  55. package/dist/routes-booking-create.d.ts.map +1 -0
  56. package/dist/routes-booking-create.js +194 -0
  57. package/dist/routes-booking-reads.d.ts +46 -0
  58. package/dist/routes-booking-reads.d.ts.map +1 -0
  59. package/dist/routes-booking-reads.js +20 -0
  60. package/dist/routes-documents.d.ts +195 -0
  61. package/dist/routes-documents.d.ts.map +1 -0
  62. package/dist/routes-documents.js +93 -0
  63. package/dist/routes-invoice-core.d.ts +794 -0
  64. package/dist/routes-invoice-core.d.ts.map +1 -0
  65. package/dist/routes-invoice-core.js +238 -0
  66. package/dist/routes-invoice-documents.d.ts +401 -0
  67. package/dist/routes-invoice-documents.d.ts.map +1 -0
  68. package/dist/routes-invoice-documents.js +91 -0
  69. package/dist/routes-invoice-issue.d.ts +384 -0
  70. package/dist/routes-invoice-issue.d.ts.map +1 -0
  71. package/dist/routes-invoice-issue.js +208 -0
  72. package/dist/routes-payment-processing.d.ts +1193 -0
  73. package/dist/routes-payment-processing.d.ts.map +1 -0
  74. package/dist/routes-payment-processing.js +238 -0
  75. package/dist/routes-payments.d.ts +309 -0
  76. package/dist/routes-payments.d.ts.map +1 -0
  77. package/dist/routes-payments.js +94 -0
  78. package/dist/routes-public.d.ts +1948 -0
  79. package/dist/routes-public.d.ts.map +1 -0
  80. package/dist/routes-public.js +275 -0
  81. package/dist/routes-reference-data.d.ts +977 -0
  82. package/dist/routes-reference-data.d.ts.map +1 -0
  83. package/dist/routes-reference-data.js +191 -0
  84. package/dist/routes-reports.d.ts +344 -0
  85. package/dist/routes-reports.d.ts.map +1 -0
  86. package/dist/routes-reports.js +93 -0
  87. package/dist/routes-runtime.d.ts +71 -0
  88. package/dist/routes-runtime.d.ts.map +1 -0
  89. package/dist/routes-runtime.js +59 -0
  90. package/dist/routes-settlement.d.ts +67 -0
  91. package/dist/routes-settlement.d.ts.map +1 -0
  92. package/dist/routes-settlement.js +23 -0
  93. package/dist/routes-shared.d.ts +35 -0
  94. package/dist/routes-shared.d.ts.map +1 -0
  95. package/dist/routes-shared.js +10 -0
  96. package/dist/routes-supplier-invoices.d.ts +778 -0
  97. package/dist/routes-supplier-invoices.d.ts.map +1 -0
  98. package/dist/routes-supplier-invoices.js +159 -0
  99. package/dist/routes-vouchers.d.ts +228 -0
  100. package/dist/routes-vouchers.d.ts.map +1 -0
  101. package/dist/routes-vouchers.js +54 -0
  102. package/dist/routes.d.ts +5577 -0
  103. package/dist/routes.d.ts.map +1 -0
  104. package/dist/routes.js +44 -0
  105. package/dist/schema/booking-billing.d.ts +1006 -0
  106. package/dist/schema/booking-billing.d.ts.map +1 -0
  107. package/dist/schema/booking-billing.js +106 -0
  108. package/dist/schema/enums.d.ts +48 -0
  109. package/dist/schema/enums.d.ts.map +1 -0
  110. package/dist/schema/enums.js +237 -0
  111. package/dist/schema/invoice-documents.d.ts +1245 -0
  112. package/dist/schema/invoice-documents.d.ts.map +1 -0
  113. package/dist/schema/invoice-documents.js +140 -0
  114. package/dist/schema/payment-instruments.d.ts +418 -0
  115. package/dist/schema/payment-instruments.d.ts.map +1 -0
  116. package/dist/schema/payment-instruments.js +45 -0
  117. package/dist/schema/payment-processing.d.ts +563 -0
  118. package/dist/schema/payment-processing.d.ts.map +1 -0
  119. package/dist/schema/payment-processing.js +65 -0
  120. package/dist/schema/payment-sessions.d.ts +728 -0
  121. package/dist/schema/payment-sessions.d.ts.map +1 -0
  122. package/dist/schema/payment-sessions.js +79 -0
  123. package/dist/schema/receivables.d.ts +1474 -0
  124. package/dist/schema/receivables.d.ts.map +1 -0
  125. package/dist/schema/receivables.js +179 -0
  126. package/dist/schema/relations.d.ts +82 -0
  127. package/dist/schema/relations.d.ts.map +1 -0
  128. package/dist/schema/relations.js +144 -0
  129. package/dist/schema/supplier-invoices.d.ts +1619 -0
  130. package/dist/schema/supplier-invoices.d.ts.map +1 -0
  131. package/dist/schema/supplier-invoices.js +228 -0
  132. package/dist/schema/tax.d.ts +712 -0
  133. package/dist/schema/tax.d.ts.map +1 -0
  134. package/dist/schema/tax.js +98 -0
  135. package/dist/schema/vouchers.d.ts +444 -0
  136. package/dist/schema/vouchers.d.ts.map +1 -0
  137. package/dist/schema/vouchers.js +64 -0
  138. package/dist/schema.d.ts +12 -0
  139. package/dist/schema.d.ts.map +1 -0
  140. package/dist/schema.js +11 -0
  141. package/dist/service-accountant-shares.d.ts +106 -0
  142. package/dist/service-accountant-shares.d.ts.map +1 -0
  143. package/dist/service-accountant-shares.js +331 -0
  144. package/dist/service-action-ledger-accounting.d.ts +104 -0
  145. package/dist/service-action-ledger-accounting.d.ts.map +1 -0
  146. package/dist/service-action-ledger-accounting.js +386 -0
  147. package/dist/service-action-ledger-booking-payments.d.ts +48 -0
  148. package/dist/service-action-ledger-booking-payments.d.ts.map +1 -0
  149. package/dist/service-action-ledger-booking-payments.js +178 -0
  150. package/dist/service-action-ledger-bookings.d.ts +44 -0
  151. package/dist/service-action-ledger-bookings.d.ts.map +1 -0
  152. package/dist/service-action-ledger-bookings.js +81 -0
  153. package/dist/service-action-ledger-payment-authorizations.d.ts +48 -0
  154. package/dist/service-action-ledger-payment-authorizations.d.ts.map +1 -0
  155. package/dist/service-action-ledger-payment-authorizations.js +209 -0
  156. package/dist/service-action-ledger-payment-sessions.d.ts +83 -0
  157. package/dist/service-action-ledger-payment-sessions.d.ts.map +1 -0
  158. package/dist/service-action-ledger-payment-sessions.js +294 -0
  159. package/dist/service-action-ledger-supplier-invoices.d.ts +27 -0
  160. package/dist/service-action-ledger-supplier-invoices.d.ts.map +1 -0
  161. package/dist/service-action-ledger-supplier-invoices.js +111 -0
  162. package/dist/service-action-ledger-supplier-payments.d.ts +21 -0
  163. package/dist/service-action-ledger-supplier-payments.d.ts.map +1 -0
  164. package/dist/service-action-ledger-supplier-payments.js +97 -0
  165. package/dist/service-action-ledger.d.ts +7 -0
  166. package/dist/service-action-ledger.d.ts.map +1 -0
  167. package/dist/service-action-ledger.js +6 -0
  168. package/dist/service-aggregates.d.ts +96 -0
  169. package/dist/service-aggregates.d.ts.map +1 -0
  170. package/dist/service-aggregates.js +294 -0
  171. package/dist/service-booking-billing.d.ts +2322 -0
  172. package/dist/service-booking-billing.d.ts.map +1 -0
  173. package/dist/service-booking-billing.js +8 -0
  174. package/dist/service-booking-create.d.ts +410 -0
  175. package/dist/service-booking-create.d.ts.map +1 -0
  176. package/dist/service-booking-create.js +1256 -0
  177. package/dist/service-booking-guarantees.d.ts +725 -0
  178. package/dist/service-booking-guarantees.d.ts.map +1 -0
  179. package/dist/service-booking-guarantees.js +153 -0
  180. package/dist/service-booking-item-billing.d.ts +1062 -0
  181. package/dist/service-booking-item-billing.d.ts.map +1 -0
  182. package/dist/service-booking-item-billing.js +77 -0
  183. package/dist/service-booking-payment-schedules.d.ts +557 -0
  184. package/dist/service-booking-payment-schedules.d.ts.map +1 -0
  185. package/dist/service-booking-payment-schedules.js +372 -0
  186. package/dist/service-bookings-dual-create.d.ts +308 -0
  187. package/dist/service-bookings-dual-create.d.ts.map +1 -0
  188. package/dist/service-bookings-dual-create.js +131 -0
  189. package/dist/service-boundary-sql.d.ts +6 -0
  190. package/dist/service-boundary-sql.d.ts.map +1 -0
  191. package/dist/service-boundary-sql.js +15 -0
  192. package/dist/service-cost-categories.d.ts +26 -0
  193. package/dist/service-cost-categories.d.ts.map +1 -0
  194. package/dist/service-cost-categories.js +76 -0
  195. package/dist/service-documents.d.ts +80 -0
  196. package/dist/service-documents.d.ts.map +1 -0
  197. package/dist/service-documents.js +228 -0
  198. package/dist/service-invoice-artifacts.d.ts +246 -0
  199. package/dist/service-invoice-artifacts.d.ts.map +1 -0
  200. package/dist/service-invoice-artifacts.js +277 -0
  201. package/dist/service-invoice-core.d.ts +405 -0
  202. package/dist/service-invoice-core.d.ts.map +1 -0
  203. package/dist/service-invoice-core.js +290 -0
  204. package/dist/service-invoice-credit-notes.d.ts +973 -0
  205. package/dist/service-invoice-credit-notes.d.ts.map +1 -0
  206. package/dist/service-invoice-credit-notes.js +142 -0
  207. package/dist/service-invoice-from-booking.d.ts +41 -0
  208. package/dist/service-invoice-from-booking.d.ts.map +1 -0
  209. package/dist/service-invoice-from-booking.js +267 -0
  210. package/dist/service-invoice-line-items.d.ts +432 -0
  211. package/dist/service-invoice-line-items.d.ts.map +1 -0
  212. package/dist/service-invoice-line-items.js +102 -0
  213. package/dist/service-invoice-numbering.d.ts +227 -0
  214. package/dist/service-invoice-numbering.d.ts.map +1 -0
  215. package/dist/service-invoice-numbering.js +260 -0
  216. package/dist/service-invoice-payments.d.ts +673 -0
  217. package/dist/service-invoice-payments.d.ts.map +1 -0
  218. package/dist/service-invoice-payments.js +398 -0
  219. package/dist/service-invoices.d.ts +2501 -0
  220. package/dist/service-invoices.d.ts.map +1 -0
  221. package/dist/service-invoices.js +12 -0
  222. package/dist/service-issue.d.ts +207 -0
  223. package/dist/service-issue.d.ts.map +1 -0
  224. package/dist/service-issue.js +431 -0
  225. package/dist/service-payment-authorizations.d.ts +164 -0
  226. package/dist/service-payment-authorizations.d.ts.map +1 -0
  227. package/dist/service-payment-authorizations.js +227 -0
  228. package/dist/service-payment-instruments.d.ts +116 -0
  229. package/dist/service-payment-instruments.d.ts.map +1 -0
  230. package/dist/service-payment-instruments.js +99 -0
  231. package/dist/service-payment-processing.d.ts +676 -0
  232. package/dist/service-payment-processing.d.ts.map +1 -0
  233. package/dist/service-payment-processing.js +10 -0
  234. package/dist/service-payment-session-completion.d.ts +48 -0
  235. package/dist/service-payment-session-completion.d.ts.map +1 -0
  236. package/dist/service-payment-session-completion.js +238 -0
  237. package/dist/service-payment-sessions.d.ts +361 -0
  238. package/dist/service-payment-sessions.d.ts.map +1 -0
  239. package/dist/service-payment-sessions.js +280 -0
  240. package/dist/service-profitability.d.ts +114 -0
  241. package/dist/service-profitability.d.ts.map +1 -0
  242. package/dist/service-profitability.js +794 -0
  243. package/dist/service-public.d.ts +553 -0
  244. package/dist/service-public.d.ts.map +1 -0
  245. package/dist/service-public.js +583 -0
  246. package/dist/service-reference-data.d.ts +272 -0
  247. package/dist/service-reference-data.d.ts.map +1 -0
  248. package/dist/service-reference-data.js +280 -0
  249. package/dist/service-rendition-wait.d.ts +38 -0
  250. package/dist/service-rendition-wait.d.ts.map +1 -0
  251. package/dist/service-rendition-wait.js +67 -0
  252. package/dist/service-reports.d.ts +37 -0
  253. package/dist/service-reports.d.ts.map +1 -0
  254. package/dist/service-reports.js +62 -0
  255. package/dist/service-settlement.d.ts +46 -0
  256. package/dist/service-settlement.d.ts.map +1 -0
  257. package/dist/service-settlement.js +185 -0
  258. package/dist/service-shared.d.ts +541 -0
  259. package/dist/service-shared.d.ts.map +1 -0
  260. package/dist/service-shared.js +764 -0
  261. package/dist/service-supplier-invoices.d.ts +871 -0
  262. package/dist/service-supplier-invoices.d.ts.map +1 -0
  263. package/dist/service-supplier-invoices.js +744 -0
  264. package/dist/service-supplier-payments.d.ts +69 -0
  265. package/dist/service-supplier-payments.d.ts.map +1 -0
  266. package/dist/service-supplier-payments.js +136 -0
  267. package/dist/service-vouchers-migration.d.ts +44 -0
  268. package/dist/service-vouchers-migration.d.ts.map +1 -0
  269. package/dist/service-vouchers-migration.js +148 -0
  270. package/dist/service-vouchers.d.ts +157 -0
  271. package/dist/service-vouchers.d.ts.map +1 -0
  272. package/dist/service-vouchers.js +191 -0
  273. package/dist/service.d.ts +6490 -0
  274. package/dist/service.d.ts.map +1 -0
  275. package/dist/service.js +29 -0
  276. package/dist/validation-billing.d.ts +2 -0
  277. package/dist/validation-billing.d.ts.map +1 -0
  278. package/dist/validation-billing.js +1 -0
  279. package/dist/validation-payments.d.ts +2 -0
  280. package/dist/validation-payments.d.ts.map +1 -0
  281. package/dist/validation-payments.js +1 -0
  282. package/dist/validation-public.d.ts +2 -0
  283. package/dist/validation-public.d.ts.map +1 -0
  284. package/dist/validation-public.js +1 -0
  285. package/dist/validation-shared.d.ts +2 -0
  286. package/dist/validation-shared.d.ts.map +1 -0
  287. package/dist/validation-shared.js +1 -0
  288. package/dist/validation-vouchers.d.ts +2 -0
  289. package/dist/validation-vouchers.d.ts.map +1 -0
  290. package/dist/validation-vouchers.js +1 -0
  291. package/dist/validation.d.ts +2 -0
  292. package/dist/validation.d.ts.map +1 -0
  293. package/dist/validation.js +1 -0
  294. package/package.json +121 -0
@@ -0,0 +1,764 @@
1
+ // agent-quality: file-size exception -- owner: finance; compatibility exports and shared service helpers stay co-located while the domain-operation service modules are split out.
2
+ export { appendActionLedgerMutation, } from "@voyant-travel/action-ledger";
3
+ export { actionMutationDetails } from "@voyant-travel/action-ledger/schema";
4
+ export { bookingItems, bookings } from "@voyant-travel/bookings/schema";
5
+ export { newId } from "@voyant-travel/db/lib/typeid";
6
+ export { renderStructuredTemplate } from "@voyant-travel/utils/template-renderer";
7
+ export { and, asc, desc, eq, gt, gte, ilike, inArray, isNotNull, lte, ne, or, sql, } from "drizzle-orm";
8
+ export { resolveFxMoneyBaseAmount } from "./fx-money.js";
9
+ export { isInvoiceNumberUniqueConstraintError } from "./invoice-number-errors.js";
10
+ export { bookingGuarantees, bookingItemCommissions, bookingItemTaxLines, bookingPaymentSchedules, creditNoteLineItems, creditNotes, financeNotes, invoiceAttachments, invoiceExternalRefs, invoiceLineItems, invoiceNumberSeries, invoiceRenditions, invoices, invoiceTemplates, paymentAuthorizations, paymentCaptures, paymentInstruments, paymentSessions, payments, supplierInvoices, supplierPayments, taxClasses, taxPolicyProfiles, taxPolicyRules, taxRegimes, } from "./schema.js";
11
+ export { recomputeSupplierInvoiceBalance } from "./service-supplier-invoices.js";
12
+ import { actionMutationDetails } from "@voyant-travel/action-ledger/schema";
13
+ import { bookings } from "@voyant-travel/bookings/schema";
14
+ import { renderStructuredTemplate } from "@voyant-travel/utils/template-renderer";
15
+ import { and, desc, eq, gt, ne, sql } from "drizzle-orm";
16
+ import { resolveFxMoneyBaseAmount } from "./fx-money.js";
17
+ import { bookingPaymentSchedules, creditNotes, invoices, payments, supplierInvoices, supplierPayments, } from "./schema.js";
18
+ export { buildBookingCreateRejectedActionLedgerInput, buildBookingCreateSucceededActionLedgerInput, buildBookingGuaranteeCreateActionLedgerInput, buildBookingGuaranteeDeleteActionLedgerInput, buildBookingGuaranteeUpdateActionLedgerInput, buildBookingPaymentScheduleCreateActionLedgerInput, buildBookingPaymentScheduleDeleteActionLedgerInput, buildBookingPaymentScheduleUpdateActionLedgerInput, buildCreditNoteCreationActionLedgerInput, buildCreditNoteLineItemCreateActionLedgerInput, buildCreditNoteUpdateActionLedgerInput, buildInvoiceDeleteActionLedgerInput, buildInvoiceIssuedActionLedgerInput, buildInvoiceLineItemCreateActionLedgerInput, buildInvoiceLineItemDeleteActionLedgerInput, buildInvoiceLineItemUpdateActionLedgerInput, buildInvoiceUpdateActionLedgerInput, buildPaymentAuthorizationCreateActionLedgerInput, buildPaymentAuthorizationDeleteActionLedgerInput, buildPaymentAuthorizationUpdateActionLedgerInput, buildPaymentCaptureCreateActionLedgerInput, buildPaymentCaptureDeleteActionLedgerInput, buildPaymentCaptureUpdateActionLedgerInput, buildPaymentDeleteActionLedgerInput, buildPaymentInstrumentCreateActionLedgerInput, buildPaymentInstrumentDeleteActionLedgerInput, buildPaymentInstrumentUpdateActionLedgerInput, buildPaymentSessionCancelledActionLedgerInput, buildPaymentSessionCompletionActionLedgerInput, buildPaymentSessionCreateActionLedgerInput, buildPaymentSessionExpiredActionLedgerInput, buildPaymentSessionFailedActionLedgerInput, buildPaymentSessionRequiresRedirectActionLedgerInput, buildPaymentSessionUpdateActionLedgerInput, buildPaymentUpdateActionLedgerInput, buildRecordPaymentActionLedgerInput, buildSupplierPaymentCreateActionLedgerInput, buildSupplierPaymentUpdateActionLedgerInput, } from "./service-action-ledger.js";
19
+ export class PaymentValidationError extends Error {
20
+ status;
21
+ code;
22
+ details;
23
+ constructor(message, details, options = {}) {
24
+ super(message);
25
+ this.name = "PaymentValidationError";
26
+ this.status = options.status ?? 400;
27
+ this.code = options.code ?? "invalid_request";
28
+ this.details = details;
29
+ }
30
+ }
31
+ export class InvoiceNumberAllocationError extends Error {
32
+ code;
33
+ scope;
34
+ seriesId;
35
+ constructor(code, details) {
36
+ super(code);
37
+ this.name = "InvoiceNumberAllocationError";
38
+ this.code = code;
39
+ this.scope = details.scope;
40
+ this.seriesId = details.seriesId;
41
+ }
42
+ }
43
+ export class ExternalInvoiceNumberSeriesCollisionError extends Error {
44
+ code = "external_invoice_number_series_code_conflict";
45
+ seriesCode;
46
+ provider;
47
+ scope;
48
+ existingProvider;
49
+ existingScope;
50
+ constructor(details) {
51
+ super(`Invoice number series code "${details.seriesCode}" already belongs to ${details.existingProvider ?? "a local series"} in scope "${details.existingScope}"`);
52
+ this.name = "ExternalInvoiceNumberSeriesCollisionError";
53
+ this.seriesCode = details.seriesCode;
54
+ this.provider = details.provider;
55
+ this.scope = details.scope;
56
+ this.existingProvider = details.existingProvider;
57
+ this.existingScope = details.existingScope;
58
+ }
59
+ }
60
+ export class InvoiceNumberConflictError extends Error {
61
+ code = "invoice_number_conflict";
62
+ invoiceNumber;
63
+ constructor(invoiceNumber) {
64
+ super("Invoice number already exists");
65
+ this.name = "InvoiceNumberConflictError";
66
+ this.invoiceNumber = invoiceNumber;
67
+ }
68
+ }
69
+ export class InvoiceFromBookingValidationError extends Error {
70
+ status = 400;
71
+ code = "invalid_invoice_from_booking";
72
+ details;
73
+ constructor(message, details) {
74
+ super(message);
75
+ this.name = "InvoiceFromBookingValidationError";
76
+ this.details = details;
77
+ }
78
+ }
79
+ export class InvoiceLineItemsPersistenceError extends Error {
80
+ code = "invoice_line_items_not_persisted";
81
+ invoiceId;
82
+ expectedCount;
83
+ actualCount;
84
+ constructor(invoiceId, expectedCount, actualCount) {
85
+ super("Invoice line items were not persisted");
86
+ this.name = "InvoiceLineItemsPersistenceError";
87
+ this.invoiceId = invoiceId;
88
+ this.expectedCount = expectedCount;
89
+ this.actualCount = actualCount;
90
+ }
91
+ }
92
+ export const PAYMENT_SCHEDULE_LINE_LABELS = {
93
+ deposit: "Deposit",
94
+ installment: "Installment",
95
+ balance: "Balance",
96
+ hold: "Hold",
97
+ other: "Payment schedule",
98
+ };
99
+ export function bookingItemToInvoiceLine(item, taxes, sortOrder) {
100
+ const quantity = Math.max(item.quantity, 1);
101
+ const totalCents = item.totalSellAmountCents ?? (item.unitSellAmountCents ?? 0) * Math.max(item.quantity, 1);
102
+ const firstTaxWithRate = taxes.find((tax) => tax.scope !== "withheld" && tax.rateBasisPoints != null);
103
+ return {
104
+ bookingItemId: item.id,
105
+ bookingPaymentScheduleId: null,
106
+ description: renderBookingItemInvoiceLineDescription(item),
107
+ quantity: item.quantity,
108
+ unitPriceCents: item.unitSellAmountCents ??
109
+ (item.totalSellAmountCents !== null && item.totalSellAmountCents !== undefined
110
+ ? Math.floor(item.totalSellAmountCents / quantity)
111
+ : 0),
112
+ totalCents,
113
+ taxAmountCents: 0,
114
+ taxRate: firstTaxWithRate?.rateBasisPoints != null
115
+ ? Math.round(firstTaxWithRate.rateBasisPoints / 100)
116
+ : null,
117
+ sortOrder,
118
+ };
119
+ }
120
+ export function renderBookingItemInvoiceLineDescription(item) {
121
+ const base = resolveBookingItemDisplayName(item) ?? item.title;
122
+ const dates = formatInvoiceLineDateRange(resolveBookingItemStartDate(item), resolveBookingItemEndDate(item));
123
+ return dates ? `${base} | ${dates}` : base;
124
+ }
125
+ export function bookingPaymentScheduleToInvoiceLine(booking, schedule, item, descriptionFormat = "schedule_first") {
126
+ const label = PAYMENT_SCHEDULE_LINE_LABELS[schedule.scheduleType];
127
+ const percent = getPaymentSchedulePercent(booking, schedule);
128
+ const head = percent != null && percent < 100 ? `${label} ${percent}%` : label;
129
+ const base = resolveBookingItemDisplayName(item) ?? `booking ${booking.bookingNumber}`;
130
+ const dates = formatInvoiceLineDateRange(item ? (resolveBookingItemStartDate(item) ?? booking.startDate) : booking.startDate, item ? (resolveBookingItemEndDate(item) ?? booking.endDate) : booking.endDate);
131
+ return {
132
+ bookingItemId: schedule.bookingItemId ?? null,
133
+ bookingPaymentScheduleId: schedule.id,
134
+ description: renderPaymentScheduleLineDescription({ base, dates, head, descriptionFormat }),
135
+ quantity: 1,
136
+ unitPriceCents: schedule.amountCents,
137
+ totalCents: schedule.amountCents,
138
+ taxAmountCents: 0,
139
+ taxRate: null,
140
+ sortOrder: 0,
141
+ };
142
+ }
143
+ export function renderPaymentScheduleLineDescription(input) {
144
+ switch (input.descriptionFormat) {
145
+ case "product_only":
146
+ return input.dates ? `${input.base} | ${input.dates}` : input.base;
147
+ case "product_first":
148
+ return input.dates
149
+ ? `${input.base} - ${input.head} | ${input.dates}`
150
+ : `${input.base} - ${input.head}`;
151
+ case "schedule_first":
152
+ return input.dates
153
+ ? `${input.head} ${input.base} | ${input.dates}`
154
+ : `${input.head} ${input.base}`;
155
+ }
156
+ }
157
+ export function resolvePaymentScheduleDisplayItem(schedule, items) {
158
+ if (schedule.bookingItemId) {
159
+ return items.find((item) => item.id === schedule.bookingItemId);
160
+ }
161
+ const namedItems = items.filter((item) => resolveBookingItemDisplayName(item));
162
+ return [...(namedItems.length > 0 ? namedItems : items)].sort(compareBookingItemsForScheduleDisplay)[0];
163
+ }
164
+ export function resolveBookingItemDisplayName(item) {
165
+ return (item?.productNameSnapshot?.trim() || item?.productName?.trim() || item?.title?.trim() || null);
166
+ }
167
+ export function resolveBookingItemStartDate(item) {
168
+ return item.startDate ?? item.serviceDate ?? item.startsAt;
169
+ }
170
+ export function resolveBookingItemEndDate(item) {
171
+ return item.endDate ?? item.endsAt ?? item.serviceDate ?? resolveBookingItemStartDate(item);
172
+ }
173
+ export function compareBookingItemsForScheduleDisplay(left, right) {
174
+ return (compareNullableStrings(resolveBookingItemDateSortKey(left), resolveBookingItemDateSortKey(right)) ||
175
+ compareNullableStrings(resolveBookingItemDisplayName(left), resolveBookingItemDisplayName(right)) ||
176
+ left.id.localeCompare(right.id));
177
+ }
178
+ export function resolveBookingItemDateSortKey(item) {
179
+ return (toDateOnly(resolveBookingItemStartDate(item)) ?? toDateOnly(resolveBookingItemEndDate(item)));
180
+ }
181
+ export function compareNullableStrings(left, right) {
182
+ if (left && right)
183
+ return left.localeCompare(right);
184
+ if (left)
185
+ return -1;
186
+ if (right)
187
+ return 1;
188
+ return 0;
189
+ }
190
+ export function getPaymentSchedulePercent(booking, schedule) {
191
+ if (!booking.sellAmountCents || booking.sellAmountCents <= 0)
192
+ return null;
193
+ return Math.round((schedule.amountCents / booking.sellAmountCents) * 100);
194
+ }
195
+ export function formatInvoiceLineDateRange(startDate, endDate) {
196
+ const start = toDateOnly(startDate);
197
+ const end = toDateOnly(endDate);
198
+ if (!start)
199
+ return null;
200
+ if (!end || end === start)
201
+ return start;
202
+ return `${start} - ${end}`;
203
+ }
204
+ export function toDateOnly(value) {
205
+ if (!value)
206
+ return null;
207
+ if (value instanceof Date)
208
+ return value.toISOString().slice(0, 10);
209
+ return value.slice(0, 10);
210
+ }
211
+ export function invoiceFromBookingOverrideLineItems(lineItems) {
212
+ return lineItems.map((line, sortOrder) => {
213
+ const lineSubtotalCents = line.quantity * line.unitAmountCents;
214
+ const taxAmountCents = line.taxAmountCents ??
215
+ (line.taxRateBps != null ? Math.round((lineSubtotalCents * line.taxRateBps) / 10_000) : 0);
216
+ return {
217
+ bookingItemId: null,
218
+ bookingPaymentScheduleId: null,
219
+ description: line.description,
220
+ quantity: line.quantity,
221
+ unitPriceCents: line.unitAmountCents,
222
+ totalCents: lineSubtotalCents,
223
+ taxAmountCents,
224
+ taxRate: line.taxRateBps != null ? Math.round(line.taxRateBps / 100) : null,
225
+ sortOrder,
226
+ };
227
+ });
228
+ }
229
+ export async function resolveInvoiceLineDescriptions(lineItems, context) {
230
+ if (!context.descriptionResolver)
231
+ return lineItems;
232
+ const scheduleItem = context.paymentSchedule
233
+ ? resolvePaymentScheduleDisplayItem(context.paymentSchedule, context.items)
234
+ : undefined;
235
+ return Promise.all(lineItems.map(async (line) => ({
236
+ ...line,
237
+ description: (await context.descriptionResolver?.({
238
+ booking: context.booking,
239
+ schedule: context.paymentSchedule ?? undefined,
240
+ item: context.paymentSchedule
241
+ ? scheduleItem
242
+ : context.items.find((item) => item.id === line.bookingItemId),
243
+ line,
244
+ })) ?? line.description,
245
+ })));
246
+ }
247
+ export async function resolveInvoiceFromBookingDueDate(data, bookingData, runtime) {
248
+ if (!runtime.invoiceDueDateResolver)
249
+ return data.dueDate;
250
+ const dueDate = await runtime.invoiceDueDateResolver({
251
+ issueDate: data.issueDate,
252
+ dueDate: data.dueDate,
253
+ invoiceType: data.invoiceType ?? "invoice",
254
+ booking: bookingData.booking,
255
+ bookingPaymentSchedule: bookingData.paymentSchedule ?? bookingData.dueDatePaymentSchedule ?? undefined,
256
+ });
257
+ if (!dueDate) {
258
+ throw new InvoiceFromBookingValidationError("Invoice due date resolver returned an empty date", {
259
+ issueDate: data.issueDate,
260
+ dueDate: data.dueDate,
261
+ });
262
+ }
263
+ return dueDate;
264
+ }
265
+ export function assertInvoiceFromBookingOverrideTotals(data, totals) {
266
+ if (data.subtotalCents !== undefined && data.subtotalCents !== totals.subtotalCents) {
267
+ throw new InvoiceFromBookingValidationError("Invoice subtotal does not match line items", {
268
+ expectedSubtotalCents: totals.subtotalCents,
269
+ subtotalCents: data.subtotalCents,
270
+ });
271
+ }
272
+ if (data.taxCents !== undefined && data.taxCents !== totals.taxCents) {
273
+ throw new InvoiceFromBookingValidationError("Invoice tax does not match line items", {
274
+ expectedTaxCents: totals.taxCents,
275
+ taxCents: data.taxCents,
276
+ });
277
+ }
278
+ if (data.totalCents !== undefined && data.totalCents !== totals.totalCents) {
279
+ throw new InvoiceFromBookingValidationError("Invoice total does not match subtotal plus tax", {
280
+ expectedTotalCents: totals.totalCents,
281
+ totalCents: data.totalCents,
282
+ });
283
+ }
284
+ }
285
+ export function normalizeCurrencyCode(value) {
286
+ return value?.trim().toUpperCase() ?? null;
287
+ }
288
+ export function invoiceFromBookingExternalRefValues(invoiceId, refs) {
289
+ return refs.map((ref) => ({
290
+ invoiceId,
291
+ provider: ref.provider,
292
+ externalId: ref.externalId ?? null,
293
+ externalNumber: ref.externalNumber ?? null,
294
+ externalUrl: ref.externalUrl ?? null,
295
+ status: ref.status ?? null,
296
+ metadata: ref.metadata ?? null,
297
+ syncedAt: toTimestamp(ref.syncedAt),
298
+ syncError: ref.syncError ?? null,
299
+ }));
300
+ }
301
+ export function resolveBookingInvoiceBaseAmount(booking, invoiceCurrency, amountCents) {
302
+ if (!booking.baseCurrency)
303
+ return null;
304
+ if (invoiceCurrency === booking.baseCurrency)
305
+ return amountCents;
306
+ if (invoiceCurrency !== booking.sellCurrency || booking.baseSellAmountCents == null)
307
+ return null;
308
+ if (!booking.sellAmountCents || booking.sellAmountCents <= 0)
309
+ return booking.baseSellAmountCents;
310
+ return Math.round((amountCents / booking.sellAmountCents) * booking.baseSellAmountCents);
311
+ }
312
+ export function toTimestamp(value) {
313
+ return value ? new Date(value) : null;
314
+ }
315
+ export function toDateString(value) {
316
+ return value.toISOString().slice(0, 10);
317
+ }
318
+ export function readStringMetadata(value, key) {
319
+ if (value == null || typeof value !== "object")
320
+ return null;
321
+ const candidate = value[key];
322
+ return typeof candidate === "string" && candidate.trim() ? candidate : null;
323
+ }
324
+ export function startOfUtcDay(value) {
325
+ return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
326
+ }
327
+ export function parseDateString(value) {
328
+ return new Date(`${value}T00:00:00.000Z`);
329
+ }
330
+ export function derivePaymentSessionTarget(input) {
331
+ const explicitTarget = "target" in input ? input.target : undefined;
332
+ if (explicitTarget) {
333
+ switch (explicitTarget.type) {
334
+ case "booking":
335
+ return { targetType: "booking", targetId: explicitTarget.bookingId };
336
+ case "invoice":
337
+ return { targetType: "invoice", targetId: explicitTarget.invoiceId };
338
+ case "booking_payment_schedule":
339
+ return {
340
+ targetType: "booking_payment_schedule",
341
+ targetId: explicitTarget.bookingPaymentScheduleId,
342
+ };
343
+ case "booking_guarantee":
344
+ return {
345
+ targetType: "booking_guarantee",
346
+ targetId: explicitTarget.bookingGuaranteeId,
347
+ };
348
+ case "flight_order":
349
+ return { targetType: "flight_order", targetId: explicitTarget.flightOrderId };
350
+ case "legacy_order":
351
+ return { targetType: "order", targetId: explicitTarget.legacyOrderId };
352
+ case "program":
353
+ return { targetType: "other", targetId: explicitTarget.programId };
354
+ case "supplier_settlement":
355
+ return { targetType: "other", targetId: explicitTarget.supplierSettlementId };
356
+ case "channel_settlement":
357
+ return { targetType: "other", targetId: explicitTarget.channelSettlementId };
358
+ case "provider_reference":
359
+ return { targetType: "other", targetId: explicitTarget.reference };
360
+ }
361
+ }
362
+ const legacyOrderId = input.legacyOrderId ?? null;
363
+ if (input.targetType && input.targetType !== "other") {
364
+ return {
365
+ targetType: input.targetType,
366
+ targetId: input.targetId ??
367
+ (input.targetType === "booking"
368
+ ? input.bookingId
369
+ : input.targetType === "order"
370
+ ? legacyOrderId
371
+ : input.targetType === "invoice"
372
+ ? input.invoiceId
373
+ : input.targetType === "booking_payment_schedule"
374
+ ? input.bookingPaymentScheduleId
375
+ : input.targetType === "booking_guarantee"
376
+ ? input.bookingGuaranteeId
377
+ : input.targetId),
378
+ };
379
+ }
380
+ if (input.bookingPaymentScheduleId) {
381
+ return {
382
+ targetType: "booking_payment_schedule",
383
+ targetId: input.bookingPaymentScheduleId,
384
+ };
385
+ }
386
+ if (input.bookingGuaranteeId) {
387
+ return { targetType: "booking_guarantee", targetId: input.bookingGuaranteeId };
388
+ }
389
+ if (input.invoiceId) {
390
+ return { targetType: "invoice", targetId: input.invoiceId };
391
+ }
392
+ if (legacyOrderId) {
393
+ return { targetType: "order", targetId: legacyOrderId };
394
+ }
395
+ if (input.bookingId) {
396
+ return { targetType: "booking", targetId: input.bookingId };
397
+ }
398
+ return {
399
+ targetType: (input.targetType ?? "other"),
400
+ targetId: input.targetId ?? null,
401
+ };
402
+ }
403
+ // ============================================================================
404
+ // Invoice number allocation (transactional)
405
+ // ============================================================================
406
+ export function currentPeriodBoundary(strategy, now) {
407
+ if (strategy === "never")
408
+ return null;
409
+ if (strategy === "annual") {
410
+ return new Date(Date.UTC(now.getUTCFullYear(), 0, 1));
411
+ }
412
+ // monthly
413
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
414
+ }
415
+ export function formatNumber(prefix, separator, padLength, sequence) {
416
+ const padded = String(sequence).padStart(padLength, "0");
417
+ return `${prefix}${separator}${padded}`;
418
+ }
419
+ export function invoiceScopeForType(invoiceType) {
420
+ return invoiceType === "proforma" ? "proforma" : "invoice";
421
+ }
422
+ export function pendingExternalInvoiceNumber(scope) {
423
+ const uuid = globalThis.crypto?.randomUUID?.().replace(/-/g, "") ?? randomId();
424
+ return `PENDING-${scope.toUpperCase()}-${uuid.slice(0, 32)}`;
425
+ }
426
+ export function randomId() {
427
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`.padEnd(32, "0");
428
+ }
429
+ export function renderInvoiceBody(body, bodyFormat, variables) {
430
+ return renderStructuredTemplate(body, bodyFormat, variables);
431
+ }
432
+ export async function paginate(rowsQuery, countQuery, limit, offset) {
433
+ const [data, countResult] = await Promise.all([rowsQuery, countQuery]);
434
+ return { data, total: countResult[0]?.total ?? 0, limit, offset };
435
+ }
436
+ export function buildBookingPaymentSchedulePaidEvent(schedule, session, paymentId) {
437
+ return {
438
+ bookingId: schedule.bookingId,
439
+ bookingPaymentScheduleId: schedule.id,
440
+ paymentSessionId: session.id,
441
+ paymentId,
442
+ scheduleType: schedule.scheduleType,
443
+ amountCents: schedule.amountCents,
444
+ currency: schedule.currency,
445
+ provider: session.provider,
446
+ };
447
+ }
448
+ export function buildPaymentCompletedEvent(session) {
449
+ return {
450
+ paymentSessionId: session.id,
451
+ targetType: session.targetType,
452
+ targetId: session.targetId,
453
+ bookingId: session.bookingId,
454
+ legacyOrderId: session.orderId,
455
+ invoiceId: session.invoiceId,
456
+ bookingPaymentScheduleId: session.bookingPaymentScheduleId,
457
+ bookingGuaranteeId: session.bookingGuaranteeId,
458
+ amountCents: session.amountCents,
459
+ currency: session.currency,
460
+ provider: session.provider,
461
+ };
462
+ }
463
+ /**
464
+ * Normalize `db.execute(sql)` results across drizzle drivers.
465
+ * `drizzle-orm/postgres-js` returns rows directly (an array), while
466
+ * `drizzle-orm/node-postgres` (used by the operator starter against a
467
+ * local pg server) and `drizzle-orm/neon-serverless` return pg's
468
+ * `QueryResult<T>` wrapper with `.rows`. Casting to `Array<T>` and
469
+ * calling `.map` blows up under the wrapper shape — surface the rows
470
+ * regardless of which driver is bound.
471
+ */
472
+ export function toRows(result) {
473
+ if (Array.isArray(result))
474
+ return result;
475
+ if (result && typeof result === "object" && "rows" in result) {
476
+ const rows = result.rows;
477
+ return Array.isArray(rows) ? rows : [];
478
+ }
479
+ return [];
480
+ }
481
+ export function mapRawPayment(row) {
482
+ // Person display name: "First Last", trimmed. Falls back to null when both
483
+ // halves are missing so the UI can swap to organization or hide the field.
484
+ const personName = row.person_first_name || row.person_last_name
485
+ ? `${row.person_first_name ?? ""} ${row.person_last_name ?? ""}`.trim() || null
486
+ : null;
487
+ return {
488
+ kind: row.kind,
489
+ id: row.id,
490
+ invoiceId: row.invoice_id,
491
+ invoiceNumber: row.invoice_number,
492
+ bookingId: row.booking_id,
493
+ bookingNumber: row.booking_number,
494
+ supplierId: row.supplier_id,
495
+ supplierName: row.supplier_name,
496
+ personId: row.person_id,
497
+ personName,
498
+ organizationId: row.organization_id,
499
+ organizationName: row.organization_name,
500
+ amountCents: row.amount_cents,
501
+ currency: row.currency,
502
+ baseCurrency: row.base_currency,
503
+ baseAmountCents: row.base_amount_cents,
504
+ paymentMethod: row.payment_method,
505
+ status: row.status,
506
+ referenceNumber: row.reference_number,
507
+ paymentDate: row.payment_date,
508
+ notes: row.notes,
509
+ createdAt: row.created_at instanceof Date ? row.created_at : new Date(row.created_at),
510
+ updatedAt: row.updated_at instanceof Date ? row.updated_at : new Date(row.updated_at),
511
+ };
512
+ }
513
+ export function paymentSettlementAmountSql(invoiceCurrency) {
514
+ return sql `
515
+ coalesce(
516
+ sum(
517
+ case
518
+ when ${payments.currency} = ${invoiceCurrency} then ${payments.amountCents}
519
+ when ${payments.baseCurrency} = ${invoiceCurrency} then coalesce(${payments.baseAmountCents}, 0)
520
+ else 0
521
+ end
522
+ ),
523
+ 0
524
+ )::int
525
+ `;
526
+ }
527
+ export async function recomputeInvoiceTotalsAfterPaymentChange(tx, invoice) {
528
+ const [sumResult] = await tx
529
+ .select({ total: paymentSettlementAmountSql(invoice.currency) })
530
+ .from(payments)
531
+ .where(and(eq(payments.invoiceId, invoice.id), eq(payments.status, "completed")));
532
+ const paidCents = sumResult?.total ?? 0;
533
+ const balanceDueCents = Math.max(0, invoice.totalCents - paidCents);
534
+ let nextStatus = invoice.status;
535
+ if (invoice.status !== "void" && invoice.status !== "draft") {
536
+ if (paidCents >= invoice.totalCents && invoice.totalCents > 0) {
537
+ nextStatus = "paid";
538
+ }
539
+ else if (paidCents > 0) {
540
+ nextStatus = "partially_paid";
541
+ }
542
+ else if (invoice.status === "paid" || invoice.status === "partially_paid") {
543
+ nextStatus = "issued";
544
+ }
545
+ }
546
+ await tx
547
+ .update(invoices)
548
+ .set({ paidCents, balanceDueCents, status: nextStatus, updatedAt: new Date() })
549
+ .where(eq(invoices.id, invoice.id));
550
+ }
551
+ export async function assertInvoiceAcceptsNewPayment(db, invoice) {
552
+ if (invoice.status !== "void")
553
+ return;
554
+ let redirectInvoiceId = null;
555
+ let redirectInvoiceNumber = null;
556
+ if (invoice.invoiceType === "proforma") {
557
+ const [successor] = await db
558
+ .select({ id: invoices.id, invoiceNumber: invoices.invoiceNumber })
559
+ .from(invoices)
560
+ .where(eq(invoices.convertedFromInvoiceId, invoice.id))
561
+ .limit(1);
562
+ redirectInvoiceId = successor?.id ?? null;
563
+ redirectInvoiceNumber = successor?.invoiceNumber ?? null;
564
+ }
565
+ throw new PaymentValidationError(redirectInvoiceId
566
+ ? `This proforma was converted to invoice ${redirectInvoiceNumber ?? redirectInvoiceId}; record the payment there instead.`
567
+ : `Cannot record payment against voided invoice ${invoice.id}.`, {
568
+ invoiceId: invoice.id,
569
+ ...(redirectInvoiceId ? { redirectInvoiceId, redirectInvoiceNumber } : {}),
570
+ }, { status: 409, code: "invoice_void" });
571
+ }
572
+ export function assertPaymentCanSettleInvoice(invoiceCurrency, data) {
573
+ if (data.status !== "completed" || data.currency === invoiceCurrency) {
574
+ return;
575
+ }
576
+ if (data.baseCurrency === invoiceCurrency && data.baseAmountCents && data.baseAmountCents > 0) {
577
+ return;
578
+ }
579
+ throw new PaymentValidationError("Completed cross-currency payments require a base amount in the invoice currency", {
580
+ invoiceCurrency,
581
+ paymentCurrency: data.currency,
582
+ fields: ["baseCurrency", "baseAmountCents"],
583
+ });
584
+ }
585
+ export async function getPaymentFromReplayedLedgerEntry(db, actionId) {
586
+ const [detail] = await db
587
+ .select({ commandResultRef: actionMutationDetails.commandResultRef })
588
+ .from(actionMutationDetails)
589
+ .where(eq(actionMutationDetails.actionId, actionId))
590
+ .limit(1);
591
+ const paymentId = parsePaymentCommandResultRef(detail?.commandResultRef ?? null);
592
+ if (!paymentId) {
593
+ throw new Error(`Replayed payment ledger entry ${actionId} did not reference a payment`);
594
+ }
595
+ const [payment] = await db.select().from(payments).where(eq(payments.id, paymentId)).limit(1);
596
+ if (!payment) {
597
+ throw new Error(`Replayed payment ledger entry ${actionId} referenced missing payment ${paymentId}`);
598
+ }
599
+ return payment;
600
+ }
601
+ export function parsePaymentCommandResultRef(commandResultRef) {
602
+ const prefix = "payment:";
603
+ if (!commandResultRef?.startsWith(prefix))
604
+ return null;
605
+ const paymentId = commandResultRef.slice(prefix.length).trim();
606
+ return paymentId ? paymentId : null;
607
+ }
608
+ export function shouldNormalizeBaseAmount(data) {
609
+ return (data.amountCents !== undefined ||
610
+ data.currency !== undefined ||
611
+ data.baseCurrency !== undefined ||
612
+ data.baseAmountCents !== undefined ||
613
+ data.fxRateSetId !== undefined ||
614
+ data.paymentDate !== undefined);
615
+ }
616
+ export async function resolveSupplierPaymentUpdateData(db, id, data, runtime) {
617
+ const [existing] = await db
618
+ .select()
619
+ .from(supplierPayments)
620
+ .where(eq(supplierPayments.id, id))
621
+ .limit(1);
622
+ if (!existing)
623
+ return null;
624
+ if (!shouldNormalizeBaseAmount(data))
625
+ return data;
626
+ const bookingId = data.bookingId ?? existing.bookingId;
627
+ const supplierInvoiceId = data.supplierInvoiceId ?? existing.supplierInvoiceId;
628
+ let targetBaseCurrency = null;
629
+ let fallbackFxRateSetId = null;
630
+ if (bookingId) {
631
+ const [booking] = await db.select().from(bookings).where(eq(bookings.id, bookingId)).limit(1);
632
+ targetBaseCurrency = booking?.baseCurrency ?? null;
633
+ fallbackFxRateSetId = booking?.fxRateSetId ?? null;
634
+ }
635
+ else if (supplierInvoiceId) {
636
+ const [invoice] = await db
637
+ .select()
638
+ .from(supplierInvoices)
639
+ .where(eq(supplierInvoices.id, supplierInvoiceId))
640
+ .limit(1);
641
+ targetBaseCurrency = invoice?.baseCurrency ?? null;
642
+ fallbackFxRateSetId = invoice?.fxRateSetId ?? null;
643
+ }
644
+ const normalized = await resolveFxMoneyBaseAmount(db, {
645
+ amountCents: data.amountCents ?? existing.amountCents,
646
+ currency: data.currency ?? existing.currency,
647
+ baseCurrency: data.baseCurrency ?? existing.baseCurrency,
648
+ baseAmountCents: data.baseAmountCents ?? null,
649
+ fxRateSetId: data.fxRateSetId ?? existing.fxRateSetId,
650
+ }, {
651
+ ...runtime,
652
+ targetBaseCurrency,
653
+ fallbackFxRateSetId,
654
+ date: data.paymentDate ?? existing.paymentDate,
655
+ });
656
+ return {
657
+ ...data,
658
+ baseCurrency: normalized.baseCurrency ?? null,
659
+ baseAmountCents: normalized.baseAmountCents ?? null,
660
+ fxRateSetId: normalized.fxRateSetId ?? null,
661
+ };
662
+ }
663
+ export async function resolveCreditNoteUpdateData(db, id, data, runtime) {
664
+ const [existing] = await db.select().from(creditNotes).where(eq(creditNotes.id, id)).limit(1);
665
+ if (!existing)
666
+ return null;
667
+ if (!shouldNormalizeBaseAmount(data))
668
+ return data;
669
+ const [invoice] = await db
670
+ .select()
671
+ .from(invoices)
672
+ .where(eq(invoices.id, existing.invoiceId))
673
+ .limit(1);
674
+ if (!invoice)
675
+ return null;
676
+ const normalized = await resolveFxMoneyBaseAmount(db, {
677
+ amountCents: data.amountCents ?? existing.amountCents,
678
+ currency: data.currency ?? existing.currency,
679
+ baseCurrency: data.baseCurrency ?? existing.baseCurrency,
680
+ baseAmountCents: data.baseAmountCents ?? null,
681
+ fxRateSetId: data.fxRateSetId ?? existing.fxRateSetId,
682
+ }, {
683
+ ...runtime,
684
+ targetBaseCurrency: invoice.currency,
685
+ fallbackFxRateSetId: invoice.fxRateSetId ?? null,
686
+ date: new Date(),
687
+ });
688
+ return {
689
+ ...data,
690
+ baseCurrency: normalized.baseCurrency ?? null,
691
+ baseAmountCents: normalized.baseAmountCents ?? null,
692
+ fxRateSetId: normalized.fxRateSetId ?? null,
693
+ };
694
+ }
695
+ export async function resolveInvoiceForPaymentSession(db, session) {
696
+ if (session.invoiceId) {
697
+ const [invoice] = await db.select().from(invoices).where(eq(invoices.id, session.invoiceId));
698
+ return invoice ?? null;
699
+ }
700
+ if (!session.bookingPaymentScheduleId) {
701
+ return null;
702
+ }
703
+ const [schedule] = await db
704
+ .select()
705
+ .from(bookingPaymentSchedules)
706
+ .where(eq(bookingPaymentSchedules.id, session.bookingPaymentScheduleId))
707
+ .limit(1);
708
+ if (!schedule) {
709
+ return null;
710
+ }
711
+ const [invoice] = await db
712
+ .select()
713
+ .from(invoices)
714
+ .where(and(eq(invoices.bookingId, schedule.bookingId), eq(invoices.currency, schedule.currency), ne(invoices.status, "void"), gt(invoices.balanceDueCents, 0)))
715
+ .orderBy(desc(invoices.createdAt))
716
+ .limit(1);
717
+ return invoice ?? null;
718
+ }
719
+ export async function assertBookingPaymentScheduleHasPaymentCoverage(db, schedule) {
720
+ // Sum every completed payment recorded against this booking's invoices
721
+ // and convert to the schedule's currency:
722
+ // - same-currency payments contribute their `amountCents`
723
+ // - cross-currency payments contribute their `baseAmountCents` when
724
+ // `baseCurrency` matches the schedule (the BookingPaymentsSummary
725
+ // "FX equivalent" column already exposes this conversion to the
726
+ // operator, so reusing it here keeps the math consistent)
727
+ // We then subtract any *other* schedules already flagged paid in the
728
+ // same currency, so the operator can't double-count payments by
729
+ // marking multiple schedules paid when only one schedule's worth of
730
+ // money actually came in.
731
+ const paymentRows = await db
732
+ .select({
733
+ amountCents: payments.amountCents,
734
+ currency: payments.currency,
735
+ baseCurrency: payments.baseCurrency,
736
+ baseAmountCents: payments.baseAmountCents,
737
+ })
738
+ .from(payments)
739
+ .innerJoin(invoices, eq(payments.invoiceId, invoices.id))
740
+ .where(and(eq(invoices.bookingId, schedule.bookingId), eq(payments.status, "completed")));
741
+ const totalPaidInScheduleCurrency = paymentRows.reduce((sum, payment) => {
742
+ if (payment.currency === schedule.currency)
743
+ return sum + payment.amountCents;
744
+ if (payment.baseCurrency === schedule.currency && typeof payment.baseAmountCents === "number") {
745
+ return sum + payment.baseAmountCents;
746
+ }
747
+ return sum;
748
+ }, 0);
749
+ const otherPaidSchedules = await db
750
+ .select({ amountCents: bookingPaymentSchedules.amountCents })
751
+ .from(bookingPaymentSchedules)
752
+ .where(and(eq(bookingPaymentSchedules.bookingId, schedule.bookingId), eq(bookingPaymentSchedules.status, "paid"), eq(bookingPaymentSchedules.currency, schedule.currency), ne(bookingPaymentSchedules.id, schedule.id)));
753
+ const alreadyClaimed = otherPaidSchedules.reduce((sum, row) => sum + row.amountCents, 0);
754
+ const availableCoverage = totalPaidInScheduleCurrency - alreadyClaimed;
755
+ if (availableCoverage < schedule.amountCents) {
756
+ throw new PaymentValidationError("Cannot mark booking payment schedule as paid without linked completed payment coverage", {
757
+ scheduleId: schedule.id,
758
+ bookingId: schedule.bookingId,
759
+ requiredCents: schedule.amountCents,
760
+ coveredCents: availableCoverage,
761
+ currency: schedule.currency,
762
+ });
763
+ }
764
+ }