@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,447 @@
|
|
|
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, assertTripComponentCanBeReserved, assertTripComponentCanBeUpdated, reserveResultToComponentPatch, shouldReplayReserve, } from "./service-helpers.js";
|
|
5
|
+
import { appendWarningCodes, commonString, createComponentEvent, markComponentForStaffRemediation, minComponentPriceExpiry, statusToEventType, } from "./service-internals.js";
|
|
6
|
+
import { applyQuoteToComponent } from "./service-pricing.js";
|
|
7
|
+
import { getTrip } from "./service-trips.js";
|
|
8
|
+
import { TravelComposerInvariantError } from "./service-types.js";
|
|
9
|
+
import { assertTripTravelerPartyComplete } from "./traveler-party-validation.js";
|
|
10
|
+
import { isAllowedTripComponentStatusTransition } from "./validation.js";
|
|
11
|
+
export async function reserveTrip(db, input, deps) {
|
|
12
|
+
let trip = await getTrip(db, input.envelopeId);
|
|
13
|
+
if (!trip) {
|
|
14
|
+
throw new TravelComposerInvariantError(`Trip envelope ${input.envelopeId} was not found`);
|
|
15
|
+
}
|
|
16
|
+
if (shouldReplayReserve(trip.envelope, input.idempotencyKey)) {
|
|
17
|
+
return {
|
|
18
|
+
envelope: trip.envelope,
|
|
19
|
+
components: trip.components,
|
|
20
|
+
reserved: trip.components
|
|
21
|
+
.filter(isReservedTripComponent)
|
|
22
|
+
.map((component) => ({ componentId: component.id, status: component.status })),
|
|
23
|
+
failures: [],
|
|
24
|
+
compensations: [],
|
|
25
|
+
warnings: ["idempotent_replay"],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
assertTripTravelerPartyComplete(trip.envelope.travelerParty, "Trip reserve");
|
|
29
|
+
const preflight = await refreshComponentsBeforeReserve(db, trip, input, deps);
|
|
30
|
+
if (preflight.failures.length > 0) {
|
|
31
|
+
return {
|
|
32
|
+
envelope: preflight.trip.envelope,
|
|
33
|
+
components: preflight.trip.components,
|
|
34
|
+
reserved: [],
|
|
35
|
+
failures: preflight.failures,
|
|
36
|
+
compensations: [],
|
|
37
|
+
warnings: preflight.warnings,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
trip = preflight.trip;
|
|
41
|
+
await db
|
|
42
|
+
.update(tripEnvelopes)
|
|
43
|
+
.set({
|
|
44
|
+
status: "reserve_in_progress",
|
|
45
|
+
reserveIdempotencyKey: input.idempotencyKey ?? null,
|
|
46
|
+
reserveStartedAt: new Date(),
|
|
47
|
+
updatedAt: new Date(),
|
|
48
|
+
})
|
|
49
|
+
.where(eq(tripEnvelopes.id, input.envelopeId));
|
|
50
|
+
const warnings = new Set();
|
|
51
|
+
const failures = [];
|
|
52
|
+
const reserved = [];
|
|
53
|
+
const componentsById = new Map(trip.components.map((component) => [component.id, component]));
|
|
54
|
+
for (const component of trip.components) {
|
|
55
|
+
if (component.status === "removed" || component.status === "cancelled") {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// Non-catalog-backed components (manual_placeholder, flight_placeholder,
|
|
59
|
+
// etc.) can either be reserved by a vertical hook (flights, insurance,
|
|
60
|
+
// transfer connectors) or held internally when no external reservation
|
|
61
|
+
// system exists.
|
|
62
|
+
if (!isCatalogBackedTripComponent(component)) {
|
|
63
|
+
try {
|
|
64
|
+
const result = await deps.reserveNonCatalogComponent?.({
|
|
65
|
+
envelope: trip.envelope,
|
|
66
|
+
component,
|
|
67
|
+
});
|
|
68
|
+
if (result) {
|
|
69
|
+
const updated = await applyReserveResultToComponent(db, component, result);
|
|
70
|
+
componentsById.set(updated.id, updated);
|
|
71
|
+
reserved.push({ component: updated, result });
|
|
72
|
+
for (const warning of result.warnings ?? [])
|
|
73
|
+
warnings.add(warning);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const held = await holdNonCatalogComponent(db, component);
|
|
77
|
+
componentsById.set(held.id, held);
|
|
78
|
+
reserved.push({
|
|
79
|
+
component: held,
|
|
80
|
+
result: { status: "held", warnings: undefined },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const reason = error instanceof Error ? error.message : "internal_hold_failed";
|
|
86
|
+
warnings.add(reason);
|
|
87
|
+
failures.push({ componentId: component.id, reason });
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
assertTripComponentCanBeReserved(component);
|
|
93
|
+
const result = await deps.reserveCatalogComponent({ envelope: trip.envelope, component });
|
|
94
|
+
const updated = await applyReserveResultToComponent(db, component, result);
|
|
95
|
+
componentsById.set(updated.id, updated);
|
|
96
|
+
reserved.push({ component: updated, result });
|
|
97
|
+
for (const warning of result.warnings ?? [])
|
|
98
|
+
warnings.add(warning);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const reason = error instanceof Error ? error.message : "reservation_failed";
|
|
102
|
+
warnings.add(reason);
|
|
103
|
+
failures.push({ componentId: component.id, reason });
|
|
104
|
+
const failed = await applyReservationFailureToComponent(db, component, reason);
|
|
105
|
+
componentsById.set(failed.id, failed);
|
|
106
|
+
const compensations = await compensateReservedComponents(db, reserved, deps);
|
|
107
|
+
for (const compensation of compensations) {
|
|
108
|
+
if (compensation.status !== "released")
|
|
109
|
+
warnings.add(compensation.status);
|
|
110
|
+
}
|
|
111
|
+
const [failedEnvelope] = (await db
|
|
112
|
+
.update(tripEnvelopes)
|
|
113
|
+
.set({
|
|
114
|
+
status: "failed",
|
|
115
|
+
updatedAt: new Date(),
|
|
116
|
+
})
|
|
117
|
+
.where(eq(tripEnvelopes.id, input.envelopeId))
|
|
118
|
+
.returning());
|
|
119
|
+
if (!failedEnvelope) {
|
|
120
|
+
throw new Error("reserveTrip: failed envelope update returned no rows");
|
|
121
|
+
}
|
|
122
|
+
const refreshed = await getTrip(db, input.envelopeId);
|
|
123
|
+
return {
|
|
124
|
+
envelope: failedEnvelope,
|
|
125
|
+
components: refreshed?.components ?? [...componentsById.values()],
|
|
126
|
+
reserved: reserved.map((item) => ({
|
|
127
|
+
componentId: item.component.id,
|
|
128
|
+
status: item.component.status,
|
|
129
|
+
})),
|
|
130
|
+
failures,
|
|
131
|
+
compensations,
|
|
132
|
+
warnings: [...warnings],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const refs = commonEnvelopeRefs(reserved.map(({ result }) => result));
|
|
137
|
+
const [envelope] = (await db
|
|
138
|
+
.update(tripEnvelopes)
|
|
139
|
+
.set({
|
|
140
|
+
status: "reserved",
|
|
141
|
+
...refs,
|
|
142
|
+
reservedAt: new Date(),
|
|
143
|
+
updatedAt: new Date(),
|
|
144
|
+
})
|
|
145
|
+
.where(eq(tripEnvelopes.id, input.envelopeId))
|
|
146
|
+
.returning());
|
|
147
|
+
if (!envelope) {
|
|
148
|
+
throw new Error("reserveTrip: envelope update returned no rows");
|
|
149
|
+
}
|
|
150
|
+
const refreshed = await getTrip(db, input.envelopeId);
|
|
151
|
+
return {
|
|
152
|
+
envelope,
|
|
153
|
+
components: refreshed?.components ?? [...componentsById.values()],
|
|
154
|
+
reserved: reserved.map((item) => ({
|
|
155
|
+
componentId: item.component.id,
|
|
156
|
+
status: item.component.status,
|
|
157
|
+
})),
|
|
158
|
+
failures,
|
|
159
|
+
compensations: [],
|
|
160
|
+
warnings: [...warnings],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async function refreshComponentsBeforeReserve(db, trip, input, deps) {
|
|
164
|
+
const warnings = new Set();
|
|
165
|
+
const failures = [];
|
|
166
|
+
let changed = false;
|
|
167
|
+
const componentsById = new Map(trip.components.map((component) => [component.id, component]));
|
|
168
|
+
for (const component of trip.components) {
|
|
169
|
+
if (component.status === "removed" || component.status === "cancelled")
|
|
170
|
+
continue;
|
|
171
|
+
if (isCatalogBackedTripComponent(component)) {
|
|
172
|
+
if (!deps.quoteCatalogComponentBeforeReserve)
|
|
173
|
+
continue;
|
|
174
|
+
const quote = await deps.quoteCatalogComponentBeforeReserve({
|
|
175
|
+
component,
|
|
176
|
+
bookingDraft: toBookingDraftV1(component),
|
|
177
|
+
scope: input.refreshScope ?? defaultReserveRefreshScope(trip.envelope),
|
|
178
|
+
});
|
|
179
|
+
const updated = await applyQuoteToComponent(db, component, quote);
|
|
180
|
+
componentsById.set(updated.id, updated);
|
|
181
|
+
changed = true;
|
|
182
|
+
if (!quote.available || !quote.pricing) {
|
|
183
|
+
const reason = quote.invalidReason ?? "quote_unavailable";
|
|
184
|
+
warnings.add(reason);
|
|
185
|
+
failures.push({
|
|
186
|
+
componentId: component.id,
|
|
187
|
+
reason,
|
|
188
|
+
code: "unavailable",
|
|
189
|
+
details: { quoteId: quote.quoteId },
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (componentPricingChanged(component, updated)) {
|
|
194
|
+
warnings.add("price_changed");
|
|
195
|
+
failures.push({
|
|
196
|
+
componentId: component.id,
|
|
197
|
+
reason: "price_changed",
|
|
198
|
+
code: "price_changed",
|
|
199
|
+
details: pricingChangeDetails(component, updated),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const result = await deps.validateNonCatalogComponentBeforeReserve?.({
|
|
205
|
+
envelope: trip.envelope,
|
|
206
|
+
component,
|
|
207
|
+
});
|
|
208
|
+
if (!result || result.status === "ok") {
|
|
209
|
+
for (const warning of result?.warnings ?? [])
|
|
210
|
+
warnings.add(warning);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const reason = result.reason ?? result.status;
|
|
214
|
+
warnings.add(reason);
|
|
215
|
+
failures.push({
|
|
216
|
+
componentId: component.id,
|
|
217
|
+
reason,
|
|
218
|
+
code: result.status,
|
|
219
|
+
details: result.details,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (changed || failures.length > 0) {
|
|
223
|
+
const components = [...componentsById.values()];
|
|
224
|
+
const aggregate = aggregateComponentPricing(components, input.refreshScope?.currency ?? trip.envelope.aggregateCurrency ?? undefined);
|
|
225
|
+
const [envelope] = (await db
|
|
226
|
+
.update(tripEnvelopes)
|
|
227
|
+
.set({
|
|
228
|
+
status: "priced",
|
|
229
|
+
aggregateCurrency: aggregate.currency,
|
|
230
|
+
aggregateSubtotalAmountCents: aggregate.subtotalAmountCents,
|
|
231
|
+
aggregateTaxAmountCents: aggregate.taxAmountCents,
|
|
232
|
+
aggregateTotalAmountCents: aggregate.totalAmountCents,
|
|
233
|
+
aggregatePricingSnapshot: aggregate,
|
|
234
|
+
currentPriceExpiresAt: minComponentPriceExpiry(components),
|
|
235
|
+
updatedAt: new Date(),
|
|
236
|
+
})
|
|
237
|
+
.where(eq(tripEnvelopes.id, trip.envelope.id))
|
|
238
|
+
.returning());
|
|
239
|
+
const refreshed = await getTrip(db, trip.envelope.id);
|
|
240
|
+
return {
|
|
241
|
+
trip: refreshed ?? { envelope: envelope ?? trip.envelope, components },
|
|
242
|
+
failures,
|
|
243
|
+
warnings: [...warnings, ...(aggregate.warnings ?? [])],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return { trip, failures, warnings: [...warnings] };
|
|
247
|
+
}
|
|
248
|
+
function defaultReserveRefreshScope(envelope) {
|
|
249
|
+
return {
|
|
250
|
+
locale: "en-US",
|
|
251
|
+
audience: "staff",
|
|
252
|
+
market: "default",
|
|
253
|
+
currency: envelope.aggregateCurrency ?? undefined,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function componentPricingChanged(before, after) {
|
|
257
|
+
return (before.componentCurrency !== after.componentCurrency ||
|
|
258
|
+
before.componentSubtotalAmountCents !== after.componentSubtotalAmountCents ||
|
|
259
|
+
before.componentTaxAmountCents !== after.componentTaxAmountCents ||
|
|
260
|
+
before.componentTotalAmountCents !== after.componentTotalAmountCents);
|
|
261
|
+
}
|
|
262
|
+
function pricingChangeDetails(before, after) {
|
|
263
|
+
return {
|
|
264
|
+
previous: {
|
|
265
|
+
currency: before.componentCurrency,
|
|
266
|
+
subtotalAmountCents: before.componentSubtotalAmountCents,
|
|
267
|
+
taxAmountCents: before.componentTaxAmountCents,
|
|
268
|
+
totalAmountCents: before.componentTotalAmountCents,
|
|
269
|
+
},
|
|
270
|
+
current: {
|
|
271
|
+
currency: after.componentCurrency,
|
|
272
|
+
subtotalAmountCents: after.componentSubtotalAmountCents,
|
|
273
|
+
taxAmountCents: after.componentTaxAmountCents,
|
|
274
|
+
totalAmountCents: after.componentTotalAmountCents,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// Internal "reservation" for manual / placeholder components — no supplier
|
|
279
|
+
// dispatch, no booking engine. We just flip status to `held` so the envelope
|
|
280
|
+
// can move on to `reserved` and downstream totals/cancellation flows treat
|
|
281
|
+
// the line item as locked in.
|
|
282
|
+
async function holdNonCatalogComponent(db, component) {
|
|
283
|
+
if (component.status === "held" || component.status === "booked") {
|
|
284
|
+
return component;
|
|
285
|
+
}
|
|
286
|
+
if (!isAllowedTripComponentStatusTransition(component.status, "held")) {
|
|
287
|
+
throw new TravelComposerInvariantError(`Trip component ${component.id} is ${component.status} and cannot be held`);
|
|
288
|
+
}
|
|
289
|
+
const [updated] = (await db
|
|
290
|
+
.update(tripComponents)
|
|
291
|
+
.set({ status: "held", updatedAt: new Date() })
|
|
292
|
+
.where(eq(tripComponents.id, component.id))
|
|
293
|
+
.returning());
|
|
294
|
+
if (!updated) {
|
|
295
|
+
throw new Error(`holdNonCatalogComponent: update returned no row for ${component.id}`);
|
|
296
|
+
}
|
|
297
|
+
await createComponentEvent(db, {
|
|
298
|
+
envelopeId: updated.envelopeId,
|
|
299
|
+
componentId: updated.id,
|
|
300
|
+
eventType: statusToEventType(updated.status),
|
|
301
|
+
fromStatus: component.status,
|
|
302
|
+
toStatus: updated.status,
|
|
303
|
+
payload: { reason: "internal_hold" },
|
|
304
|
+
});
|
|
305
|
+
return updated;
|
|
306
|
+
}
|
|
307
|
+
async function applyReserveResultToComponent(db, component, result) {
|
|
308
|
+
assertTripComponentCanBeUpdated(component, { status: result.status });
|
|
309
|
+
const [updated] = (await db
|
|
310
|
+
.update(tripComponents)
|
|
311
|
+
.set({
|
|
312
|
+
...reserveResultToComponentPatch(result),
|
|
313
|
+
warningCodes: appendWarningCodes(component.warningCodes, result.warnings ?? []),
|
|
314
|
+
updatedAt: new Date(),
|
|
315
|
+
})
|
|
316
|
+
.where(eq(tripComponents.id, component.id))
|
|
317
|
+
.returning());
|
|
318
|
+
if (!updated) {
|
|
319
|
+
throw new Error(`applyReserveResultToComponent: update returned no row for ${component.id}`);
|
|
320
|
+
}
|
|
321
|
+
await createComponentEvent(db, {
|
|
322
|
+
envelopeId: updated.envelopeId,
|
|
323
|
+
componentId: updated.id,
|
|
324
|
+
eventType: statusToEventType(updated.status),
|
|
325
|
+
fromStatus: component.status,
|
|
326
|
+
toStatus: updated.status,
|
|
327
|
+
payload: {
|
|
328
|
+
bookingId: updated.bookingId,
|
|
329
|
+
bookingGroupId: updated.bookingGroupId,
|
|
330
|
+
orderId: updated.orderId,
|
|
331
|
+
paymentSessionId: updated.paymentSessionId,
|
|
332
|
+
providerRef: updated.providerRef,
|
|
333
|
+
supplierRef: updated.supplierRef,
|
|
334
|
+
holdExpiresAt: updated.holdExpiresAt?.toISOString(),
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
return updated;
|
|
338
|
+
}
|
|
339
|
+
async function applyReservationFailureToComponent(db, component, reason) {
|
|
340
|
+
if (!isAllowedTripComponentStatusTransition(component.status, "failed")) {
|
|
341
|
+
return markComponentForStaffRemediation(db, component, reason);
|
|
342
|
+
}
|
|
343
|
+
const [updated] = (await db
|
|
344
|
+
.update(tripComponents)
|
|
345
|
+
.set({
|
|
346
|
+
status: "failed",
|
|
347
|
+
warningCodes: appendWarningCodes(component.warningCodes, [reason]),
|
|
348
|
+
updatedAt: new Date(),
|
|
349
|
+
})
|
|
350
|
+
.where(eq(tripComponents.id, component.id))
|
|
351
|
+
.returning());
|
|
352
|
+
if (!updated) {
|
|
353
|
+
throw new Error(`applyReservationFailureToComponent: update returned no row for ${component.id}`);
|
|
354
|
+
}
|
|
355
|
+
await createComponentEvent(db, {
|
|
356
|
+
envelopeId: updated.envelopeId,
|
|
357
|
+
componentId: updated.id,
|
|
358
|
+
eventType: "failed",
|
|
359
|
+
fromStatus: component.status,
|
|
360
|
+
toStatus: updated.status,
|
|
361
|
+
payload: { reason },
|
|
362
|
+
});
|
|
363
|
+
return updated;
|
|
364
|
+
}
|
|
365
|
+
async function compensateReservedComponents(db, reserved, deps) {
|
|
366
|
+
const compensations = [];
|
|
367
|
+
for (const item of [...reserved].reverse()) {
|
|
368
|
+
if (!deps.releaseCatalogComponent) {
|
|
369
|
+
await markComponentForStaffRemediation(db, item.component, "release_not_configured");
|
|
370
|
+
compensations.push({
|
|
371
|
+
componentId: item.component.id,
|
|
372
|
+
status: "release_not_configured",
|
|
373
|
+
reason: "release_not_configured",
|
|
374
|
+
});
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const released = await deps.releaseCatalogComponent({
|
|
379
|
+
component: item.component,
|
|
380
|
+
reserveResult: item.result,
|
|
381
|
+
});
|
|
382
|
+
compensations.push(await compensationFromRelease(db, item.component, released));
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
const reason = error instanceof Error ? error.message : "release_failed";
|
|
386
|
+
await markComponentForStaffRemediation(db, item.component, reason);
|
|
387
|
+
compensations.push({
|
|
388
|
+
componentId: item.component.id,
|
|
389
|
+
status: "release_failed",
|
|
390
|
+
reason,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return compensations;
|
|
395
|
+
}
|
|
396
|
+
async function compensationFromRelease(db, component, released) {
|
|
397
|
+
if (!released.released) {
|
|
398
|
+
const reason = released.reason ?? "release_failed";
|
|
399
|
+
await markComponentForStaffRemediation(db, component, reason);
|
|
400
|
+
return {
|
|
401
|
+
componentId: component.id,
|
|
402
|
+
status: "release_failed",
|
|
403
|
+
reason,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
await markReservedComponentReleased(db, component);
|
|
407
|
+
return { componentId: component.id, status: "released" };
|
|
408
|
+
}
|
|
409
|
+
async function markReservedComponentReleased(db, component) {
|
|
410
|
+
const [updated] = (await db
|
|
411
|
+
.update(tripComponents)
|
|
412
|
+
.set({
|
|
413
|
+
status: "cancelled",
|
|
414
|
+
warningCodes: appendWarningCodes(component.warningCodes, ["reservation_released"]),
|
|
415
|
+
updatedAt: new Date(),
|
|
416
|
+
})
|
|
417
|
+
.where(eq(tripComponents.id, component.id))
|
|
418
|
+
.returning());
|
|
419
|
+
if (!updated) {
|
|
420
|
+
throw new Error(`markReservedComponentReleased: update returned no row for ${component.id}`);
|
|
421
|
+
}
|
|
422
|
+
await createComponentEvent(db, {
|
|
423
|
+
envelopeId: updated.envelopeId,
|
|
424
|
+
componentId: updated.id,
|
|
425
|
+
eventType: "cancelled",
|
|
426
|
+
fromStatus: component.status,
|
|
427
|
+
toStatus: updated.status,
|
|
428
|
+
payload: { reason: "reservation_released" },
|
|
429
|
+
});
|
|
430
|
+
return updated;
|
|
431
|
+
}
|
|
432
|
+
function isReservedTripComponent(component) {
|
|
433
|
+
return component.status === "held" || component.status === "booked";
|
|
434
|
+
}
|
|
435
|
+
function commonEnvelopeRefs(results) {
|
|
436
|
+
const refs = {};
|
|
437
|
+
const bookingGroupId = commonString(results.map((result) => result.bookingGroupId));
|
|
438
|
+
const orderId = commonString(results.map((result) => result.orderId));
|
|
439
|
+
const paymentSessionId = commonString(results.map((result) => result.paymentSessionId));
|
|
440
|
+
if (bookingGroupId)
|
|
441
|
+
refs.bookingGroupId = bookingGroupId;
|
|
442
|
+
if (orderId)
|
|
443
|
+
refs.orderId = orderId;
|
|
444
|
+
if (paymentSessionId)
|
|
445
|
+
refs.paymentSessionId = paymentSessionId;
|
|
446
|
+
return refs;
|
|
447
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
2
|
+
import type { TripComponent, TripEnvelope } from "./schema.js";
|
|
3
|
+
import { type Trip, type TripListResult } from "./service-types.js";
|
|
4
|
+
import type { CreateTripComponentInput, CreateTripEnvelopeInput, ListTripsQuery, ReorderTripComponentsInput, UpdateTripComponentInput, UpdateTripComponentRefsInput, UpdateTripEnvelopeInput } from "./validation.js";
|
|
5
|
+
export declare function createTrip(db: AnyDrizzleDb, input: CreateTripEnvelopeInput): Promise<Trip>;
|
|
6
|
+
export declare function getTrip(db: AnyDrizzleDb, envelopeId: string): Promise<Trip | null>;
|
|
7
|
+
export declare function listTrips(db: AnyDrizzleDb, input?: ListTripsQuery): Promise<TripListResult>;
|
|
8
|
+
export declare function updateTrip(db: AnyDrizzleDb, envelopeId: string, input: UpdateTripEnvelopeInput): Promise<TripEnvelope | null>;
|
|
9
|
+
export declare function addComponent(db: AnyDrizzleDb, input: CreateTripComponentInput): Promise<TripComponent>;
|
|
10
|
+
export declare function updateComponent(db: AnyDrizzleDb, componentId: string, input: UpdateTripComponentInput): Promise<TripComponent | null>;
|
|
11
|
+
export declare function updateComponentRefs(db: AnyDrizzleDb, componentId: string, input: UpdateTripComponentRefsInput): Promise<TripComponent | null>;
|
|
12
|
+
export declare function removeComponent(db: AnyDrizzleDb, componentId: string): Promise<TripComponent | null>;
|
|
13
|
+
export declare function reorderComponents(db: AnyDrizzleDb, input: ReorderTripComponentsInput): Promise<TripComponent[]>;
|
|
14
|
+
//# sourceMappingURL=service-trips.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-trips.d.ts","sourceRoot":"","sources":["../src/service-trips.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGhD,OAAO,KAAK,EAAqC,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAOjG,OAAO,EAAgC,KAAK,IAAI,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAEjG,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,cAAc,EACd,0BAA0B,EAC1B,wBAAwB,EACxB,4BAA4B,EAC5B,uBAAuB,EACxB,MAAM,iBAAiB,CAAA;AAExB,wBAAsB,UAAU,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhG;AAED,wBAAsB,OAAO,CAAC,EAAE,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAexF;AAED,wBAAsB,SAAS,CAC7B,EAAE,EAAE,YAAY,EAChB,KAAK,GAAE,cAKN,GACA,OAAO,CAAC,cAAc,CAAC,CAqIzB;AAqBD,wBAAsB,UAAU,CAC9B,EAAE,EAAE,YAAY,EAChB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAsB9B;AAED,wBAAsB,YAAY,CAChC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,aAAa,CAAC,CAkCxB;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA+B/B;AAED,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,YAAY,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA8B/B;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAE/B;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,aAAa,EAAE,CAAC,CAoC1B"}
|