@voyantjs/travel-composer 0.55.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/README.md +38 -0
- package/dist/catalog-component-adapter.d.ts +16 -0
- package/dist/catalog-component-adapter.d.ts.map +1 -0
- package/dist/catalog-component-adapter.js +34 -0
- package/dist/cruise-extension.d.ts +48 -0
- package/dist/cruise-extension.d.ts.map +1 -0
- package/dist/cruise-extension.js +66 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/mcp-tools.d.ts +157 -0
- package/dist/mcp-tools.d.ts.map +1 -0
- package/dist/mcp-tools.js +109 -0
- package/dist/routes.d.ts +1988 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +239 -0
- package/dist/schema.d.ts +1228 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +163 -0
- package/dist/service-cancellation.d.ts +6 -0
- package/dist/service-cancellation.d.ts.map +1 -0
- package/dist/service-cancellation.js +251 -0
- package/dist/service-checkout.d.ts +6 -0
- package/dist/service-checkout.d.ts.map +1 -0
- package/dist/service-checkout.js +328 -0
- package/dist/service-drafts.d.ts +13 -0
- package/dist/service-drafts.d.ts.map +1 -0
- package/dist/service-drafts.js +223 -0
- package/dist/service-helpers.d.ts +17 -0
- package/dist/service-helpers.d.ts.map +1 -0
- package/dist/service-helpers.js +161 -0
- package/dist/service-internals.d.ts +11 -0
- package/dist/service-internals.d.ts.map +1 -0
- package/dist/service-internals.js +72 -0
- package/dist/service-pricing.d.ts +8 -0
- package/dist/service-pricing.d.ts.map +1 -0
- package/dist/service-pricing.js +142 -0
- package/dist/service-reservation.d.ts +5 -0
- package/dist/service-reservation.d.ts.map +1 -0
- package/dist/service-reservation.js +447 -0
- package/dist/service-trips.d.ts +14 -0
- package/dist/service-trips.d.ts.map +1 -0
- package/dist/service-trips.js +377 -0
- package/dist/service-types.d.ts +252 -0
- package/dist/service-types.d.ts.map +1 -0
- package/dist/service-types.js +6 -0
- package/dist/service.d.ts +33 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +35 -0
- package/dist/traveler-party-validation.d.ts +3 -0
- package/dist/traveler-party-validation.d.ts.map +1 -0
- package/dist/traveler-party-validation.js +68 -0
- package/dist/validation.d.ts +363 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +226 -0
- package/package.json +88 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { isCatalogBackedTripComponent } from "./catalog-component-adapter.js";
|
|
2
|
+
import { TravelComposerInvariantError } from "./service-types.js";
|
|
3
|
+
import { isAllowedTripComponentStatusTransition, isTerminalTripComponentStatus, } from "./validation.js";
|
|
4
|
+
export function hasCommittedComponentReference(component) {
|
|
5
|
+
return Boolean(component.bookingId ||
|
|
6
|
+
component.bookingGroupId ||
|
|
7
|
+
component.orderId ||
|
|
8
|
+
component.paymentSessionId ||
|
|
9
|
+
component.providerRef ||
|
|
10
|
+
component.supplierRef);
|
|
11
|
+
}
|
|
12
|
+
export function assertTripComponentCanBeUpdated(component, patch) {
|
|
13
|
+
if (isTerminalTripComponentStatus(component.status)) {
|
|
14
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} is ${component.status} and cannot be updated`);
|
|
15
|
+
}
|
|
16
|
+
if (patch.status && !isAllowedTripComponentStatusTransition(component.status, patch.status)) {
|
|
17
|
+
throw new TravelComposerInvariantError(`Invalid trip component status transition: ${component.status} -> ${patch.status}`);
|
|
18
|
+
}
|
|
19
|
+
if (hasCommittedComponentReference(component) && patch.catalogRef !== undefined) {
|
|
20
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} has committed references and cannot change catalog identity`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function assertTripComponentCanReceiveRefs(component, refs) {
|
|
24
|
+
if (isTerminalTripComponentStatus(component.status)) {
|
|
25
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} is ${component.status} and cannot receive references`);
|
|
26
|
+
}
|
|
27
|
+
if (hasCommittedComponentReference(component) &&
|
|
28
|
+
refs.committedRef &&
|
|
29
|
+
Object.keys(refs.committedRef).length > 0) {
|
|
30
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} already has committed references`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function assertTripComponentCanBeReserved(component, now = new Date()) {
|
|
34
|
+
if (component.status !== "priced") {
|
|
35
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} is ${component.status} and must be priced before reserve`);
|
|
36
|
+
}
|
|
37
|
+
if (!isCatalogBackedTripComponent(component)) {
|
|
38
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} is not a catalog-backed booking component`);
|
|
39
|
+
}
|
|
40
|
+
if (!component.pricingSnapshot) {
|
|
41
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} has no pricing snapshot to reserve`);
|
|
42
|
+
}
|
|
43
|
+
if (component.priceExpiresAt && component.priceExpiresAt.getTime() <= now.getTime()) {
|
|
44
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} price has expired`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function reserveResultToComponentPatch(result) {
|
|
48
|
+
const patch = {
|
|
49
|
+
status: result.status,
|
|
50
|
+
};
|
|
51
|
+
if (result.bookingId !== undefined)
|
|
52
|
+
patch.bookingId = result.bookingId;
|
|
53
|
+
if (result.bookingGroupId !== undefined)
|
|
54
|
+
patch.bookingGroupId = result.bookingGroupId;
|
|
55
|
+
if (result.orderId !== undefined)
|
|
56
|
+
patch.orderId = result.orderId;
|
|
57
|
+
if (result.paymentSessionId !== undefined)
|
|
58
|
+
patch.paymentSessionId = result.paymentSessionId;
|
|
59
|
+
if (result.providerRef !== undefined)
|
|
60
|
+
patch.providerRef = result.providerRef;
|
|
61
|
+
if (result.supplierRef !== undefined)
|
|
62
|
+
patch.supplierRef = result.supplierRef;
|
|
63
|
+
if (result.holdToken !== undefined)
|
|
64
|
+
patch.holdToken = result.holdToken;
|
|
65
|
+
if (result.holdExpiresAt !== undefined)
|
|
66
|
+
patch.holdExpiresAt = new Date(result.holdExpiresAt);
|
|
67
|
+
return patch;
|
|
68
|
+
}
|
|
69
|
+
export function shouldReplayReserve(envelope, idempotencyKey) {
|
|
70
|
+
return Boolean(idempotencyKey &&
|
|
71
|
+
envelope.reserveIdempotencyKey === idempotencyKey &&
|
|
72
|
+
["reserved", "checkout_started", "booked"].includes(envelope.status));
|
|
73
|
+
}
|
|
74
|
+
export function shouldReplayCheckout(envelope, idempotencyKey) {
|
|
75
|
+
return Boolean(idempotencyKey &&
|
|
76
|
+
envelope.checkoutIdempotencyKey === idempotencyKey &&
|
|
77
|
+
["checkout_started", "booked"].includes(envelope.status));
|
|
78
|
+
}
|
|
79
|
+
export function assertTripComponentCanStartCheckout(component, now = new Date()) {
|
|
80
|
+
if (component.status !== "held" && component.status !== "booked") {
|
|
81
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} is ${component.status} and must be held or booked before checkout`);
|
|
82
|
+
}
|
|
83
|
+
if (!component.bookingId && !component.orderId) {
|
|
84
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} has no booking/order reference for checkout`);
|
|
85
|
+
}
|
|
86
|
+
if (component.holdExpiresAt && component.holdExpiresAt.getTime() <= now.getTime()) {
|
|
87
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} hold has expired`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function checkoutResultToComponentPatch(result) {
|
|
91
|
+
const patch = {
|
|
92
|
+
status: result.status ?? "checkout_started",
|
|
93
|
+
};
|
|
94
|
+
if (result.bookingId !== undefined)
|
|
95
|
+
patch.bookingId = result.bookingId;
|
|
96
|
+
if (result.bookingGroupId !== undefined)
|
|
97
|
+
patch.bookingGroupId = result.bookingGroupId;
|
|
98
|
+
if (result.orderId !== undefined)
|
|
99
|
+
patch.orderId = result.orderId;
|
|
100
|
+
if (result.paymentSessionId !== undefined)
|
|
101
|
+
patch.paymentSessionId = result.paymentSessionId;
|
|
102
|
+
if (result.providerRef !== undefined)
|
|
103
|
+
patch.providerRef = result.providerRef;
|
|
104
|
+
if (result.supplierRef !== undefined)
|
|
105
|
+
patch.supplierRef = result.supplierRef;
|
|
106
|
+
return patch;
|
|
107
|
+
}
|
|
108
|
+
export function pricingSnapshotFromBreakdown(pricing, priceExpiresAt, warnings) {
|
|
109
|
+
return {
|
|
110
|
+
currency: pricing.currency,
|
|
111
|
+
subtotalAmountCents: pricing.subtotal,
|
|
112
|
+
taxAmountCents: pricing.taxTotal,
|
|
113
|
+
totalAmountCents: pricing.total,
|
|
114
|
+
priceExpiresAt,
|
|
115
|
+
warnings,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function taxLinesFromBreakdown(pricing) {
|
|
119
|
+
return pricing.taxes.map((tax) => ({
|
|
120
|
+
code: tax.code,
|
|
121
|
+
label: tax.label,
|
|
122
|
+
amountCents: tax.amount,
|
|
123
|
+
baseAmountCents: tax.base,
|
|
124
|
+
rate: tax.rate,
|
|
125
|
+
includedInPrice: tax.includedInPrice,
|
|
126
|
+
source: tax.scope,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
export function aggregateComponentPricing(components, preferredCurrency) {
|
|
130
|
+
const priced = components.filter((component) => component.pricingSnapshot);
|
|
131
|
+
const currency = preferredCurrency ?? priced[0]?.pricingSnapshot?.currency ?? "EUR";
|
|
132
|
+
const warnings = new Set();
|
|
133
|
+
let subtotalAmountCents = 0;
|
|
134
|
+
let taxAmountCents = 0;
|
|
135
|
+
let totalAmountCents = 0;
|
|
136
|
+
for (const component of priced) {
|
|
137
|
+
const snapshot = component.pricingSnapshot;
|
|
138
|
+
if (!snapshot)
|
|
139
|
+
continue;
|
|
140
|
+
if (snapshot.currency !== currency) {
|
|
141
|
+
warnings.add(`currency_mismatch:${snapshot.currency}`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
subtotalAmountCents += snapshot.subtotalAmountCents;
|
|
145
|
+
taxAmountCents += snapshot.taxAmountCents;
|
|
146
|
+
totalAmountCents += snapshot.totalAmountCents;
|
|
147
|
+
for (const warning of snapshot.warnings ?? [])
|
|
148
|
+
warnings.add(warning);
|
|
149
|
+
for (const warning of component.warningCodes ?? [])
|
|
150
|
+
warnings.add(warning);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
currency,
|
|
154
|
+
subtotalAmountCents,
|
|
155
|
+
taxAmountCents,
|
|
156
|
+
totalAmountCents,
|
|
157
|
+
componentCount: components.length,
|
|
158
|
+
pricedComponentCount: priced.length,
|
|
159
|
+
warnings: [...warnings],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
2
|
+
import type { NewTripComponentEvent, TripComponent } from "./schema.js";
|
|
3
|
+
import type { TripComponentStatus } from "./validation.js";
|
|
4
|
+
export declare function createComponentEvent(db: AnyDrizzleDb, data: Omit<NewTripComponentEvent, "id" | "occurredAt">): Promise<void>;
|
|
5
|
+
export declare function statusToEventType(to: TripComponentStatus): NewTripComponentEvent["eventType"];
|
|
6
|
+
export declare function appendWarningCodes(existing: string[], incoming: string[]): string[];
|
|
7
|
+
export declare function markComponentForStaffRemediation(db: AnyDrizzleDb, component: TripComponent, reason: string): Promise<TripComponent>;
|
|
8
|
+
export declare function commonString(values: Array<string | undefined>): string | undefined;
|
|
9
|
+
export declare function minComponentPriceExpiry(components: TripComponent[]): Date | null;
|
|
10
|
+
export declare function minComponentHoldExpiry(components: TripComponent[]): Date | null;
|
|
11
|
+
//# sourceMappingURL=service-internals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-internals.d.ts","sourceRoot":"","sources":["../src/service-internals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGhD,OAAO,KAAK,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAEvE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAE1D,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,IAAI,CAAC,qBAAqB,EAAE,IAAI,GAAG,YAAY,CAAC,iBAGvD;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,mBAAmB,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAoB7F;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEnF;AAED,wBAAsB,gCAAgC,CACpD,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,aAAa,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,CAAC,CA2BxB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,SAAS,CAGlF;AAED,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,IAAI,GAAG,IAAI,CAMhF;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,IAAI,GAAG,IAAI,CAM/E"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { tripComponentEvents, tripComponents } from "./schema.js";
|
|
3
|
+
export async function createComponentEvent(db, data) {
|
|
4
|
+
await db.insert(tripComponentEvents).values(data);
|
|
5
|
+
}
|
|
6
|
+
export function statusToEventType(to) {
|
|
7
|
+
switch (to) {
|
|
8
|
+
case "priced":
|
|
9
|
+
case "unavailable":
|
|
10
|
+
return "priced";
|
|
11
|
+
case "held":
|
|
12
|
+
return "hold_placed";
|
|
13
|
+
case "booked":
|
|
14
|
+
return "booked";
|
|
15
|
+
case "checkout_started":
|
|
16
|
+
return "checkout_started";
|
|
17
|
+
case "failed":
|
|
18
|
+
return "failed";
|
|
19
|
+
case "cancelled":
|
|
20
|
+
return "cancelled";
|
|
21
|
+
case "removed":
|
|
22
|
+
return "removed";
|
|
23
|
+
case "draft":
|
|
24
|
+
return "updated";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function appendWarningCodes(existing, incoming) {
|
|
28
|
+
return [...new Set([...existing, ...incoming])];
|
|
29
|
+
}
|
|
30
|
+
export async function markComponentForStaffRemediation(db, component, reason) {
|
|
31
|
+
const [updated] = (await db
|
|
32
|
+
.update(tripComponents)
|
|
33
|
+
.set({
|
|
34
|
+
warningCodes: appendWarningCodes(component.warningCodes, [
|
|
35
|
+
"staff_remediation_required",
|
|
36
|
+
reason,
|
|
37
|
+
]),
|
|
38
|
+
updatedAt: new Date(),
|
|
39
|
+
})
|
|
40
|
+
.where(eq(tripComponents.id, component.id))
|
|
41
|
+
.returning());
|
|
42
|
+
if (!updated) {
|
|
43
|
+
throw new Error(`markComponentForStaffRemediation: update returned no row for ${component.id}`);
|
|
44
|
+
}
|
|
45
|
+
await createComponentEvent(db, {
|
|
46
|
+
envelopeId: updated.envelopeId,
|
|
47
|
+
componentId: updated.id,
|
|
48
|
+
eventType: "staff_remediation_required",
|
|
49
|
+
fromStatus: component.status,
|
|
50
|
+
toStatus: updated.status,
|
|
51
|
+
payload: { reason },
|
|
52
|
+
});
|
|
53
|
+
return updated;
|
|
54
|
+
}
|
|
55
|
+
export function commonString(values) {
|
|
56
|
+
const unique = [...new Set(values.filter((value) => Boolean(value)))];
|
|
57
|
+
return unique.length === 1 ? unique[0] : undefined;
|
|
58
|
+
}
|
|
59
|
+
export function minComponentPriceExpiry(components) {
|
|
60
|
+
const expiries = components
|
|
61
|
+
.map((component) => component.priceExpiresAt)
|
|
62
|
+
.filter((value) => value instanceof Date)
|
|
63
|
+
.sort((a, b) => a.getTime() - b.getTime());
|
|
64
|
+
return expiries[0] ?? null;
|
|
65
|
+
}
|
|
66
|
+
export function minComponentHoldExpiry(components) {
|
|
67
|
+
const expiries = components
|
|
68
|
+
.map((component) => component.holdExpiresAt)
|
|
69
|
+
.filter((value) => value instanceof Date)
|
|
70
|
+
.sort((a, b) => a.getTime() - b.getTime());
|
|
71
|
+
return expiries[0] ?? null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { QuoteResponseV1 } from "@voyantjs/catalog/booking-engine";
|
|
2
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
3
|
+
import type { TripComponent } from "./schema.js";
|
|
4
|
+
import type { PriceTripDeps, PriceTripResult } from "./service-types.js";
|
|
5
|
+
import type { PriceTripInput } from "./validation.js";
|
|
6
|
+
export declare function priceTrip(db: AnyDrizzleDb, input: PriceTripInput, deps: PriceTripDeps): Promise<PriceTripResult>;
|
|
7
|
+
export declare function applyQuoteToComponent(db: AnyDrizzleDb, component: TripComponent, quote: QuoteResponseV1): Promise<TripComponent>;
|
|
8
|
+
//# sourceMappingURL=service-pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-pricing.d.ts","sourceRoot":"","sources":["../src/service-pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAA;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAIhD,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,aAAa,CAAA;AAU9D,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAExE,OAAO,KAAK,EAAE,cAAc,EAAuB,MAAM,iBAAiB,CAAA;AAE1E,wBAAsB,SAAS,CAC7B,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,eAAe,CAAC,CA0E1B;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,YAAY,EAChB,SAAS,EAAE,aAAa,EACxB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,aAAa,CAAC,CA8CxB"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { isCatalogBackedTripComponent, toBookingDraftV1 } from "./catalog-component-adapter.js";
|
|
3
|
+
import { tripComponents, tripEnvelopes } from "./schema.js";
|
|
4
|
+
import { aggregateComponentPricing, assertTripComponentCanBeUpdated, pricingSnapshotFromBreakdown, taxLinesFromBreakdown, } from "./service-helpers.js";
|
|
5
|
+
import { createComponentEvent, minComponentPriceExpiry } from "./service-internals.js";
|
|
6
|
+
import { getTrip } from "./service-trips.js";
|
|
7
|
+
import { TravelComposerInvariantError } from "./service-types.js";
|
|
8
|
+
export async function priceTrip(db, input, deps) {
|
|
9
|
+
const trip = await getTrip(db, input.envelopeId);
|
|
10
|
+
if (!trip) {
|
|
11
|
+
throw new TravelComposerInvariantError(`Trip envelope ${input.envelopeId} was not found`);
|
|
12
|
+
}
|
|
13
|
+
const warnings = new Set();
|
|
14
|
+
const failures = [];
|
|
15
|
+
const pricedComponents = [];
|
|
16
|
+
for (const component of trip.components) {
|
|
17
|
+
if (component.status === "removed" || component.status === "cancelled") {
|
|
18
|
+
pricedComponents.push(component);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (isCatalogBackedTripComponent(component)) {
|
|
22
|
+
const quote = await deps.quoteCatalogComponent({
|
|
23
|
+
component,
|
|
24
|
+
bookingDraft: toBookingDraftV1(component, deps.componentBookingDraftOverrides?.[component.id]),
|
|
25
|
+
scope: input.scope,
|
|
26
|
+
ttlMs: input.ttlMs,
|
|
27
|
+
});
|
|
28
|
+
const updated = await applyQuoteToComponent(db, component, quote);
|
|
29
|
+
pricedComponents.push(updated);
|
|
30
|
+
if (!quote.available || !quote.pricing) {
|
|
31
|
+
const reason = quote.invalidReason ?? "quote_unavailable";
|
|
32
|
+
warnings.add(reason);
|
|
33
|
+
failures.push({ componentId: component.id, reason });
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const updated = await applyPlaceholderPricing(db, component);
|
|
38
|
+
pricedComponents.push(updated);
|
|
39
|
+
for (const warning of updated.warningCodes)
|
|
40
|
+
warnings.add(warning);
|
|
41
|
+
if (!updated.pricingSnapshot) {
|
|
42
|
+
failures.push({ componentId: component.id, reason: "manual_price_missing" });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const aggregate = aggregateComponentPricing(pricedComponents, input.scope.currency);
|
|
46
|
+
for (const warning of aggregate.warnings ?? [])
|
|
47
|
+
warnings.add(warning);
|
|
48
|
+
const pricing = { ...aggregate, warnings: [...warnings] };
|
|
49
|
+
const [envelope] = (await db
|
|
50
|
+
.update(tripEnvelopes)
|
|
51
|
+
.set({
|
|
52
|
+
status: "priced",
|
|
53
|
+
aggregateCurrency: pricing.currency,
|
|
54
|
+
aggregateSubtotalAmountCents: pricing.subtotalAmountCents,
|
|
55
|
+
aggregateTaxAmountCents: pricing.taxAmountCents,
|
|
56
|
+
aggregateTotalAmountCents: pricing.totalAmountCents,
|
|
57
|
+
aggregatePricingSnapshot: pricing,
|
|
58
|
+
currentPriceExpiresAt: minComponentPriceExpiry(pricedComponents),
|
|
59
|
+
updatedAt: new Date(),
|
|
60
|
+
})
|
|
61
|
+
.where(eq(tripEnvelopes.id, input.envelopeId))
|
|
62
|
+
.returning());
|
|
63
|
+
if (!envelope) {
|
|
64
|
+
throw new Error("priceTrip: envelope update returned no rows");
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
envelope,
|
|
68
|
+
components: pricedComponents,
|
|
69
|
+
pricing,
|
|
70
|
+
warnings: [...warnings],
|
|
71
|
+
failures,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export async function applyQuoteToComponent(db, component, quote) {
|
|
75
|
+
const pricingSnapshot = quote.pricing
|
|
76
|
+
? pricingSnapshotFromBreakdown(quote.pricing, quote.expiresAt)
|
|
77
|
+
: undefined;
|
|
78
|
+
const warningCodes = quote.invalidReason ? [quote.invalidReason] : [];
|
|
79
|
+
const nextStatus = quote.available && quote.pricing ? "priced" : "unavailable";
|
|
80
|
+
assertTripComponentCanBeUpdated(component, { status: nextStatus });
|
|
81
|
+
const [updated] = (await db
|
|
82
|
+
.update(tripComponents)
|
|
83
|
+
.set({
|
|
84
|
+
status: nextStatus,
|
|
85
|
+
catalogQuoteId: quote.quoteId,
|
|
86
|
+
componentCurrency: pricingSnapshot?.currency ?? null,
|
|
87
|
+
componentSubtotalAmountCents: pricingSnapshot?.subtotalAmountCents ?? null,
|
|
88
|
+
componentTaxAmountCents: pricingSnapshot?.taxAmountCents ?? null,
|
|
89
|
+
componentTotalAmountCents: pricingSnapshot?.totalAmountCents ?? null,
|
|
90
|
+
pricingSnapshot,
|
|
91
|
+
taxLines: quote.pricing ? taxLinesFromBreakdown(quote.pricing) : [],
|
|
92
|
+
priceExpiresAt: quote.expiresAt ? new Date(quote.expiresAt) : null,
|
|
93
|
+
warningCodes,
|
|
94
|
+
updatedAt: new Date(),
|
|
95
|
+
})
|
|
96
|
+
.where(eq(tripComponents.id, component.id))
|
|
97
|
+
.returning());
|
|
98
|
+
if (!updated) {
|
|
99
|
+
throw new Error(`applyQuoteToComponent: update returned no row for ${component.id}`);
|
|
100
|
+
}
|
|
101
|
+
await createComponentEvent(db, {
|
|
102
|
+
envelopeId: updated.envelopeId,
|
|
103
|
+
componentId: updated.id,
|
|
104
|
+
eventType: "priced",
|
|
105
|
+
fromStatus: component.status,
|
|
106
|
+
toStatus: updated.status,
|
|
107
|
+
payload: {
|
|
108
|
+
quoteId: quote.quoteId,
|
|
109
|
+
available: quote.available,
|
|
110
|
+
invalidReason: quote.invalidReason,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
return updated;
|
|
114
|
+
}
|
|
115
|
+
async function applyPlaceholderPricing(db, component) {
|
|
116
|
+
const warningCodes = component.pricingSnapshot
|
|
117
|
+
? ["manual_placeholder_price"]
|
|
118
|
+
: ["manual_price_missing"];
|
|
119
|
+
const nextStatus = component.pricingSnapshot ? "priced" : "unavailable";
|
|
120
|
+
assertTripComponentCanBeUpdated(component, { status: nextStatus });
|
|
121
|
+
const [updated] = (await db
|
|
122
|
+
.update(tripComponents)
|
|
123
|
+
.set({
|
|
124
|
+
status: nextStatus,
|
|
125
|
+
warningCodes,
|
|
126
|
+
updatedAt: new Date(),
|
|
127
|
+
})
|
|
128
|
+
.where(eq(tripComponents.id, component.id))
|
|
129
|
+
.returning());
|
|
130
|
+
if (!updated) {
|
|
131
|
+
throw new Error(`applyPlaceholderPricing: update returned no row for ${component.id}`);
|
|
132
|
+
}
|
|
133
|
+
await createComponentEvent(db, {
|
|
134
|
+
envelopeId: updated.envelopeId,
|
|
135
|
+
componentId: updated.id,
|
|
136
|
+
eventType: "priced",
|
|
137
|
+
fromStatus: component.status,
|
|
138
|
+
toStatus: updated.status,
|
|
139
|
+
payload: { placeholder: true },
|
|
140
|
+
});
|
|
141
|
+
return updated;
|
|
142
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
2
|
+
import type { ReserveTripDeps, ReserveTripResult } from "./service-types.js";
|
|
3
|
+
import { type ReserveTripInput } from "./validation.js";
|
|
4
|
+
export declare function reserveTrip(db: AnyDrizzleDb, input: ReserveTripInput, deps: ReserveTripDeps): Promise<ReserveTripResult>;
|
|
5
|
+
//# sourceMappingURL=service-reservation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-reservation.d.ts","sourceRoot":"","sources":["../src/service-reservation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAuBhD,OAAO,KAAK,EAGV,eAAe,EACf,iBAAiB,EAElB,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAA0C,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAE/F,wBAAsB,WAAW,CAC/B,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,iBAAiB,CAAC,CAqK5B"}
|