@voyant-travel/finance 0.164.0 → 0.166.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.d.ts +31 -0
- package/dist/booking-tax.js +17 -0
- package/dist/card-payment.d.ts +8 -0
- package/dist/card-payment.js +57 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -0
- package/dist/payment-adapter-events.d.ts +47 -0
- package/dist/payment-adapter-events.js +34 -0
- package/dist/payment-schedule/routes.js +12 -2
- package/dist/proforma-conversion-runtime.d.ts +34 -0
- package/dist/proforma-conversion-runtime.js +73 -0
- package/dist/runtime-port.d.ts +76 -1
- package/dist/runtime-port.js +36 -1
- package/dist/service-payment-session-completion.js +2 -1
- package/dist/voyant.js +9 -0
- package/openapi/admin/booking-tax.json +7 -3
- package/package.json +14 -8
package/dist/booking-tax.d.ts
CHANGED
|
@@ -12,9 +12,26 @@ export type ResolvedBookingSellTaxRate = {
|
|
|
12
12
|
rate: number;
|
|
13
13
|
priceMode: "inclusive" | "exclusive";
|
|
14
14
|
};
|
|
15
|
+
export type InvoicingMode = "direct" | "proforma-first";
|
|
16
|
+
/**
|
|
17
|
+
* Official FX reference-rate source. `ecb` (default) covers most EU
|
|
18
|
+
* operators; `bnr` is the National Bank of Romania. A setting, not
|
|
19
|
+
* per-country code — new jurisdictions add values here, not modules.
|
|
20
|
+
*/
|
|
21
|
+
export type FxReferenceSource = "ecb" | "bnr";
|
|
15
22
|
export type BookingTaxSettings = {
|
|
16
23
|
taxPriceMode?: "inclusive" | "exclusive" | null;
|
|
17
24
|
taxPolicyProfileId?: string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Operator invoicing mode. `direct` bills the fiscal invoice
|
|
27
|
+
* straight away; `proforma-first` issues a proforma and converts it
|
|
28
|
+
* to a fiscal invoice on full settlement. Absent/null → `direct`.
|
|
29
|
+
*/
|
|
30
|
+
invoicingMode?: InvoicingMode | null;
|
|
31
|
+
/**
|
|
32
|
+
* Operator's official FX reference-rate source. Absent/null → `ecb`.
|
|
33
|
+
*/
|
|
34
|
+
fxReferenceSource?: FxReferenceSource | null;
|
|
18
35
|
};
|
|
19
36
|
export type ResolveBookingTaxSettings = (db: PostgresJsDatabase) => BookingTaxSettings | null | undefined | Promise<BookingTaxSettings | null | undefined>;
|
|
20
37
|
export type UpdateBookingTaxSettings = (db: PostgresJsDatabase, settings: BookingTaxSettings) => BookingTaxSettings | null | undefined | Promise<BookingTaxSettings | null | undefined>;
|
|
@@ -40,6 +57,12 @@ export declare function resolveBookingSellTaxRate(db: PostgresJsDatabase, args:
|
|
|
40
57
|
productId?: string | null;
|
|
41
58
|
facts?: Partial<ProductTaxFacts>;
|
|
42
59
|
}, options?: ResolveBookingSellTaxRateOptions): Promise<ResolvedBookingSellTaxRate | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve just the operator invoicing mode, defaulting to `direct`
|
|
62
|
+
* when no settings row exists. Shared by the proforma-conversion
|
|
63
|
+
* subscriber so it never converts unless the operator opted in.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveInvoicingModeOrDefault(db: PostgresJsDatabase, options?: ResolveBookingSellTaxRateOptions): Promise<InvoicingMode>;
|
|
43
66
|
export declare function computeBookingItemTaxLine(taxRate: ResolvedBookingSellTaxRate | null, amountCents: number, currency: string, sortOrder?: number): {
|
|
44
67
|
code: string;
|
|
45
68
|
name: string;
|
|
@@ -64,6 +87,8 @@ export declare function createBookingTaxRoutes(options?: BookingTaxRouteOptions)
|
|
|
64
87
|
data: {
|
|
65
88
|
taxPriceMode: "inclusive" | "exclusive";
|
|
66
89
|
taxPolicyProfileId: string | null;
|
|
90
|
+
invoicingMode: "direct" | "proforma-first";
|
|
91
|
+
fxReferenceSource: "ecb" | "bnr";
|
|
67
92
|
};
|
|
68
93
|
};
|
|
69
94
|
outputFormat: "json";
|
|
@@ -77,6 +102,8 @@ export declare function createBookingTaxRoutes(options?: BookingTaxRouteOptions)
|
|
|
77
102
|
json: {
|
|
78
103
|
taxPriceMode?: "inclusive" | "exclusive" | undefined;
|
|
79
104
|
taxPolicyProfileId?: string | null | undefined;
|
|
105
|
+
invoicingMode?: "direct" | "proforma-first" | undefined;
|
|
106
|
+
fxReferenceSource?: "ecb" | "bnr" | undefined;
|
|
80
107
|
};
|
|
81
108
|
};
|
|
82
109
|
output: {};
|
|
@@ -87,12 +114,16 @@ export declare function createBookingTaxRoutes(options?: BookingTaxRouteOptions)
|
|
|
87
114
|
json: {
|
|
88
115
|
taxPriceMode?: "inclusive" | "exclusive" | undefined;
|
|
89
116
|
taxPolicyProfileId?: string | null | undefined;
|
|
117
|
+
invoicingMode?: "direct" | "proforma-first" | undefined;
|
|
118
|
+
fxReferenceSource?: "ecb" | "bnr" | undefined;
|
|
90
119
|
};
|
|
91
120
|
};
|
|
92
121
|
output: {
|
|
93
122
|
data: {
|
|
94
123
|
taxPriceMode: "inclusive" | "exclusive";
|
|
95
124
|
taxPolicyProfileId: string | null;
|
|
125
|
+
invoicingMode: "direct" | "proforma-first";
|
|
126
|
+
fxReferenceSource: "ecb" | "bnr";
|
|
96
127
|
};
|
|
97
128
|
};
|
|
98
129
|
outputFormat: "json";
|
package/dist/booking-tax.js
CHANGED
|
@@ -14,11 +14,15 @@ const taxPreviewBodySchema = z.object({
|
|
|
14
14
|
const bookingTaxSettingsPatchSchema = z.object({
|
|
15
15
|
taxPriceMode: z.enum(["inclusive", "exclusive"]).optional(),
|
|
16
16
|
taxPolicyProfileId: z.string().min(1).nullable().optional(),
|
|
17
|
+
invoicingMode: z.enum(["direct", "proforma-first"]).optional(),
|
|
18
|
+
fxReferenceSource: z.enum(["ecb", "bnr"]).optional(),
|
|
17
19
|
});
|
|
18
20
|
const bookingTaxSettingsResponseSchema = z.object({
|
|
19
21
|
data: z.object({
|
|
20
22
|
taxPriceMode: z.enum(["inclusive", "exclusive"]),
|
|
21
23
|
taxPolicyProfileId: z.string().nullable(),
|
|
24
|
+
invoicingMode: z.enum(["direct", "proforma-first"]),
|
|
25
|
+
fxReferenceSource: z.enum(["ecb", "bnr"]),
|
|
22
26
|
}),
|
|
23
27
|
});
|
|
24
28
|
const bookingTaxPreviewResponseSchema = z.object({
|
|
@@ -113,8 +117,19 @@ async function resolveBookingTaxSettingsOrDefault(db, options = {}) {
|
|
|
113
117
|
return {
|
|
114
118
|
taxPriceMode: settings?.taxPriceMode === "exclusive" ? "exclusive" : "inclusive",
|
|
115
119
|
taxPolicyProfileId: settings?.taxPolicyProfileId ?? null,
|
|
120
|
+
invoicingMode: settings?.invoicingMode === "proforma-first" ? "proforma-first" : "direct",
|
|
121
|
+
fxReferenceSource: settings?.fxReferenceSource === "bnr" ? "bnr" : "ecb",
|
|
116
122
|
};
|
|
117
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve just the operator invoicing mode, defaulting to `direct`
|
|
126
|
+
* when no settings row exists. Shared by the proforma-conversion
|
|
127
|
+
* subscriber so it never converts unless the operator opted in.
|
|
128
|
+
*/
|
|
129
|
+
export async function resolveInvoicingModeOrDefault(db, options = {}) {
|
|
130
|
+
const settings = await resolveBookingTaxSettingsOrDefault(db, options);
|
|
131
|
+
return settings.invoicingMode;
|
|
132
|
+
}
|
|
118
133
|
export function computeBookingItemTaxLine(taxRate, amountCents, currency, sortOrder = 0) {
|
|
119
134
|
if (!taxRate || taxRate.rate <= 0 || amountCents <= 0)
|
|
120
135
|
return null;
|
|
@@ -275,6 +290,8 @@ export function createBookingTaxRoutes(options = {}) {
|
|
|
275
290
|
taxPolicyProfileId: patch.taxPolicyProfileId === undefined
|
|
276
291
|
? current.taxPolicyProfileId
|
|
277
292
|
: patch.taxPolicyProfileId,
|
|
293
|
+
invoicingMode: patch.invoicingMode ?? current.invoicingMode,
|
|
294
|
+
fxReferenceSource: patch.fxReferenceSource ?? current.fxReferenceSource,
|
|
278
295
|
});
|
|
279
296
|
return c.json({
|
|
280
297
|
data: await resolveBookingTaxSettingsOrDefault(c.get("db"), { settings: next }),
|
package/dist/card-payment.d.ts
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* Returning `null` from a starter means "this processor isn't configured" so
|
|
12
12
|
* callers fall back gracefully (bank-transfer paths still work).
|
|
13
13
|
*/
|
|
14
|
+
import type { PaymentAdapter, PaymentAdapterRuntimeContext } from "@voyant-travel/payments";
|
|
14
15
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
15
16
|
import type { Context } from "hono";
|
|
17
|
+
import type { FinanceServiceRuntime } from "./service-shared.js";
|
|
16
18
|
/**
|
|
17
19
|
* Billing details a card processor needs to start a hosted payment.
|
|
18
20
|
*
|
|
@@ -54,3 +56,9 @@ export interface CardPaymentStartResult {
|
|
|
54
56
|
* configured" — callers fall back (bank transfer still works).
|
|
55
57
|
*/
|
|
56
58
|
export type CardPaymentStarter = (c: Context, args: CardPaymentStartArgs) => Promise<CardPaymentStartResult | null>;
|
|
59
|
+
export interface PaymentAdapterCardPaymentStarterOptions {
|
|
60
|
+
resolveContext?(c: Context): PaymentAdapterRuntimeContext;
|
|
61
|
+
resolveRuntime?(c: Context): FinanceServiceRuntime;
|
|
62
|
+
idempotencyKey?(sessionId: string): string;
|
|
63
|
+
}
|
|
64
|
+
export declare function createPaymentAdapterCardPaymentStarter(adapter: PaymentAdapter, options?: PaymentAdapterCardPaymentStarterOptions): CardPaymentStarter;
|
package/dist/card-payment.js
CHANGED
|
@@ -1 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { paymentSessions } from "./schema/payment-sessions.js";
|
|
3
|
+
import { financePaymentSessionCompletionService } from "./service-payment-session-completion.js";
|
|
4
|
+
import { financePaymentSessionService } from "./service-payment-sessions.js";
|
|
5
|
+
export function createPaymentAdapterCardPaymentStarter(adapter, options = {}) {
|
|
6
|
+
return async (c, args) => {
|
|
7
|
+
const [session] = await args.db
|
|
8
|
+
.select()
|
|
9
|
+
.from(paymentSessions)
|
|
10
|
+
.where(eq(paymentSessions.id, args.sessionId))
|
|
11
|
+
.limit(1);
|
|
12
|
+
if (!session)
|
|
13
|
+
return null;
|
|
14
|
+
const idempotencyKey = session.idempotencyKey ?? options.idempotencyKey?.(session.id) ?? `payment:${session.id}`;
|
|
15
|
+
const result = await adapter.initiate(options.resolveContext?.(c) ?? { env: c.env }, {
|
|
16
|
+
paymentSessionId: session.id,
|
|
17
|
+
money: { amountMinor: session.amountCents, currency: session.currency },
|
|
18
|
+
description: args.description ?? session.notes ?? undefined,
|
|
19
|
+
returnUrl: args.returnUrl ?? session.returnUrl ?? undefined,
|
|
20
|
+
idempotencyKey,
|
|
21
|
+
customer: {
|
|
22
|
+
email: args.billing.email,
|
|
23
|
+
phone: args.billing.phone ?? null,
|
|
24
|
+
firstName: args.billing.firstName,
|
|
25
|
+
lastName: args.billing.lastName ?? null,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const providerData = {
|
|
29
|
+
providerSessionId: result.processorSessionId ?? undefined,
|
|
30
|
+
providerPaymentId: result.processorPaymentId ?? undefined,
|
|
31
|
+
providerPayload: result.raw === undefined ? undefined : { initiation: result.raw },
|
|
32
|
+
metadata: { paymentAdapterInitiationIdempotencyKey: result.idempotencyKey },
|
|
33
|
+
};
|
|
34
|
+
if (result.nextState === "paid" || result.nextState === "authorized") {
|
|
35
|
+
await financePaymentSessionService.updatePaymentSession(args.db, session.id, {
|
|
36
|
+
provider: adapter.id,
|
|
37
|
+
redirectUrl: result.checkout?.url ?? undefined,
|
|
38
|
+
idempotencyKey,
|
|
39
|
+
});
|
|
40
|
+
await financePaymentSessionCompletionService.completePaymentSession(args.db, session.id, {
|
|
41
|
+
status: result.nextState,
|
|
42
|
+
captureMode: "automatic",
|
|
43
|
+
...providerData,
|
|
44
|
+
}, options.resolveRuntime?.(c));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await financePaymentSessionService.updatePaymentSession(args.db, session.id, {
|
|
48
|
+
provider: adapter.id,
|
|
49
|
+
status: result.nextState,
|
|
50
|
+
redirectUrl: result.checkout?.url ?? undefined,
|
|
51
|
+
idempotencyKey,
|
|
52
|
+
...providerData,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return { redirectUrl: result.checkout?.url ?? null };
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,12 +23,16 @@ export interface FinanceApiModuleOptions extends FinanceRuntimeOptions, PublicFi
|
|
|
23
23
|
export declare function createFinanceApiModule(options?: FinanceApiModuleOptions): ApiModule;
|
|
24
24
|
export declare const financeApiModule: ApiModule;
|
|
25
25
|
export declare const createFinanceVoyantRuntime: import("@voyant-travel/core/project").VoyantGraphRuntimeFactory<ApiModule>;
|
|
26
|
+
export type { PaymentAdapter, PaymentAdapterCapabilities, PaymentAdapterConformanceHarness, PaymentAdapterConformanceResult, PaymentAdapterDiagnostics, PaymentAdapterRuntimeContext, PaymentCallbackEvent, PaymentCallbackRequest, PaymentCallbackVerificationResult, PaymentInitiationInput, PaymentInitiationResult, PaymentMoney, PaymentOperationInput, PaymentOperationResult, PaymentStatusInput, PaymentStatusResult, } from "@voyant-travel/payments";
|
|
27
|
+
export { PAYMENT_ADAPTER_CONTRACT_VERSION, PAYMENT_ADAPTER_RUNTIME_PORT_ID, paymentAdapterRuntimePort, runPaymentAdapterConformance, } from "@voyant-travel/payments";
|
|
26
28
|
export { type BookingCancellationSettlementInput, buildPaidBookingCancellationSettlementNote, closeTerminalBookingPaymentSchedules, financeBookingLifecycle, recordPaidBookingCancellationSettlement, } from "./booking-lifecycle.js";
|
|
27
|
-
export { type BookingTaxRouteOptions, type BookingTaxSettings, computeBookingItemTaxLine, createBookingTaxApiExtension, createBookingTaxRoutes, createBookingTaxVoyantRuntime, loadProductTaxFacts, matchesTaxPolicyCondition, mountBookingTaxRoutes, type ProductTaxFacts, type ResolveBookingSellTaxRateOptions, type ResolveBookingTaxSettings, type ResolvedBookingSellTaxRate, resolveBookingSellTaxRate, type TaxPolicyCondition, type UpdateBookingTaxSettings, } from "./booking-tax.js";
|
|
28
|
-
export type { CardPaymentBilling, CardPaymentStartArgs, CardPaymentStarter, CardPaymentStartResult, } from "./card-payment.js";
|
|
29
|
+
export { type BookingTaxRouteOptions, type BookingTaxSettings, computeBookingItemTaxLine, createBookingTaxApiExtension, createBookingTaxRoutes, createBookingTaxVoyantRuntime, type InvoicingMode, loadProductTaxFacts, matchesTaxPolicyCondition, mountBookingTaxRoutes, type ProductTaxFacts, type ResolveBookingSellTaxRateOptions, type ResolveBookingTaxSettings, type ResolvedBookingSellTaxRate, resolveBookingSellTaxRate, type TaxPolicyCondition, type UpdateBookingTaxSettings, } from "./booking-tax.js";
|
|
30
|
+
export type { CardPaymentBilling, CardPaymentStartArgs, CardPaymentStarter, CardPaymentStartResult, PaymentAdapterCardPaymentStarterOptions, } from "./card-payment.js";
|
|
31
|
+
export { createPaymentAdapterCardPaymentStarter } from "./card-payment.js";
|
|
29
32
|
export { type DocumentDownloadEnvelope, type DocumentDownloadResolution, type DocumentDownloadResolver, resolveStoredDocumentDownload, type StoredDocumentReference, } from "./document-download.js";
|
|
30
33
|
export { createInvoiceFxApiExtension, createInvoiceFxRoutes, createVoyantDataFxExchangeRateResolver, type InvoiceExchangeRateResolution, type InvoiceFxContext, type InvoiceFxOptions, type InvoiceFxRouteOptions, type InvoiceFxSettings, mountInvoiceFxRoutes, type ResolvedInvoiceFxSettings, type ResolveInvoiceExchangeRate, type ResolveInvoiceExchangeRateInput, type ResolveInvoiceFxSettings, resolveInvoiceFxContext, resolveInvoiceFxSettingsOrDefault, type UpdateInvoiceFxSettings, type VoyantDataFxResolverOptions, } from "./invoice-fx.js";
|
|
31
34
|
export { type CreateOrderPaymentSessionsOptions, createOrderPaymentSessions, type EnsureOrderSessionParams, type OrderPaymentSessionSummary, type OrderPaymentSessions, type OrderPaymentSessionTargetType, type StartOrderPaymentProvider, } from "./order-payment-sessions.js";
|
|
35
|
+
export { applyPaymentAdapterCallbackEvent } from "./payment-adapter-events.js";
|
|
32
36
|
export type { ComputedScheduleEntry, ComputeScheduleInput, DepositKind, DepositRule, PaymentPolicy, PaymentPolicyCascadeLayers, PaymentPolicySource, PaymentScheduleEntryType, ResolvedPaymentPolicy, } from "./payment-policy.js";
|
|
33
37
|
export { computePaymentSchedule, isPaymentPolicyEmpty, noDepositPolicy, normalizePaymentPolicy, policyShouldRequireFullPayment, resolveEffectivePaymentPolicy, } from "./payment-policy.js";
|
|
34
38
|
export { createPaymentPolicyCascade, type PaymentPolicyCascade, type PaymentPolicyCascadeOptions, type PaymentPolicyCascadeReaders, readPolicySourceFromInternalNotes, stampPolicySourceOnBooking, } from "./payment-policy-cascade.js";
|
package/dist/index.js
CHANGED
|
@@ -87,11 +87,14 @@ export const createFinanceVoyantRuntime = defineGraphRuntimeFactory(async ({ api
|
|
|
87
87
|
}
|
|
88
88
|
return selected;
|
|
89
89
|
});
|
|
90
|
+
export { PAYMENT_ADAPTER_CONTRACT_VERSION, PAYMENT_ADAPTER_RUNTIME_PORT_ID, paymentAdapterRuntimePort, runPaymentAdapterConformance, } from "@voyant-travel/payments";
|
|
90
91
|
export { buildPaidBookingCancellationSettlementNote, closeTerminalBookingPaymentSchedules, financeBookingLifecycle, recordPaidBookingCancellationSettlement, } from "./booking-lifecycle.js";
|
|
91
92
|
export { computeBookingItemTaxLine, createBookingTaxApiExtension, createBookingTaxRoutes, createBookingTaxVoyantRuntime, loadProductTaxFacts, matchesTaxPolicyCondition, mountBookingTaxRoutes, resolveBookingSellTaxRate, } from "./booking-tax.js";
|
|
93
|
+
export { createPaymentAdapterCardPaymentStarter } from "./card-payment.js";
|
|
92
94
|
export { resolveStoredDocumentDownload, } from "./document-download.js";
|
|
93
95
|
export { createInvoiceFxApiExtension, createInvoiceFxRoutes, createVoyantDataFxExchangeRateResolver, mountInvoiceFxRoutes, resolveInvoiceFxContext, resolveInvoiceFxSettingsOrDefault, } from "./invoice-fx.js";
|
|
94
96
|
export { createOrderPaymentSessions, } from "./order-payment-sessions.js";
|
|
97
|
+
export { applyPaymentAdapterCallbackEvent } from "./payment-adapter-events.js";
|
|
95
98
|
export { computePaymentSchedule, isPaymentPolicyEmpty, noDepositPolicy, normalizePaymentPolicy, policyShouldRequireFullPayment, resolveEffectivePaymentPolicy, } from "./payment-policy.js";
|
|
96
99
|
export { createPaymentPolicyCascade, readPolicySourceFromInternalNotes, stampPolicySourceOnBooking, } from "./payment-policy-cascade.js";
|
|
97
100
|
export { createBookingScheduleAdminRoutes, createBookingScheduleApiExtension, createBookingScheduleVoyantRuntime, createPaymentPolicyPublicRoutes, generatePaymentScheduleForBooking, } from "./payment-schedule/routes.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PaymentCallbackEvent } from "@voyant-travel/payments";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
import type { FinanceServiceRuntime } from "./service-shared.js";
|
|
4
|
+
export declare function applyPaymentAdapterCallbackEvent(db: PostgresJsDatabase, event: PaymentCallbackEvent, runtime?: FinanceServiceRuntime): Promise<{
|
|
5
|
+
id: string;
|
|
6
|
+
targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
|
|
7
|
+
targetId: string | null;
|
|
8
|
+
bookingId: string | null;
|
|
9
|
+
orderId: string | null;
|
|
10
|
+
invoiceId: string | null;
|
|
11
|
+
bookingPaymentScheduleId: string | null;
|
|
12
|
+
bookingGuaranteeId: string | null;
|
|
13
|
+
paymentInstrumentId: string | null;
|
|
14
|
+
paymentAuthorizationId: string | null;
|
|
15
|
+
paymentCaptureId: string | null;
|
|
16
|
+
paymentId: string | null;
|
|
17
|
+
status: "paid" | "pending" | "failed" | "requires_redirect" | "processing" | "authorized" | "cancelled" | "expired";
|
|
18
|
+
provider: string | null;
|
|
19
|
+
providerSessionId: string | null;
|
|
20
|
+
providerPaymentId: string | null;
|
|
21
|
+
externalReference: string | null;
|
|
22
|
+
idempotencyKey: string | null;
|
|
23
|
+
clientReference: string | null;
|
|
24
|
+
currency: string;
|
|
25
|
+
amountCents: number;
|
|
26
|
+
paymentMethod: "bank_transfer" | "credit_card" | "debit_card" | "cash" | "cheque" | "wallet" | "direct_bill" | "travel_credit" | "other" | null;
|
|
27
|
+
payerPersonId: string | null;
|
|
28
|
+
payerOrganizationId: string | null;
|
|
29
|
+
payerEmail: string | null;
|
|
30
|
+
payerName: string | null;
|
|
31
|
+
redirectUrl: string | null;
|
|
32
|
+
returnUrl: string | null;
|
|
33
|
+
cancelUrl: string | null;
|
|
34
|
+
callbackUrl: string | null;
|
|
35
|
+
expiresAt: Date | null;
|
|
36
|
+
completedAt: Date | null;
|
|
37
|
+
failedAt: Date | null;
|
|
38
|
+
cancelledAt: Date | null;
|
|
39
|
+
expiredAt: Date | null;
|
|
40
|
+
failureCode: string | null;
|
|
41
|
+
failureMessage: string | null;
|
|
42
|
+
notes: string | null;
|
|
43
|
+
providerPayload: Record<string, unknown> | null;
|
|
44
|
+
metadata: Record<string, unknown> | null;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
updatedAt: Date;
|
|
47
|
+
} | null>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { financePaymentSessionCompletionService } from "./service-payment-session-completion.js";
|
|
2
|
+
import { financePaymentSessionService } from "./service-payment-sessions.js";
|
|
3
|
+
export async function applyPaymentAdapterCallbackEvent(db, event, runtime = {}) {
|
|
4
|
+
const providerData = {
|
|
5
|
+
providerSessionId: event.processorSessionId ?? undefined,
|
|
6
|
+
providerPaymentId: event.processorPaymentId ?? undefined,
|
|
7
|
+
providerPayload: event.raw === undefined ? undefined : { callback: event.raw },
|
|
8
|
+
metadata: { paymentAdapterEventId: event.eventId, paymentAdapterOccurredAt: event.occurredAt },
|
|
9
|
+
};
|
|
10
|
+
if (event.nextState === "paid" || event.nextState === "authorized") {
|
|
11
|
+
return financePaymentSessionCompletionService.completePaymentSession(db, event.paymentSessionId, {
|
|
12
|
+
status: event.nextState,
|
|
13
|
+
captureMode: "automatic",
|
|
14
|
+
...providerData,
|
|
15
|
+
}, runtime);
|
|
16
|
+
}
|
|
17
|
+
if (event.nextState === "failed") {
|
|
18
|
+
return financePaymentSessionService.failPaymentSession(db, event.paymentSessionId, {
|
|
19
|
+
...providerData,
|
|
20
|
+
failureCode: "payment_adapter_callback",
|
|
21
|
+
failureMessage: "Payment adapter callback mapped this session to failed.",
|
|
22
|
+
}, runtime);
|
|
23
|
+
}
|
|
24
|
+
if (event.nextState === "cancelled") {
|
|
25
|
+
return financePaymentSessionService.cancelPaymentSession(db, event.paymentSessionId, providerData, runtime);
|
|
26
|
+
}
|
|
27
|
+
if (event.nextState === "expired") {
|
|
28
|
+
return financePaymentSessionService.expirePaymentSession(db, event.paymentSessionId, providerData, runtime);
|
|
29
|
+
}
|
|
30
|
+
return financePaymentSessionService.updatePaymentSession(db, event.paymentSessionId, {
|
|
31
|
+
status: event.nextState,
|
|
32
|
+
...providerData,
|
|
33
|
+
}, runtime);
|
|
34
|
+
}
|
|
@@ -30,6 +30,7 @@ import { openApiValidationHook, parseJsonBody } from "@voyant-travel/hono";
|
|
|
30
30
|
import { asc, eq } from "drizzle-orm";
|
|
31
31
|
import { BOOKING_SCHEDULE_SUBSCRIBER_RUNTIME_KEY } from "../booking-schedule/subscriber-runtime.js";
|
|
32
32
|
import { computePaymentSchedule, noDepositPolicy, resolveEffectivePaymentPolicy, } from "../payment-policy.js";
|
|
33
|
+
import { PROFORMA_CONVERSION_SUBSCRIBER_RUNTIME_KEY } from "../proforma-conversion-runtime.js";
|
|
33
34
|
import { createFinanceBookingScheduleRuntime } from "../runtime.js";
|
|
34
35
|
import { financeAccommodationsPaymentPolicyRuntimePort, financeCruisesPaymentPolicyRuntimePort, financeDistributionPaymentPolicyRuntimePort, financeHostRuntimePort, financeInventoryPaymentPolicyRuntimePort, financeOperatorSettingsRuntimePort, } from "../runtime-port.js";
|
|
35
36
|
import { bookingPaymentSchedules } from "../schema/booking-billing.js";
|
|
@@ -373,16 +374,25 @@ export function createBookingScheduleApiExtension(options) {
|
|
|
373
374
|
};
|
|
374
375
|
}
|
|
375
376
|
export const createBookingScheduleVoyantRuntime = defineGraphRuntimeFactory(async ({ api, getPort }) => {
|
|
376
|
-
const
|
|
377
|
+
const operatorSettings = await getPort(financeOperatorSettingsRuntimePort);
|
|
378
|
+
const provider = createFinanceBookingScheduleRuntime(await getPort(financeHostRuntimePort), operatorSettings, await getPort(financeDistributionPaymentPolicyRuntimePort), await getPort(financeAccommodationsPaymentPolicyRuntimePort), await getPort(financeCruisesPaymentPolicyRuntimePort), await getPort(financeInventoryPaymentPolicyRuntimePort));
|
|
377
379
|
const configured = createBookingScheduleApiExtension(provider.options);
|
|
378
380
|
const selected = {
|
|
379
381
|
extension: {
|
|
380
382
|
...configured.extension,
|
|
381
|
-
bootstrap: async ({ container }) => {
|
|
383
|
+
bootstrap: async ({ container, eventBus }) => {
|
|
382
384
|
container.register(BOOKING_SCHEDULE_SUBSCRIBER_RUNTIME_KEY, {
|
|
383
385
|
resolveRoutesOptions: () => provider.options,
|
|
384
386
|
withDb: provider.withDb,
|
|
385
387
|
});
|
|
388
|
+
// Same host/settings wiring powers the proforma-conversion
|
|
389
|
+
// subscriber: it reads the operator invoicing mode and, in
|
|
390
|
+
// proforma-first mode, mints the fiscal invoice on settlement.
|
|
391
|
+
container.register(PROFORMA_CONVERSION_SUBSCRIBER_RUNTIME_KEY, {
|
|
392
|
+
resolveInvoicingMode: operatorSettings.resolveInvoicingMode,
|
|
393
|
+
withDb: provider.withDb,
|
|
394
|
+
eventBus,
|
|
395
|
+
});
|
|
386
396
|
},
|
|
387
397
|
},
|
|
388
398
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EventBus, SubscriberRuntimeDescriptor } from "@voyant-travel/core";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
import type { InvoicingMode } from "./booking-tax.js";
|
|
4
|
+
/**
|
|
5
|
+
* Standard proforma → fiscal invoice conversion.
|
|
6
|
+
*
|
|
7
|
+
* When the operator runs `invoicing.mode: "proforma-first"`, a proforma
|
|
8
|
+
* is issued at checkout and the fiscal invoice is minted once the
|
|
9
|
+
* proforma is fully settled. This subscriber watches settlement signals
|
|
10
|
+
* (`invoice.settled` from the settlement poller and
|
|
11
|
+
* `invoice.payment.recorded` from manual/processed payments) and, in
|
|
12
|
+
* proforma-first mode only, converts a fully-paid proforma to its fiscal
|
|
13
|
+
* invoice via {@link convertProformaToInvoice} (which copies lines,
|
|
14
|
+
* assigns the fiscal number, links both documents, and voids the
|
|
15
|
+
* proforma). `direct` mode is a no-op — zero behaviour change for
|
|
16
|
+
* existing deployments — and the conversion is idempotent (the service
|
|
17
|
+
* guards against double-conversion under an advisory lock).
|
|
18
|
+
*/
|
|
19
|
+
export declare const FINANCE_PROFORMA_CONVERSION_SUBSCRIBER_ID = "@voyant-travel/finance#subscriber.proforma-conversion";
|
|
20
|
+
export declare const PROFORMA_CONVERSION_SUBSCRIBER_RUNTIME_KEY = "finance.proformaConversionSubscriberRuntime";
|
|
21
|
+
export interface ProformaConversionSubscriberRuntime {
|
|
22
|
+
/** Operator invoicing mode; defaults to `direct` when unconfigured. */
|
|
23
|
+
resolveInvoicingMode(db: PostgresJsDatabase): Promise<InvoicingMode>;
|
|
24
|
+
/** Resolve the deployment database and retain ownership of its lifecycle. */
|
|
25
|
+
withDb<T>(bindings: unknown, operation: (db: PostgresJsDatabase) => Promise<T>): Promise<T>;
|
|
26
|
+
/** Event bus used to re-emit conversion events for downstream plugins. */
|
|
27
|
+
eventBus?: EventBus;
|
|
28
|
+
}
|
|
29
|
+
export interface ProformaConversionSubscriberDependencies {
|
|
30
|
+
logger?: Pick<Console, "error" | "info">;
|
|
31
|
+
}
|
|
32
|
+
/** Build the package-owned descriptor resolved by selected-graph lowering. */
|
|
33
|
+
export declare function createProformaConversionSubscriberRuntime(dependencies?: ProformaConversionSubscriberDependencies): SubscriberRuntimeDescriptor;
|
|
34
|
+
export declare const proformaConversionSubscriber: SubscriberRuntimeDescriptor;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { financeService } from "./service.js";
|
|
2
|
+
import { convertProformaToInvoice } from "./service-issue.js";
|
|
3
|
+
/**
|
|
4
|
+
* Standard proforma → fiscal invoice conversion.
|
|
5
|
+
*
|
|
6
|
+
* When the operator runs `invoicing.mode: "proforma-first"`, a proforma
|
|
7
|
+
* is issued at checkout and the fiscal invoice is minted once the
|
|
8
|
+
* proforma is fully settled. This subscriber watches settlement signals
|
|
9
|
+
* (`invoice.settled` from the settlement poller and
|
|
10
|
+
* `invoice.payment.recorded` from manual/processed payments) and, in
|
|
11
|
+
* proforma-first mode only, converts a fully-paid proforma to its fiscal
|
|
12
|
+
* invoice via {@link convertProformaToInvoice} (which copies lines,
|
|
13
|
+
* assigns the fiscal number, links both documents, and voids the
|
|
14
|
+
* proforma). `direct` mode is a no-op — zero behaviour change for
|
|
15
|
+
* existing deployments — and the conversion is idempotent (the service
|
|
16
|
+
* guards against double-conversion under an advisory lock).
|
|
17
|
+
*/
|
|
18
|
+
export const FINANCE_PROFORMA_CONVERSION_SUBSCRIBER_ID = "@voyant-travel/finance#subscriber.proforma-conversion";
|
|
19
|
+
export const PROFORMA_CONVERSION_SUBSCRIBER_RUNTIME_KEY = "finance.proformaConversionSubscriberRuntime";
|
|
20
|
+
const SETTLEMENT_EVENTS = ["invoice.settled", "invoice.payment.recorded"];
|
|
21
|
+
/** Build the package-owned descriptor resolved by selected-graph lowering. */
|
|
22
|
+
export function createProformaConversionSubscriberRuntime(dependencies = {}) {
|
|
23
|
+
const logger = dependencies.logger ?? console;
|
|
24
|
+
return {
|
|
25
|
+
id: FINANCE_PROFORMA_CONVERSION_SUBSCRIBER_ID,
|
|
26
|
+
eventType: "invoice.settled",
|
|
27
|
+
register: ({ bindings, container, eventBus }) => {
|
|
28
|
+
const handler = async ({ data }) => {
|
|
29
|
+
// Runtime is only registered when the operator-settings port is
|
|
30
|
+
// available. Absent → treat as `direct` mode → no-op.
|
|
31
|
+
if (!container.has(PROFORMA_CONVERSION_SUBSCRIBER_RUNTIME_KEY))
|
|
32
|
+
return;
|
|
33
|
+
const runtime = resolveProformaConversionSubscriberRuntime(container);
|
|
34
|
+
try {
|
|
35
|
+
await runtime.withDb(bindings, async (db) => {
|
|
36
|
+
if ((await runtime.resolveInvoicingMode(db)) !== "proforma-first")
|
|
37
|
+
return;
|
|
38
|
+
const invoice = await financeService.getInvoiceById(db, data.invoiceId);
|
|
39
|
+
if (!invoice)
|
|
40
|
+
return;
|
|
41
|
+
// Only fully-settled proformas convert. Partial payments leave
|
|
42
|
+
// the proforma outstanding until the balance is cleared.
|
|
43
|
+
if (invoice.invoiceType !== "proforma" || invoice.balanceDueCents !== 0)
|
|
44
|
+
return;
|
|
45
|
+
const result = await convertProformaToInvoice(db, invoice.id, {}, { eventBus: runtime.eventBus ?? eventBus });
|
|
46
|
+
if (result.status !== "ok" && result.status !== "already_converted") {
|
|
47
|
+
logger.error("[proforma-conversion] conversion did not complete", {
|
|
48
|
+
invoiceId: invoice.id,
|
|
49
|
+
status: result.status,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
logger.error("[proforma-conversion] failed to convert settled proforma", {
|
|
56
|
+
invoiceId: data.invoiceId,
|
|
57
|
+
error: error instanceof Error ? error.message : String(error),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
for (const eventType of SETTLEMENT_EVENTS) {
|
|
62
|
+
eventBus.subscribe(eventType, handler);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function resolveProformaConversionSubscriberRuntime(container) {
|
|
68
|
+
if (!container.has(PROFORMA_CONVERSION_SUBSCRIBER_RUNTIME_KEY)) {
|
|
69
|
+
throw new Error(`Proforma-conversion subscriber runtime is not registered at "${PROFORMA_CONVERSION_SUBSCRIBER_RUNTIME_KEY}".`);
|
|
70
|
+
}
|
|
71
|
+
return container.resolve(PROFORMA_CONVERSION_SUBSCRIBER_RUNTIME_KEY);
|
|
72
|
+
}
|
|
73
|
+
export const proformaConversionSubscriber = createProformaConversionSubscriberRuntime();
|
package/dist/runtime-port.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { VoyantRuntimeHostPrimitives } from "@voyant-travel/core";
|
|
2
2
|
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
3
3
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
4
|
-
import type { BookingTaxRouteOptions } from "./booking-tax.js";
|
|
4
|
+
import type { BookingTaxRouteOptions, FxReferenceSource } from "./booking-tax.js";
|
|
5
5
|
import type { CheckoutRoutesOptions } from "./checkout-routes.js";
|
|
6
6
|
import type { CheckoutPaymentStarter } from "./checkout-service.js";
|
|
7
7
|
import type { PaymentPolicy } from "./payment-policy.js";
|
|
@@ -17,11 +17,57 @@ export interface FinanceOperatorSettingsRuntime {
|
|
|
17
17
|
resolveOperatorDefaultPaymentPolicy: BookingScheduleRoutesOptions["resolveOperatorDefaultPaymentPolicy"];
|
|
18
18
|
resolveBookingTaxSettings: NonNullable<BookingTaxRouteOptions["resolveBookingTaxSettings"]>;
|
|
19
19
|
updateBookingTaxSettings: NonNullable<BookingTaxRouteOptions["updateBookingTaxSettings"]>;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the operator's invoicing mode (`direct` | `proforma-first`).
|
|
22
|
+
* Defaults to `direct` when unconfigured. The finance
|
|
23
|
+
* proforma-conversion subscriber reads this to decide whether a
|
|
24
|
+
* settled proforma should be auto-converted to a fiscal invoice.
|
|
25
|
+
*/
|
|
26
|
+
resolveInvoicingMode: (db: PostgresJsDatabase) => Promise<"direct" | "proforma-first">;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the operator's official FX reference-rate source
|
|
29
|
+
* (`ecb` | `bnr`, default `ecb`). Read by the finance fx-reference
|
|
30
|
+
* helper to pick the host-provided rate source for the operator.
|
|
31
|
+
*/
|
|
32
|
+
resolveFxReferenceSource: (db: PostgresJsDatabase) => Promise<FxReferenceSource>;
|
|
20
33
|
}
|
|
21
34
|
export interface FinanceNotificationsRuntime {
|
|
22
35
|
resolveNotificationDispatcher: NonNullable<CheckoutRoutesOptions["resolveNotificationDispatcher"]>;
|
|
23
36
|
listBookingReminderRuns: NonNullable<CheckoutRoutesOptions["listBookingReminderRuns"]>;
|
|
24
37
|
}
|
|
38
|
+
/** Request for one official FX reference rate on a given date. */
|
|
39
|
+
export interface FxReferenceRateRequest {
|
|
40
|
+
/** Currency to convert from, e.g. `EUR` (ISO 4217). */
|
|
41
|
+
base: string;
|
|
42
|
+
/** Currency to convert into, e.g. `RON` (ISO 4217). */
|
|
43
|
+
quote: string;
|
|
44
|
+
/** Reference date in `YYYY-MM-DD`. Defaults to the host's latest published rate. */
|
|
45
|
+
date?: string;
|
|
46
|
+
}
|
|
47
|
+
/** One resolved official FX reference rate. */
|
|
48
|
+
export interface FxReferenceRate {
|
|
49
|
+
/** Units of `quote` per one unit of `base`. */
|
|
50
|
+
rate: number;
|
|
51
|
+
/** The reference source that published the rate, e.g. `ecb` or `bnr`. */
|
|
52
|
+
source: FxReferenceSource;
|
|
53
|
+
/** Date the returned rate was published (`YYYY-MM-DD`). */
|
|
54
|
+
asOf: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Host-provided official FX reference-rate source. Finance defines the
|
|
58
|
+
* seam only; hosts/deployments wire it to their own FX data source. No
|
|
59
|
+
* HTTP client or API key lives inside finance.
|
|
60
|
+
*/
|
|
61
|
+
export interface FinanceFxReferenceRuntime {
|
|
62
|
+
/**
|
|
63
|
+
* Resolve one official reference rate. `source` is the operator's
|
|
64
|
+
* configured reference source (e.g. `ecb`, `bnr`); the host uses it
|
|
65
|
+
* to pick the correct published series.
|
|
66
|
+
*/
|
|
67
|
+
resolveReferenceRate(request: FxReferenceRateRequest & {
|
|
68
|
+
source: FxReferenceSource;
|
|
69
|
+
}): Promise<FxReferenceRate>;
|
|
70
|
+
}
|
|
25
71
|
export interface FinanceDistributionPaymentPolicyRuntime {
|
|
26
72
|
resolveSupplierPolicy: PolicyReader;
|
|
27
73
|
resolveSupplierPolicyById(db: PostgresJsDatabase, supplierId: string): Promise<PaymentPolicy | null>;
|
|
@@ -60,6 +106,35 @@ export interface FinanceBookingScheduleRuntime {
|
|
|
60
106
|
export declare const financeHostRuntimePort: import("@voyant-travel/core/project").VoyantPort<FinanceHostRuntime>;
|
|
61
107
|
export declare const financeOperatorSettingsRuntimePort: import("@voyant-travel/core/project").VoyantPort<FinanceOperatorSettingsRuntime>;
|
|
62
108
|
export declare const financeNotificationsRuntimePort: import("@voyant-travel/core/project").VoyantPort<FinanceNotificationsRuntime>;
|
|
109
|
+
export declare const financeFxReferenceRuntimePort: import("@voyant-travel/core/project").VoyantPort<FinanceFxReferenceRuntime>;
|
|
110
|
+
/**
|
|
111
|
+
* Raised when a caller explicitly requests an official FX reference
|
|
112
|
+
* rate but no `finance.fx-reference.runtime` provider is wired. Callers
|
|
113
|
+
* that never request a reference rate are unaffected — the seam is
|
|
114
|
+
* inert until used.
|
|
115
|
+
*/
|
|
116
|
+
export declare class FinanceFxReferenceSourceUnavailableError extends Error {
|
|
117
|
+
readonly code = "finance_fx_reference_source_unavailable";
|
|
118
|
+
constructor(source: FxReferenceSource);
|
|
119
|
+
}
|
|
120
|
+
export interface ResolveReferenceRateHelperInput {
|
|
121
|
+
base: string;
|
|
122
|
+
quote: string;
|
|
123
|
+
date?: string;
|
|
124
|
+
/** The operator's configured reference source (e.g. `ecb`, `bnr`). */
|
|
125
|
+
source: FxReferenceSource;
|
|
126
|
+
/** Host-provided implementation. Absent → typed unavailable error. */
|
|
127
|
+
provider?: FinanceFxReferenceRuntime | null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Typed helper that resolves an official FX reference rate for an
|
|
131
|
+
* operator's configured source by delegating to the host-provided
|
|
132
|
+
* `finance.fx-reference.runtime` implementation. When no provider is
|
|
133
|
+
* wired, throws {@link FinanceFxReferenceSourceUnavailableError} — a
|
|
134
|
+
* clear, typed signal rather than a silent fallback. Finance holds no
|
|
135
|
+
* FX data itself.
|
|
136
|
+
*/
|
|
137
|
+
export declare function resolveReferenceRate(input: ResolveReferenceRateHelperInput): Promise<FxReferenceRate>;
|
|
63
138
|
export declare const financeDistributionPaymentPolicyRuntimePort: import("@voyant-travel/core/project").VoyantPort<FinanceDistributionPaymentPolicyRuntime>;
|
|
64
139
|
export declare const financeAccommodationsPaymentPolicyRuntimePort: import("@voyant-travel/core/project").VoyantPort<FinanceAccommodationsPaymentPolicyRuntime>;
|
|
65
140
|
export declare const financeCruisesPaymentPolicyRuntimePort: import("@voyant-travel/core/project").VoyantPort<FinanceCruisesPaymentPolicyRuntime>;
|
package/dist/runtime-port.js
CHANGED
|
@@ -15,8 +15,43 @@ function objectPort(id, methods = []) {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
export const financeHostRuntimePort = objectPort("finance.host.runtime");
|
|
18
|
-
export const financeOperatorSettingsRuntimePort = objectPort("finance.operator-settings.runtime", [
|
|
18
|
+
export const financeOperatorSettingsRuntimePort = objectPort("finance.operator-settings.runtime", [
|
|
19
|
+
"resolveOperatorDefaultPaymentPolicy",
|
|
20
|
+
"resolveBookingTaxSettings",
|
|
21
|
+
"updateBookingTaxSettings",
|
|
22
|
+
"resolveInvoicingMode",
|
|
23
|
+
"resolveFxReferenceSource",
|
|
24
|
+
]);
|
|
19
25
|
export const financeNotificationsRuntimePort = objectPort("finance.notifications.runtime", ["resolveNotificationDispatcher", "listBookingReminderRuns"]);
|
|
26
|
+
export const financeFxReferenceRuntimePort = objectPort("finance.fx-reference.runtime", ["resolveReferenceRate"]);
|
|
27
|
+
/**
|
|
28
|
+
* Raised when a caller explicitly requests an official FX reference
|
|
29
|
+
* rate but no `finance.fx-reference.runtime` provider is wired. Callers
|
|
30
|
+
* that never request a reference rate are unaffected — the seam is
|
|
31
|
+
* inert until used.
|
|
32
|
+
*/
|
|
33
|
+
export class FinanceFxReferenceSourceUnavailableError extends Error {
|
|
34
|
+
code = "finance_fx_reference_source_unavailable";
|
|
35
|
+
constructor(source) {
|
|
36
|
+
super(`No FX reference-rate source is configured for "${source}". A host must provide the finance.fx-reference.runtime port.`);
|
|
37
|
+
this.name = "FinanceFxReferenceSourceUnavailableError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Typed helper that resolves an official FX reference rate for an
|
|
42
|
+
* operator's configured source by delegating to the host-provided
|
|
43
|
+
* `finance.fx-reference.runtime` implementation. When no provider is
|
|
44
|
+
* wired, throws {@link FinanceFxReferenceSourceUnavailableError} — a
|
|
45
|
+
* clear, typed signal rather than a silent fallback. Finance holds no
|
|
46
|
+
* FX data itself.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveReferenceRate(input) {
|
|
49
|
+
const { provider, source, base, quote, date } = input;
|
|
50
|
+
if (!provider) {
|
|
51
|
+
throw new FinanceFxReferenceSourceUnavailableError(source);
|
|
52
|
+
}
|
|
53
|
+
return provider.resolveReferenceRate({ base, quote, date, source });
|
|
54
|
+
}
|
|
20
55
|
export const financeDistributionPaymentPolicyRuntimePort = objectPort("finance.distribution-payment-policy.runtime", ["resolveSupplierPolicy", "resolveSupplierPolicyById"]);
|
|
21
56
|
export const financeAccommodationsPaymentPolicyRuntimePort = objectPort("finance.accommodations-payment-policy.runtime", ["resolveBookingPolicy", "resolveEntityPolicy"]);
|
|
22
57
|
export const financeCruisesPaymentPolicyRuntimePort = objectPort("finance.cruises-payment-policy.runtime", [
|
|
@@ -9,6 +9,7 @@ export const financePaymentSessionCompletionService = {
|
|
|
9
9
|
if (!session) {
|
|
10
10
|
return null;
|
|
11
11
|
}
|
|
12
|
+
const shouldEmitPaymentCompleted = data.status === "paid" && session.status !== "paid";
|
|
12
13
|
const txResult = await db.transaction(async (tx) => {
|
|
13
14
|
let authorizationId = session.paymentAuthorizationId;
|
|
14
15
|
let captureId = session.paymentCaptureId;
|
|
@@ -257,7 +258,7 @@ export const financePaymentSessionCompletionService = {
|
|
|
257
258
|
// Some aggregate flows, such as composed trips, intentionally use a
|
|
258
259
|
// generic target instead of booking/order/invoice columns; those still
|
|
259
260
|
// need the completion event keyed by targetType/targetId.
|
|
260
|
-
if (
|
|
261
|
+
if (shouldEmitPaymentCompleted && txResult.updated) {
|
|
261
262
|
await runtime.eventBus?.emit("payment.completed", buildPaymentCompletedEvent(txResult.updated), {
|
|
262
263
|
category: "domain",
|
|
263
264
|
source: "service",
|
package/dist/voyant.js
CHANGED
|
@@ -487,6 +487,15 @@ export const financeBookingScheduleVoyantPlugin = defineExtension({
|
|
|
487
487
|
export: "bookingScheduleConfirmedSubscriber",
|
|
488
488
|
},
|
|
489
489
|
},
|
|
490
|
+
{
|
|
491
|
+
id: "@voyant-travel/finance#subscriber.proforma-conversion",
|
|
492
|
+
eventType: "invoice.settled",
|
|
493
|
+
source: "@voyant-travel/finance/proforma-conversion-subscriber",
|
|
494
|
+
runtime: {
|
|
495
|
+
entry: "@voyant-travel/finance/proforma-conversion-subscriber",
|
|
496
|
+
export: "proformaConversionSubscriber",
|
|
497
|
+
},
|
|
498
|
+
},
|
|
490
499
|
],
|
|
491
500
|
meta: {
|
|
492
501
|
ownership: "package",
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"type": "object",
|
|
36
36
|
"properties": {
|
|
37
37
|
"taxPriceMode": { "type": "string", "enum": ["inclusive", "exclusive"] },
|
|
38
|
-
"taxPolicyProfileId": { "type": ["string", "null"] }
|
|
38
|
+
"taxPolicyProfileId": { "type": ["string", "null"] },
|
|
39
|
+
"invoicingMode": { "type": "string", "enum": ["direct", "proforma-first"] },
|
|
40
|
+
"fxReferenceSource": { "type": "string", "enum": ["ecb", "bnr"] }
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
}
|
|
@@ -75,10 +77,12 @@
|
|
|
75
77
|
"schemas": {
|
|
76
78
|
"BookingTaxSettings": {
|
|
77
79
|
"type": "object",
|
|
78
|
-
"required": ["taxPriceMode", "taxPolicyProfileId"],
|
|
80
|
+
"required": ["taxPriceMode", "taxPolicyProfileId", "invoicingMode", "fxReferenceSource"],
|
|
79
81
|
"properties": {
|
|
80
82
|
"taxPriceMode": { "type": "string", "enum": ["inclusive", "exclusive"] },
|
|
81
|
-
"taxPolicyProfileId": { "type": ["string", "null"] }
|
|
83
|
+
"taxPolicyProfileId": { "type": ["string", "null"] },
|
|
84
|
+
"invoicingMode": { "type": "string", "enum": ["direct", "proforma-first"] },
|
|
85
|
+
"fxReferenceSource": { "type": "string", "enum": ["ecb", "bnr"] }
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyant-travel/finance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.166.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -104,6 +104,11 @@
|
|
|
104
104
|
"import": "./dist/booking-schedule/subscriber-runtime.js",
|
|
105
105
|
"default": "./dist/booking-schedule/subscriber-runtime.js"
|
|
106
106
|
},
|
|
107
|
+
"./proforma-conversion-subscriber": {
|
|
108
|
+
"types": "./dist/proforma-conversion-runtime.d.ts",
|
|
109
|
+
"import": "./dist/proforma-conversion-runtime.js",
|
|
110
|
+
"default": "./dist/proforma-conversion-runtime.js"
|
|
111
|
+
},
|
|
107
112
|
"./routes": {
|
|
108
113
|
"types": "./dist/routes.d.ts",
|
|
109
114
|
"import": "./dist/routes.js",
|
|
@@ -146,16 +151,17 @@
|
|
|
146
151
|
"hono": "^4.12.27",
|
|
147
152
|
"zod": "^4.4.3",
|
|
148
153
|
"@voyant-travel/action-ledger": "^0.111.1",
|
|
149
|
-
"@voyant-travel/
|
|
150
|
-
"@voyant-travel/
|
|
151
|
-
"@voyant-travel/
|
|
152
|
-
"@voyant-travel/types": "^0.109.2",
|
|
153
|
-
"@voyant-travel/finance-contracts": "^0.106.2",
|
|
154
|
+
"@voyant-travel/db": "^0.114.10",
|
|
155
|
+
"@voyant-travel/types": "^0.109.3",
|
|
156
|
+
"@voyant-travel/bookings": "^0.166.0",
|
|
154
157
|
"@voyant-travel/hono": "^0.128.1",
|
|
155
|
-
"@voyant-travel/
|
|
158
|
+
"@voyant-travel/core": "^0.125.0",
|
|
159
|
+
"@voyant-travel/storage": "^0.111.1",
|
|
160
|
+
"@voyant-travel/payments": "^0.2.0",
|
|
156
161
|
"@voyant-travel/tools": "^0.3.0",
|
|
157
162
|
"@voyant-travel/public-document-delivery": "^0.4.1",
|
|
158
|
-
"@voyant-travel/
|
|
163
|
+
"@voyant-travel/finance-contracts": "^0.106.2",
|
|
164
|
+
"@voyant-travel/utils": "^0.107.1"
|
|
159
165
|
},
|
|
160
166
|
"devDependencies": {
|
|
161
167
|
"drizzle-kit": "^0.31.10",
|