@voyant-travel/finance 0.191.0 → 0.192.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/card-payment.d.ts +2 -0
  2. package/dist/card-payment.js +59 -43
  3. package/dist/checkout-routes.d.ts +4 -0
  4. package/dist/checkout-routes.js +6 -0
  5. package/dist/checkout-service.js +24 -14
  6. package/dist/checkout-validation.d.ts +10 -0
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +2 -1
  9. package/dist/payment-adapter-events.d.ts +96 -2
  10. package/dist/payment-adapter-events.js +150 -24
  11. package/dist/payment-adapter-session-guard.d.ts +18 -0
  12. package/dist/payment-adapter-session-guard.js +64 -0
  13. package/dist/payment-adapter-status.d.ts +61 -0
  14. package/dist/payment-adapter-status.js +49 -0
  15. package/dist/payment-link.js +29 -3
  16. package/dist/routes-booking-billing.d.ts +10 -0
  17. package/dist/routes-invoice-core.d.ts +5 -0
  18. package/dist/routes-invoice-schemas.d.ts +1 -0
  19. package/dist/routes-invoice-schemas.js +1 -0
  20. package/dist/routes-payment-processing.d.ts +18 -0
  21. package/dist/routes-payment-processing.js +1 -0
  22. package/dist/routes-public.d.ts +34 -0
  23. package/dist/routes-public.js +16 -1
  24. package/dist/routes.d.ts +33 -0
  25. package/dist/runtime.js +11 -0
  26. package/dist/schema/payment-sessions.d.ts +17 -0
  27. package/dist/schema/payment-sessions.js +2 -0
  28. package/dist/service-booking-billing.d.ts +3 -0
  29. package/dist/service-booking-guarantees.d.ts +1 -0
  30. package/dist/service-booking-payment-schedules.d.ts +2 -0
  31. package/dist/service-payment-processing.d.ts +10 -1
  32. package/dist/service-payment-session-completion.d.ts +10 -1
  33. package/dist/service-payment-session-completion.js +104 -24
  34. package/dist/service-payment-sessions.d.ts +8 -0
  35. package/dist/service-payment-sessions.js +27 -8
  36. package/dist/service-public.d.ts +4 -0
  37. package/dist/service-public.js +1 -0
  38. package/dist/service.d.ts +13 -1
  39. package/migrations/20260722053500_payment_session_provider_connection.sql +2 -0
  40. package/migrations/meta/_journal.json +8 -1
  41. package/package.json +13 -13
@@ -41,6 +41,8 @@ export interface CardPaymentStartArgs {
41
41
  billing: CardPaymentBilling;
42
42
  description?: string;
43
43
  returnUrl?: string;
44
+ cancelUrl?: string;
45
+ shipping?: Record<string, unknown>;
44
46
  metadata?: Record<string, unknown>;
45
47
  }
46
48
  /** Result of a card-payment start: the hosted-payment URL to redirect to. */
@@ -1,66 +1,82 @@
1
- import { eq } from "drizzle-orm";
1
+ import { and, eq, isNull, sql } from "drizzle-orm";
2
+ import { applyPaymentAdapterInitiationResult } from "./payment-adapter-events.js";
2
3
  import { paymentSessions } from "./schema/payment-sessions.js";
3
- import { financePaymentSessionCompletionService } from "./service-payment-session-completion.js";
4
- import { financePaymentSessionService } from "./service-payment-sessions.js";
4
+ const PAYMENT_ADAPTER_INITIATION_CLAIM_KEY = "paymentAdapterInitiationClaim";
5
+ const PAYMENT_ADAPTER_INITIATION_STATE_KEY = "paymentAdapterInitiationState";
5
6
  /**
6
7
  * Start a payment through a selected adapter without requiring an HTTP
7
8
  * framework context. Package runtimes use this function; the Hono-oriented
8
9
  * starter below remains the checkout-surface convenience wrapper.
9
10
  */
