@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.
Files changed (56) hide show
  1. package/README.md +38 -0
  2. package/dist/catalog-component-adapter.d.ts +16 -0
  3. package/dist/catalog-component-adapter.d.ts.map +1 -0
  4. package/dist/catalog-component-adapter.js +34 -0
  5. package/dist/cruise-extension.d.ts +48 -0
  6. package/dist/cruise-extension.d.ts.map +1 -0
  7. package/dist/cruise-extension.js +66 -0
  8. package/dist/index.d.ts +21 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +29 -0
  11. package/dist/mcp-tools.d.ts +157 -0
  12. package/dist/mcp-tools.d.ts.map +1 -0
  13. package/dist/mcp-tools.js +109 -0
  14. package/dist/routes.d.ts +1988 -0
  15. package/dist/routes.d.ts.map +1 -0
  16. package/dist/routes.js +239 -0
  17. package/dist/schema.d.ts +1228 -0
  18. package/dist/schema.d.ts.map +1 -0
  19. package/dist/schema.js +163 -0
  20. package/dist/service-cancellation.d.ts +6 -0
  21. package/dist/service-cancellation.d.ts.map +1 -0
  22. package/dist/service-cancellation.js +251 -0
  23. package/dist/service-checkout.d.ts +6 -0
  24. package/dist/service-checkout.d.ts.map +1 -0
  25. package/dist/service-checkout.js +328 -0
  26. package/dist/service-drafts.d.ts +13 -0
  27. package/dist/service-drafts.d.ts.map +1 -0
  28. package/dist/service-drafts.js +223 -0
  29. package/dist/service-helpers.d.ts +17 -0
  30. package/dist/service-helpers.d.ts.map +1 -0
  31. package/dist/service-helpers.js +161 -0
  32. package/dist/service-internals.d.ts +11 -0
  33. package/dist/service-internals.d.ts.map +1 -0
  34. package/dist/service-internals.js +72 -0
  35. package/dist/service-pricing.d.ts +8 -0
  36. package/dist/service-pricing.d.ts.map +1 -0
  37. package/dist/service-pricing.js +142 -0
  38. package/dist/service-reservation.d.ts +5 -0
  39. package/dist/service-reservation.d.ts.map +1 -0
  40. package/dist/service-reservation.js +447 -0
  41. package/dist/service-trips.d.ts +14 -0
  42. package/dist/service-trips.d.ts.map +1 -0
  43. package/dist/service-trips.js +377 -0
  44. package/dist/service-types.d.ts +252 -0
  45. package/dist/service-types.d.ts.map +1 -0
  46. package/dist/service-types.js +6 -0
  47. package/dist/service.d.ts +33 -0
  48. package/dist/service.d.ts.map +1 -0
  49. package/dist/service.js +35 -0
  50. package/dist/traveler-party-validation.d.ts +3 -0
  51. package/dist/traveler-party-validation.d.ts.map +1 -0
  52. package/dist/traveler-party-validation.js +68 -0
  53. package/dist/validation.d.ts +363 -0
  54. package/dist/validation.d.ts.map +1 -0
  55. package/dist/validation.js +226 -0
  56. package/package.json +88 -0
