@voyant-travel/charters 0.117.2
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/LICENSE +201 -0
- package/README.md +16 -0
- package/dist/adapters/index.d.ts +254 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/memoize.d.ts +28 -0
- package/dist/adapters/memoize.d.ts.map +1 -0
- package/dist/adapters/memoize.js +121 -0
- package/dist/adapters/mock.d.ts +50 -0
- package/dist/adapters/mock.d.ts.map +1 -0
- package/dist/adapters/mock.js +194 -0
- package/dist/adapters/registry.d.ts +24 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +40 -0
- package/dist/booking-extension.d.ts +895 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +339 -0
- package/dist/catalog-policy.d.ts +23 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +400 -0
- package/dist/content-shape.d.ts +5 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +13 -0
- package/dist/draft-shape.d.ts +29 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +63 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/lib/key.d.ts +22 -0
- package/dist/lib/key.d.ts.map +1 -0
- package/dist/lib/key.js +24 -0
- package/dist/routes-public.d.ts +785 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +234 -0
- package/dist/routes.d.ts +1744 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +543 -0
- package/dist/schema-core.d.ts +815 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +98 -0
- package/dist/schema-itinerary.d.ts +239 -0
- package/dist/schema-itinerary.d.ts.map +1 -0
- package/dist/schema-itinerary.js +30 -0
- package/dist/schema-pricing.d.ts +385 -0
- package/dist/schema-pricing.d.ts.map +1 -0
- package/dist/schema-pricing.js +62 -0
- package/dist/schema-shared.d.ts +8 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +37 -0
- package/dist/schema-sourced-content.d.ts +253 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +44 -0
- package/dist/schema-yachts.d.ts +367 -0
- package/dist/schema-yachts.d.ts.map +1 -0
- package/dist/schema-yachts.js +30 -0
- package/dist/schema.d.ts +8 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +7 -0
- package/dist/service-bookings-helpers.d.ts +20 -0
- package/dist/service-bookings-helpers.d.ts.map +1 -0
- package/dist/service-bookings-helpers.js +67 -0
- package/dist/service-bookings-local.d.ts +5 -0
- package/dist/service-bookings-local.d.ts.map +1 -0
- package/dist/service-bookings-local.js +177 -0
- package/dist/service-bookings-types.d.ts +88 -0
- package/dist/service-bookings-types.d.ts.map +1 -0
- package/dist/service-bookings-types.js +1 -0
- package/dist/service-bookings.d.ts +36 -0
- package/dist/service-bookings.d.ts.map +1 -0
- package/dist/service-bookings.js +267 -0
- package/dist/service-catalog-plane.d.ts +58 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +145 -0
- package/dist/service-content-synthesizer.d.ts +42 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +122 -0
- package/dist/service-content.d.ts +43 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +248 -0
- package/dist/service-myba.d.ts +85 -0
- package/dist/service-myba.d.ts.map +1 -0
- package/dist/service-myba.js +88 -0
- package/dist/service-pricing.d.ts +64 -0
- package/dist/service-pricing.d.ts.map +1 -0
- package/dist/service-pricing.js +167 -0
- package/dist/service.d.ts +131 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +279 -0
- package/dist/validation-core.d.ts +152 -0
- package/dist/validation-core.d.ts.map +1 -0
- package/dist/validation-core.js +66 -0
- package/dist/validation-itinerary.d.ts +43 -0
- package/dist/validation-itinerary.d.ts.map +1 -0
- package/dist/validation-itinerary.js +19 -0
- package/dist/validation-pricing.d.ts +103 -0
- package/dist/validation-pricing.d.ts.map +1 -0
- package/dist/validation-pricing.js +28 -0
- package/dist/validation-shared.d.ts +61 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +60 -0
- package/dist/validation-yachts.d.ts +76 -0
- package/dist/validation-yachts.d.ts.map +1 -0
- package/dist/validation-yachts.js +36 -0
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +5 -0
- package/package.json +116 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { bookingsService } from "@voyant-travel/bookings";
|
|
2
|
+
import { bookingCharterDetailsService } from "./booking-extension.js";
|
|
3
|
+
import { createCharterTravelers, generateCharterBookingNumber, priceCentsFromString, sourceRefEquals, } from "./service-bookings-helpers.js";
|
|
4
|
+
import { createPerSuiteBooking, createWholeYachtBooking } from "./service-bookings-local.js";
|
|
5
|
+
import { composePerSuiteQuote, composeWholeYachtQuote, } from "./service-pricing.js";
|
|
6
|
+
// ---------- service ----------
|
|
7
|
+
export const chartersBookingService = {
|
|
8
|
+
createPerSuiteBooking,
|
|
9
|
+
createWholeYachtBooking,
|
|
10
|
+
/**
|
|
11
|
+
* Create a per-suite booking against an external (adapter-sourced) voyage.
|
|
12
|
+
*
|
|
13
|
+
* 1. Fetch the upstream voyage + its suites; locate the matching suite and
|
|
14
|
+
* compose a `PerSuiteQuote` locally from its multi-currency price columns.
|
|
15
|
+
* 2. Commit upstream BEFORE writing local rows so we can fail loudly if the
|
|
16
|
+
* broker rejects the booking.
|
|
17
|
+
* 3. Inside a single transaction, create the local booking + travelers +
|
|
18
|
+
* snapshot the quote into `booking_charter_details` with `source='external'`
|
|
19
|
+
* and the upstream connectorBookingRef.
|
|
20
|
+
*
|
|
21
|
+
* If the upstream commit succeeds but the local insert fails, the upstream
|
|
22
|
+
* booking exists with no local trace — we surface the upstream ref in the
|
|
23
|
+
* thrown error so the operator can manually reconcile via the broker's UI.
|
|
24
|
+
*/
|
|
25
|
+
async createExternalPerSuiteBooking(db, input, userId) {
|
|
26
|
+
if (input.guests.length < 1)
|
|
27
|
+
throw new Error("At least one guest is required");
|
|
28
|
+
const voyage = await input.adapter.fetchVoyage(input.voyageRef);
|
|
29
|
+
if (!voyage) {
|
|
30
|
+
throw new Error(`Adapter '${input.adapter.name}' has no voyage for sourceRef ${JSON.stringify(input.voyageRef)}`);
|
|
31
|
+
}
|
|
32
|
+
if (!voyage.bookingModes.includes("per_suite")) {
|
|
33
|
+
throw new Error(`External voyage ${voyage.voyageCode} does not offer per_suite bookings`);
|
|
34
|
+
}
|
|
35
|
+
const suites = await input.adapter.fetchVoyageSuites(input.voyageRef);
|
|
36
|
+
const suite = suites.find((s) => sourceRefEquals(s.sourceRef, input.suiteRef));
|
|
37
|
+
if (!suite) {
|
|
38
|
+
throw new Error(`Adapter '${input.adapter.name}' has no suite ${JSON.stringify(input.suiteRef)} on voyage ${voyage.voyageCode}`);
|
|
39
|
+
}
|
|
40
|
+
if (suite.maxGuests != null && input.guests.length > suite.maxGuests) {
|
|
41
|
+
throw new Error(`External suite ${suite.suiteCode} max guests is ${suite.maxGuests}; got ${input.guests.length}`);
|
|
42
|
+
}
|
|
43
|
+
const composed = composePerSuiteQuote({
|
|
44
|
+
voyageId: voyage.sourceRef.externalId,
|
|
45
|
+
suite: {
|
|
46
|
+
id: suite.sourceRef.externalId,
|
|
47
|
+
suiteName: suite.suiteName,
|
|
48
|
+
pricesByCurrency: suite.pricesByCurrency ?? {},
|
|
49
|
+
portFeesByCurrency: suite.portFeesByCurrency ?? {},
|
|
50
|
+
},
|
|
51
|
+
currency: input.currency,
|
|
52
|
+
});
|
|
53
|
+
const yacht = await input.adapter.fetchYacht(voyage.yachtRef);
|
|
54
|
+
// Commit upstream first — failure here means no local row is created.
|
|
55
|
+
const upstream = await input.adapter.createPerSuiteBooking({
|
|
56
|
+
voyageRef: input.voyageRef,
|
|
57
|
+
suiteRef: input.suiteRef,
|
|
58
|
+
currency: input.currency,
|
|
59
|
+
guests: input.guests,
|
|
60
|
+
contact: input.contact,
|
|
61
|
+
notes: input.notes ?? null,
|
|
62
|
+
});
|
|
63
|
+
const finalQuote = {
|
|
64
|
+
...composed,
|
|
65
|
+
suitePrice: upstream.finalSuitePrice ?? composed.suitePrice,
|
|
66
|
+
portFee: upstream.finalPortFee !== undefined ? upstream.finalPortFee : composed.portFee,
|
|
67
|
+
total: upstream.finalTotal ?? composed.total,
|
|
68
|
+
currency: (upstream.finalCurrency ?? composed.currency),
|
|
69
|
+
};
|
|
70
|
+
return db.transaction(async (tx) => {
|
|
71
|
+
const bookingNumber = generateCharterBookingNumber("CHT");
|
|
72
|
+
const totalCents = priceCentsFromString(finalQuote.total);
|
|
73
|
+
const booking = await bookingsService.createBooking(tx, {
|
|
74
|
+
bookingNumber,
|
|
75
|
+
sellCurrency: finalQuote.currency,
|
|
76
|
+
status: "draft",
|
|
77
|
+
sourceType: "manual",
|
|
78
|
+
personId: input.personId ?? null,
|
|
79
|
+
organizationId: input.organizationId ?? null,
|
|
80
|
+
contactFirstName: input.contact.firstName,
|
|
81
|
+
contactLastName: input.contact.lastName,
|
|
82
|
+
contactEmail: input.contact.email ?? null,
|
|
83
|
+
contactPhone: input.contact.phone ?? null,
|
|
84
|
+
contactPreferredLanguage: input.contact.language ?? null,
|
|
85
|
+
contactCountry: input.contact.country ?? null,
|
|
86
|
+
contactRegion: input.contact.region ?? null,
|
|
87
|
+
contactCity: input.contact.city ?? null,
|
|
88
|
+
contactAddressLine1: input.contact.address ?? null,
|
|
89
|
+
contactPostalCode: input.contact.postalCode ?? null,
|
|
90
|
+
sellAmountCents: totalCents,
|
|
91
|
+
pax: input.guests.length,
|
|
92
|
+
startDate: voyage.departureDate,
|
|
93
|
+
endDate: voyage.returnDate,
|
|
94
|
+
internalNotes: input.notes ?? null,
|
|
95
|
+
}, userId);
|
|
96
|
+
if (!booking) {
|
|
97
|
+
throw new Error(`Upstream booking ${upstream.connectorBookingRef} succeeded but local createBooking returned null. Operator must reconcile manually via '${input.adapter.name}'.`);
|
|
98
|
+
}
|
|
99
|
+
await createCharterTravelers(tx, booking.id, input.guests, userId, {
|
|
100
|
+
includeGuestNotes: false,
|
|
101
|
+
});
|
|
102
|
+
const charterDetails = await bookingCharterDetailsService.upsert(tx, booking.id, {
|
|
103
|
+
bookingMode: "per_suite",
|
|
104
|
+
source: "external",
|
|
105
|
+
sourceProvider: input.adapter.name,
|
|
106
|
+
sourceRef: input.voyageRef,
|
|
107
|
+
voyageId: null,
|
|
108
|
+
suiteId: null,
|
|
109
|
+
yachtId: null,
|
|
110
|
+
voyageDisplayName: voyage.name ?? voyage.voyageCode,
|
|
111
|
+
suiteDisplayName: suite.suiteName,
|
|
112
|
+
yachtName: yacht?.name ?? null,
|
|
113
|
+
charterAreaSnapshot: voyage.charterAreaOverride ?? null,
|
|
114
|
+
guestCount: input.guests.length,
|
|
115
|
+
quotedCurrency: finalQuote.currency,
|
|
116
|
+
quotedSuitePrice: finalQuote.suitePrice,
|
|
117
|
+
quotedPortFee: finalQuote.portFee,
|
|
118
|
+
quotedCharterFee: null,
|
|
119
|
+
apaPercent: null,
|
|
120
|
+
apaAmount: null,
|
|
121
|
+
quotedTotal: finalQuote.total,
|
|
122
|
+
mybaTemplateIdSnapshot: null,
|
|
123
|
+
mybaContractId: null,
|
|
124
|
+
apaPaidAmount: null,
|
|
125
|
+
apaSpentAmount: null,
|
|
126
|
+
apaRefundAmount: null,
|
|
127
|
+
connectorBookingRef: upstream.connectorBookingRef,
|
|
128
|
+
connectorStatus: upstream.connectorStatus ?? null,
|
|
129
|
+
notes: input.notes ?? null,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
bookingId: booking.id,
|
|
133
|
+
bookingNumber: booking.bookingNumber,
|
|
134
|
+
charterDetails,
|
|
135
|
+
quote: finalQuote,
|
|
136
|
+
sourceProvider: input.adapter.name,
|
|
137
|
+
sourceRef: input.voyageRef,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Create a whole-yacht booking against an external (adapter-sourced) voyage.
|
|
143
|
+
*
|
|
144
|
+
* Same atomicity model as `createExternalPerSuiteBooking`. External
|
|
145
|
+
* whole-yacht bookings still require a Voyant-side MYBA template — the
|
|
146
|
+
* adapter must surface it via `voyage.mybaTemplateRefOverride` or
|
|
147
|
+
* `product.defaultMybaTemplateRef`. The string is stored as
|
|
148
|
+
* `mybaTemplateIdSnapshot` and the operator wires up an actual contract
|
|
149
|
+
* later via `mybaService.generateContract`.
|
|
150
|
+
*/
|
|
151
|
+
async createExternalWholeYachtBooking(db, input, userId) {
|
|
152
|
+
const voyage = await input.adapter.fetchVoyage(input.voyageRef);
|
|
153
|
+
if (!voyage) {
|
|
154
|
+
throw new Error(`Adapter '${input.adapter.name}' has no voyage for sourceRef ${JSON.stringify(input.voyageRef)}`);
|
|
155
|
+
}
|
|
156
|
+
if (!voyage.bookingModes.includes("whole_yacht")) {
|
|
157
|
+
throw new Error(`External voyage ${voyage.voyageCode} does not offer whole_yacht bookings`);
|
|
158
|
+
}
|
|
159
|
+
// Resolve product (for default APA + default MYBA template ref).
|
|
160
|
+
const product = await input.adapter.fetchProduct(voyage.productRef);
|
|
161
|
+
const apaPercent = voyage.apaPercentOverride ?? product?.defaultApaPercent ?? null;
|
|
162
|
+
if (!apaPercent) {
|
|
163
|
+
throw new Error(`External voyage ${voyage.voyageCode} has no APA percent set (neither voyage override nor product default).`);
|
|
164
|
+
}
|
|
165
|
+
const mybaTemplateRef = voyage.mybaTemplateRefOverride ?? product?.defaultMybaTemplateRef ?? null;
|
|
166
|
+
if (!mybaTemplateRef) {
|
|
167
|
+
throw new Error(`External voyage ${voyage.voyageCode} cannot be booked whole-yacht: no MYBA template ref configured (neither voyage override nor product default).`);
|
|
168
|
+
}
|
|
169
|
+
const composed = composeWholeYachtQuote({
|
|
170
|
+
voyage: {
|
|
171
|
+
id: voyage.sourceRef.externalId,
|
|
172
|
+
wholeYachtPricesByCurrency: voyage.wholeYachtPricesByCurrency ?? {},
|
|
173
|
+
apaPercentOverride: voyage.apaPercentOverride ?? null,
|
|
174
|
+
},
|
|
175
|
+
productDefaultApaPercent: product?.defaultApaPercent ?? null,
|
|
176
|
+
currency: input.currency,
|
|
177
|
+
});
|
|
178
|
+
const yacht = await input.adapter.fetchYacht(voyage.yachtRef);
|
|
179
|
+
// Commit upstream first — failure rolls everything back without writing.
|
|
180
|
+
const upstream = await input.adapter.createWholeYachtBooking({
|
|
181
|
+
voyageRef: input.voyageRef,
|
|
182
|
+
currency: input.currency,
|
|
183
|
+
guests: input.guests,
|
|
184
|
+
contact: input.contact,
|
|
185
|
+
notes: input.notes ?? null,
|
|
186
|
+
});
|
|
187
|
+
const finalQuote = {
|
|
188
|
+
...composed,
|
|
189
|
+
charterFee: upstream.finalCharterFee ?? composed.charterFee,
|
|
190
|
+
apaPercent: upstream.finalApaPercent ?? composed.apaPercent,
|
|
191
|
+
apaAmount: upstream.finalApaAmount ?? composed.apaAmount,
|
|
192
|
+
total: upstream.finalTotal ?? composed.total,
|
|
193
|
+
currency: (upstream.finalCurrency ?? composed.currency),
|
|
194
|
+
};
|
|
195
|
+
const guestCount = Math.max(1, input.guests?.length ?? 1);
|
|
196
|
+
return db.transaction(async (tx) => {
|
|
197
|
+
const bookingNumber = generateCharterBookingNumber("WYC");
|
|
198
|
+
const totalCents = priceCentsFromString(finalQuote.total);
|
|
199
|
+
const booking = await bookingsService.createBooking(tx, {
|
|
200
|
+
bookingNumber,
|
|
201
|
+
sellCurrency: finalQuote.currency,
|
|
202
|
+
status: "draft",
|
|
203
|
+
sourceType: "manual",
|
|
204
|
+
personId: input.personId ?? null,
|
|
205
|
+
organizationId: input.organizationId ?? null,
|
|
206
|
+
contactFirstName: input.contact.firstName,
|
|
207
|
+
contactLastName: input.contact.lastName,
|
|
208
|
+
contactEmail: input.contact.email ?? null,
|
|
209
|
+
contactPhone: input.contact.phone ?? null,
|
|
210
|
+
contactPreferredLanguage: input.contact.language ?? null,
|
|
211
|
+
contactCountry: input.contact.country ?? null,
|
|
212
|
+
contactRegion: input.contact.region ?? null,
|
|
213
|
+
contactCity: input.contact.city ?? null,
|
|
214
|
+
contactAddressLine1: input.contact.address ?? null,
|
|
215
|
+
contactPostalCode: input.contact.postalCode ?? null,
|
|
216
|
+
sellAmountCents: totalCents,
|
|
217
|
+
pax: guestCount,
|
|
218
|
+
startDate: voyage.departureDate,
|
|
219
|
+
endDate: voyage.returnDate,
|
|
220
|
+
internalNotes: input.notes ?? null,
|
|
221
|
+
}, userId);
|
|
222
|
+
if (!booking) {
|
|
223
|
+
throw new Error(`Upstream booking ${upstream.connectorBookingRef} succeeded but local createBooking returned null. Operator must reconcile manually via '${input.adapter.name}'.`);
|
|
224
|
+
}
|
|
225
|
+
await createCharterTravelers(tx, booking.id, input.guests ?? [], userId, {
|
|
226
|
+
includeGuestNotes: false,
|
|
227
|
+
});
|
|
228
|
+
const charterDetails = await bookingCharterDetailsService.upsert(tx, booking.id, {
|
|
229
|
+
bookingMode: "whole_yacht",
|
|
230
|
+
source: "external",
|
|
231
|
+
sourceProvider: input.adapter.name,
|
|
232
|
+
sourceRef: input.voyageRef,
|
|
233
|
+
voyageId: null,
|
|
234
|
+
suiteId: null,
|
|
235
|
+
yachtId: null,
|
|
236
|
+
voyageDisplayName: voyage.name ?? voyage.voyageCode,
|
|
237
|
+
suiteDisplayName: null,
|
|
238
|
+
yachtName: yacht?.name ?? null,
|
|
239
|
+
charterAreaSnapshot: voyage.charterAreaOverride ?? null,
|
|
240
|
+
guestCount,
|
|
241
|
+
quotedCurrency: finalQuote.currency,
|
|
242
|
+
quotedSuitePrice: null,
|
|
243
|
+
quotedPortFee: null,
|
|
244
|
+
quotedCharterFee: finalQuote.charterFee,
|
|
245
|
+
apaPercent: finalQuote.apaPercent,
|
|
246
|
+
apaAmount: finalQuote.apaAmount,
|
|
247
|
+
quotedTotal: finalQuote.total,
|
|
248
|
+
mybaTemplateIdSnapshot: mybaTemplateRef,
|
|
249
|
+
mybaContractId: null,
|
|
250
|
+
apaPaidAmount: "0.00",
|
|
251
|
+
apaSpentAmount: "0.00",
|
|
252
|
+
apaRefundAmount: "0.00",
|
|
253
|
+
connectorBookingRef: upstream.connectorBookingRef,
|
|
254
|
+
connectorStatus: upstream.connectorStatus ?? null,
|
|
255
|
+
notes: input.notes ?? null,
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
bookingId: booking.id,
|
|
259
|
+
bookingNumber: booking.bookingNumber,
|
|
260
|
+
charterDetails,
|
|
261
|
+
quote: finalQuote,
|
|
262
|
+
sourceProvider: input.adapter.name,
|
|
263
|
+
sourceRef: input.voyageRef,
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
},
|
|
267
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-plane integration for the charters service.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the products / cruises / accommodations pattern. Charters carry a
|
|
5
|
+
* couple of vertical-specific managed fields — `defaultMybaTemplateId` and
|
|
6
|
+
* `defaultApaPercent` — that participate in the snapshot at quote/book time.
|
|
7
|
+
*
|
|
8
|
+
* See `docs/architecture/catalog-architecture.md` §9.1 and
|
|
9
|
+
* `docs/architecture/charters-module.md`.
|
|
10
|
+
*/
|
|
11
|
+
import { type CaptureSnapshotInput, type DocumentBuilder, type DocumentEmitter, type IndexerDocument, type IndexerSlice, type PricingBasis, type Provenance, type ResolvedView, type ResolverScope } from "@voyant-travel/catalog";
|
|
12
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
13
|
+
import { charterProducts } from "./schema-core.js";
|
|
14
|
+
/**
|
|
15
|
+
* Maps a charter-product row to a field-keyed projection. Most charters are
|
|
16
|
+
* operator-owned (yacht agencies typically own the inventory they sell);
|
|
17
|
+
* sourced charters from a brand's central booking system would carry their
|
|
18
|
+
* actual provenance.
|
|
19
|
+
*/
|
|
20
|
+
export declare function charterProductRowToProjection(row: typeof charterProducts.$inferSelect, context: {
|
|
21
|
+
sellerOperatorId: string;
|
|
22
|
+
sourceKind?: string;
|
|
23
|
+
sourceRef?: string;
|
|
24
|
+
}): ReadonlyMap<string, unknown>;
|
|
25
|
+
export declare function charterProvenance(_row: typeof charterProducts.$inferSelect, context: {
|
|
26
|
+
sellerOperatorId: string;
|
|
27
|
+
sourceKind?: string;
|
|
28
|
+
sourceRef?: string;
|
|
29
|
+
}): Provenance;
|
|
30
|
+
export interface CharterCatalogContext {
|
|
31
|
+
sellerOperatorId: string;
|
|
32
|
+
scope: ResolverScope;
|
|
33
|
+
sourceKind?: string;
|
|
34
|
+
sourceRef?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function getResolvedCharterById(db: AnyDrizzleDb, id: string, context: CharterCatalogContext): Promise<ResolvedView | null>;
|
|
37
|
+
export declare function listResolvedCharters(db: AnyDrizzleDb, rows: ReadonlyArray<typeof charterProducts.$inferSelect>, context: CharterCatalogContext): Promise<ResolvedView[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Build a `CaptureSnapshotInput` for a charter product. Charters' MYBA
|
|
40
|
+
* template id and APA percent participate in the snapshot per the policy
|
|
41
|
+
* (snapshot mode `on-book` and `on-quote-and-book` respectively) — the
|
|
42
|
+
* resolved view already includes them, so no special handling needed here.
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildCharterSnapshotInput(db: AnyDrizzleDb, charterProductId: string, context: CharterCatalogContext & {
|
|
45
|
+
pricingBasis?: PricingBasis;
|
|
46
|
+
}): Promise<Omit<CaptureSnapshotInput, "bookingId"> | null>;
|
|
47
|
+
export declare function createCharterDocumentEmitter(context: {
|
|
48
|
+
sellerOperatorId: string;
|
|
49
|
+
sourceKind?: string;
|
|
50
|
+
sourceRef?: string;
|
|
51
|
+
}): DocumentEmitter<typeof charterProducts.$inferSelect>;
|
|
52
|
+
export declare function createCharterDocumentBuilder(db: AnyDrizzleDb, context: {
|
|
53
|
+
sellerOperatorId: string;
|
|
54
|
+
sourceKind?: string;
|
|
55
|
+
sourceRef?: string;
|
|
56
|
+
}): DocumentBuilder;
|
|
57
|
+
export type { CaptureSnapshotInput, DocumentBuilder, DocumentEmitter, IndexerDocument, IndexerSlice, PricingBasis, Provenance, ResolvedView, ResolverScope, };
|
|
58
|
+
//# sourceMappingURL=service-catalog-plane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,eAAe,EACpB,KAAK,eAAe,EAEpB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIrD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAUlD;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,OAAO,eAAe,CAAC,YAAY,EACxC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAwC9B;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,OAAO,eAAe,CAAC,YAAY,EACzC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,UAAU,CAMZ;AAED,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,EAAE,MAAM,CAAA;IACxB,KAAK,EAAE,aAAa,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAW9B;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,aAAa,CAAC,OAAO,eAAe,CAAC,YAAY,CAAC,EACxD,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,YAAY,EAAE,CAAC,CAoBzB;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,YAAY,EAChB,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,qBAAqB,GAAG;IAAE,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GAC/D,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,CAUzD;AAMD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,eAAe,CAAC,OAAO,eAAe,CAAC,YAAY,CAAC,CAavD;AAED,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,eAAe,CAYjB;AAED,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,aAAa,GACd,CAAA"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-plane integration for the charters service.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the products / cruises / accommodations pattern. Charters carry a
|
|
5
|
+
* couple of vertical-specific managed fields — `defaultMybaTemplateId` and
|
|
6
|
+
* `defaultApaPercent` — that participate in the snapshot at quote/book time.
|
|
7
|
+
*
|
|
8
|
+
* See `docs/architecture/catalog-architecture.md` §9.1 and
|
|
9
|
+
* `docs/architecture/charters-module.md`.
|
|
10
|
+
*/
|
|
11
|
+
import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, resolveEntityView, } from "@voyant-travel/catalog";
|
|
12
|
+
import { eq } from "drizzle-orm";
|
|
13
|
+
import { charterCatalogPolicy } from "./catalog-policy.js";
|
|
14
|
+
import { charterProducts } from "./schema-core.js";
|
|
15
|
+
let _registry;
|
|
16
|
+
function getChartersRegistry() {
|
|
17
|
+
if (!_registry) {
|
|
18
|
+
_registry = createFieldPolicyRegistry(charterCatalogPolicy);
|
|
19
|
+
}
|
|
20
|
+
return _registry;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Maps a charter-product row to a field-keyed projection. Most charters are
|
|
24
|
+
* operator-owned (yacht agencies typically own the inventory they sell);
|
|
25
|
+
* sourced charters from a brand's central booking system would carry their
|
|
26
|
+
* actual provenance.
|
|
27
|
+
*/
|
|
28
|
+
export function charterProductRowToProjection(row, context) {
|
|
29
|
+
return new Map([
|
|
30
|
+
// Provenance
|
|
31
|
+
["source.kind", context.sourceKind ?? "owned"],
|
|
32
|
+
["source.ref", context.sourceRef],
|
|
33
|
+
["seller.operator_id", context.sellerOperatorId],
|
|
34
|
+
// Identity
|
|
35
|
+
["id", row.id],
|
|
36
|
+
["slug", row.slug],
|
|
37
|
+
["createdAt", row.createdAt],
|
|
38
|
+
["updatedAt", row.updatedAt],
|
|
39
|
+
["externalRefs", row.externalRefs],
|
|
40
|
+
// Merchandisable
|
|
41
|
+
["name", row.name],
|
|
42
|
+
["description", row.description],
|
|
43
|
+
["shortDescription", row.shortDescription],
|
|
44
|
+
["heroImageUrl", row.heroImageUrl],
|
|
45
|
+
["thumbnailUrl", row.heroImageUrl],
|
|
46
|
+
["mapImageUrl", row.mapImageUrl],
|
|
47
|
+
// Structural
|
|
48
|
+
["status", row.status],
|
|
49
|
+
["lineSupplierId", row.lineSupplierId],
|
|
50
|
+
["defaultYachtId", row.defaultYachtId],
|
|
51
|
+
["regions[]", row.regions],
|
|
52
|
+
["themes[]", row.themes],
|
|
53
|
+
["defaultBookingModes[]", row.defaultBookingModes],
|
|
54
|
+
// Charter-specific managed fields (legal-sensitive — captured in snapshot)
|
|
55
|
+
["defaultMybaTemplateId", row.defaultMybaTemplateId],
|
|
56
|
+
["defaultApaPercent", row.defaultApaPercent],
|
|
57
|
+
// Volatile-indexed
|
|
58
|
+
["lowestPriceCachedAmount", row.lowestPriceCachedAmount],
|
|
59
|
+
["lowestPriceCachedCurrency", row.lowestPriceCachedCurrency],
|
|
60
|
+
["earliestVoyageCached", row.earliestVoyageCached],
|
|
61
|
+
["latestVoyageCached", row.latestVoyageCached],
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
export function charterProvenance(_row, context) {
|
|
65
|
+
return {
|
|
66
|
+
source_kind: context.sourceKind ?? "owned",
|
|
67
|
+
source_freshness: context.sourceKind && context.sourceKind !== "owned" ? "sync" : "static",
|
|
68
|
+
source_ref: context.sourceRef,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export async function getResolvedCharterById(db, id, context) {
|
|
72
|
+
const rows = await db.select().from(charterProducts).where(eq(charterProducts.id, id)).limit(1);
|
|
73
|
+
const row = rows[0];
|
|
74
|
+
if (!row)
|
|
75
|
+
return null;
|
|
76
|
+
const projection = charterProductRowToProjection(row, {
|
|
77
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
78
|
+
sourceKind: context.sourceKind,
|
|
79
|
+
sourceRef: context.sourceRef,
|
|
80
|
+
});
|
|
81
|
+
return resolveEntityView(db, getChartersRegistry(), "charters", id, projection, context.scope);
|
|
82
|
+
}
|
|
83
|
+
export async function listResolvedCharters(db, rows, context) {
|
|
84
|
+
const registry = getChartersRegistry();
|
|
85
|
+
const views = [];
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
const projection = charterProductRowToProjection(row, {
|
|
88
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
89
|
+
sourceKind: context.sourceKind,
|
|
90
|
+
sourceRef: context.sourceRef,
|
|
91
|
+
});
|
|
92
|
+
const view = await resolveEntityView(db, registry, "charters", row.id, projection, context.scope);
|
|
93
|
+
views.push(view);
|
|
94
|
+
}
|
|
95
|
+
return views;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build a `CaptureSnapshotInput` for a charter product. Charters' MYBA
|
|
99
|
+
* template id and APA percent participate in the snapshot per the policy
|
|
100
|
+
* (snapshot mode `on-book` and `on-quote-and-book` respectively) — the
|
|
101
|
+
* resolved view already includes them, so no special handling needed here.
|
|
102
|
+
*/
|
|
103
|
+
export async function buildCharterSnapshotInput(db, charterProductId, context) {
|
|
104
|
+
const view = await getResolvedCharterById(db, charterProductId, context);
|
|
105
|
+
if (!view)
|
|
106
|
+
return null;
|
|
107
|
+
return buildSnapshotInputFromView(view, {
|
|
108
|
+
entityModule: "charters",
|
|
109
|
+
entityId: charterProductId,
|
|
110
|
+
sourceKind: context.sourceKind ?? "owned",
|
|
111
|
+
sourceRef: context.sourceRef,
|
|
112
|
+
pricingBasis: context.pricingBasis,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
// Indexer document emission
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
export function createCharterDocumentEmitter(context) {
|
|
119
|
+
const registry = getChartersRegistry();
|
|
120
|
+
return {
|
|
121
|
+
vertical: "charters",
|
|
122
|
+
emit(source, slice) {
|
|
123
|
+
const projection = charterProductRowToProjection(source, {
|
|
124
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
125
|
+
sourceKind: context.sourceKind,
|
|
126
|
+
sourceRef: context.sourceRef,
|
|
127
|
+
});
|
|
128
|
+
return buildIndexerDocument(registry, projection, slice, source.id);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function createCharterDocumentBuilder(db, context) {
|
|
133
|
+
const emitter = createCharterDocumentEmitter(context);
|
|
134
|
+
return async (entityId, slice) => {
|
|
135
|
+
const rows = await db
|
|
136
|
+
.select()
|
|
137
|
+
.from(charterProducts)
|
|
138
|
+
.where(eq(charterProducts.id, entityId))
|
|
139
|
+
.limit(1);
|
|
140
|
+
const row = rows[0];
|
|
141
|
+
if (!row)
|
|
142
|
+
return null;
|
|
143
|
+
return emitter.emit(row, slice);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Charter content synthesizer — fallback for thin adapters that
|
|
3
|
+
* declare `supportsContentFetch: false`.
|
|
4
|
+
*
|
|
5
|
+
* Produces the most complete `CharterContent` blob we can legitimately
|
|
6
|
+
* synthesize from the durable sourced-entry projection + locale-aware
|
|
7
|
+
* overlays + plane-level provenance. Yacht spec, voyage list, suite
|
|
8
|
+
* map, and schedule_days render as typed empty states unless the
|
|
9
|
+
* projection genuinely carries them.
|
|
10
|
+
*
|
|
11
|
+
* Per §3.6: never invents plausible-but-unverified fields, never
|
|
12
|
+
* machine-translates, never mines snapshots, never caches its own
|
|
13
|
+
* output.
|
|
14
|
+
*/
|
|
15
|
+
import { type ProvenanceReadResult } from "@voyant-travel/catalog";
|
|
16
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
17
|
+
import { type CharterContent } from "./content-shape.js";
|
|
18
|
+
export interface SynthesizeCharterContentOptions {
|
|
19
|
+
provenance: Extract<ProvenanceReadResult, {
|
|
20
|
+
kind: "sourced";
|
|
21
|
+
}>;
|
|
22
|
+
overlays?: ReadonlyArray<{
|
|
23
|
+
field_path: string;
|
|
24
|
+
value: unknown;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export interface SynthesizedCharterContent {
|
|
28
|
+
content: CharterContent;
|
|
29
|
+
content_schema_version: string;
|
|
30
|
+
served_locale: string;
|
|
31
|
+
source_kind: string;
|
|
32
|
+
source_provider?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function synthesizeCharterContent(scope: {
|
|
35
|
+
locale: string;
|
|
36
|
+
}, options: SynthesizeCharterContentOptions): SynthesizedCharterContent;
|
|
37
|
+
export declare function synthesizeCharterContentFromDb(db: AnyDrizzleDb, scope: {
|
|
38
|
+
locale: string;
|
|
39
|
+
}, provenance: Extract<ProvenanceReadResult, {
|
|
40
|
+
kind: "sourced";
|
|
41
|
+
}>): Promise<SynthesizedCharterContent>;
|
|
42
|
+
//# sourceMappingURL=service-content-synthesizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-content-synthesizer.d.ts","sourceRoot":"","sources":["../src/service-content-synthesizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,+BAA+B;IAC9C,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IAC9D,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CACjE;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,cAAc,CAAA;IACvB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,OAAO,EAAE,+BAA+B,GACvC,yBAAyB,CAmC3B;AAED,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAC7D,OAAO,CAAC,yBAAyB,CAAC,CAOpC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Charter content synthesizer — fallback for thin adapters that
|
|
3
|
+
* declare `supportsContentFetch: false`.
|
|
4
|
+
*
|
|
5
|
+
* Produces the most complete `CharterContent` blob we can legitimately
|
|
6
|
+
* synthesize from the durable sourced-entry projection + locale-aware
|
|
7
|
+
* overlays + plane-level provenance. Yacht spec, voyage list, suite
|
|
8
|
+
* map, and schedule_days render as typed empty states unless the
|
|
9
|
+
* projection genuinely carries them.
|
|
10
|
+
*
|
|
11
|
+
* Per §3.6: never invents plausible-but-unverified fields, never
|
|
12
|
+
* machine-translates, never mines snapshots, never caches its own
|
|
13
|
+
* output.
|
|
14
|
+
*/
|
|
15
|
+
import { fetchOverlaysForEntity, mergeOverlaysIntoContent, } from "@voyant-travel/catalog";
|
|
16
|
+
import { CHARTERS_CONTENT_SCHEMA_VERSION, charterContentSchema, } from "./content-shape.js";
|
|
17
|
+
export function synthesizeCharterContent(scope, options) {
|
|
18
|
+
const projection = options.provenance.projection;
|
|
19
|
+
const charter = pickCharterSummary(projection, options.provenance);
|
|
20
|
+
const yacht = pickYacht(projection);
|
|
21
|
+
const policies = pickPolicies(projection);
|
|
22
|
+
const baseContent = {
|
|
23
|
+
charter,
|
|
24
|
+
yacht,
|
|
25
|
+
voyages: [],
|
|
26
|
+
suites: [],
|
|
27
|
+
schedule_days: [],
|
|
28
|
+
policies,
|
|
29
|
+
};
|
|
30
|
+
let merged = baseContent;
|
|
31
|
+
if (options.overlays && options.overlays.length > 0) {
|
|
32
|
+
const result = mergeOverlaysIntoContent(baseContent, options.overlays, {
|
|
33
|
+
validate(p) {
|
|
34
|
+
const r = charterContentSchema.safeParse(p);
|
|
35
|
+
return r.success
|
|
36
|
+
? { valid: true }
|
|
37
|
+
: { valid: false, reason: r.error.issues[0]?.message ?? "invalid" };
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
merged = charterContentSchema.parse(result);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
content: merged,
|
|
44
|
+
content_schema_version: CHARTERS_CONTENT_SCHEMA_VERSION,
|
|
45
|
+
served_locale: scope.locale,
|
|
46
|
+
source_kind: options.provenance.provenance.source_kind,
|
|
47
|
+
source_provider: options.provenance.provenance.source_provider,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export async function synthesizeCharterContentFromDb(db, scope, provenance) {
|
|
51
|
+
const entityId = entityIdFromProvenance(provenance);
|
|
52
|
+
const overlays = await fetchOverlaysForEntity(db, "charters", entityId);
|
|
53
|
+
return synthesizeCharterContent(scope, {
|
|
54
|
+
provenance,
|
|
55
|
+
overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function entityIdFromProvenance(provenance) {
|
|
59
|
+
const fromProjection = provenance.projection.id;
|
|
60
|
+
if (typeof fromProjection === "string" && fromProjection.length > 0) {
|
|
61
|
+
return fromProjection;
|
|
62
|
+
}
|
|
63
|
+
return provenance.entry_id;
|
|
64
|
+
}
|
|
65
|
+
function pickCharterSummary(projection, provenance) {
|
|
66
|
+
return {
|
|
67
|
+
id: stringOr(projection.id, "") || provenance.entry_id,
|
|
68
|
+
name: stringOr(projection.name, "") || stringOr(projection.title, "") || "Unnamed charter",
|
|
69
|
+
status: stringOr(projection.status, undefined),
|
|
70
|
+
description: stringOr(projection.description, null),
|
|
71
|
+
charter_type: stringOr(projection.charter_type, null),
|
|
72
|
+
hero_image_url: stringOr(projection.hero_image_url, null),
|
|
73
|
+
highlights: stringArrayOr(projection.highlights, []),
|
|
74
|
+
cruising_area: stringOr(projection.cruising_area, null) ?? stringOr(projection.area, null),
|
|
75
|
+
base_port: stringOr(projection.base_port, null),
|
|
76
|
+
duration_nights: numberOr(projection.duration_nights, null),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function pickYacht(projection) {
|
|
80
|
+
const yachtName = stringOr(projection.yacht_name, null) ?? stringOr(projection.yacht, null);
|
|
81
|
+
if (!yachtName)
|
|
82
|
+
return null;
|
|
83
|
+
return {
|
|
84
|
+
name: yachtName,
|
|
85
|
+
description: stringOr(projection.yacht_description, null),
|
|
86
|
+
type: stringOr(projection.yacht_type, null),
|
|
87
|
+
length_meters: numberOr(projection.length_meters, null),
|
|
88
|
+
capacity_guests: numberOr(projection.capacity_guests, null),
|
|
89
|
+
capacity_crew: numberOr(projection.capacity_crew, null),
|
|
90
|
+
cabins: numberOr(projection.cabins, null),
|
|
91
|
+
year_built: numberOr(projection.year_built, null),
|
|
92
|
+
builder: stringOr(projection.builder, null),
|
|
93
|
+
flag: stringOr(projection.flag, null),
|
|
94
|
+
amenities: stringArrayOr(projection.yacht_amenities, []),
|
|
95
|
+
images: stringArrayOr(projection.yacht_images, []),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function pickPolicies(projection) {
|
|
99
|
+
const out = [];
|
|
100
|
+
const cancel = stringOr(projection.cancellation_policy, null);
|
|
101
|
+
if (cancel)
|
|
102
|
+
out.push({ kind: "cancellation", body: cancel });
|
|
103
|
+
const payment = stringOr(projection.payment_terms, null);
|
|
104
|
+
if (payment)
|
|
105
|
+
out.push({ kind: "payment", body: payment });
|
|
106
|
+
const apa = stringOr(projection.apa_terms, null);
|
|
107
|
+
if (apa)
|
|
108
|
+
out.push({ kind: "apa", body: apa });
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function stringOr(value, fallback) {
|
|
112
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
113
|
+
}
|
|
114
|
+
function numberOr(value, fallback) {
|
|
115
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
116
|
+
}
|
|
117
|
+
function stringArrayOr(value, fallback) {
|
|
118
|
+
if (!Array.isArray(value))
|
|
119
|
+
return fallback;
|
|
120
|
+
const out = value.filter((v) => typeof v === "string");
|
|
121
|
+
return out.length > 0 ? out : fallback;
|
|
122
|
+
}
|