@voyant-travel/commerce 0.2.2 → 0.3.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/checkout/acceptance-signature.d.ts +4 -0
- package/dist/checkout/acceptance-signature.d.ts.map +1 -0
- package/dist/checkout/acceptance-signature.js +95 -0
- package/dist/checkout/finalize.d.ts +42 -0
- package/dist/checkout/finalize.d.ts.map +1 -0
- package/dist/checkout/finalize.js +208 -0
- package/dist/checkout/index.d.ts +26 -0
- package/dist/checkout/index.d.ts.map +1 -0
- package/dist/checkout/index.js +24 -0
- package/dist/checkout/materialization-support.d.ts +105 -0
- package/dist/checkout/materialization-support.d.ts.map +1 -0
- package/dist/checkout/materialization-support.js +451 -0
- package/dist/checkout/materialization-support.test.d.ts +2 -0
- package/dist/checkout/materialization-support.test.d.ts.map +1 -0
- package/dist/checkout/materialization-support.test.js +196 -0
- package/dist/checkout/materialization-tax.d.ts +10 -0
- package/dist/checkout/materialization-tax.d.ts.map +1 -0
- package/dist/checkout/materialization-tax.js +113 -0
- package/dist/checkout/materialization-tax.test.d.ts +2 -0
- package/dist/checkout/materialization-tax.test.d.ts.map +1 -0
- package/dist/checkout/materialization-tax.test.js +69 -0
- package/dist/checkout/materialization.d.ts +99 -0
- package/dist/checkout/materialization.d.ts.map +1 -0
- package/dist/checkout/materialization.js +269 -0
- package/dist/checkout/options.d.ts +89 -0
- package/dist/checkout/options.d.ts.map +1 -0
- package/dist/checkout/options.js +21 -0
- package/dist/checkout/routes.d.ts +21 -0
- package/dist/checkout/routes.d.ts.map +1 -0
- package/dist/checkout/routes.js +59 -0
- package/dist/checkout/start-service.d.ts +75 -0
- package/dist/checkout/start-service.d.ts.map +1 -0
- package/dist/checkout/start-service.js +415 -0
- package/dist/checkout/start-service.test.d.ts +2 -0
- package/dist/checkout/start-service.test.d.ts.map +1 -0
- package/dist/checkout/start-service.test.js +57 -0
- package/dist/markets/routes.d.ts +1 -1
- package/dist/markets/service-core.d.ts +1 -1
- package/dist/pricing/routes-public.d.ts.map +1 -1
- package/dist/pricing/routes-public.js +12 -2
- package/dist/sellability/routes.d.ts +10 -10
- package/dist/sellability/service-records.d.ts +4 -4
- package/dist/sellability/service-snapshots.d.ts +2 -2
- package/dist/sellability/service.d.ts +10 -10
- package/package.json +28 -6
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EventBus } from "@voyant-travel/core";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
export declare function persistAcceptanceSignature(db: PostgresJsDatabase, contractId: string, eventBus?: EventBus): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=acceptance-signature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acceptance-signature.d.ts","sourceRoot":"","sources":["../../src/checkout/acceptance-signature.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAEnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAoCjE,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,IAAI,CAAC,CAoFf"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { bookings } from "@voyant-travel/bookings/schema";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
const ACCEPTANCE_MARKER_PREFIX = "__contract_acceptance__:";
|
|
4
|
+
function readContractAcceptance(contractMetadata, internalNotesFallback) {
|
|
5
|
+
if (contractMetadata && typeof contractMetadata === "object") {
|
|
6
|
+
const meta = contractMetadata;
|
|
7
|
+
if (meta.acceptance && typeof meta.acceptance === "object") {
|
|
8
|
+
return meta.acceptance;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (!internalNotesFallback)
|
|
12
|
+
return null;
|
|
13
|
+
for (const line of internalNotesFallback.split("\n")) {
|
|
14
|
+
if (!line.startsWith(ACCEPTANCE_MARKER_PREFIX))
|
|
15
|
+
continue;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(line.slice(ACCEPTANCE_MARKER_PREFIX.length));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Bad marker - try next line.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export async function persistAcceptanceSignature(db, contractId, eventBus) {
|
|
26
|
+
const { contractsService } = await import("@voyant-travel/legal/contracts");
|
|
27
|
+
const { contracts: contractsTable } = await import("@voyant-travel/legal/contracts");
|
|
28
|
+
const [contract] = await db
|
|
29
|
+
.select()
|
|
30
|
+
.from(contractsTable)
|
|
31
|
+
.where(eq(contractsTable.id, contractId))
|
|
32
|
+
.limit(1);
|
|
33
|
+
if (!contract?.bookingId)
|
|
34
|
+
return;
|
|
35
|
+
const [booking] = await db
|
|
36
|
+
.select()
|
|
37
|
+
.from(bookings)
|
|
38
|
+
.where(eq(bookings.id, contract.bookingId))
|
|
39
|
+
.limit(1);
|
|
40
|
+
if (!booking)
|
|
41
|
+
return;
|
|
42
|
+
const acceptance = readContractAcceptance(contract.metadata, booking.internalNotes);
|
|
43
|
+
if (!acceptance)
|
|
44
|
+
return;
|
|
45
|
+
const existing = await contractsService.listSignatures(db, contractId);
|
|
46
|
+
if (existing.length > 0)
|
|
47
|
+
return;
|
|
48
|
+
if (contract.status === "issued") {
|
|
49
|
+
const sent = await contractsService.sendContract(db, contractId, { eventBus });
|
|
50
|
+
if (sent.status !== "sent") {
|
|
51
|
+
console.warn(`[catalog-checkout] could not send contract before acceptance signature for ${contractId}: ${sent.status}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const contactName = [booking.contactFirstName, booking.contactLastName]
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join(" ")
|
|
58
|
+
.trim();
|
|
59
|
+
const signerName = contactName ||
|
|
60
|
+
`Storefront customer${booking.bookingNumber ? ` (${booking.bookingNumber})` : ""}`;
|
|
61
|
+
const result = await contractsService.signContract(db, contractId, {
|
|
62
|
+
signerName,
|
|
63
|
+
signerEmail: booking.contactEmail ?? null,
|
|
64
|
+
method: "electronic",
|
|
65
|
+
ipAddress: acceptance.clientIp ? acceptance.clientIp.slice(0, 64) : null,
|
|
66
|
+
userAgent: acceptance.userAgent ? acceptance.userAgent.slice(0, 500) : null,
|
|
67
|
+
metadata: {
|
|
68
|
+
source: "storefront-checkout",
|
|
69
|
+
templateId: acceptance.templateId,
|
|
70
|
+
templateSlug: acceptance.templateSlug,
|
|
71
|
+
acceptedAt: acceptance.acceptedAt,
|
|
72
|
+
acceptedMarketing: acceptance.acceptedMarketing,
|
|
73
|
+
renderedHtmlLength: acceptance.renderedHtmlLength,
|
|
74
|
+
},
|
|
75
|
+
}, { eventBus });
|
|
76
|
+
if (result.status !== "signed") {
|
|
77
|
+
console.warn(`[catalog-checkout] could not record acceptance signature for ${contractId}: ${result.status}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (booking.internalNotes?.includes(ACCEPTANCE_MARKER_PREFIX)) {
|
|
81
|
+
const cleanedNotes = booking.internalNotes
|
|
82
|
+
.split("\n")
|
|
83
|
+
.filter((line) => !line.startsWith(ACCEPTANCE_MARKER_PREFIX))
|
|
84
|
+
.join("\n")
|
|
85
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
86
|
+
.trim();
|
|
87
|
+
await db
|
|
88
|
+
.update(bookings)
|
|
89
|
+
.set({
|
|
90
|
+
internalNotes: cleanedNotes.length > 0 ? cleanedNotes : null,
|
|
91
|
+
updatedAt: new Date(),
|
|
92
|
+
})
|
|
93
|
+
.where(eq(bookings.id, booking.id));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type CheckoutFinalizeInput } from "@voyant-travel/catalog/booking-engine";
|
|
2
|
+
import type { EventBus } from "@voyant-travel/core";
|
|
3
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
4
|
+
/**
|
|
5
|
+
* Optional callback that generates (or fetches existing) the contract PDF for
|
|
6
|
+
* a booking. Wired by the deployment and forwarded into the explicit
|
|
7
|
+
* `generate_contract_pdf` workflow step. The deployment supplies its
|
|
8
|
+
* platform bindings (`env`) when constructing it, so this package-level type
|
|
9
|
+
* only carries the booking-scoped inputs the step needs.
|
|
10
|
+
*/
|
|
11
|
+
export type CatalogCheckoutContractPdfGenerator = (input: {
|
|
12
|
+
db: PostgresJsDatabase;
|
|
13
|
+
eventBus: EventBus;
|
|
14
|
+
bookingId: string;
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
contractId: string;
|
|
17
|
+
attachmentId: string;
|
|
18
|
+
} | null>;
|
|
19
|
+
export interface DispatchCheckoutFinalizeParams {
|
|
20
|
+
db: PostgresJsDatabase;
|
|
21
|
+
eventBus: EventBus;
|
|
22
|
+
input: CheckoutFinalizeInput;
|
|
23
|
+
trigger: string;
|
|
24
|
+
correlationId: string | null;
|
|
25
|
+
tags: ReadonlyArray<string>;
|
|
26
|
+
parentRunId?: string | null;
|
|
27
|
+
triggeredByUserId?: string | null;
|
|
28
|
+
resumeFromStep?: string;
|
|
29
|
+
seedResults?: Record<string, unknown>;
|
|
30
|
+
generateContractPdf?: CatalogCheckoutContractPdfGenerator;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Run the checkout-finalize workflow for a booking: record a workflow run,
|
|
34
|
+
* build the finalize deps (confirm booking, issue invoice, link payments,
|
|
35
|
+
* generate contract PDF), execute `runCheckoutFinalize`, and mark the run
|
|
36
|
+
* complete/failed. The deployment owns the db + event bus + (optional)
|
|
37
|
+
* contract-pdf generator; this is the reusable saga driver.
|
|
38
|
+
*/
|
|
39
|
+
export declare function dispatchCheckoutFinalize(params: DispatchCheckoutFinalizeParams): Promise<{
|
|
40
|
+
runId: string;
|
|
41
|
+
}>;
|
|
42
|
+
//# sourceMappingURL=finalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"finalize.d.ts","sourceRoot":"","sources":["../../src/checkout/finalize.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,qBAAqB,EAE3B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAInD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE;;;;;;GAMG;AACH,MAAM,MAAM,mCAAmC,GAAG,CAAC,KAAK,EAAE;IACxD,EAAE,EAAE,kBAAkB,CAAA;IACtB,QAAQ,EAAE,QAAQ,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB,KAAK,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAAA;AAuKlE,MAAM,WAAW,8BAA8B;IAC7C,EAAE,EAAE,kBAAkB,CAAA;IACtB,QAAQ,EAAE,QAAQ,CAAA;IAClB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,mBAAmB,CAAC,EAAE,mCAAmC,CAAA;CAC1D;AAMD;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,8BAA8B,GACrC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgE5B"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// agent-quality: file-size exception -- owner: commerce; the checkout-finalize
|
|
2
|
+
// step wiring (confirm booking, issue invoice, link payments, contract PDF) is
|
|
3
|
+
// one cohesive workflow definition; splitting it would scatter a single saga.
|
|
4
|
+
import { bookingsService } from "@voyant-travel/bookings";
|
|
5
|
+
import { bookingActivityLog, bookings } from "@voyant-travel/bookings/schema";
|
|
6
|
+
import { runCheckoutFinalize, } from "@voyant-travel/catalog/booking-engine";
|
|
7
|
+
import { issueInvoiceFromBooking } from "@voyant-travel/finance";
|
|
8
|
+
import { beginWorkflowRun } from "@voyant-travel/workflow-runs";
|
|
9
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
10
|
+
function buildCheckoutFinalizeDeps(db, eventBus, recorder, generateContractPdf) {
|
|
11
|
+
return {
|
|
12
|
+
db,
|
|
13
|
+
eventBus,
|
|
14
|
+
recorder: {
|
|
15
|
+
startStep: (name) => {
|
|
16
|
+
void recorder.startStep(name);
|
|
17
|
+
},
|
|
18
|
+
completeStep: (name, output) => {
|
|
19
|
+
void recorder.completeStep(name, output ?? null);
|
|
20
|
+
},
|
|
21
|
+
failStep: (name, error) => {
|
|
22
|
+
void recorder.failStep(name, error);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
confirmBooking: async (bookingId) => {
|
|
26
|
+
const result = await bookingsService.confirmBooking(db, bookingId, {}, undefined, {
|
|
27
|
+
eventBus,
|
|
28
|
+
});
|
|
29
|
+
if (result.status === "ok")
|
|
30
|
+
return;
|
|
31
|
+
if (result.status === "hold_expired") {
|
|
32
|
+
const recovered = await bookingsService.recoverExpiredPaidBooking(db, bookingId, { note: "Recovered after late payment completion" }, undefined, { eventBus });
|
|
33
|
+
if (recovered.status === "ok")
|
|
34
|
+
return;
|
|
35
|
+
throw new Error(`checkout-finalize: late payment recovery failed (${recovered.status})`);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`checkout-finalize: booking confirmation failed (${result.status})`);
|
|
38
|
+
},
|
|
39
|
+
issueInvoice: async ({ bookingId, convertedFromInvoiceId }) => {
|
|
40
|
+
const [booking] = await db.select().from(bookings).where(eq(bookings.id, bookingId)).limit(1);
|
|
41
|
+
if (!booking)
|
|
42
|
+
return null;
|
|
43
|
+
const { bookingItems } = await import("@voyant-travel/bookings/schema");
|
|
44
|
+
const items = await db
|
|
45
|
+
.select()
|
|
46
|
+
.from(bookingItems)
|
|
47
|
+
.where(eq(bookingItems.bookingId, bookingId));
|
|
48
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
49
|
+
const dueDate = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
50
|
+
const invoice = await issueInvoiceFromBooking(db, {
|
|
51
|
+
bookingId,
|
|
52
|
+
issueDate: today,
|
|
53
|
+
dueDate,
|
|
54
|
+
invoiceType: "invoice",
|
|
55
|
+
notes: convertedFromInvoiceId
|
|
56
|
+
? `Converted from proforma ${convertedFromInvoiceId}`
|
|
57
|
+
: null,
|
|
58
|
+
}, {
|
|
59
|
+
booking: {
|
|
60
|
+
id: booking.id,
|
|
61
|
+
bookingNumber: booking.bookingNumber,
|
|
62
|
+
personId: booking.personId,
|
|
63
|
+
organizationId: booking.organizationId,
|
|
64
|
+
sellCurrency: booking.sellCurrency,
|
|
65
|
+
baseCurrency: booking.baseCurrency,
|
|
66
|
+
fxRateSetId: null,
|
|
67
|
+
sellAmountCents: booking.sellAmountCents,
|
|
68
|
+
baseSellAmountCents: booking.baseSellAmountCents,
|
|
69
|
+
},
|
|
70
|
+
items: items.map((item) => ({
|
|
71
|
+
id: item.id,
|
|
72
|
+
title: item.title,
|
|
73
|
+
quantity: item.quantity,
|
|
74
|
+
unitSellAmountCents: item.unitSellAmountCents,
|
|
75
|
+
totalSellAmountCents: item.totalSellAmountCents,
|
|
76
|
+
})),
|
|
77
|
+
}, { eventBus });
|
|
78
|
+
if (invoice && convertedFromInvoiceId) {
|
|
79
|
+
const { invoices } = await import("@voyant-travel/finance");
|
|
80
|
+
await db.update(invoices).set({ convertedFromInvoiceId }).where(eq(invoices.id, invoice.id));
|
|
81
|
+
}
|
|
82
|
+
return invoice ? { invoiceId: invoice.id } : null;
|
|
83
|
+
},
|
|
84
|
+
findProformaForBooking: async (bookingId) => {
|
|
85
|
+
const { invoices } = await import("@voyant-travel/finance");
|
|
86
|
+
const [proforma] = await db
|
|
87
|
+
.select({ id: invoices.id })
|
|
88
|
+
.from(invoices)
|
|
89
|
+
.where(eq(invoices.bookingId, bookingId))
|
|
90
|
+
.limit(1);
|
|
91
|
+
return proforma ? { invoiceId: proforma.id } : null;
|
|
92
|
+
},
|
|
93
|
+
generateContractPdf: generateContractPdf
|
|
94
|
+
? async ({ bookingId }) => generateContractPdf({ db, eventBus, bookingId })
|
|
95
|
+
: undefined,
|
|
96
|
+
linkPaymentToInvoice: async ({ bookingId, invoiceId, paymentSessionId }) => {
|
|
97
|
+
const { paymentSessions } = await import("@voyant-travel/finance/schema");
|
|
98
|
+
const { financeService } = await import("@voyant-travel/finance");
|
|
99
|
+
const paidSessions = await db
|
|
100
|
+
.select()
|
|
101
|
+
.from(paymentSessions)
|
|
102
|
+
.where(and(eq(paymentSessions.bookingId, bookingId), eq(paymentSessions.status, "paid"), isNull(paymentSessions.invoiceId)));
|
|
103
|
+
let firstPaymentId = null;
|
|
104
|
+
let sessionsLinked = 0;
|
|
105
|
+
for (const session of paidSessions) {
|
|
106
|
+
await db
|
|
107
|
+
.update(paymentSessions)
|
|
108
|
+
.set({ invoiceId, updatedAt: new Date() })
|
|
109
|
+
.where(eq(paymentSessions.id, session.id));
|
|
110
|
+
const payment = await financeService.createPayment(db, invoiceId, {
|
|
111
|
+
amountCents: session.amountCents,
|
|
112
|
+
currency: session.currency,
|
|
113
|
+
paymentMethod: session.paymentMethod ?? "credit_card",
|
|
114
|
+
paymentInstrumentId: session.paymentInstrumentId ?? null,
|
|
115
|
+
paymentAuthorizationId: session.paymentAuthorizationId ?? null,
|
|
116
|
+
paymentCaptureId: session.paymentCaptureId ?? null,
|
|
117
|
+
status: "completed",
|
|
118
|
+
referenceNumber: session.providerPaymentId ??
|
|
119
|
+
session.externalReference ??
|
|
120
|
+
session.providerSessionId ??
|
|
121
|
+
session.id,
|
|
122
|
+
paymentDate: (session.completedAt ?? new Date()).toISOString().slice(0, 10),
|
|
123
|
+
notes: `Checkout-finalize linkage from session ${session.id}` +
|
|
124
|
+
(paymentSessionId && session.id !== paymentSessionId
|
|
125
|
+
? ` (workflow input session: ${paymentSessionId})`
|
|
126
|
+
: ""),
|
|
127
|
+
});
|
|
128
|
+
if (payment?.id) {
|
|
129
|
+
await db
|
|
130
|
+
.update(paymentSessions)
|
|
131
|
+
.set({ paymentId: payment.id, updatedAt: new Date() })
|
|
132
|
+
.where(eq(paymentSessions.id, session.id));
|
|
133
|
+
if (!firstPaymentId)
|
|
134
|
+
firstPaymentId = payment.id;
|
|
135
|
+
}
|
|
136
|
+
sessionsLinked++;
|
|
137
|
+
}
|
|
138
|
+
return { paymentId: firstPaymentId, sessionsLinked };
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function checkoutFinalizeInputRecord(input) {
|
|
143
|
+
return { ...input };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Run the checkout-finalize workflow for a booking: record a workflow run,
|
|
147
|
+
* build the finalize deps (confirm booking, issue invoice, link payments,
|
|
148
|
+
* generate contract PDF), execute `runCheckoutFinalize`, and mark the run
|
|
149
|
+
* complete/failed. The deployment owns the db + event bus + (optional)
|
|
150
|
+
* contract-pdf generator; this is the reusable saga driver.
|
|
151
|
+
*/
|
|
152
|
+
export async function dispatchCheckoutFinalize(params) {
|
|
153
|
+
const recorder = await beginWorkflowRun(params.db, {
|
|
154
|
+
workflowName: "checkout-finalize",
|
|
155
|
+
trigger: params.trigger,
|
|
156
|
+
correlationId: params.correlationId ?? null,
|
|
157
|
+
tags: [...params.tags],
|
|
158
|
+
input: checkoutFinalizeInputRecord(params.input),
|
|
159
|
+
parentRunId: params.parentRunId ?? null,
|
|
160
|
+
triggeredByUserId: params.triggeredByUserId ?? null,
|
|
161
|
+
resumeFromStep: params.resumeFromStep ?? null,
|
|
162
|
+
});
|
|
163
|
+
if (params.parentRunId) {
|
|
164
|
+
try {
|
|
165
|
+
const action = params.resumeFromStep ? "resumed" : "rerun";
|
|
166
|
+
const description = params.resumeFromStep
|
|
167
|
+
? `Workflow checkout-finalize ${action} from step "${params.resumeFromStep}"`
|
|
168
|
+
: `Workflow checkout-finalize ${action}`;
|
|
169
|
+
await params.db.insert(bookingActivityLog).values({
|
|
170
|
+
bookingId: params.input.bookingId,
|
|
171
|
+
actorId: params.triggeredByUserId ?? null,
|
|
172
|
+
activityType: "system_action",
|
|
173
|
+
description,
|
|
174
|
+
metadata: {
|
|
175
|
+
kind: "workflow_rerun",
|
|
176
|
+
workflowName: "checkout-finalize",
|
|
177
|
+
parentRunId: params.parentRunId,
|
|
178
|
+
newRunId: recorder.runId,
|
|
179
|
+
resumeFromStep: params.resumeFromStep ?? null,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.warn("[catalog-checkout] failed to write rerun activity log", err);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (params.resumeFromStep && params.seedResults) {
|
|
188
|
+
for (const [stepName, output] of Object.entries(params.seedResults)) {
|
|
189
|
+
if (stepName === "__deps")
|
|
190
|
+
continue;
|
|
191
|
+
await recorder.recordSkippedStep(stepName, output && typeof output === "object" ? output : null);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const deps = buildCheckoutFinalizeDeps(params.db, params.eventBus, recorder, params.generateContractPdf);
|
|
195
|
+
try {
|
|
196
|
+
await runCheckoutFinalize(params.input, deps, {
|
|
197
|
+
skipUntil: params.resumeFromStep,
|
|
198
|
+
seedResults: params.seedResults,
|
|
199
|
+
});
|
|
200
|
+
await recorder.complete();
|
|
201
|
+
return { runId: recorder.runId };
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
console.error("[catalog-checkout] checkout-finalize workflow failed", err);
|
|
205
|
+
await recorder.fail(err);
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-checkout business-logic cluster, owned by `@voyant-travel/commerce`.
|
|
3
|
+
*
|
|
4
|
+
* The reusable orchestration (snapshot→booking materialization, tax-line
|
|
5
|
+
* derivation, acceptance signature promotion, checkout-start service, the
|
|
6
|
+
* checkout-finalize saga driver, and the public checkout route) lives here.
|
|
7
|
+
* Deployment-specific dependencies are injected via the options interfaces in
|
|
8
|
+
* `./options.js`:
|
|
9
|
+
* - `resolveBookingTaxSettings` — operator tax-mode/profile reads.
|
|
10
|
+
* - `getOwnedProductName` — owned product title (injected to avoid a cycle
|
|
11
|
+
* through `@voyant-travel/inventory`, which depends on commerce).
|
|
12
|
+
* - `resolveBankTransferInstructions` — operator profile / payment rows.
|
|
13
|
+
*
|
|
14
|
+
* The deployment keeps the thin HonoBundle wiring (workflow runner registry +
|
|
15
|
+
* event-bus subscribers) and calls `dispatchCheckoutFinalize` /
|
|
16
|
+
* `persistAcceptanceSignature` from there.
|
|
17
|
+
*/
|
|
18
|
+
export { persistAcceptanceSignature } from "./acceptance-signature.js";
|
|
19
|
+
export { type CatalogCheckoutContractPdfGenerator, type DispatchCheckoutFinalizeParams, dispatchCheckoutFinalize, } from "./finalize.js";
|
|
20
|
+
export { type DraftPayload, type MaterializationSnapshot, materializeBookingFromSnapshot, rebuildBookingItemTaxLines, } from "./materialization.js";
|
|
21
|
+
export { extractBookingDates, extractItemDates, extractItemDescription, inferSnapshotTaxFacts, materializeBookingAllocations, materializeTravelerTravelDetails, resolveLineItemTitle, resolveSupplierFromSnapshot, resolveUpstreamCostCents, travelerBandToCategory, } from "./materialization-support.js";
|
|
22
|
+
export { materializeBookingItemTaxLine } from "./materialization-tax.js";
|
|
23
|
+
export type { CheckoutBankTransferInstructions, CheckoutModuleOptions, CheckoutStartOptions, } from "./options.js";
|
|
24
|
+
export { createCatalogCheckoutRoutes } from "./routes.js";
|
|
25
|
+
export { type CatalogCheckoutStartContext, CatalogCheckoutStartError, type CatalogCheckoutStartResult, type CheckoutStartInput, type CheckoutStartRequestMeta, checkoutStartSchema, startCatalogCheckout, } from "./start-service.js";
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/checkout/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AACtE,OAAO,EACL,KAAK,mCAAmC,EACxC,KAAK,8BAA8B,EACnC,wBAAwB,GACzB,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC5B,8BAA8B,EAC9B,0BAA0B,GAC3B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,EACrB,6BAA6B,EAC7B,gCAAgC,EAChC,oBAAoB,EACpB,2BAA2B,EAC3B,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAA;AACxE,YAAY,EACV,gCAAgC,EAChC,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,EACL,KAAK,2BAA2B,EAChC,yBAAyB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC7B,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,oBAAoB,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-checkout business-logic cluster, owned by `@voyant-travel/commerce`.
|
|
3
|
+
*
|
|
4
|
+
* The reusable orchestration (snapshot→booking materialization, tax-line
|
|
5
|
+
* derivation, acceptance signature promotion, checkout-start service, the
|
|
6
|
+
* checkout-finalize saga driver, and the public checkout route) lives here.
|
|
7
|
+
* Deployment-specific dependencies are injected via the options interfaces in
|
|
8
|
+
* `./options.js`:
|
|
9
|
+
* - `resolveBookingTaxSettings` — operator tax-mode/profile reads.
|
|
10
|
+
* - `getOwnedProductName` — owned product title (injected to avoid a cycle
|
|
11
|
+
* through `@voyant-travel/inventory`, which depends on commerce).
|
|
12
|
+
* - `resolveBankTransferInstructions` — operator profile / payment rows.
|
|
13
|
+
*
|
|
14
|
+
* The deployment keeps the thin HonoBundle wiring (workflow runner registry +
|
|
15
|
+
* event-bus subscribers) and calls `dispatchCheckoutFinalize` /
|
|
16
|
+
* `persistAcceptanceSignature` from there.
|
|
17
|
+
*/
|
|
18
|
+
export { persistAcceptanceSignature } from "./acceptance-signature.js";
|
|
19
|
+
export { dispatchCheckoutFinalize, } from "./finalize.js";
|
|
20
|
+
export { materializeBookingFromSnapshot, rebuildBookingItemTaxLines, } from "./materialization.js";
|
|
21
|
+
export { extractBookingDates, extractItemDates, extractItemDescription, inferSnapshotTaxFacts, materializeBookingAllocations, materializeTravelerTravelDetails, resolveLineItemTitle, resolveSupplierFromSnapshot, resolveUpstreamCostCents, travelerBandToCategory, } from "./materialization-support.js";
|
|
22
|
+
export { materializeBookingItemTaxLine } from "./materialization-tax.js";
|
|
23
|
+
export { createCatalogCheckoutRoutes } from "./routes.js";
|
|
24
|
+
export { CatalogCheckoutStartError, checkoutStartSchema, startCatalogCheckout, } from "./start-service.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { bookings } from "@voyant-travel/bookings/schema";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
import type { DraftPayload, MaterializationSnapshot } from "./materialization.js";
|
|
4
|
+
import type { CheckoutModuleOptions } from "./options.js";
|
|
5
|
+
interface InsertedBookingItem {
|
|
6
|
+
id: string;
|
|
7
|
+
quantity?: number | null;
|
|
8
|
+
optionId?: string | null;
|
|
9
|
+
optionUnitId?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function materializeBookingAllocations(db: PostgresJsDatabase, booking: typeof bookings.$inferSelect, insertedItems: ReadonlyArray<InsertedBookingItem>, draftPayload: DraftPayload, snapshot: MaterializationSnapshot): Promise<void>;
|
|
12
|
+
export declare function inferSnapshotTaxFacts(snapshot: MaterializationSnapshot): {
|
|
13
|
+
hasAccommodation: boolean;
|
|
14
|
+
accommodationCountries: string[];
|
|
15
|
+
};
|
|
16
|
+
export declare function materializeTravelerTravelDetails(db: PostgresJsDatabase, insertedTravelers: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
}>, draftTravelers: NonNullable<DraftPayload["travelers"]>, env: Record<string, unknown>): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve supplier info for the booking from the catalog snapshot.
|
|
21
|
+
* Pulls from:
|
|
22
|
+
* 1. `catalog_sourced_entries.projection.supplierId` — supplier
|
|
23
|
+
* name/id captured at sync time (covers Bokun, demo adapter, etc.).
|
|
24
|
+
* 2. The frozen payload's `upstream_payload.supplierId` — fallback
|
|
25
|
+
* when the sourced-entries row is missing (legacy bookings).
|
|
26
|
+
* 3. `frozen_payload.reserve.orderId` — used as `supplierReference`
|
|
27
|
+
* so operators can match up against the upstream provider's
|
|
28
|
+
* booking reference.
|
|
29
|
+
*
|
|
30
|
+
* Returns null when no supplier can be resolved — the caller treats
|
|
31
|
+
* that as "skip auto-fill, leave blank for manual entry".
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveSupplierFromSnapshot(db: PostgresJsDatabase, snapshot: MaterializationSnapshot): Promise<{
|
|
34
|
+
serviceName: string;
|
|
35
|
+
supplierReference: string | null;
|
|
36
|
+
supplierServiceId: string | null;
|
|
37
|
+
upstreamCostCents: number | null;
|
|
38
|
+
} | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve booking-level dates from the draft and frozen source data.
|
|
41
|
+
* `start_date`/`end_date` drive the admin booking header, while item
|
|
42
|
+
* dates drive the line table. A storefront product selection usually
|
|
43
|
+
* carries only `departureSlotId`, so we resolve that id against the
|
|
44
|
+
* quote/reserve/content payload before falling back to free-form dates.
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractBookingDates(snapshot: Pick<MaterializationSnapshot, "frozen_payload">, draftPayload: DraftPayload): {
|
|
47
|
+
startDate: string | null;
|
|
48
|
+
endDate: string | null;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Pull start/end dates for a booking-item from the most reliable
|
|
52
|
+
* source available. Order:
|
|
53
|
+
* 1. The selected `departureSlotId` resolved against reserve /
|
|
54
|
+
* quote / captured content payloads.
|
|
55
|
+
* 2. `frozen_payload.quote.upstream_payload.metadata.days[]` —
|
|
56
|
+
* Bokun-style itinerary captured at quote time, gives us per-day
|
|
57
|
+
* dates with full timezone fidelity.
|
|
58
|
+
* 3. Draft `configure.dateRange.checkIn`/`checkOut` — what the
|
|
59
|
+
* customer selected on the storefront before booking.
|
|
60
|
+
* 4. Draft `configure.departureDate` — single-day tour selection.
|
|
61
|
+
* 5. Booking row's own `start_date` / `end_date` columns — the
|
|
62
|
+
* caller already populated these from the same draft when
|
|
63
|
+
* writing the booking row, so this is a final safety net.
|
|
64
|
+
*
|
|
65
|
+
* Returns nulls when nothing resolves — the caller treats that as
|
|
66
|
+
* "no date data, leave NULL" rather than fabricating one.
|
|
67
|
+
*/
|
|
68
|
+
export declare function extractItemDates(snapshot: MaterializationSnapshot, draftPayload: DraftPayload, booking: typeof bookings.$inferSelect): {
|
|
69
|
+
serviceDate: string | null;
|
|
70
|
+
startsAt: Date | null;
|
|
71
|
+
endsAt: Date | null;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Pull a description for the booking item from the upstream payload.
|
|
75
|
+
* Sourced products typically carry rich descriptions on the upstream
|
|
76
|
+
* entry; surfacing a short snippet on the item helps operators scan
|
|
77
|
+
* a multi-item booking without clicking into each line.
|
|
78
|
+
*/
|
|
79
|
+
export declare function extractItemDescription(snapshot: MaterializationSnapshot): string | null;
|
|
80
|
+
/**
|
|
81
|
+
* Look up the upstream cost (net rate the operator pays the supplier)
|
|
82
|
+
* for a sourced entity. Returns null when the adapter doesn't expose
|
|
83
|
+
* a net/gross split — caller falls back to sell-as-cost (zero-markup
|
|
84
|
+
* default).
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveUpstreamCostCents(db: PostgresJsDatabase, snapshot: MaterializationSnapshot): Promise<number | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a human title for the booking line item. Tries:
|
|
89
|
+
* 1. `catalog_sourced_entries.projection.name` — sourced products
|
|
90
|
+
* (demo, Bokun, …) all carry the upstream title there.
|
|
91
|
+
* 2. The injected `getOwnedProductName` — owned products from this
|
|
92
|
+
* deployment's products module (injected because inventory
|
|
93
|
+
* depends on commerce; a static import would cycle).
|
|
94
|
+
* 3. A generic "$module booking" fallback.
|
|
95
|
+
*
|
|
96
|
+
* Errors fall through quietly — a title is purely cosmetic, the
|
|
97
|
+
* booking-item row should always insert successfully.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveLineItemTitle(db: PostgresJsDatabase, snapshot: {
|
|
100
|
+
entity_module: string;
|
|
101
|
+
entity_id: string;
|
|
102
|
+
}, options: Pick<CheckoutModuleOptions, "getOwnedProductName">): Promise<string>;
|
|
103
|
+
export declare function travelerBandToCategory(band: string | undefined): "adult" | "child" | "infant" | "senior" | "other";
|
|
104
|
+
export {};
|
|
105
|
+
//# sourceMappingURL=materialization-support.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"materialization-support.d.ts","sourceRoot":"","sources":["../../src/checkout/materialization-support.ts"],"names":[],"mappings":"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"}
|