@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.
- package/LICENSE +201 -0
- package/README.md +192 -0
- package/dist/action-ledger-drift.d.ts +29 -0
- package/dist/action-ledger-drift.d.ts.map +1 -0
- package/dist/action-ledger-drift.js +166 -0
- package/dist/booking-tax.d.ts +124 -0
- package/dist/booking-tax.d.ts.map +1 -0
- package/dist/booking-tax.js +264 -0
- package/dist/checkout-routes.d.ts +1154 -0
- package/dist/checkout-routes.d.ts.map +1 -0
- package/dist/checkout-routes.js +116 -0
- package/dist/checkout-service-plan.d.ts +137 -0
- package/dist/checkout-service-plan.d.ts.map +1 -0
- package/dist/checkout-service-plan.js +119 -0
- package/dist/checkout-service.d.ts +9 -0
- package/dist/checkout-service.d.ts.map +1 -0
- package/dist/checkout-service.js +324 -0
- package/dist/checkout-validation.d.ts +1682 -0
- package/dist/checkout-validation.d.ts.map +1 -0
- package/dist/checkout-validation.js +228 -0
- package/dist/document-download.d.ts +3 -0
- package/dist/document-download.d.ts.map +1 -0
- package/dist/document-download.js +1 -0
- package/dist/fx-money.d.ts +17 -0
- package/dist/fx-money.d.ts.map +1 -0
- package/dist/fx-money.js +194 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/invoice-fx.d.ts +134 -0
- package/dist/invoice-fx.d.ts.map +1 -0
- package/dist/invoice-fx.js +240 -0
- package/dist/invoice-number-errors.d.ts +2 -0
- package/dist/invoice-number-errors.d.ts.map +1 -0
- package/dist/invoice-number-errors.js +58 -0
- package/dist/markets-ref.d.ts +149 -0
- package/dist/markets-ref.d.ts.map +1 -0
- package/dist/markets-ref.js +17 -0
- package/dist/payment-link.d.ts +23 -0
- package/dist/payment-link.d.ts.map +1 -0
- package/dist/payment-link.js +30 -0
- package/dist/payment-policy.d.ts +113 -0
- package/dist/payment-policy.d.ts.map +1 -0
- package/dist/payment-policy.js +193 -0
- package/dist/route-runtime.d.ts +22 -0
- package/dist/route-runtime.d.ts.map +1 -0
- package/dist/route-runtime.js +18 -0
- package/dist/routes-action-ledger.d.ts +181 -0
- package/dist/routes-action-ledger.d.ts.map +1 -0
- package/dist/routes-action-ledger.js +142 -0
- package/dist/routes-booking-billing.d.ts +852 -0
- package/dist/routes-booking-billing.d.ts.map +1 -0
- package/dist/routes-booking-billing.js +223 -0
- package/dist/routes-booking-create.d.ts +3 -0
- package/dist/routes-booking-create.d.ts.map +1 -0
- package/dist/routes-booking-create.js +194 -0
- package/dist/routes-booking-reads.d.ts +46 -0
- package/dist/routes-booking-reads.d.ts.map +1 -0
- package/dist/routes-booking-reads.js +20 -0
- package/dist/routes-documents.d.ts +195 -0
- package/dist/routes-documents.d.ts.map +1 -0
- package/dist/routes-documents.js +93 -0
- package/dist/routes-invoice-core.d.ts +794 -0
- package/dist/routes-invoice-core.d.ts.map +1 -0
- package/dist/routes-invoice-core.js +238 -0
- package/dist/routes-invoice-documents.d.ts +401 -0
- package/dist/routes-invoice-documents.d.ts.map +1 -0
- package/dist/routes-invoice-documents.js +91 -0
- package/dist/routes-invoice-issue.d.ts +384 -0
- package/dist/routes-invoice-issue.d.ts.map +1 -0
- package/dist/routes-invoice-issue.js +208 -0
- package/dist/routes-payment-processing.d.ts +1193 -0
- package/dist/routes-payment-processing.d.ts.map +1 -0
- package/dist/routes-payment-processing.js +238 -0
- package/dist/routes-payments.d.ts +309 -0
- package/dist/routes-payments.d.ts.map +1 -0
- package/dist/routes-payments.js +94 -0
- package/dist/routes-public.d.ts +1948 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +275 -0
- package/dist/routes-reference-data.d.ts +977 -0
- package/dist/routes-reference-data.d.ts.map +1 -0
- package/dist/routes-reference-data.js +191 -0
- package/dist/routes-reports.d.ts +344 -0
- package/dist/routes-reports.d.ts.map +1 -0
- package/dist/routes-reports.js +93 -0
- package/dist/routes-runtime.d.ts +71 -0
- package/dist/routes-runtime.d.ts.map +1 -0
- package/dist/routes-runtime.js +59 -0
- package/dist/routes-settlement.d.ts +67 -0
- package/dist/routes-settlement.d.ts.map +1 -0
- package/dist/routes-settlement.js +23 -0
- package/dist/routes-shared.d.ts +35 -0
- package/dist/routes-shared.d.ts.map +1 -0
- package/dist/routes-shared.js +10 -0
- package/dist/routes-supplier-invoices.d.ts +778 -0
- package/dist/routes-supplier-invoices.d.ts.map +1 -0
- package/dist/routes-supplier-invoices.js +159 -0
- package/dist/routes-vouchers.d.ts +228 -0
- package/dist/routes-vouchers.d.ts.map +1 -0
- package/dist/routes-vouchers.js +54 -0
- package/dist/routes.d.ts +5577 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +44 -0
- package/dist/schema/booking-billing.d.ts +1006 -0
- package/dist/schema/booking-billing.d.ts.map +1 -0
- package/dist/schema/booking-billing.js +106 -0
- package/dist/schema/enums.d.ts +48 -0
- package/dist/schema/enums.d.ts.map +1 -0
- package/dist/schema/enums.js +237 -0
- package/dist/schema/invoice-documents.d.ts +1245 -0
- package/dist/schema/invoice-documents.d.ts.map +1 -0
- package/dist/schema/invoice-documents.js +140 -0
- package/dist/schema/payment-instruments.d.ts +418 -0
- package/dist/schema/payment-instruments.d.ts.map +1 -0
- package/dist/schema/payment-instruments.js +45 -0
- package/dist/schema/payment-processing.d.ts +563 -0
- package/dist/schema/payment-processing.d.ts.map +1 -0
- package/dist/schema/payment-processing.js +65 -0
- package/dist/schema/payment-sessions.d.ts +728 -0
- package/dist/schema/payment-sessions.d.ts.map +1 -0
- package/dist/schema/payment-sessions.js +79 -0
- package/dist/schema/receivables.d.ts +1474 -0
- package/dist/schema/receivables.d.ts.map +1 -0
- package/dist/schema/receivables.js +179 -0
- package/dist/schema/relations.d.ts +82 -0
- package/dist/schema/relations.d.ts.map +1 -0
- package/dist/schema/relations.js +144 -0
- package/dist/schema/supplier-invoices.d.ts +1619 -0
- package/dist/schema/supplier-invoices.d.ts.map +1 -0
- package/dist/schema/supplier-invoices.js +228 -0
- package/dist/schema/tax.d.ts +712 -0
- package/dist/schema/tax.d.ts.map +1 -0
- package/dist/schema/tax.js +98 -0
- package/dist/schema/vouchers.d.ts +444 -0
- package/dist/schema/vouchers.d.ts.map +1 -0
- package/dist/schema/vouchers.js +64 -0
- package/dist/schema.d.ts +12 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +11 -0
- package/dist/service-accountant-shares.d.ts +106 -0
- package/dist/service-accountant-shares.d.ts.map +1 -0
- package/dist/service-accountant-shares.js +331 -0
- package/dist/service-action-ledger-accounting.d.ts +104 -0
- package/dist/service-action-ledger-accounting.d.ts.map +1 -0
- package/dist/service-action-ledger-accounting.js +386 -0
- package/dist/service-action-ledger-booking-payments.d.ts +48 -0
- package/dist/service-action-ledger-booking-payments.d.ts.map +1 -0
- package/dist/service-action-ledger-booking-payments.js +178 -0
- package/dist/service-action-ledger-bookings.d.ts +44 -0
- package/dist/service-action-ledger-bookings.d.ts.map +1 -0
- package/dist/service-action-ledger-bookings.js +81 -0
- package/dist/service-action-ledger-payment-authorizations.d.ts +48 -0
- package/dist/service-action-ledger-payment-authorizations.d.ts.map +1 -0
- package/dist/service-action-ledger-payment-authorizations.js +209 -0
- package/dist/service-action-ledger-payment-sessions.d.ts +83 -0
- package/dist/service-action-ledger-payment-sessions.d.ts.map +1 -0
- package/dist/service-action-ledger-payment-sessions.js +294 -0
- package/dist/service-action-ledger-supplier-invoices.d.ts +27 -0
- package/dist/service-action-ledger-supplier-invoices.d.ts.map +1 -0
- package/dist/service-action-ledger-supplier-invoices.js +111 -0
- package/dist/service-action-ledger-supplier-payments.d.ts +21 -0
- package/dist/service-action-ledger-supplier-payments.d.ts.map +1 -0
- package/dist/service-action-ledger-supplier-payments.js +97 -0
- package/dist/service-action-ledger.d.ts +7 -0
- package/dist/service-action-ledger.d.ts.map +1 -0
- package/dist/service-action-ledger.js +6 -0
- package/dist/service-aggregates.d.ts +96 -0
- package/dist/service-aggregates.d.ts.map +1 -0
- package/dist/service-aggregates.js +294 -0
- package/dist/service-booking-billing.d.ts +2322 -0
- package/dist/service-booking-billing.d.ts.map +1 -0
- package/dist/service-booking-billing.js +8 -0
- package/dist/service-booking-create.d.ts +410 -0
- package/dist/service-booking-create.d.ts.map +1 -0
- package/dist/service-booking-create.js +1256 -0
- package/dist/service-booking-guarantees.d.ts +725 -0
- package/dist/service-booking-guarantees.d.ts.map +1 -0
- package/dist/service-booking-guarantees.js +153 -0
- package/dist/service-booking-item-billing.d.ts +1062 -0
- package/dist/service-booking-item-billing.d.ts.map +1 -0
- package/dist/service-booking-item-billing.js +77 -0
- package/dist/service-booking-payment-schedules.d.ts +557 -0
- package/dist/service-booking-payment-schedules.d.ts.map +1 -0
- package/dist/service-booking-payment-schedules.js +372 -0
- package/dist/service-bookings-dual-create.d.ts +308 -0
- package/dist/service-bookings-dual-create.d.ts.map +1 -0
- package/dist/service-bookings-dual-create.js +131 -0
- package/dist/service-boundary-sql.d.ts +6 -0
- package/dist/service-boundary-sql.d.ts.map +1 -0
- package/dist/service-boundary-sql.js +15 -0
- package/dist/service-cost-categories.d.ts +26 -0
- package/dist/service-cost-categories.d.ts.map +1 -0
- package/dist/service-cost-categories.js +76 -0
- package/dist/service-documents.d.ts +80 -0
- package/dist/service-documents.d.ts.map +1 -0
- package/dist/service-documents.js +228 -0
- package/dist/service-invoice-artifacts.d.ts +246 -0
- package/dist/service-invoice-artifacts.d.ts.map +1 -0
- package/dist/service-invoice-artifacts.js +277 -0
- package/dist/service-invoice-core.d.ts +405 -0
- package/dist/service-invoice-core.d.ts.map +1 -0
- package/dist/service-invoice-core.js +290 -0
- package/dist/service-invoice-credit-notes.d.ts +973 -0
- package/dist/service-invoice-credit-notes.d.ts.map +1 -0
- package/dist/service-invoice-credit-notes.js +142 -0
- package/dist/service-invoice-from-booking.d.ts +41 -0
- package/dist/service-invoice-from-booking.d.ts.map +1 -0
- package/dist/service-invoice-from-booking.js +267 -0
- package/dist/service-invoice-line-items.d.ts +432 -0
- package/dist/service-invoice-line-items.d.ts.map +1 -0
- package/dist/service-invoice-line-items.js +102 -0
- package/dist/service-invoice-numbering.d.ts +227 -0
- package/dist/service-invoice-numbering.d.ts.map +1 -0
- package/dist/service-invoice-numbering.js +260 -0
- package/dist/service-invoice-payments.d.ts +673 -0
- package/dist/service-invoice-payments.d.ts.map +1 -0
- package/dist/service-invoice-payments.js +398 -0
- package/dist/service-invoices.d.ts +2501 -0
- package/dist/service-invoices.d.ts.map +1 -0
- package/dist/service-invoices.js +12 -0
- package/dist/service-issue.d.ts +207 -0
- package/dist/service-issue.d.ts.map +1 -0
- package/dist/service-issue.js +431 -0
- package/dist/service-payment-authorizations.d.ts +164 -0
- package/dist/service-payment-authorizations.d.ts.map +1 -0
- package/dist/service-payment-authorizations.js +227 -0
- package/dist/service-payment-instruments.d.ts +116 -0
- package/dist/service-payment-instruments.d.ts.map +1 -0
- package/dist/service-payment-instruments.js +99 -0
- package/dist/service-payment-processing.d.ts +676 -0
- package/dist/service-payment-processing.d.ts.map +1 -0
- package/dist/service-payment-processing.js +10 -0
- package/dist/service-payment-session-completion.d.ts +48 -0
- package/dist/service-payment-session-completion.d.ts.map +1 -0
- package/dist/service-payment-session-completion.js +238 -0
- package/dist/service-payment-sessions.d.ts +361 -0
- package/dist/service-payment-sessions.d.ts.map +1 -0
- package/dist/service-payment-sessions.js +280 -0
- package/dist/service-profitability.d.ts +114 -0
- package/dist/service-profitability.d.ts.map +1 -0
- package/dist/service-profitability.js +794 -0
- package/dist/service-public.d.ts +553 -0
- package/dist/service-public.d.ts.map +1 -0
- package/dist/service-public.js +583 -0
- package/dist/service-reference-data.d.ts +272 -0
- package/dist/service-reference-data.d.ts.map +1 -0
- package/dist/service-reference-data.js +280 -0
- package/dist/service-rendition-wait.d.ts +38 -0
- package/dist/service-rendition-wait.d.ts.map +1 -0
- package/dist/service-rendition-wait.js +67 -0
- package/dist/service-reports.d.ts +37 -0
- package/dist/service-reports.d.ts.map +1 -0
- package/dist/service-reports.js +62 -0
- package/dist/service-settlement.d.ts +46 -0
- package/dist/service-settlement.d.ts.map +1 -0
- package/dist/service-settlement.js +185 -0
- package/dist/service-shared.d.ts +541 -0
- package/dist/service-shared.d.ts.map +1 -0
- package/dist/service-shared.js +764 -0
- package/dist/service-supplier-invoices.d.ts +871 -0
- package/dist/service-supplier-invoices.d.ts.map +1 -0
- package/dist/service-supplier-invoices.js +744 -0
- package/dist/service-supplier-payments.d.ts +69 -0
- package/dist/service-supplier-payments.d.ts.map +1 -0
- package/dist/service-supplier-payments.js +136 -0
- package/dist/service-vouchers-migration.d.ts +44 -0
- package/dist/service-vouchers-migration.d.ts.map +1 -0
- package/dist/service-vouchers-migration.js +148 -0
- package/dist/service-vouchers.d.ts +157 -0
- package/dist/service-vouchers.d.ts.map +1 -0
- package/dist/service-vouchers.js +191 -0
- package/dist/service.d.ts +6490 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +29 -0
- package/dist/validation-billing.d.ts +2 -0
- package/dist/validation-billing.d.ts.map +1 -0
- package/dist/validation-billing.js +1 -0
- package/dist/validation-payments.d.ts +2 -0
- package/dist/validation-payments.d.ts.map +1 -0
- package/dist/validation-payments.js +1 -0
- package/dist/validation-public.d.ts +2 -0
- package/dist/validation-public.d.ts.map +1 -0
- package/dist/validation-public.js +1 -0
- package/dist/validation-shared.d.ts +2 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +1 -0
- package/dist/validation-vouchers.d.ts +2 -0
- package/dist/validation-vouchers.d.ts.map +1 -0
- package/dist/validation-vouchers.js +1 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +1 -0
- 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
|
+
}
|