10
11
  export async function startPaymentAdapterCardPayment(adapter, args, execution) {
11
- const [session] = await args.db
12
+ const [initialSession] = await args.db
12
13
  .select()
13
14
  .from(paymentSessions)
14
15
  .where(eq(paymentSessions.id, args.sessionId))
15
16
  .limit(1);
16
- if (!session)
17
+ if (!initialSession)
17
18
  return null;
18
- const idempotencyKey = session.idempotencyKey ?? execution.idempotencyKey ?? `payment:${session.id}`;
19
+ const idempotencyKey = initialSession.idempotencyKey ?? execution.idempotencyKey ?? `payment:${initialSession.id}`;
20
+ const [session] = await args.db
21
+ .update(paymentSessions)
22
+ .set({
23
+ status: "processing",
24
+ idempotencyKey,
25
+ metadata: sql `coalesce(${paymentSessions.metadata}, '{}'::jsonb) || ${JSON.stringify({
26
+ [PAYMENT_ADAPTER_INITIATION_CLAIM_KEY]: idempotencyKey,
27
+ [PAYMENT_ADAPTER_INITIATION_STATE_KEY]: "in_flight",
28
+ })}::jsonb`,
29
+ updatedAt: new Date(),
30
+ })
31
+ .where(and(eq(paymentSessions.id, args.sessionId), eq(paymentSessions.status, "pending"), isNull(paymentSessions.providerConnectionId), isNull(paymentSessions.providerSessionId), isNull(paymentSessions.providerPaymentId)))
32
+ .returning();
33
+ if (!session) {
34
+ const [continuation] = await args.db
35
+ .select({ redirectUrl: paymentSessions.redirectUrl })
36
+ .from(paymentSessions)
37
+ .where(eq(paymentSessions.id, args.sessionId))
38
+ .limit(1);
39
+ return continuation ? { redirectUrl: continuation.redirectUrl } : null;
40
+ }
19
41
  const metadata = {
20
42
  ...(args.metadata ?? {}),
21
43
  billing: args.billing,
22
44
  ...(execution.notifyUrl ? { notifyUrl: execution.notifyUrl } : {}),
23
45
  };
24
- const result = await adapter.initiate(execution.context, {
25
- paymentSessionId: session.id,
26
- money: { amountMinor: session.amountCents, currency: session.currency },
27
- description: args.description ?? session.notes ?? undefined,
28
- returnUrl: args.returnUrl ?? session.returnUrl ?? undefined,
29
- idempotencyKey,
30
- customer: {
31
- email: args.billing.email,
32
- phone: args.billing.phone ?? null,
33
- firstName: args.billing.firstName,
34
- lastName: args.billing.lastName ?? null,
35
- },
36
- metadata,
37
- });
38
- const providerData = {
39
- providerSessionId: result.processorSessionId ?? undefined,
40
- providerPaymentId: result.processorPaymentId ?? undefined,
41
- providerPayload: result.raw === undefined ? undefined : { initiation: result.raw },
42
- metadata: { paymentAdapterInitiationIdempotencyKey: result.idempotencyKey },
43
- };
44
- if (result.nextState === "paid" || result.nextState === "authorized") {
45
- await financePaymentSessionService.updatePaymentSession(args.db, session.id, {
46
- provider: adapter.id,
47
- redirectUrl: result.checkout?.url ?? undefined,
46
+ let result;
47
+ try {
48
+ result = await adapter.initiate(execution.context, {
49
+ paymentSessionId: session.id,
50
+ money: { amountMinor: session.amountCents, currency: session.currency },
51
+ description: args.description ?? session.notes ?? undefined,
52
+ returnUrl: args.returnUrl ?? session.returnUrl ?? undefined,
53
+ cancelUrl: args.cancelUrl ?? session.cancelUrl ?? undefined,
48
54
  idempotencyKey,
55
+ customer: {
56
+ email: args.billing.email,
57
+ phone: args.billing.phone ?? null,
58
+ firstName: args.billing.firstName,
59
+ lastName: args.billing.lastName ?? null,
60
+ },
61
+ shipping: args.shipping,
62
+ metadata,
49
63
  });
50
- await financePaymentSessionCompletionService.completePaymentSession(args.db, session.id, {
51
- status: result.nextState,
52
- captureMode: "automatic",
53
- ...providerData,
54
- }, execution.runtime);
64
+ await applyPaymentAdapterInitiationResult(args.db, session.id, adapter.id, result, { idempotencyKey, claimedAt: session.updatedAt }, execution.runtime);
55
65
  }
56
- else {
57
- await financePaymentSessionService.updatePaymentSession(args.db, session.id, {
58
- provider: adapter.id,
59
- status: result.nextState,
60
- redirectUrl: result.checkout?.url ?? undefined,
61
- idempotencyKey,
62
- ...providerData,
63
- });
66
+ catch (error) {
67
+ const retrySafe = adapter.capabilities.idempotencyKeys && adapter.capabilities.retrySafeInitiation;
68
+ await args.db
69
+ .update(paymentSessions)
70
+ .set({
71
+ status: retrySafe ? "pending" : "processing",
72
+ metadata: sql `coalesce(${paymentSessions.metadata}, '{}'::jsonb) || ${JSON.stringify({
73
+ [PAYMENT_ADAPTER_INITIATION_CLAIM_KEY]: retrySafe ? null : idempotencyKey,
74
+ [PAYMENT_ADAPTER_INITIATION_STATE_KEY]: retrySafe ? "retryable" : "uncertain",
75
+ })}::jsonb`,
76
+ updatedAt: new Date(),
77
+ })
78
+ .where(and(eq(paymentSessions.id, session.id), eq(paymentSessions.status, "processing"), eq(paymentSessions.idempotencyKey, idempotencyKey), eq(paymentSessions.updatedAt, session.updatedAt), isNull(paymentSessions.providerConnectionId), isNull(paymentSessions.providerSessionId), isNull(paymentSessions.providerPaymentId), sql `${paymentSessions.metadata}->>${PAYMENT_ADAPTER_INITIATION_CLAIM_KEY} = ${idempotencyKey}`));
79
+ throw error;
64
80
  }
65
81
  return { redirectUrl: result.checkout?.url ?? null };
66
82
  }
@@ -281,6 +281,7 @@ export declare function createCheckoutRoutes(options?: CheckoutRoutesOptions): i
281
281
  targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
282
282
  targetId: string | null;
283
283
  paymentId: string | null;
284
+ providerConnectionId: string | null;
284
285
  providerSessionId: string | null;
285
286
  providerPaymentId: string | null;
286
287
  externalReference: string | null;
@@ -520,6 +521,7 @@ export declare function createCheckoutRoutes(options?: CheckoutRoutesOptions): i
520
521
  targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
521
522
  targetId: string | null;
522
523
  paymentId: string | null;
524
+ providerConnectionId: string | null;
523
525
  providerSessionId: string | null;
524
526
  providerPaymentId: string | null;
525
527
  externalReference: string | null;
@@ -853,6 +855,7 @@ export declare function createCheckoutAdminRoutes(options?: CheckoutRoutesOption
853
855
  targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
854
856
  targetId: string | null;
855
857
  paymentId: string | null;
858
+ providerConnectionId: string | null;
856
859
  providerSessionId: string | null;
857
860
  providerPaymentId: string | null;
858
861
  externalReference: string | null;
@@ -1092,6 +1095,7 @@ export declare function createCheckoutAdminRoutes(options?: CheckoutRoutesOption
1092
1095
  targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
1093
1096
  targetId: string | null;
1094
1097
  paymentId: string | null;
1098
+ providerConnectionId: string | null;
1095
1099
  providerSessionId: string | null;
1096
1100
  providerPaymentId: string | null;
1097
1101
  externalReference: string | null;
@@ -121,6 +121,12 @@ function resolveCheckoutRouteRuntime(bindings, options, container) {
121
121
  return buildCheckoutRouteRuntime(bindings, options);
122
122
  }
123
123
  function assertCheckoutRuntimeSupportsCollection(runtime, input) {
124
+ if (input.method === "card" && input.startProvider && !input.startProvider.provider) {
125
+ if (!runtime.selectedPaymentStarter) {
126
+ throw new CheckoutRouteRuntimeNotConfiguredError();
127
+ }
128
+ return;
129
+ }
124
130
  if (input.method === "card" &&
125
131
  !runtime.selectedPaymentStarter &&
126
132
  Object.keys(runtime.paymentStarters).length === 0) {
@@ -190,6 +190,27 @@ async function createCollectionInvoice(db, context, plan, notes) {
190
190
  return invoice;
191
191
  }
192
192
  export async function initiateCheckoutCollection(db, bookingId, input, options = {}, runtime = {}) {
193
+ let providerStarter = null;
194
+ if (input.startProvider) {
195
+ if (input.method !== "card") {
196
+ throw new Error("Provider start is only available for card collections");
197
+ }
198
+ // Resolve deployment-owned processor readiness before any checkout read
199
+ // that may materialize a default payment plan. The selected PaymentAdapter
200
+ // wins over legacy provider hints; keyed starters remain only for explicit
201
+ // self-hosted provider requests. A missing adapter is a zero-mutation
202
+ // failure.
203
+ const requestedProvider = input.startProvider.provider;
204
+ providerStarter =
205
+ runtime.selectedPaymentStarter ??
206
+ (requestedProvider ? runtime.paymentStarters?.[requestedProvider] : undefined) ??
207
+ null;
208
+ if (!providerStarter) {
209
+ throw new Error(requestedProvider
210
+ ? `Payment provider "${requestedProvider}" is not configured`
211
+ : "No payment adapter is selected for card collection");
212
+ }
213
+ }
193
214
  const context = await loadBookingContext(db, bookingId);
194
215
  if (!context)
195
216
  return null;
@@ -250,24 +271,13 @@ export async function initiateCheckoutCollection(db, bookingId, input, options =
250
271
  }
251
272
  }
252
273
  if (input.startProvider) {
253
- if (input.method !== "card") {
254
- throw new Error("Provider start is only available for card collections");
255
- }
256
274
  if (!paymentSession) {
257
275
  throw new Error("No payment session available for provider start");
258
276
  }
259
- // Processor selection belongs to the deployment. A selected PaymentAdapter
260
- // therefore wins even when an older client still sends a provider hint.
261
- // Keyed starters remain as a compatibility path for self-hosted plugins.
262
- const requestedProvider = input.startProvider.provider;
263
- const starter = runtime.selectedPaymentStarter ??
264
- (requestedProvider ? runtime.paymentStarters?.[requestedProvider] : undefined);
265
- if (!starter) {
266
- throw new Error(requestedProvider
267
- ? `Payment provider "${requestedProvider}" is not configured`
268
- : "No payment adapter is selected for card collection");
277
+ if (!providerStarter) {
278
+ throw new Error("No payment adapter is selected for card collection");
269
279
  }
270
- providerStart = await starter({
280
+ providerStart = await providerStarter({
271
281
  db,
272
282
  bookingId,
273
283
  plan,
@@ -249,6 +249,7 @@ export declare const initiateCheckoutCollectionSchema: z.ZodObject<{
249
249
  idempotencyKey: z.ZodNullable<z.ZodOptional<z.ZodString>>;
250
250
  }, z.core.$strip>>;
251
251
  provider: z.ZodNullable<z.ZodOptional<z.ZodString>>;
252
+ providerConnectionId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
252
253
  paymentMethod: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
253
254
  bank_transfer: "bank_transfer";
254
255
  credit_card: "credit_card";
@@ -322,6 +323,7 @@ export declare const initiateCheckoutCollectionSchema: z.ZodObject<{
322
323
  idempotencyKey: z.ZodNullable<z.ZodOptional<z.ZodString>>;
323
324
  }, z.core.$strip>>;
324
325
  provider: z.ZodNullable<z.ZodOptional<z.ZodString>>;
326
+ providerConnectionId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
325
327
  paymentMethod: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
326
328
  bank_transfer: "bank_transfer";
327
329
  credit_card: "credit_card";
@@ -501,6 +503,7 @@ export declare const bootstrapCheckoutCollectionSchema: z.ZodObject<{
501
503
  idempotencyKey: z.ZodNullable<z.ZodOptional<z.ZodString>>;
502
504
  }, z.core.$strip>>;
503
505
  provider: z.ZodNullable<z.ZodOptional<z.ZodString>>;
506
+ providerConnectionId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
504
507
  paymentMethod: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
505
508
  bank_transfer: "bank_transfer";
506
509
  credit_card: "credit_card";
@@ -574,6 +577,7 @@ export declare const bootstrapCheckoutCollectionSchema: z.ZodObject<{
574
577
  idempotencyKey: z.ZodNullable<z.ZodOptional<z.ZodString>>;
575
578
  }, z.core.$strip>>;
576
579
  provider: z.ZodNullable<z.ZodOptional<z.ZodString>>;
580
+ providerConnectionId: z.ZodNullable<z.ZodOptional<z.ZodString>>;
577
581
  paymentMethod: z.ZodNullable<z.ZodOptional<z.ZodEnum<{
578
582
  bank_transfer: "bank_transfer";
579
583
  credit_card: "credit_card";
@@ -954,6 +958,7 @@ export declare const initiatedCheckoutCollectionSchema: z.ZodObject<{
954
958
  expired: "expired";
955
959
  }>;
956
960
  provider: z.ZodNullable<z.ZodString>;
961
+ providerConnectionId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
957
962
  providerSessionId: z.ZodNullable<z.ZodString>;
958
963
  providerPaymentId: z.ZodNullable<z.ZodString>;
959
964
  externalReference: z.ZodNullable<z.ZodString>;
@@ -983,6 +988,7 @@ export declare const initiatedCheckoutCollectionSchema: z.ZodObject<{
983
988
  notes: z.ZodNullable<z.ZodString>;
984
989
  }, z.core.$strip>, z.ZodTransform<{
985
990
  legacyOrderId: string | null;
991
+ providerConnectionId: string | null;
986
992
  target: {
987
993
  type: "booking";
988
994
  bookingId: string;
@@ -1113,6 +1119,7 @@ export declare const initiatedCheckoutCollectionSchema: z.ZodObject<{
1113
1119
  idempotencyKey?: string | null | undefined;
1114
1120
  } | null | undefined;
1115
1121
  legacyOrderId?: string | null | undefined;
1122
+ providerConnectionId?: string | null | undefined;
1116
1123
  }>>>;
1117
1124
  invoiceNotification: z.ZodNullable<z.ZodObject<{
1118
1125
  id: z.ZodString;
@@ -1331,6 +1338,7 @@ export declare const bootstrappedCheckoutCollectionSchema: z.ZodObject<{
1331
1338
  expired: "expired";
1332
1339
  }>;
1333
1340
  provider: z.ZodNullable<z.ZodString>;
1341
+ providerConnectionId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
1334
1342
  providerSessionId: z.ZodNullable<z.ZodString>;
1335
1343
  providerPaymentId: z.ZodNullable<z.ZodString>;
1336
1344
  externalReference: z.ZodNullable<z.ZodString>;
@@ -1360,6 +1368,7 @@ export declare const bootstrappedCheckoutCollectionSchema: z.ZodObject<{
1360
1368
  notes: z.ZodNullable<z.ZodString>;
1361
1369
  }, z.core.$strip>, z.ZodTransform<{
1362
1370
  legacyOrderId: string | null;
1371
+ providerConnectionId: string | null;
1363
1372
  target: {
1364
1373
  type: "booking";
1365
1374
  bookingId: string;
@@ -1490,6 +1499,7 @@ export declare const bootstrappedCheckoutCollectionSchema: z.ZodObject<{
1490
1499
  idempotencyKey?: string | null | undefined;
1491
1500
  } | null | undefined;
1492
1501
  legacyOrderId?: string | null | undefined;
1502
+ providerConnectionId?: string | null | undefined;
1493
1503
  }>>>;
1494
1504
  invoiceNotification: z.ZodNullable<z.ZodObject<{
1495
1505
  id: z.ZodString;
package/dist/index.d.ts CHANGED
@@ -33,7 +33,8 @@ export { createPaymentAdapterCardPaymentStarter, startPaymentAdapterCardPayment,
33
33
  export { type DocumentDownloadEnvelope, type DocumentDownloadResolution, type DocumentDownloadResolver, resolveStoredDocumentDownload, type StoredDocumentReference, } from "./document-download.js";
34
34
  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";
35
35
  export { type CreateOrderPaymentSessionsOptions, createOrderPaymentSessions, type EnsureOrderSessionParams, type OrderPaymentSessionSummary, type OrderPaymentSessions, type OrderPaymentSessionTargetType, type StartOrderPaymentProvider, } from "./order-payment-sessions.js";
36
- export { applyPaymentAdapterCallbackEvent } from "./payment-adapter-events.js";
36
+ export { applyPaymentAdapterCallbackEvent, applyPaymentAdapterInitiationResult, applyPaymentAdapterStatusResult, } from "./payment-adapter-events.js";
37
+ export { type PaymentAdapterStatusRefreshExecution, refreshPaymentAdapterStatus, } from "./payment-adapter-status.js";
37
38
  export type { ComputedScheduleEntry, ComputeScheduleInput, DepositKind, DepositRule, PaymentPolicy, PaymentPolicyCascadeLayers, PaymentPolicySource, PaymentScheduleEntryType, ResolvedPaymentPolicy, } from "./payment-policy.js";
38
39
  export { computePaymentSchedule, isPaymentPolicyEmpty, noDepositPolicy, normalizePaymentPolicy, policyShouldRequireFullPayment, resolveEffectivePaymentPolicy, } from "./payment-policy.js";
39
40
  export { createPaymentPolicyCascade, type PaymentPolicyCascade, type PaymentPolicyCascadeOptions, type PaymentPolicyCascadeReaders, readPolicySourceFromInternalNotes, stampPolicySourceOnBooking, } from "./payment-policy-cascade.js";
package/dist/index.js CHANGED
@@ -98,7 +98,8 @@ export { createPaymentAdapterCardPaymentStarter, startPaymentAdapterCardPayment,
98
98
  export { resolveStoredDocumentDownload, } from "./document-download.js";
99
99
  export { createInvoiceFxApiExtension, createInvoiceFxRoutes, createVoyantDataFxExchangeRateResolver, mountInvoiceFxRoutes, resolveInvoiceFxContext, resolveInvoiceFxSettingsOrDefault, } from "./invoice-fx.js";
100
100
  export { createOrderPaymentSessions, } from "./order-payment-sessions.js";
101
- export { applyPaymentAdapterCallbackEvent } from "./payment-adapter-events.js";
101
+ export { applyPaymentAdapterCallbackEvent, applyPaymentAdapterInitiationResult, applyPaymentAdapterStatusResult, } from "./payment-adapter-events.js";
102
+ export { refreshPaymentAdapterStatus, } from "./payment-adapter-status.js";
102
103
  export { computePaymentSchedule, isPaymentPolicyEmpty, noDepositPolicy, normalizePaymentPolicy, policyShouldRequireFullPayment, resolveEffectivePaymentPolicy, } from "./payment-policy.js";
103
104
  export { createPaymentPolicyCascade, readPolicySourceFromInternalNotes, stampPolicySourceOnBooking, } from "./payment-policy-cascade.js";
104
105
  export { createBookingScheduleAdminRoutes, createBookingScheduleApiExtension, createBookingScheduleVoyantRuntime, createPaymentPolicyPublicRoutes, generatePaymentScheduleForBooking, } from "./payment-schedule/routes.js";
@@ -1,6 +1,54 @@
1
- import type { PaymentCallbackEvent } from "@voyant-travel/payments";
1
+ import type { PaymentCallbackEvent, PaymentInitiationResult, PaymentStatusResult } from "@voyant-travel/payments";
2
2
  import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
- import type { FinanceServiceRuntime } from "./service-shared.js";
3
+ import { type FinanceServiceRuntime } from "./service-shared.js";
4
+ export declare function applyPaymentAdapterInitiationResult(db: PostgresJsDatabase, paymentSessionId: string, adapterId: string, result: PaymentInitiationResult, claim: {
5
+ idempotencyKey: string;
6
+ claimedAt: Date;
7
+ }, runtime?: FinanceServiceRuntime): Promise<{
8
+ id: string;
9
+ targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
10
+ targetId: string | null;
11
+ bookingId: string | null;
12
+ orderId: string | null;
13
+ invoiceId: string | null;
14
+ bookingPaymentScheduleId: string | null;
15
+ bookingGuaranteeId: string | null;
16
+ paymentInstrumentId: string | null;
17
+ paymentAuthorizationId: string | null;
18
+ paymentCaptureId: string | null;
19
+ paymentId: string | null;
20
+ status: "paid" | "pending" | "failed" | "requires_redirect" | "processing" | "authorized" | "cancelled" | "expired";
21
+ provider: string | null;
22
+ providerConnectionId: string | null;
23
+ providerSessionId: string | null;
24
+ providerPaymentId: string | null;
25
+ externalReference: string | null;
26
+ idempotencyKey: string | null;
27
+ clientReference: string | null;
28
+ currency: string;
29
+ amountCents: number;
30
+ paymentMethod: "bank_transfer" | "credit_card" | "debit_card" | "cash" | "cheque" | "wallet" | "direct_bill" | "travel_credit" | "other" | null;
31
+ payerPersonId: string | null;
32
+ payerOrganizationId: string | null;
33
+ payerEmail: string | null;
34
+ payerName: string | null;
35
+ redirectUrl: string | null;
36
+ returnUrl: string | null;
37
+ cancelUrl: string | null;
38
+ callbackUrl: string | null;
39
+ expiresAt: Date | null;
40
+ completedAt: Date | null;
41
+ failedAt: Date | null;
42
+ cancelledAt: Date | null;
43
+ expiredAt: Date | null;
44
+ failureCode: string | null;
45
+ failureMessage: string | null;
46
+ notes: string | null;
47
+ providerPayload: Record<string, unknown> | null;
48
+ metadata: Record<string, unknown> | null;
49
+ createdAt: Date;
50
+ updatedAt: Date;
51
+ } | null>;
4
52
  export declare function applyPaymentAdapterCallbackEvent(db: PostgresJsDatabase, event: PaymentCallbackEvent, runtime?: FinanceServiceRuntime): Promise<{
5
53
  id: string;
6
54
  targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
@@ -16,6 +64,52 @@ export declare function applyPaymentAdapterCallbackEvent(db: PostgresJsDatabase,
16
64
  paymentId: string | null;
17
65
  status: "paid" | "pending" | "failed" | "requires_redirect" | "processing" | "authorized" | "cancelled" | "expired";
18
66
  provider: string | null;
67
+ providerConnectionId: string | null;
68
+ providerSessionId: string | null;
69
+ providerPaymentId: string | null;
70
+ externalReference: string | null;
71
+ idempotencyKey: string | null;
72
+ clientReference: string | null;
73
+ currency: string;
74
+ amountCents: number;
75
+ paymentMethod: "bank_transfer" | "credit_card" | "debit_card" | "cash" | "cheque" | "wallet" | "direct_bill" | "travel_credit" | "other" | null;
76
+ payerPersonId: string | null;
77
+ payerOrganizationId: string | null;
78
+ payerEmail: string | null;
79
+ payerName: string | null;
80
+ redirectUrl: string | null;
81
+ returnUrl: string | null;
82
+ cancelUrl: string | null;
83
+ callbackUrl: string | null;
84
+ expiresAt: Date | null;
85
+ completedAt: Date | null;
86
+ failedAt: Date | null;
87
+ cancelledAt: Date | null;
88
+ expiredAt: Date | null;
89
+ failureCode: string | null;
90
+ failureMessage: string | null;
91
+ notes: string | null;
92
+ providerPayload: Record<string, unknown> | null;
93
+ metadata: Record<string, unknown> | null;
94
+ createdAt: Date;
95
+ updatedAt: Date;
96
+ } | null>;
97
+ export declare function applyPaymentAdapterStatusResult(db: PostgresJsDatabase, paymentSessionId: string, result: PaymentStatusResult, statusLeaseToken: string, runtime?: FinanceServiceRuntime, checkedAt?: Date): Promise<{
98
+ id: string;
99
+ targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee" | "flight_order";
100
+ targetId: string | null;
101
+ bookingId: string | null;
102
+ orderId: string | null;
103
+ invoiceId: string | null;
104
+ bookingPaymentScheduleId: string | null;
105
+ bookingGuaranteeId: string | null;
106
+ paymentInstrumentId: string | null;
107
+ paymentAuthorizationId: string | null;
108
+ paymentCaptureId: string | null;
109
+ paymentId: string | null;
110
+ status: "paid" | "pending" | "failed" | "requires_redirect" | "processing" | "authorized" | "cancelled" | "expired";
111
+ provider: string | null;
112
+ providerConnectionId: string | null;
19
113
  providerSessionId: string | null;
20
114
  providerPaymentId: string | null;
21
115
  externalReference: string | null;
@@ -1,34 +1,160 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { assertPaymentAdapterProcessorIdentityForLockedSession, assertPaymentAdapterProcessorReferencesForLockedSession, canApplyPaymentAdapterStateTransition, PAYMENT_ADAPTER_STATUS_LEASE_TOKEN_KEY, } from "./payment-adapter-session-guard.js";
3
+ import { paymentSessions } from "./schema/payment-sessions.js";
1
4
  import { financePaymentSessionCompletionService } from "./service-payment-session-completion.js";
2
- import { financePaymentSessionService } from "./service-payment-sessions.js";
5
+ import { PaymentValidationError, sql, toTimestamp, touchLinkedBookingUpdatedAt, } from "./service-shared.js";
6
+ function mergeJsonbColumn(column, value) {
7
+ if (value === undefined)
8
+ return undefined;
9
+ if (value === null)
10
+ return null;
11
+ return sql `coalesce(${column}, '{}'::jsonb) || ${JSON.stringify(value)}::jsonb`;
12
+ }
13
+ const PAYMENT_ADAPTER_INITIATION_CLAIM_KEY = "paymentAdapterInitiationClaim";
14
+ async function applyLockedNonCompletionStateUpdate(db, update, providerData) {
15
+ return db.transaction(async (tx) => {
16
+ const [session] = await tx
17
+ .select()
18
+ .from(paymentSessions)
19
+ .where(eq(paymentSessions.id, update.paymentSessionId))
20
+ .for("update")
21
+ .limit(1);
22
+ if (!session)
23
+ return null;
24
+ if (update.source === "status" &&
25
+ session.metadata?.[PAYMENT_ADAPTER_STATUS_LEASE_TOKEN_KEY] !== update.statusLeaseToken) {
26
+ return null;
27
+ }
28
+ const adoptedIdentity = assertPaymentAdapterProcessorIdentityForLockedSession(session, update.processorIdentity);
29
+ const pinnedReferences = assertPaymentAdapterProcessorReferencesForLockedSession(session, {
30
+ processorSessionId: update.processorSessionId,
31
+ processorPaymentId: update.processorPaymentId,
32
+ });
33
+ const provider = adoptedIdentity.provider ?? session.provider ?? providerData.provider;
34
+ const providerConnectionId = adoptedIdentity.providerConnectionId ?? providerData.providerConnectionId;
35
+ const nextState = update.nextState;
36
+ const mayFinalizeUncontestedInitiationClaim = update.source === "initiation" &&
37
+ session.status === "processing" &&
38
+ (nextState === "pending" || nextState === "requires_redirect") &&
39
+ session.metadata?.[PAYMENT_ADAPTER_INITIATION_CLAIM_KEY] === update.idempotencyKey &&
40
+ session.updatedAt.getTime() === update.initiationClaimedAt?.getTime() &&
41
+ !session.providerConnectionId &&
42
+ !session.providerSessionId &&
43
+ !session.providerPaymentId;
44
+ const shouldTransition = mayFinalizeUncontestedInitiationClaim ||
45
+ canApplyPaymentAdapterStateTransition(session.status, nextState);
46
+ const failedAt = shouldTransition && nextState === "failed" ? new Date() : undefined;
47
+ const cancelledAt = shouldTransition && nextState === "cancelled" ? new Date() : undefined;
48
+ const expiredAt = shouldTransition && nextState === "expired" ? new Date() : undefined;
49
+ const [updated] = await tx
50
+ .update(paymentSessions)
51
+ .set({
52
+ status: shouldTransition ? nextState : undefined,
53
+ provider,
54
+ providerConnectionId,
55
+ providerSessionId: pinnedReferences.providerSessionId,
56
+ providerPaymentId: pinnedReferences.providerPaymentId,
57
+ providerPayload: mergeJsonbColumn(paymentSessions.providerPayload, providerData.providerPayload),
58
+ metadata: mergeJsonbColumn(paymentSessions.metadata, providerData.metadata),
59
+ redirectUrl: update.redirectUrl,
60
+ idempotencyKey: update.idempotencyKey,
61
+ failedAt,
62
+ cancelledAt,
63
+ expiredAt,
64
+ failureCode: shouldTransition && nextState === "failed"
65
+ ? `payment_adapter_${update.source}`
66
+ : undefined,
67
+ failureMessage: shouldTransition && nextState === "failed"
68
+ ? `Payment adapter ${update.source} mapped this session to failed.`
69
+ : undefined,
70
+ completedAt: shouldTransition && nextState === "paid" ? new Date() : undefined,
71
+ expiresAt: shouldTransition && nextState === "expired" ? toTimestamp(update.occurredAt) : undefined,
72
+ updatedAt: new Date(),
73
+ })
74
+ .where(eq(paymentSessions.id, update.paymentSessionId))
75
+ .returning();
76
+ await touchLinkedBookingUpdatedAt(tx, updated?.bookingId);
77
+ return updated ?? null;
78
+ });
79
+ }
80
+ async function applyPaymentAdapterStateUpdate(db, update, providerData, runtime) {
81
+ if (update.nextState === "paid" || update.nextState === "authorized") {
82
+ return financePaymentSessionCompletionService.completePaymentSession(db, update.paymentSessionId, {
83
+ status: update.nextState,
84
+ captureMode: "automatic",
85
+ ...providerData,
86
+ }, runtime, {
87
+ requireProcessorIdentityWhenConnectionPinned: true,
88
+ expectedPaymentAdapterStatusLeaseToken: update.statusLeaseToken,
89
+ sessionUpdate: {
90
+ redirectUrl: update.redirectUrl,
91
+ idempotencyKey: update.idempotencyKey,
92
+ },
93
+ });
94
+ }
95
+ return applyLockedNonCompletionStateUpdate(db, update, providerData);
96
+ }
97
+ export async function applyPaymentAdapterInitiationResult(db, paymentSessionId, adapterId, result, claim, runtime = {}) {
98
+ if (result.idempotencyKey !== claim.idempotencyKey) {
99
+ throw new PaymentValidationError("Payment adapter initiation returned a different idempotency key", { paymentSessionId, expectedIdempotencyKey: claim.idempotencyKey }, { status: 409, code: "payment_adapter_idempotency_mismatch" });
100
+ }
101
+ return applyPaymentAdapterStateUpdate(db, {
102
+ source: "initiation",
103
+ paymentSessionId,
104
+ nextState: result.nextState,
105
+ occurredAt: new Date().toISOString(),
106
+ processorIdentity: result.processorIdentity,
107
+ processorSessionId: result.processorSessionId,
108
+ processorPaymentId: result.processorPaymentId,
109
+ redirectUrl: result.checkout?.url ?? null,
110
+ idempotencyKey: result.idempotencyKey,
111
+ initiationClaimedAt: claim.claimedAt,
112
+ }, {
113
+ provider: result.processorIdentity?.providerId ?? adapterId,
114
+ providerConnectionId: result.processorIdentity?.connectionId,
115
+ providerSessionId: result.processorSessionId ?? undefined,
116
+ providerPaymentId: result.processorPaymentId ?? undefined,
117
+ providerPayload: result.raw === undefined ? undefined : { initiation: result.raw },
118
+ metadata: {
119
+ paymentAdapterInitiationClaim: null,
120
+ paymentAdapterInitiationIdempotencyKey: result.idempotencyKey,
121
+ paymentAdapterInitiationState: "complete",
122
+ },
123
+ }, runtime);
124
+ }
3
125
  export async function applyPaymentAdapterCallbackEvent(db, event, runtime = {}) {
126
+ const update = { ...event, source: "callback" };
4
127
  const providerData = {
128
+ provider: event.processorIdentity?.providerId,
129
+ providerConnectionId: event.processorIdentity?.connectionId,
5
130
  providerSessionId: event.processorSessionId ?? undefined,
6
131
  providerPaymentId: event.processorPaymentId ?? undefined,
7
132
  providerPayload: event.raw === undefined ? undefined : { callback: event.raw },
8
133
  metadata: { paymentAdapterEventId: event.eventId, paymentAdapterOccurredAt: event.occurredAt },
9
134
  };
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,
135
+ return applyPaymentAdapterStateUpdate(db, update, providerData, runtime);
136
+ }
137
+ export async function applyPaymentAdapterStatusResult(db, paymentSessionId, result, statusLeaseToken, runtime = {}, checkedAt = new Date()) {
138
+ const occurredAt = checkedAt.toISOString();
139
+ return applyPaymentAdapterStateUpdate(db, {
140
+ source: "status",
141
+ paymentSessionId,
142
+ nextState: result.nextState,
143
+ occurredAt,
144
+ processorIdentity: result.processorIdentity,
145
+ processorSessionId: result.processorSessionId,
146
+ processorPaymentId: result.processorPaymentId,
147
+ statusLeaseToken,
148
+ }, {
149
+ provider: result.processorIdentity?.providerId,
150
+ providerConnectionId: result.processorIdentity?.connectionId,
151
+ providerSessionId: result.processorSessionId ?? undefined,
152
+ providerPaymentId: result.processorPaymentId ?? undefined,
153
+ providerPayload: result.raw === undefined ? undefined : { status: result.raw },
154
+ metadata: {
155
+ paymentAdapterStatusCheckedAt: occurredAt,
156
+ paymentAdapterStatusRefreshAfter: checkedAt.getTime() + 30_000,
157
+ [PAYMENT_ADAPTER_STATUS_LEASE_TOKEN_KEY]: null,
158
+ },
33
159
  }, runtime);
34
160
  }