@voyant-travel/finance 0.180.1 → 0.181.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/booking-tax-runtime.js +10 -1
- package/dist/invoice-fx.d.ts +10 -0
- package/dist/invoice-fx.js +19 -2
- package/dist/reporting-definitions.d.ts +1 -0
- package/dist/reporting-definitions.js +1 -0
- package/dist/reporting.d.ts +2 -1
- package/dist/reporting.js +67 -3
- package/dist/runtime-port.d.ts +5 -0
- package/dist/runtime-port.js +2 -0
- package/package.json +4 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineGraphRuntimeFactory } from "@voyant-travel/core/project";
|
|
2
2
|
import { createBookingTaxPreviewApiExtension, createBookingTaxSettingsApiExtension, } from "./booking-tax.js";
|
|
3
|
+
import { INVOICE_FX_SETTINGS_RUNTIME_KEY } from "./invoice-fx.js";
|
|
3
4
|
import { createFinanceBookingTaxRuntime } from "./runtime.js";
|
|
4
5
|
import { financeOperatorSettingsRuntimePort } from "./runtime-port.js";
|
|
5
6
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -35,7 +36,8 @@ export function resolveBookingTaxRouteOptions(c, runtimeKey, fallback) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
export const createBookingTaxSettingsVoyantRuntime = defineGraphRuntimeFactory(async ({ getPort }) => {
|
|
38
|
-
const
|
|
39
|
+
const settings = await getPort(financeOperatorSettingsRuntimePort);
|
|
40
|
+
const options = createFinanceBookingTaxRuntime(settings);
|
|
39
41
|
const configured = createBookingTaxSettingsApiExtension(options);
|
|
40
42
|
const selected = {
|
|
41
43
|
...configured,
|
|
@@ -45,6 +47,13 @@ export const createBookingTaxSettingsVoyantRuntime = defineGraphRuntimeFactory(a
|
|
|
45
47
|
container.register(BOOKING_TAX_SETTINGS_RUNTIME_KEY, {
|
|
46
48
|
resolveRoutesOptions: () => options,
|
|
47
49
|
});
|
|
50
|
+
// The same operator-settings port also backs invoice-FX settings, so the
|
|
51
|
+
// invoice-fx routes (in the core finance module) can persist the base
|
|
52
|
+
// currency without a separate extension/graph plugin.
|
|
53
|
+
container.register(INVOICE_FX_SETTINGS_RUNTIME_KEY, {
|
|
54
|
+
resolveInvoiceFxSettings: settings.resolveInvoiceFxSettings,
|
|
55
|
+
updateInvoiceFxSettings: settings.updateInvoiceFxSettings,
|
|
56
|
+
});
|
|
48
57
|
},
|
|
49
58
|
},
|
|
50
59
|
};
|
package/dist/invoice-fx.d.ts
CHANGED
|
@@ -71,6 +71,16 @@ type InvoiceFxRouteEnv = Env & {
|
|
|
71
71
|
container?: ModuleContainer;
|
|
72
72
|
};
|
|
73
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Container key under which the operator-settings bridge registers persistable
|
|
76
|
+
* invoice-FX settings (base currency + commission). When present, it overlays
|
|
77
|
+
* the base route runtime so `PATCH /invoice-fx-settings` can actually write.
|
|
78
|
+
*/
|
|
79
|
+
export declare const INVOICE_FX_SETTINGS_RUNTIME_KEY = "finance.invoiceFxSettingsRuntime";
|
|
80
|
+
export interface InvoiceFxSettingsRuntime {
|
|
81
|
+
resolveInvoiceFxSettings: ResolveInvoiceFxSettings;
|
|
82
|
+
updateInvoiceFxSettings: UpdateInvoiceFxSettings;
|
|
83
|
+
}
|
|
74
84
|
export declare function resolveInvoiceFxSettingsOrDefault(db: PostgresJsDatabase, options?: InvoiceFxOptions): Promise<ResolvedInvoiceFxSettings>;
|
|
75
85
|
export declare function resolveInvoiceFxContext(db: PostgresJsDatabase, invoice: InvoiceFxInvoice, options?: InvoiceFxOptions): Promise<InvoiceFxContext | null>;
|
|
76
86
|
export declare function createVoyantDataFxExchangeRateResolver(options: VoyantDataFxResolverOptions): ResolveInvoiceExchangeRate;
|
package/dist/invoice-fx.js
CHANGED
|
@@ -13,10 +13,27 @@ const invoiceExchangeRateQuerySchema = z.object({
|
|
|
13
13
|
quoteCurrency: z.string().min(3).max(8),
|
|
14
14
|
date: z.string().optional(),
|
|
15
15
|
});
|
|
16
|
+
/**
|
|
17
|
+
* Container key under which the operator-settings bridge registers persistable
|
|
18
|
+
* invoice-FX settings (base currency + commission). When present, it overlays
|
|
19
|
+
* the base route runtime so `PATCH /invoice-fx-settings` can actually write.
|
|
20
|
+
*/
|
|
21
|
+
export const INVOICE_FX_SETTINGS_RUNTIME_KEY = "finance.invoiceFxSettingsRuntime";
|
|
16
22
|
function getInvoiceFxRuntime(options, bindings, container) {
|
|
17
|
-
|
|
23
|
+
const base = (container?.has(FINANCE_ROUTE_RUNTIME_CONTAINER_KEY)
|
|
18
24
|
? container.resolve(FINANCE_ROUTE_RUNTIME_CONTAINER_KEY)
|
|
19
|
-
: undefined) ?? buildFinanceRouteRuntime(bindings, options)
|
|
25
|
+
: undefined) ?? buildFinanceRouteRuntime(bindings, options);
|
|
26
|
+
// Overlay the operator-settings-backed resolver/updater at request time so the
|
|
27
|
+
// wiring is independent of module-bootstrap ordering.
|
|
28
|
+
if (container?.has(INVOICE_FX_SETTINGS_RUNTIME_KEY)) {
|
|
29
|
+
const settings = container.resolve(INVOICE_FX_SETTINGS_RUNTIME_KEY);
|
|
30
|
+
return {
|
|
31
|
+
...base,
|
|
32
|
+
resolveInvoiceFxSettings: settings.resolveInvoiceFxSettings,
|
|
33
|
+
updateInvoiceFxSettings: settings.updateInvoiceFxSettings,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return base;
|
|
20
37
|
}
|
|
21
38
|
export async function resolveInvoiceFxSettingsOrDefault(db, options = {}) {
|
|
22
39
|
const settings = await resolveConfiguredInvoiceFxSettings(db, options);
|
package/dist/reporting.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ReportDatasetContribution, ReportDatasetExecutionInput, ReportResult } from "@voyant-travel/reporting-contracts";
|
|
2
|
+
import { ReportDatasetQueryError } from "@voyant-travel/reporting-contracts";
|
|
2
3
|
import { type SQL } from "drizzle-orm";
|
|
3
4
|
/** A query shape outside Finance's deliberately small reporting surface. */
|
|
4
|
-
export declare class FinanceReportingQueryError extends
|
|
5
|
+
export declare class FinanceReportingQueryError extends ReportDatasetQueryError {
|
|
5
6
|
constructor(message: string);
|
|
6
7
|
}
|
|
7
8
|
/**
|
package/dist/reporting.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ReportDatasetQueryError } from "@voyant-travel/reporting-contracts";
|
|
1
2
|
import { hasApiKeyPermission, permissionStringsToPermissions } from "@voyant-travel/types/api-keys";
|
|
2
3
|
import { sql } from "drizzle-orm";
|
|
3
4
|
import { FINANCE_RECEIVABLES_DATASET_ID, financeReceivablesDatasetDefinition, } from "./reporting-definitions.js";
|
|
@@ -74,8 +75,67 @@ const semanticReceivables = sql `
|
|
|
74
75
|
WHERE invoice.invoice_type = 'invoice'
|
|
75
76
|
AND invoice.status IN ('issued', 'partially_paid', 'paid', 'overdue')
|
|
76
77
|
`;
|
|
78
|
+
/** Reserved report parameter that switches money measures to the operator base currency. */
|
|
79
|
+
const REPORT_CURRENCY_PARAM = "reportCurrency";
|
|
80
|
+
const BASE_CURRENCY_MODE = "base";
|
|
81
|
+
/**
|
|
82
|
+
* Base-currency variant of {@link semanticReceivables}. Every monetary figure is
|
|
83
|
+
* read from the record's persisted base-currency snapshot (`base_*_cents`), which
|
|
84
|
+
* was converted at the moment the record was created using that day's FX rate set
|
|
85
|
+
* — so aggregating across documents is exact and recording-time accurate. Records
|
|
86
|
+
* without an FX snapshot (base currency/amount unset) are excluded rather than
|
|
87
|
+
* silently counted at parity.
|
|
88
|
+
*/
|
|
89
|
+
const baseReceivables = sql `
|
|
90
|
+
SELECT
|
|
91
|
+
invoice.issue_date AS "issueDate",
|
|
92
|
+
invoice.due_date AS "dueDate",
|
|
93
|
+
invoice.status::text AS status,
|
|
94
|
+
invoice.base_currency AS currency,
|
|
95
|
+
invoice.base_total_cents::bigint AS "grossIssuedCents",
|
|
96
|
+
credit_totals.credited_cents::bigint AS "creditedCents",
|
|
97
|
+
greatest(invoice.base_total_cents::bigint - credit_totals.credited_cents, 0) AS "netIssuedCents",
|
|
98
|
+
payment_totals.settled_cents::bigint AS "settledCents",
|
|
99
|
+
payment_totals.refunded_cents::bigint AS "refundedCents",
|
|
100
|
+
greatest(
|
|
101
|
+
invoice.base_total_cents::bigint
|
|
102
|
+
- credit_totals.credited_cents
|
|
103
|
+
- payment_totals.settled_cents,
|
|
104
|
+
0
|
|
105
|
+
) AS "outstandingBalanceCents"
|
|
106
|
+
FROM invoices invoice
|
|
107
|
+
LEFT JOIN LATERAL (
|
|
108
|
+
SELECT coalesce(sum(credit.base_amount_cents), 0)::bigint AS credited_cents
|
|
109
|
+
FROM credit_notes credit
|
|
110
|
+
WHERE credit.invoice_id = invoice.id
|
|
111
|
+
AND credit.status IN ('issued', 'applied')
|
|
112
|
+
AND credit.base_amount_cents IS NOT NULL
|
|
113
|
+
) credit_totals ON true
|
|
114
|
+
LEFT JOIN LATERAL (
|
|
115
|
+
SELECT
|
|
116
|
+
coalesce(
|
|
117
|
+
sum(
|
|
118
|
+
CASE WHEN payment.status = 'completed' THEN payment.base_amount_cents ELSE 0 END
|
|
119
|
+
),
|
|
120
|
+
0
|
|
121
|
+
)::bigint AS settled_cents,
|
|
122
|
+
coalesce(
|
|
123
|
+
sum(
|
|
124
|
+
CASE WHEN payment.status = 'refunded' THEN payment.base_amount_cents ELSE 0 END
|
|
125
|
+
),
|
|
126
|
+
0
|
|
127
|
+
)::bigint AS refunded_cents
|
|
128
|
+
FROM payments payment
|
|
129
|
+
WHERE payment.invoice_id = invoice.id
|
|
130
|
+
AND payment.base_amount_cents IS NOT NULL
|
|
131
|
+
) payment_totals ON true
|
|
132
|
+
WHERE invoice.invoice_type = 'invoice'
|
|
133
|
+
AND invoice.status IN ('issued', 'partially_paid', 'paid', 'overdue')
|
|
134
|
+
AND invoice.base_currency IS NOT NULL
|
|
135
|
+
AND invoice.base_total_cents IS NOT NULL
|
|
136
|
+
`;
|
|
77
137
|
/** A query shape outside Finance's deliberately small reporting surface. */
|
|
78
|
-
export class FinanceReportingQueryError extends
|
|
138
|
+
export class FinanceReportingQueryError extends ReportDatasetQueryError {
|
|
79
139
|
constructor(message) {
|
|
80
140
|
super(message);
|
|
81
141
|
this.name = "FinanceReportingQueryError";
|
|
@@ -88,6 +148,9 @@ export class FinanceReportingQueryError extends Error {
|
|
|
88
148
|
*/
|
|
89
149
|
export function compileFinanceReceivablesQuery(input) {
|
|
90
150
|
const { query, parameters } = input;
|
|
151
|
+
// Page-level "show in base currency": read money from recording-time base
|
|
152
|
+
// snapshots so every amount is already in one currency (the operator base).
|
|
153
|
+
const baseMode = parameters[REPORT_CURRENCY_PARAM] === BASE_CURRENCY_MODE;
|
|
91
154
|
if (!Number.isInteger(input.maximumRows) || input.maximumRows < 1) {
|
|
92
155
|
throw new FinanceReportingQueryError("maximumRows must be a positive integer.");
|
|
93
156
|
}
|
|
@@ -173,7 +236,8 @@ export function compileFinanceReceivablesQuery(input) {
|
|
|
173
236
|
const currencyIsExplicit = aggregateQuery
|
|
174
237
|
? groups.has("currency")
|
|
175
238
|
: query.select.some((selection) => selection.kind === "field" && selection.field === "currency");
|
|
176
|
-
if (
|
|
239
|
+
if (!baseMode &&
|
|
240
|
+
moneySelections.length > 0 &&
|
|
177
241
|
!currencyIsExplicit &&
|
|
178
242
|
!hasSingleCurrencyFilter(query, parameters)) {
|
|
179
243
|
throw new FinanceReportingQueryError("Currency measures must include or group by currency, or be filtered to exactly one currency.");
|
|
@@ -190,7 +254,7 @@ export function compileFinanceReceivablesQuery(input) {
|
|
|
190
254
|
const rowLimit = Math.min(query.limit ?? input.maximumRows, input.maximumRows);
|
|
191
255
|
return {
|
|
192
256
|
statement: sql `
|
|
193
|
-
WITH receivable AS (${semanticReceivables})
|
|
257
|
+
WITH receivable AS (${baseMode ? baseReceivables : semanticReceivables})
|
|
194
258
|
SELECT ${selectSql}
|
|
195
259
|
FROM receivable
|
|
196
260
|
${filters.length ? sql `WHERE ${sql.join(filters, sql ` AND `)}` : sql ``}
|
package/dist/runtime-port.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
|
4
4
|
import type { BookingTaxRouteOptions } from "./booking-tax.js";
|
|
5
5
|
import type { CheckoutRoutesOptions } from "./checkout-routes.js";
|
|
6
6
|
import type { CheckoutPaymentStarter } from "./checkout-service.js";
|
|
7
|
+
import type { ResolveInvoiceFxSettings, UpdateInvoiceFxSettings } from "./invoice-fx.js";
|
|
7
8
|
import type { PaymentPolicy } from "./payment-policy.js";
|
|
8
9
|
import type { PaymentPolicyEntityContext } from "./payment-policy-cascade.js";
|
|
9
10
|
import type { BookingScheduleRoutesOptions } from "./payment-schedule/routes.js";
|
|
@@ -24,6 +25,10 @@ export interface FinanceOperatorSettingsRuntime {
|
|
|
24
25
|
* settled proforma should be auto-converted to a fiscal invoice.
|
|
25
26
|
*/
|
|
26
27
|
resolveInvoicingMode: (db: PostgresJsDatabase) => Promise<"direct" | "proforma-first">;
|
|
28
|
+
/** Resolve the operator's invoice-FX settings (base currency, commission). */
|
|
29
|
+
resolveInvoiceFxSettings: ResolveInvoiceFxSettings;
|
|
30
|
+
/** Persist the operator's invoice-FX settings. */
|
|
31
|
+
updateInvoiceFxSettings: UpdateInvoiceFxSettings;
|
|
27
32
|
}
|
|
28
33
|
export interface FinanceNotificationsRuntime {
|
|
29
34
|
resolveNotificationDispatcher: NonNullable<CheckoutRoutesOptions["resolveNotificationDispatcher"]>;
|
package/dist/runtime-port.js
CHANGED
|
@@ -20,6 +20,8 @@ export const financeOperatorSettingsRuntimePort = objectPort("finance.operator-s
|
|
|
20
20
|
"resolveBookingTaxSettings",
|
|
21
21
|
"updateBookingTaxSettings",
|
|
22
22
|
"resolveInvoicingMode",
|
|
23
|
+
"resolveInvoiceFxSettings",
|
|
24
|
+
"updateInvoiceFxSettings",
|
|
23
25
|
]);
|
|
24
26
|
export const financeNotificationsRuntimePort = objectPort("finance.notifications.runtime", ["resolveNotificationDispatcher", "listBookingReminderRuns"]);
|
|
25
27
|
export const financeFxReferenceRuntimePort = objectPort("finance.fx-reference.runtime", ["resolveReferenceRate"]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/finance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.181.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -156,16 +156,16 @@
|
|
|
156
156
|
"fflate": "^0.8.2",
|
|
157
157
|
"hono": "^4.12.27",
|
|
158
158
|
"zod": "^4.4.3",
|
|
159
|
-
"@voyant-travel/bookings": "^0.180.1",
|
|
160
159
|
"@voyant-travel/action-ledger": "^0.111.9",
|
|
161
|
-
"@voyant-travel/
|
|
160
|
+
"@voyant-travel/bookings": "^0.181.0",
|
|
162
161
|
"@voyant-travel/core": "^0.130.0",
|
|
162
|
+
"@voyant-travel/db": "^0.117.1",
|
|
163
163
|
"@voyant-travel/types": "^0.109.8",
|
|
164
164
|
"@voyant-travel/finance-contracts": "^0.107.1",
|
|
165
165
|
"@voyant-travel/hono": "^0.131.2",
|
|
166
166
|
"@voyant-travel/payments": "^0.3.0",
|
|
167
167
|
"@voyant-travel/public-document-delivery": "^0.4.8",
|
|
168
|
-
"@voyant-travel/reporting-contracts": "^0.
|
|
168
|
+
"@voyant-travel/reporting-contracts": "^0.3.0",
|
|
169
169
|
"@voyant-travel/storage": "^0.111.6",
|
|
170
170
|
"@voyant-travel/utils": "^0.108.0",
|
|
171
171
|
"@voyant-travel/tools": "^0.3.0"
|