@@ -0,0 +1,328 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { tripComponents, tripEnvelopes } from "./schema.js";
3
+ import { aggregateComponentPricing, assertTripComponentCanBeUpdated, assertTripComponentCanStartCheckout, checkoutResultToComponentPatch, shouldReplayCheckout, } from "./service-helpers.js";
4
+ import { appendWarningCodes, commonString, createComponentEvent, markComponentForStaffRemediation, minComponentHoldExpiry, statusToEventType, } from "./service-internals.js";
5
+ import { getTrip } from "./service-trips.js";
6
+ import { TravelComposerInvariantError } from "./service-types.js";
7
+ import { assertTripTravelerPartyComplete } from "./traveler-party-validation.js";
8
+ const payableEnvelopeStatuses = new Set(["reserved", "checkout_started", "booked"]);
9
+ const payableComponentStatuses = new Set(["held", "checkout_started", "booked"]);
10
+ export async function startCheckout(db, input, deps) {
11
+ const trip = await getTrip(db, input.envelopeId);
12
+ if (!trip) {
13
+ throw new TravelComposerInvariantError(`Trip envelope ${input.envelopeId} was not found`);
14
+ }
15
+ if (shouldReplayCheckout(trip.envelope, input.idempotencyKey)) {
16
+ return replayCheckoutResult(trip);
17
+ }
18
+ if (trip.envelope.status !== "reserved") {
19
+ throw new TravelComposerInvariantError(`Trip envelope ${input.envelopeId} is ${trip.envelope.status} and must be reserved before checkout`);
20
+ }
21
+ assertTripTravelerPartyComplete(trip.envelope.travelerParty, "Trip checkout");
22
+ const warnings = new Set();
23
+ const failures = [];
24
+ const componentCheckouts = [];
25
+ const componentsById = new Map(trip.components.map((component) => [component.id, component]));
26
+ const checkoutable = trip.components.filter((component) => component.status !== "removed" && component.status !== "cancelled");
27
+ if (deps.startTripCheckout) {
28
+ const result = await deps.startTripCheckout({
29
+ trip,
30
+ intent: input.intent,
31
+ request: input.request,
32
+ });
33
+ for (const warning of result.warnings ?? [])
34
+ warnings.add(warning);
35
+ const [envelope] = (await db
36
+ .update(tripEnvelopes)
37
+ .set({
38
+ status: result.status ?? "checkout_started",
39
+ checkoutIdempotencyKey: input.idempotencyKey ?? null,
40
+ checkoutStartedAt: new Date(),
41
+ paymentSessionId: result.paymentSessionId ?? null,
42
+ updatedAt: new Date(),
43
+ })
44
+ .where(eq(tripEnvelopes.id, input.envelopeId))
45
+ .returning());
46
+ if (!envelope) {
47
+ throw new Error("startCheckout: aggregate envelope update returned no rows");
48
+ }
49
+ const refreshed = await getTrip(db, input.envelopeId);
50
+ const components = refreshed?.components ?? trip.components;
51
+ const componentCheckouts = aggregateComponentCheckouts(components, result);
52
+ return {
53
+ envelope,
54
+ components,
55
+ target: checkoutTargetFromTrip(envelope, components, componentCheckouts),
56
+ componentCheckouts,
57
+ failures: [],
58
+ warnings: [...warnings],
59
+ };
60
+ }
61
+ for (const component of checkoutable) {
62
+ try {
63
+ assertTripComponentCanStartCheckout(component);
64
+ const result = await deps.startComponentCheckout({
65
+ envelope: trip.envelope,
66
+ component,
67
+ intent: input.intent,
68
+ request: input.request,
69
+ });
70
+ const updated = await applyCheckoutResultToComponent(db, component, result);
71
+ componentsById.set(updated.id, updated);
72
+ componentCheckouts.push(toStartedComponentCheckout(updated, result));
73
+ for (const warning of result.warnings ?? [])
74
+ warnings.add(warning);
75
+ }
76
+ catch (error) {
77
+ const reason = error instanceof Error ? error.message : "checkout_start_failed";
78
+ warnings.add(reason);
79
+ failures.push({ componentId: component.id, reason });
80
+ const updated = await markComponentForStaffRemediation(db, component, reason);
81
+ componentsById.set(updated.id, updated);
82
+ }
83
+ }
84
+ if (failures.length > 0) {
85
+ const [failedEnvelope] = (await db
86
+ .update(tripEnvelopes)
87
+ .set({
88
+ status: "failed",
89
+ updatedAt: new Date(),
90
+ })
91
+ .where(eq(tripEnvelopes.id, input.envelopeId))
92
+ .returning());
93
+ if (!failedEnvelope) {
94
+ throw new Error("startCheckout: failed envelope update returned no rows");
95
+ }
96
+ const refreshed = await getTrip(db, input.envelopeId);
97
+ const components = refreshed?.components ?? [...componentsById.values()];
98
+ return {
99
+ envelope: failedEnvelope,
100
+ components,
101
+ target: checkoutTargetFromTrip(failedEnvelope, components, componentCheckouts),
102
+ componentCheckouts,
103
+ failures,
104
+ warnings: [...warnings],
105
+ };
106
+ }
107
+ const refs = commonEnvelopeCheckoutRefs(componentCheckouts);
108
+ const [envelope] = (await db
109
+ .update(tripEnvelopes)
110
+ .set({
111
+ status: "checkout_started",
112
+ checkoutIdempotencyKey: input.idempotencyKey ?? null,
113
+ checkoutStartedAt: new Date(),
114
+ ...refs,
115
+ updatedAt: new Date(),
116
+ })
117
+ .where(eq(tripEnvelopes.id, input.envelopeId))
118
+ .returning());
119
+ if (!envelope) {
120
+ throw new Error("startCheckout: envelope update returned no rows");
121
+ }
122
+ const refreshed = await getTrip(db, input.envelopeId);
123
+ const components = refreshed?.components ?? [...componentsById.values()];
124
+ return {
125
+ envelope,
126
+ components,
127
+ target: checkoutTargetFromTrip(envelope, components, componentCheckouts),
128
+ componentCheckouts,
129
+ failures,
130
+ warnings: [...warnings],
131
+ };
132
+ }
133
+ export async function completeTripCheckout(db, input) {
134
+ if (!input.envelopeId && !input.paymentSessionId) {
135
+ throw new TravelComposerInvariantError("completeTripCheckout requires an envelopeId or paymentSessionId");
136
+ }
137
+ const trip = await findTripForCheckoutCompletion(db, input);
138
+ if (!trip)
139
+ return null;
140
+ if (!payableEnvelopeStatuses.has(trip.envelope.status)) {
141
+ throw new TravelComposerInvariantError(`Trip envelope ${trip.envelope.id} is ${trip.envelope.status} and cannot be completed from payment`);
142
+ }
143
+ const paymentSessionId = input.paymentSessionId ?? trip.envelope.paymentSessionId ?? null;
144
+ if (input.paymentSessionId &&
145
+ trip.envelope.paymentSessionId &&
146
+ trip.envelope.paymentSessionId !== input.paymentSessionId) {
147
+ throw new TravelComposerInvariantError(`Payment session ${input.paymentSessionId} does not belong to trip envelope ${trip.envelope.id}`);
148
+ }
149
+ const alreadyCompleted = trip.envelope.status === "booked" &&
150
+ trip.components
151
+ .filter((component) => component.status !== "removed" && component.status !== "cancelled")
152
+ .every((component) => component.status === "booked");
153
+ if (alreadyCompleted) {
154
+ return {
155
+ envelope: trip.envelope,
156
+ components: trip.components,
157
+ updatedComponentIds: [],
158
+ alreadyCompleted: true,
159
+ };
160
+ }
161
+ const paidAt = input.paidAt ? new Date(input.paidAt) : new Date();
162
+ const updatedComponentIds = [];
163
+ for (const component of trip.components) {
164
+ if (component.status === "removed" || component.status === "cancelled")
165
+ continue;
166
+ if (!payableComponentStatuses.has(component.status)) {
167
+ throw new TravelComposerInvariantError(`Trip component ${component.id} is ${component.status} and cannot be completed from payment`);
168
+ }
169
+ if (component.status === "booked")
170
+ continue;
171
+ const [updated] = (await db
172
+ .update(tripComponents)
173
+ .set({ status: "booked", updatedAt: paidAt })
174
+ .where(eq(tripComponents.id, component.id))
175
+ .returning());
176
+ if (!updated) {
177
+ throw new Error(`completeTripCheckout: update returned no row for ${component.id}`);
178
+ }
179
+ updatedComponentIds.push(updated.id);
180
+ await createComponentEvent(db, {
181
+ envelopeId: updated.envelopeId,
182
+ componentId: updated.id,
183
+ eventType: "booked",
184
+ fromStatus: component.status,
185
+ toStatus: "booked",
186
+ actorId: input.actorId ?? null,
187
+ payload: {
188
+ paymentSessionId,
189
+ paidAt: paidAt.toISOString(),
190
+ ...(input.payload ?? {}),
191
+ },
192
+ });
193
+ }
194
+ const [envelope] = (await db
195
+ .update(tripEnvelopes)
196
+ .set({
197
+ status: "booked",
198
+ paymentSessionId: paymentSessionId ?? trip.envelope.paymentSessionId ?? null,
199
+ updatedAt: paidAt,
200
+ })
201
+ .where(eq(tripEnvelopes.id, trip.envelope.id))
202
+ .returning());
203
+ if (!envelope) {
204
+ throw new Error(`completeTripCheckout: envelope update returned no row for ${trip.envelope.id}`);
205
+ }
206
+ const refreshed = await getTrip(db, envelope.id);
207
+ return {
208
+ envelope,
209
+ components: refreshed?.components ?? trip.components,
210
+ updatedComponentIds,
211
+ alreadyCompleted: false,
212
+ };
213
+ }
214
+ async function findTripForCheckoutCompletion(db, input) {
215
+ if (input.envelopeId) {
216
+ return getTrip(db, input.envelopeId);
217
+ }
218
+ const [envelope] = (await db
219
+ .select()
220
+ .from(tripEnvelopes)
221
+ .where(eq(tripEnvelopes.paymentSessionId, input.paymentSessionId))
222
+ .limit(1));
223
+ if (!envelope)
224
+ return null;
225
+ return getTrip(db, envelope.id);
226
+ }
227
+ function aggregateComponentCheckouts(components, result) {
228
+ return components
229
+ .filter((component) => component.status !== "removed" && component.status !== "cancelled")
230
+ .map((component) => ({
231
+ componentId: component.id,
232
+ kind: result.kind,
233
+ bookingId: component.bookingId,
234
+ orderId: component.orderId,
235
+ paymentSessionId: result.paymentSessionId ?? null,
236
+ checkoutUrl: result.checkoutUrl ?? null,
237
+ bankTransferInstructions: result.bankTransferInstructions ?? null,
238
+ expiresAt: result.expiresAt ?? component.holdExpiresAt?.toISOString() ?? null,
239
+ }));
240
+ }
241
+ async function applyCheckoutResultToComponent(db, component, result) {
242
+ const nextStatus = result.status ?? "checkout_started";
243
+ assertTripComponentCanBeUpdated(component, { status: nextStatus });
244
+ const [updated] = (await db
245
+ .update(tripComponents)
246
+ .set({
247
+ ...checkoutResultToComponentPatch(result),
248
+ warningCodes: appendWarningCodes(component.warningCodes, result.warnings ?? []),
249
+ updatedAt: new Date(),
250
+ })
251
+ .where(eq(tripComponents.id, component.id))
252
+ .returning());
253
+ if (!updated) {
254
+ throw new Error(`applyCheckoutResultToComponent: update returned no row for ${component.id}`);
255
+ }
256
+ await createComponentEvent(db, {
257
+ envelopeId: updated.envelopeId,
258
+ componentId: updated.id,
259
+ eventType: statusToEventType(updated.status),
260
+ fromStatus: component.status,
261
+ toStatus: updated.status,
262
+ payload: {
263
+ kind: result.kind,
264
+ bookingId: updated.bookingId,
265
+ bookingGroupId: updated.bookingGroupId,
266
+ orderId: updated.orderId,
267
+ paymentSessionId: updated.paymentSessionId,
268
+ providerRef: updated.providerRef,
269
+ supplierRef: updated.supplierRef,
270
+ checkoutUrl: result.checkoutUrl,
271
+ externalReference: result.externalReference,
272
+ },
273
+ });
274
+ return updated;
275
+ }
276
+ function replayCheckoutResult(trip) {
277
+ const components = trip.components;
278
+ const componentCheckouts = components
279
+ .filter((component) => component.status === "checkout_started" || component.status === "booked")
280
+ .map((component) => toStartedComponentCheckout(component, {
281
+ kind: component.paymentSessionId ? "payment_session" : "hold_placed",
282
+ status: component.status === "booked" ? "booked" : "checkout_started",
283
+ paymentSessionId: component.paymentSessionId ?? undefined,
284
+ }));
285
+ return {
286
+ envelope: trip.envelope,
287
+ components,
288
+ target: checkoutTargetFromTrip(trip.envelope, components, componentCheckouts),
289
+ componentCheckouts,
290
+ failures: [],
291
+ warnings: ["idempotent_replay"],
292
+ };
293
+ }
294
+ function toStartedComponentCheckout(component, result) {
295
+ return {
296
+ componentId: component.id,
297
+ kind: result.kind,
298
+ bookingId: component.bookingId,
299
+ orderId: component.orderId,
300
+ paymentSessionId: component.paymentSessionId,
301
+ checkoutUrl: result.checkoutUrl ?? null,
302
+ bankTransferInstructions: result.bankTransferInstructions ?? null,
303
+ expiresAt: result.expiresAt ?? component.holdExpiresAt?.toISOString() ?? null,
304
+ };
305
+ }
306
+ function checkoutTargetFromTrip(envelope, components, componentCheckouts) {
307
+ return {
308
+ envelopeId: envelope.id,
309
+ status: "checkout_started",
310
+ currency: envelope.aggregateCurrency ?? aggregateComponentPricing(components).currency,
311
+ totalAmountCents: envelope.aggregateTotalAmountCents ?? aggregateComponentPricing(components).totalAmountCents,
312
+ paymentSessionId: envelope.paymentSessionId ??
313
+ commonString(componentCheckouts.map((checkout) => checkout.paymentSessionId ?? undefined)) ??
314
+ null,
315
+ checkoutUrl: commonString(componentCheckouts.map((checkout) => checkout.checkoutUrl ?? undefined)) ?? null,
316
+ holdExpiresAt: minComponentHoldExpiry(components)?.toISOString() ?? null,
317
+ };
318
+ }
319
+ function commonEnvelopeCheckoutRefs(componentCheckouts) {
320
+ const refs = {};
321
+ const paymentSessionId = commonString(componentCheckouts.map((checkout) => checkout.paymentSessionId ?? undefined));
322
+ const orderId = commonString(componentCheckouts.map((checkout) => checkout.orderId ?? undefined));
323
+ if (paymentSessionId)
324
+ refs.paymentSessionId = paymentSessionId;
325
+ if (orderId)
326
+ refs.orderId = orderId;
327
+ return refs;
328
+ }
@@ -0,0 +1,13 @@
1
+ import type { AnyDrizzleDb } from "@voyantjs/db";
2
+ import type { TripComponent, TripEnvelope } from "./schema.js";
3
+ import { type TripDraft } from "./service-types.js";
4
+ import type { CreateTripComponentInput, CreateTripEnvelopeInput, ReorderTripComponentsInput, UpdateTripComponentInput, UpdateTripComponentRefsInput, UpdateTripEnvelopeInput } from "./validation.js";
5
+ export declare function createDraft(db: AnyDrizzleDb, input: CreateTripEnvelopeInput): Promise<TripDraft>;
6
+ export declare function getDraft(db: AnyDrizzleDb, envelopeId: string): Promise<TripDraft | null>;
7
+ export declare function updateDraft(db: AnyDrizzleDb, envelopeId: string, input: UpdateTripEnvelopeInput): Promise<TripEnvelope | null>;
8
+ export declare function addComponent(db: AnyDrizzleDb, input: CreateTripComponentInput): Promise<TripComponent>;
9
+ export declare function updateComponent(db: AnyDrizzleDb, componentId: string, input: UpdateTripComponentInput): Promise<TripComponent | null>;
10
+ export declare function updateComponentRefs(db: AnyDrizzleDb, componentId: string, input: UpdateTripComponentRefsInput): Promise<TripComponent | null>;
11
+ export declare function removeComponent(db: AnyDrizzleDb, componentId: string): Promise<TripComponent | null>;
12
+ export declare function reorderComponents(db: AnyDrizzleDb, input: ReorderTripComponentsInput): Promise<TripComponent[]>;
13
+ //# sourceMappingURL=service-drafts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-drafts.d.ts","sourceRoot":"","sources":["../src/service-drafts.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,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACjF,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,4BAA4B,EAC5B,uBAAuB,EACxB,MAAM,iBAAiB,CAAA;AAExB,wBAAsB,WAAW,CAC/B,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,SAAS,CAAC,CAcpB;AAED,wBAAsB,QAAQ,CAAC,EAAE,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAe9F;AAED,wBAAsB,WAAW,CAC/B,EAAE,EAAE,YAAY,EAChB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAkB9B;AAED,wBAAsB,YAAY,CAChC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,aAAa,CAAC,CAmCxB;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAgC/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"}
@@ -0,0 +1,223 @@
1
+ import { asc, eq, inArray } from "drizzle-orm";
2
+ import { tripComponents, tripEnvelopes } from "./schema.js";
3
+ import { assertTripComponentCanBeUpdated, assertTripComponentCanReceiveRefs, } from "./service-helpers.js";
4
+ import { createComponentEvent, statusToEventType } from "./service-internals.js";
5
+ import { TravelComposerInvariantError } from "./service-types.js";
6
+ export async function createDraft(db, input) {
7
+ const values = {
8
+ title: input.title,
9
+ description: input.description,
10
+ travelerParty: input.travelerParty,
11
+ constraints: input.constraints,
12
+ createdBy: input.createdBy,
13
+ updatedBy: input.createdBy,
14
+ };
15
+ const [envelope] = (await db.insert(tripEnvelopes).values(values).returning());
16
+ if (!envelope)
17
+ throw new Error("createDraft: insert returned no envelope");
18
+ return { envelope, components: [] };
19
+ }
20
+ export async function getDraft(db, envelopeId) {
21
+ const [envelope] = (await db
22
+ .select()
23
+ .from(tripEnvelopes)
24
+ .where(eq(tripEnvelopes.id, envelopeId))
25
+ .limit(1));
26
+ if (!envelope)
27
+ return null;
28
+ const components = (await db
29
+ .select()
30
+ .from(tripComponents)
31
+ .where(eq(tripComponents.envelopeId, envelopeId))
32
+ .orderBy(asc(tripComponents.sequence), asc(tripComponents.createdAt)));
33
+ return { envelope, components };
34
+ }
35
+ export async function updateDraft(db, envelopeId, input) {
36
+ const updates = {
37
+ updatedAt: new Date(),
38
+ };
39
+ if (input.title !== undefined)
40
+ updates.title = input.title;
41
+ if (input.description !== undefined)
42
+ updates.description = input.description;
43
+ if (input.travelerParty !== undefined)
44
+ updates.travelerParty = input.travelerParty;
45
+ if (input.constraints !== undefined)
46
+ updates.constraints = input.constraints;
47
+ if (input.status !== undefined)
48
+ updates.status = input.status;
49
+ if (input.updatedBy !== undefined)
50
+ updates.updatedBy = input.updatedBy;
51
+ const [row] = (await db
52
+ .update(tripEnvelopes)
53
+ .set(updates)
54
+ .where(eq(tripEnvelopes.id, envelopeId))
55
+ .returning());
56
+ return row ?? null;
57
+ }
58
+ export async function addComponent(db, input) {
59
+ const values = {
60
+ envelopeId: input.envelopeId,
61
+ sequence: input.sequence,
62
+ kind: input.kind,
63
+ title: input.title,
64
+ description: input.description,
65
+ entityModule: input.catalogRef?.entityModule,
66
+ entityId: input.catalogRef?.entityId,
67
+ sourceKind: input.catalogRef?.sourceKind,
68
+ sourceConnectionId: input.catalogRef?.sourceConnectionId,
69
+ sourceRef: input.catalogRef?.sourceRef,
70
+ componentCurrency: input.estimatedPricing?.currency,
71
+ componentSubtotalAmountCents: input.estimatedPricing?.subtotalAmountCents,
72
+ componentTaxAmountCents: input.estimatedPricing?.taxAmountCents,
73
+ componentTotalAmountCents: input.estimatedPricing?.totalAmountCents,
74
+ pricingSnapshot: input.estimatedPricing,
75
+ metadata: input.metadata,
76
+ };
77
+ const [component] = (await db
78
+ .insert(tripComponents)
79
+ .values(values)
80
+ .returning());
81
+ if (!component)
82
+ throw new Error("addComponent: insert returned no component");
83
+ await createComponentEvent(db, {
84
+ envelopeId: component.envelopeId,
85
+ componentId: component.id,
86
+ eventType: "created",
87
+ toStatus: component.status,
88
+ payload: {},
89
+ });
90
+ return component;
91
+ }
92
+ export async function updateComponent(db, componentId, input) {
93
+ const existing = await getTripComponentOrThrow(db, componentId);
94
+ assertTripComponentCanBeUpdated(existing, input);
95
+ const updates = {
96
+ updatedAt: new Date(),
97
+ ...toCatalogRefPatch(input.catalogRef),
98
+ };
99
+ if (input.sequence !== undefined)
100
+ updates.sequence = input.sequence;
101
+ if (input.status !== undefined)
102
+ updates.status = input.status;
103
+ if (input.title !== undefined)
104
+ updates.title = input.title;
105
+ if (input.description !== undefined)
106
+ updates.description = input.description;
107
+ if (input.metadata !== undefined)
108
+ updates.metadata = input.metadata;
109
+ if (input.warningCodes !== undefined)
110
+ updates.warningCodes = input.warningCodes;
111
+ const [component] = (await db
112
+ .update(tripComponents)
113
+ .set(updates)
114
+ .where(eq(tripComponents.id, componentId))
115
+ .returning());
116
+ if (!component)
117
+ return null;
118
+ await createComponentEvent(db, {
119
+ envelopeId: component.envelopeId,
120
+ componentId: component.id,
121
+ eventType: input.status ? statusToEventType(input.status) : "updated",
122
+ fromStatus: existing.status,
123
+ toStatus: component.status,
124
+ payload: {},
125
+ });
126
+ return component;
127
+ }
128
+ export async function updateComponentRefs(db, componentId, input) {
129
+ const existing = await getTripComponentOrThrow(db, componentId);
130
+ assertTripComponentCanReceiveRefs(existing, input);
131
+ const updates = {
132
+ updatedAt: new Date(),
133
+ };
134
+ if (input.bookingDraftId !== undefined)
135
+ updates.bookingDraftId = input.bookingDraftId;
136
+ if (input.catalogQuoteId !== undefined)
137
+ updates.catalogQuoteId = input.catalogQuoteId;
138
+ if (input.committedRef?.bookingId !== undefined)
139
+ updates.bookingId = input.committedRef.bookingId;
140
+ if (input.committedRef?.bookingGroupId !== undefined) {
141
+ updates.bookingGroupId = input.committedRef.bookingGroupId;
142
+ }
143
+ if (input.committedRef?.orderId !== undefined)
144
+ updates.orderId = input.committedRef.orderId;
145
+ if (input.committedRef?.paymentSessionId !== undefined) {
146
+ updates.paymentSessionId = input.committedRef.paymentSessionId;
147
+ }
148
+ if (input.committedRef?.providerRef !== undefined) {
149
+ updates.providerRef = input.committedRef.providerRef;
150
+ }
151
+ if (input.committedRef?.supplierRef !== undefined) {
152
+ updates.supplierRef = input.committedRef.supplierRef;
153
+ }
154
+ const [component] = (await db
155
+ .update(tripComponents)
156
+ .set(updates)
157
+ .where(eq(tripComponents.id, componentId))
158
+ .returning());
159
+ return component ?? null;
160
+ }
161
+ export async function removeComponent(db, componentId) {
162
+ return updateComponent(db, componentId, { status: "removed" });
163
+ }
164
+ export async function reorderComponents(db, input) {
165
+ const rows = (await db
166
+ .select()
167
+ .from(tripComponents)
168
+ .where(inArray(tripComponents.id, input.componentIds)));
169
+ const found = new Set(rows.map((row) => row.id));
170
+ const missing = input.componentIds.filter((id) => !found.has(id));
171
+ if (missing.length > 0) {
172
+ throw new TravelComposerInvariantError(`Cannot reorder missing trip components: ${missing.join(", ")}`);
173
+ }
174
+ for (const row of rows) {
175
+ if (row.envelopeId !== input.envelopeId) {
176
+ throw new TravelComposerInvariantError(`Trip component ${row.id} does not belong to envelope ${input.envelopeId}`);
177
+ }
178
+ if (row.status === "removed") {
179
+ throw new TravelComposerInvariantError(`Trip component ${row.id} is removed`);
180
+ }
181
+ }
182
+ const updated = [];
183
+ for (const [sequence, componentId] of input.componentIds.entries()) {
184
+ const [row] = (await db
185
+ .update(tripComponents)
186
+ .set({ sequence, updatedAt: new Date() })
187
+ .where(eq(tripComponents.id, componentId))
188
+ .returning());
189
+ if (row)
190
+ updated.push(row);
191
+ }
192
+ return updated.sort((a, b) => a.sequence - b.sequence);
193
+ }
194
+ function toCatalogRefPatch(input) {
195
+ if (input === undefined)
196
+ return {};
197
+ if (input === null) {
198
+ return {
199
+ entityModule: null,
200
+ entityId: null,
201
+ sourceKind: null,
202
+ sourceConnectionId: null,
203
+ sourceRef: null,
204
+ };
205
+ }
206
+ return {
207
+ entityModule: input.entityModule,
208
+ entityId: input.entityId,
209
+ sourceKind: input.sourceKind,
210
+ sourceConnectionId: input.sourceConnectionId ?? null,
211
+ sourceRef: input.sourceRef ?? null,
212
+ };
213
+ }
214
+ async function getTripComponentOrThrow(db, id) {
215
+ const [component] = (await db
216
+ .select()
217
+ .from(tripComponents)
218
+ .where(eq(tripComponents.id, id))
219
+ .limit(1));
220
+ if (!component)
221
+ throw new TravelComposerInvariantError(`Trip component ${id} was not found`);
222
+ return component;
223
+ }
@@ -0,0 +1,17 @@
1
+ import type { PricingBreakdownV1 } from "@voyantjs/catalog/booking-engine";
2
+ import type { NewTripComponent, TripComponent, TripEnvelope } from "./schema.js";
3
+ import type { ComponentCheckoutResult, ReserveComponentResult } from "./service-types.js";
4
+ import { type TripComponentPricingSnapshot, type TripComponentTaxLine, type TripEnvelopePricingSnapshot, type UpdateTripComponentInput, type UpdateTripComponentRefsInput } from "./validation.js";
5
+ export declare function hasCommittedComponentReference(component: Pick<TripComponent, "bookingId" | "bookingGroupId" | "orderId" | "paymentSessionId" | "providerRef" | "supplierRef">): boolean;
6
+ export declare function assertTripComponentCanBeUpdated(component: TripComponent, patch: UpdateTripComponentInput): void;
7
+ export declare function assertTripComponentCanReceiveRefs(component: TripComponent, refs: UpdateTripComponentRefsInput): void;
8
+ export declare function assertTripComponentCanBeReserved(component: TripComponent, now?: Date): void;
9
+ export declare function reserveResultToComponentPatch(result: ReserveComponentResult): Partial<NewTripComponent>;
10
+ export declare function shouldReplayReserve(envelope: Pick<TripEnvelope, "reserveIdempotencyKey" | "status">, idempotencyKey?: string): boolean;
11
+ export declare function shouldReplayCheckout(envelope: Pick<TripEnvelope, "checkoutIdempotencyKey" | "status">, idempotencyKey?: string): boolean;
12
+ export declare function assertTripComponentCanStartCheckout(component: TripComponent, now?: Date): void;
13
+ export declare function checkoutResultToComponentPatch(result: ComponentCheckoutResult): Partial<NewTripComponent>;
14
+ export declare function pricingSnapshotFromBreakdown(pricing: PricingBreakdownV1, priceExpiresAt?: string, warnings?: string[]): TripComponentPricingSnapshot;
15
+ export declare function taxLinesFromBreakdown(pricing: PricingBreakdownV1): TripComponentTaxLine[];
16
+ export declare function aggregateComponentPricing(components: Array<Pick<TripComponent, "pricingSnapshot" | "warningCodes">>, preferredCurrency?: string): TripEnvelopePricingSnapshot;
17
+ //# sourceMappingURL=service-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-helpers.d.ts","sourceRoot":"","sources":["../src/service-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AAG1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAChF,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAEzF,OAAO,EAGL,KAAK,4BAA4B,EACjC,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,4BAA4B,EAClC,MAAM,iBAAiB,CAAA;AAExB,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,IAAI,CACb,aAAa,EACb,WAAW,GAAG,gBAAgB,GAAG,SAAS,GAAG,kBAAkB,GAAG,aAAa,GAAG,aAAa,CAChG,GACA,OAAO,CAST;AAED,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,aAAa,EACxB,KAAK,EAAE,wBAAwB,GAC9B,IAAI,CAkBN;AAED,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,aAAa,EACxB,IAAI,EAAE,4BAA4B,GACjC,IAAI,CAgBN;AAED,wBAAgB,gCAAgC,CAC9C,SAAS,EAAE,aAAa,EACxB,GAAG,GAAE,IAAiB,GACrB,IAAI,CAsBN;AAED,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,gBAAgB,CAAC,CAe3B;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,uBAAuB,GAAG,QAAQ,CAAC,EAChE,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAMT;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,wBAAwB,GAAG,QAAQ,CAAC,EACjE,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAMT;AAED,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,aAAa,EACxB,GAAG,GAAE,IAAiB,GACrB,IAAI,CAgBN;AAED,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAa3B;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,cAAc,CAAC,EAAE,MAAM,EACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,GAClB,4BAA4B,CAS9B;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,kBAAkB,GAAG,oBAAoB,EAAE,CAUzF;AAED,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,iBAAiB,GAAG,cAAc,CAAC,CAAC,EAC1E,iBAAiB,CAAC,EAAE,MAAM,GACzB,2BAA2B,CAiC7B"}