@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 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { typeId, typeIdRef } from "@voyantjs/db/lib/typeid-column";
|
|
2
|
+
import { relations } from "drizzle-orm";
|
|
3
|
+
import { index, integer, jsonb, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
4
|
+
export const tripEnvelopeStatusEnum = pgEnum("trip_envelope_status", [
|
|
5
|
+
"draft",
|
|
6
|
+
"priced",
|
|
7
|
+
"reserve_in_progress",
|
|
8
|
+
"reserved",
|
|
9
|
+
"checkout_started",
|
|
10
|
+
"booked",
|
|
11
|
+
"failed",
|
|
12
|
+
"cancelled",
|
|
13
|
+
]);
|
|
14
|
+
export const tripComponentKindEnum = pgEnum("trip_component_kind", [
|
|
15
|
+
"catalog_booking",
|
|
16
|
+
"manual_placeholder",
|
|
17
|
+
"flight_placeholder",
|
|
18
|
+
"flight_order",
|
|
19
|
+
"external_order",
|
|
20
|
+
]);
|
|
21
|
+
export const tripComponentStatusEnum = pgEnum("trip_component_status", [
|
|
22
|
+
"draft",
|
|
23
|
+
"priced",
|
|
24
|
+
"unavailable",
|
|
25
|
+
"held",
|
|
26
|
+
"booked",
|
|
27
|
+
"checkout_started",
|
|
28
|
+
"failed",
|
|
29
|
+
"cancelled",
|
|
30
|
+
"removed",
|
|
31
|
+
]);
|
|
32
|
+
export const tripComponentEventTypeEnum = pgEnum("trip_component_event_type", [
|
|
33
|
+
"created",
|
|
34
|
+
"updated",
|
|
35
|
+
"priced",
|
|
36
|
+
"hold_placed",
|
|
37
|
+
"booked",
|
|
38
|
+
"checkout_started",
|
|
39
|
+
"failed",
|
|
40
|
+
"cancelled",
|
|
41
|
+
"removed",
|
|
42
|
+
"staff_remediation_required",
|
|
43
|
+
]);
|
|
44
|
+
export const tripEnvelopes = pgTable("trip_envelopes", {
|
|
45
|
+
id: typeId("trip_envelopes"),
|
|
46
|
+
status: tripEnvelopeStatusEnum("status").notNull().default("draft"),
|
|
47
|
+
title: text("title"),
|
|
48
|
+
description: text("description"),
|
|
49
|
+
travelerParty: jsonb("traveler_party").$type().notNull().default({}),
|
|
50
|
+
constraints: jsonb("constraints").$type().notNull().default({}),
|
|
51
|
+
aggregateCurrency: text("aggregate_currency"),
|
|
52
|
+
aggregateSubtotalAmountCents: integer("aggregate_subtotal_amount_cents"),
|
|
53
|
+
aggregateTaxAmountCents: integer("aggregate_tax_amount_cents"),
|
|
54
|
+
aggregateTotalAmountCents: integer("aggregate_total_amount_cents"),
|
|
55
|
+
aggregatePricingSnapshot: jsonb("aggregate_pricing_snapshot").$type(),
|
|
56
|
+
currentPriceExpiresAt: timestamp("current_price_expires_at", { withTimezone: true }),
|
|
57
|
+
bookingGroupId: text("booking_group_id"),
|
|
58
|
+
orderId: text("order_id"),
|
|
59
|
+
paymentSessionId: text("payment_session_id"),
|
|
60
|
+
reserveIdempotencyKey: text("reserve_idempotency_key"),
|
|
61
|
+
reserveStartedAt: timestamp("reserve_started_at", { withTimezone: true }),
|
|
62
|
+
reservedAt: timestamp("reserved_at", { withTimezone: true }),
|
|
63
|
+
checkoutIdempotencyKey: text("checkout_idempotency_key"),
|
|
64
|
+
checkoutStartedAt: timestamp("checkout_started_at", { withTimezone: true }),
|
|
65
|
+
createdBy: text("created_by"),
|
|
66
|
+
updatedBy: text("updated_by"),
|
|
67
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
68
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
69
|
+
}, (table) => [
|
|
70
|
+
index("idx_trip_envelopes_status_updated").on(table.status, table.updatedAt),
|
|
71
|
+
index("idx_trip_envelopes_created_by_updated").on(table.createdBy, table.updatedAt),
|
|
72
|
+
index("idx_trip_envelopes_booking_group").on(table.bookingGroupId),
|
|
73
|
+
index("idx_trip_envelopes_payment_session").on(table.paymentSessionId),
|
|
74
|
+
index("idx_trip_envelopes_reserve_idempotency").on(table.reserveIdempotencyKey),
|
|
75
|
+
index("idx_trip_envelopes_checkout_idempotency").on(table.checkoutIdempotencyKey),
|
|
76
|
+
]);
|
|
77
|
+
export const tripComponents = pgTable("trip_components", {
|
|
78
|
+
id: typeId("trip_components"),
|
|
79
|
+
envelopeId: typeIdRef("envelope_id")
|
|
80
|
+
.notNull()
|
|
81
|
+
.references(() => tripEnvelopes.id, { onDelete: "cascade" }),
|
|
82
|
+
sequence: integer("sequence").notNull().default(0),
|
|
83
|
+
kind: tripComponentKindEnum("kind").notNull(),
|
|
84
|
+
status: tripComponentStatusEnum("status").notNull().default("draft"),
|
|
85
|
+
title: text("title"),
|
|
86
|
+
description: text("description"),
|
|
87
|
+
entityModule: text("entity_module"),
|
|
88
|
+
entityId: text("entity_id"),
|
|
89
|
+
sourceKind: text("source_kind"),
|
|
90
|
+
sourceConnectionId: text("source_connection_id"),
|
|
91
|
+
sourceRef: text("source_ref"),
|
|
92
|
+
bookingDraftId: text("booking_draft_id"),
|
|
93
|
+
catalogQuoteId: text("catalog_quote_id"),
|
|
94
|
+
bookingId: text("booking_id"),
|
|
95
|
+
bookingGroupId: text("booking_group_id"),
|
|
96
|
+
orderId: text("order_id"),
|
|
97
|
+
paymentSessionId: text("payment_session_id"),
|
|
98
|
+
providerRef: text("provider_ref"),
|
|
99
|
+
supplierRef: text("supplier_ref"),
|
|
100
|
+
componentCurrency: text("component_currency"),
|
|
101
|
+
componentSubtotalAmountCents: integer("component_subtotal_amount_cents"),
|
|
102
|
+
componentTaxAmountCents: integer("component_tax_amount_cents"),
|
|
103
|
+
componentTotalAmountCents: integer("component_total_amount_cents"),
|
|
104
|
+
pricingSnapshot: jsonb("pricing_snapshot").$type(),
|
|
105
|
+
taxLines: jsonb("tax_lines").$type().default([]),
|
|
106
|
+
cancellationSnapshot: jsonb("cancellation_snapshot").$type(),
|
|
107
|
+
holdToken: text("hold_token"),
|
|
108
|
+
holdExpiresAt: timestamp("hold_expires_at", { withTimezone: true }),
|
|
109
|
+
priceExpiresAt: timestamp("price_expires_at", { withTimezone: true }),
|
|
110
|
+
warningCodes: jsonb("warning_codes").$type().notNull().default([]),
|
|
111
|
+
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
112
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
113
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
114
|
+
}, (table) => [
|
|
115
|
+
index("idx_trip_components_envelope_sequence").on(table.envelopeId, table.sequence),
|
|
116
|
+
index("idx_trip_components_envelope_status").on(table.envelopeId, table.status),
|
|
117
|
+
index("idx_trip_components_catalog_entity").on(table.entityModule, table.entityId),
|
|
118
|
+
index("idx_trip_components_booking_draft").on(table.bookingDraftId),
|
|
119
|
+
index("idx_trip_components_catalog_quote").on(table.catalogQuoteId),
|
|
120
|
+
index("idx_trip_components_booking").on(table.bookingId),
|
|
121
|
+
index("idx_trip_components_order").on(table.orderId),
|
|
122
|
+
index("idx_trip_components_payment_session").on(table.paymentSessionId),
|
|
123
|
+
]);
|
|
124
|
+
export const tripComponentEvents = pgTable("trip_component_events", {
|
|
125
|
+
id: typeId("trip_component_events"),
|
|
126
|
+
envelopeId: typeIdRef("envelope_id")
|
|
127
|
+
.notNull()
|
|
128
|
+
.references(() => tripEnvelopes.id, { onDelete: "cascade" }),
|
|
129
|
+
componentId: typeIdRef("component_id").references(() => tripComponents.id, {
|
|
130
|
+
onDelete: "set null",
|
|
131
|
+
}),
|
|
132
|
+
eventType: tripComponentEventTypeEnum("event_type").notNull(),
|
|
133
|
+
fromStatus: tripComponentStatusEnum("from_status"),
|
|
134
|
+
toStatus: tripComponentStatusEnum("to_status"),
|
|
135
|
+
payload: jsonb("payload").$type().notNull().default({}),
|
|
136
|
+
actorId: text("actor_id"),
|
|
137
|
+
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
|
|
138
|
+
}, (table) => [
|
|
139
|
+
index("idx_trip_component_events_envelope_time").on(table.envelopeId, table.occurredAt),
|
|
140
|
+
index("idx_trip_component_events_component_time").on(table.componentId, table.occurredAt),
|
|
141
|
+
index("idx_trip_component_events_type_time").on(table.eventType, table.occurredAt),
|
|
142
|
+
]);
|
|
143
|
+
export const tripEnvelopeRelations = relations(tripEnvelopes, ({ many }) => ({
|
|
144
|
+
components: many(tripComponents),
|
|
145
|
+
events: many(tripComponentEvents),
|
|
146
|
+
}));
|
|
147
|
+
export const tripComponentRelations = relations(tripComponents, ({ one, many }) => ({
|
|
148
|
+
envelope: one(tripEnvelopes, {
|
|
149
|
+
fields: [tripComponents.envelopeId],
|
|
150
|
+
references: [tripEnvelopes.id],
|
|
151
|
+
}),
|
|
152
|
+
events: many(tripComponentEvents),
|
|
153
|
+
}));
|
|
154
|
+
export const tripComponentEventRelations = relations(tripComponentEvents, ({ one }) => ({
|
|
155
|
+
envelope: one(tripEnvelopes, {
|
|
156
|
+
fields: [tripComponentEvents.envelopeId],
|
|
157
|
+
references: [tripEnvelopes.id],
|
|
158
|
+
}),
|
|
159
|
+
component: one(tripComponents, {
|
|
160
|
+
fields: [tripComponentEvents.componentId],
|
|
161
|
+
references: [tripComponents.id],
|
|
162
|
+
}),
|
|
163
|
+
}));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
2
|
+
import type { CancelTripComponentsDeps, CancelTripComponentsResult, PreviewTripCancellationDeps, TripCancellationPreviewResult } from "./service-types.js";
|
|
3
|
+
import { type CancelTripComponentsInput, type PreviewTripCancellationInput } from "./validation.js";
|
|
4
|
+
export declare function previewCancellation(db: AnyDrizzleDb, input: PreviewTripCancellationInput, deps?: PreviewTripCancellationDeps): Promise<TripCancellationPreviewResult>;
|
|
5
|
+
export declare function cancelComponents(db: AnyDrizzleDb, input: CancelTripComponentsInput, deps?: CancelTripComponentsDeps): Promise<CancelTripComponentsResult>;
|
|
6
|
+
//# sourceMappingURL=service-cancellation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-cancellation.d.ts","sourceRoot":"","sources":["../src/service-cancellation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAWhD,OAAO,KAAK,EAEV,wBAAwB,EACxB,0BAA0B,EAE1B,2BAA2B,EAE3B,6BAA6B,EAC9B,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EACL,KAAK,yBAAyB,EAE9B,KAAK,4BAA4B,EAClC,MAAM,iBAAiB,CAAA;AAExB,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,4BAA4B,EACnC,IAAI,GAAE,2BAAgC,GACrC,OAAO,CAAC,6BAA6B,CAAC,CAqBxC;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,yBAAyB,EAChC,IAAI,GAAE,wBAA6B,GAClC,OAAO,CAAC,0BAA0B,CAAC,CAsFrC"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { tripComponents, tripEnvelopes } from "./schema.js";
|
|
3
|
+
import { assertTripComponentCanBeUpdated, hasCommittedComponentReference, } from "./service-helpers.js";
|
|
4
|
+
import { createComponentEvent, markComponentForStaffRemediation } from "./service-internals.js";
|
|
5
|
+
import { getTrip } from "./service-trips.js";
|
|
6
|
+
import { TravelComposerInvariantError } from "./service-types.js";
|
|
7
|
+
import { isTerminalTripComponentStatus, } from "./validation.js";
|
|
8
|
+
export async function previewCancellation(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 requestedAt = input.requestedAt ? new Date(input.requestedAt) : new Date();
|
|
14
|
+
const selected = selectComponentsForCancellation(trip, input.componentIds);
|
|
15
|
+
const componentPreviews = [];
|
|
16
|
+
for (const component of selected) {
|
|
17
|
+
componentPreviews.push(await previewComponentCancellation(trip.envelope, component, input, requestedAt, deps));
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
envelope: trip.envelope,
|
|
21
|
+
components: trip.components,
|
|
22
|
+
preview: buildCancellationPreviewAggregate(trip.envelope, selected, componentPreviews),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export async function cancelComponents(db, input, deps = {}) {
|
|
26
|
+
const preview = await previewCancellation(db, input, deps);
|
|
27
|
+
const requestedAt = input.requestedAt ? new Date(input.requestedAt) : new Date();
|
|
28
|
+
const componentsById = new Map(preview.components.map((component) => [component.id, component]));
|
|
29
|
+
const cancelled = [];
|
|
30
|
+
const remediation = [];
|
|
31
|
+
const skipped = [];
|
|
32
|
+
for (const item of preview.preview.components) {
|
|
33
|
+
const component = componentsById.get(item.componentId);
|
|
34
|
+
if (!component)
|
|
35
|
+
continue;
|
|
36
|
+
if (item.action === "no_op") {
|
|
37
|
+
skipped.push({ componentId: item.componentId, reason: item.reason ?? "no_action" });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (item.action === "staff_remediation") {
|
|
41
|
+
const reason = item.reason ?? "staff_remediation_required";
|
|
42
|
+
const updated = await markComponentForStaffRemediation(db, component, reason);
|
|
43
|
+
componentsById.set(updated.id, updated);
|
|
44
|
+
remediation.push({ componentId: item.componentId, reason });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!deps.cancelComponent && !canCancelComponentLocally(component)) {
|
|
48
|
+
const reason = "cancel_adapter_not_configured";
|
|
49
|
+
const updated = await markComponentForStaffRemediation(db, component, reason);
|
|
50
|
+
componentsById.set(updated.id, updated);
|
|
51
|
+
remediation.push({ componentId: item.componentId, reason });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const result = canCancelComponentLocally(component)
|
|
56
|
+
? localCancellationResult(item)
|
|
57
|
+
: await deps.cancelComponent?.({
|
|
58
|
+
envelope: preview.envelope,
|
|
59
|
+
component,
|
|
60
|
+
preview: item,
|
|
61
|
+
reason: input.reason,
|
|
62
|
+
requestedAt,
|
|
63
|
+
request: input.request,
|
|
64
|
+
});
|
|
65
|
+
if (!result || result.status !== "cancelled") {
|
|
66
|
+
const reason = result?.reason ?? `cancel_${result?.status ?? "not_configured"}`;
|
|
67
|
+
const updated = await markComponentForStaffRemediation(db, component, reason);
|
|
68
|
+
componentsById.set(updated.id, updated);
|
|
69
|
+
remediation.push({ componentId: item.componentId, reason });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const updated = await markComponentCancelled(db, component, {
|
|
73
|
+
...item,
|
|
74
|
+
refundAmountCents: result.refundAmountCents ?? item.refundAmountCents,
|
|
75
|
+
refundCurrency: result.refundCurrency ?? item.refundCurrency,
|
|
76
|
+
snapshot: result.snapshot ?? item.snapshot,
|
|
77
|
+
});
|
|
78
|
+
componentsById.set(updated.id, updated);
|
|
79
|
+
cancelled.push({ componentId: item.componentId, status: "cancelled" });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const reason = error instanceof Error ? error.message : "cancel_failed";
|
|
83
|
+
const updated = await markComponentForStaffRemediation(db, component, reason);
|
|
84
|
+
componentsById.set(updated.id, updated);
|
|
85
|
+
remediation.push({ componentId: item.componentId, reason });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const refreshed = await getTrip(db, input.envelopeId);
|
|
89
|
+
const components = refreshed?.components ?? [...componentsById.values()];
|
|
90
|
+
const envelope = await maybeCancelEnvelope(db, preview.envelope, components);
|
|
91
|
+
const finalTrip = envelope.status === preview.envelope.status ? refreshed : await getTrip(db, input.envelopeId);
|
|
92
|
+
return {
|
|
93
|
+
envelope,
|
|
94
|
+
components: finalTrip?.components ?? components,
|
|
95
|
+
preview: {
|
|
96
|
+
...preview.preview,
|
|
97
|
+
staffActionRequired: preview.preview.staffActionRequired || remediation.length > 0,
|
|
98
|
+
},
|
|
99
|
+
cancelled,
|
|
100
|
+
remediation,
|
|
101
|
+
skipped,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function selectComponentsForCancellation(trip, componentIds) {
|
|
105
|
+
if (!componentIds || componentIds.length === 0) {
|
|
106
|
+
return trip.components.filter((component) => !isTerminalTripComponentStatus(component.status));
|
|
107
|
+
}
|
|
108
|
+
const componentsById = new Map(trip.components.map((component) => [component.id, component]));
|
|
109
|
+
const selected = [];
|
|
110
|
+
for (const componentId of componentIds) {
|
|
111
|
+
const component = componentsById.get(componentId);
|
|
112
|
+
if (!component) {
|
|
113
|
+
throw new TravelComposerInvariantError(`Trip component ${componentId} was not found on envelope ${trip.envelope.id}`);
|
|
114
|
+
}
|
|
115
|
+
selected.push(component);
|
|
116
|
+
}
|
|
117
|
+
return selected;
|
|
118
|
+
}
|
|
119
|
+
async function previewComponentCancellation(envelope, component, input, requestedAt, deps) {
|
|
120
|
+
if (isTerminalTripComponentStatus(component.status)) {
|
|
121
|
+
return componentCancellationPreview(component, {
|
|
122
|
+
action: "no_op",
|
|
123
|
+
staffActionRequired: false,
|
|
124
|
+
reason: `already_${component.status}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (canCancelComponentLocally(component)) {
|
|
128
|
+
return componentCancellationPreview(component, {
|
|
129
|
+
action: "cancel",
|
|
130
|
+
staffActionRequired: false,
|
|
131
|
+
reason: "local_component_cancel",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (deps.previewComponentCancellation) {
|
|
135
|
+
return deps.previewComponentCancellation({
|
|
136
|
+
envelope,
|
|
137
|
+
component,
|
|
138
|
+
reason: input.reason,
|
|
139
|
+
requestedAt,
|
|
140
|
+
request: input.request,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return componentCancellationPreview(component, {
|
|
144
|
+
action: "staff_remediation",
|
|
145
|
+
staffActionRequired: true,
|
|
146
|
+
reason: "cancel_preview_not_configured",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function canCancelComponentLocally(component) {
|
|
150
|
+
if (component.kind === "manual_placeholder" || component.kind === "flight_placeholder") {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return (component.kind === "catalog_booking" &&
|
|
154
|
+
!hasCommittedComponentReference(component) &&
|
|
155
|
+
(component.status === "draft" ||
|
|
156
|
+
component.status === "priced" ||
|
|
157
|
+
component.status === "unavailable" ||
|
|
158
|
+
component.status === "failed"));
|
|
159
|
+
}
|
|
160
|
+
function componentCancellationPreview(component, overrides) {
|
|
161
|
+
return {
|
|
162
|
+
componentId: component.id,
|
|
163
|
+
currentStatus: component.status,
|
|
164
|
+
refundAmountCents: 0,
|
|
165
|
+
refundCurrency: component.componentCurrency ?? undefined,
|
|
166
|
+
penaltyAmountCents: 0,
|
|
167
|
+
...overrides,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function buildCancellationPreviewAggregate(envelope, selected, componentPreviews) {
|
|
171
|
+
const warnings = new Set();
|
|
172
|
+
let currency = envelope.aggregateCurrency ?? null;
|
|
173
|
+
let estimatedRefundAmountCents = 0;
|
|
174
|
+
let estimatedPenaltyAmountCents = 0;
|
|
175
|
+
for (const item of componentPreviews) {
|
|
176
|
+
if (item.refundCurrency) {
|
|
177
|
+
currency ??= item.refundCurrency;
|
|
178
|
+
if (currency !== item.refundCurrency) {
|
|
179
|
+
warnings.add(`refund_currency_mismatch:${item.refundCurrency}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!item.refundCurrency || !currency || item.refundCurrency === currency) {
|
|
183
|
+
estimatedRefundAmountCents += item.refundAmountCents ?? 0;
|
|
184
|
+
}
|
|
185
|
+
estimatedPenaltyAmountCents += item.penaltyAmountCents ?? 0;
|
|
186
|
+
if (item.reason && item.action !== "cancel")
|
|
187
|
+
warnings.add(item.reason);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
envelopeId: envelope.id,
|
|
191
|
+
selectedComponentIds: selected.map((component) => component.id),
|
|
192
|
+
currency,
|
|
193
|
+
estimatedRefundAmountCents,
|
|
194
|
+
estimatedPenaltyAmountCents,
|
|
195
|
+
staffActionRequired: componentPreviews.some((item) => item.staffActionRequired),
|
|
196
|
+
components: componentPreviews,
|
|
197
|
+
warnings: [...warnings],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function localCancellationResult(preview) {
|
|
201
|
+
return {
|
|
202
|
+
status: "cancelled",
|
|
203
|
+
refundAmountCents: preview.refundAmountCents,
|
|
204
|
+
refundCurrency: preview.refundCurrency,
|
|
205
|
+
snapshot: preview.snapshot,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function markComponentCancelled(db, component, preview) {
|
|
209
|
+
assertTripComponentCanBeUpdated(component, { status: "cancelled" });
|
|
210
|
+
const cancellationSnapshot = {
|
|
211
|
+
action: preview.action,
|
|
212
|
+
refundAmountCents: preview.refundAmountCents ?? 0,
|
|
213
|
+
refundCurrency: preview.refundCurrency ?? component.componentCurrency ?? null,
|
|
214
|
+
penaltyAmountCents: preview.penaltyAmountCents ?? 0,
|
|
215
|
+
supplierCancellationDeadline: preview.supplierCancellationDeadline ?? null,
|
|
216
|
+
policySummary: preview.policySummary ?? null,
|
|
217
|
+
snapshot: preview.snapshot ?? null,
|
|
218
|
+
};
|
|
219
|
+
const [updated] = (await db
|
|
220
|
+
.update(tripComponents)
|
|
221
|
+
.set({
|
|
222
|
+
status: "cancelled",
|
|
223
|
+
cancellationSnapshot,
|
|
224
|
+
updatedAt: new Date(),
|
|
225
|
+
})
|
|
226
|
+
.where(eq(tripComponents.id, component.id))
|
|
227
|
+
.returning());
|
|
228
|
+
if (!updated) {
|
|
229
|
+
throw new Error(`markComponentCancelled: update returned no row for ${component.id}`);
|
|
230
|
+
}
|
|
231
|
+
await createComponentEvent(db, {
|
|
232
|
+
envelopeId: updated.envelopeId,
|
|
233
|
+
componentId: updated.id,
|
|
234
|
+
eventType: "cancelled",
|
|
235
|
+
fromStatus: component.status,
|
|
236
|
+
toStatus: updated.status,
|
|
237
|
+
payload: cancellationSnapshot,
|
|
238
|
+
});
|
|
239
|
+
return updated;
|
|
240
|
+
}
|
|
241
|
+
async function maybeCancelEnvelope(db, envelope, components) {
|
|
242
|
+
const hasActiveComponent = components.some((component) => component.status !== "cancelled" && component.status !== "removed");
|
|
243
|
+
if (hasActiveComponent || envelope.status === "cancelled")
|
|
244
|
+
return envelope;
|
|
245
|
+
const [updated] = (await db
|
|
246
|
+
.update(tripEnvelopes)
|
|
247
|
+
.set({ status: "cancelled", updatedAt: new Date() })
|
|
248
|
+
.where(eq(tripEnvelopes.id, envelope.id))
|
|
249
|
+
.returning());
|
|
250
|
+
return updated ?? envelope;
|
|
251
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
2
|
+
import type { CompleteTripCheckoutInput, CompleteTripCheckoutResult, StartCheckoutDeps, StartCheckoutResult } from "./service-types.js";
|
|
3
|
+
import type { StartTripCheckoutInput } from "./validation.js";
|
|
4
|
+
export declare function startCheckout(db: AnyDrizzleDb, input: StartTripCheckoutInput, deps: StartCheckoutDeps): Promise<StartCheckoutResult>;
|
|
5
|
+
export declare function completeTripCheckout(db: AnyDrizzleDb, input: CompleteTripCheckoutInput): Promise<CompleteTripCheckoutResult | null>;
|
|
6
|
+
//# sourceMappingURL=service-checkout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-checkout.d.ts","sourceRoot":"","sources":["../src/service-checkout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAqBhD,OAAO,KAAK,EACV,yBAAyB,EACzB,0BAA0B,EAE1B,iBAAiB,EACjB,mBAAmB,EAKpB,MAAM,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAK7D,wBAAsB,aAAa,CACjC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,sBAAsB,EAC7B,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,mBAAmB,CAAC,CAyI9B;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAAC,CAoG5C"}
|