@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.
Files changed (108) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +16 -0
  3. package/dist/adapters/index.d.ts +254 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +16 -0
  6. package/dist/adapters/memoize.d.ts +28 -0
  7. package/dist/adapters/memoize.d.ts.map +1 -0
  8. package/dist/adapters/memoize.js +121 -0
  9. package/dist/adapters/mock.d.ts +50 -0
  10. package/dist/adapters/mock.d.ts.map +1 -0
  11. package/dist/adapters/mock.js +194 -0
  12. package/dist/adapters/registry.d.ts +24 -0
  13. package/dist/adapters/registry.d.ts.map +1 -0
  14. package/dist/adapters/registry.js +40 -0
  15. package/dist/booking-extension.d.ts +895 -0
  16. package/dist/booking-extension.d.ts.map +1 -0
  17. package/dist/booking-extension.js +339 -0
  18. package/dist/catalog-policy.d.ts +23 -0
  19. package/dist/catalog-policy.d.ts.map +1 -0
  20. package/dist/catalog-policy.js +400 -0
  21. package/dist/content-shape.d.ts +5 -0
  22. package/dist/content-shape.d.ts.map +1 -0
  23. package/dist/content-shape.js +13 -0
  24. package/dist/draft-shape.d.ts +29 -0
  25. package/dist/draft-shape.d.ts.map +1 -0
  26. package/dist/draft-shape.js +63 -0
  27. package/dist/index.d.ts +31 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +55 -0
  30. package/dist/lib/key.d.ts +22 -0
  31. package/dist/lib/key.d.ts.map +1 -0
  32. package/dist/lib/key.js +24 -0
  33. package/dist/routes-public.d.ts +785 -0
  34. package/dist/routes-public.d.ts.map +1 -0
  35. package/dist/routes-public.js +234 -0
  36. package/dist/routes.d.ts +1744 -0
  37. package/dist/routes.d.ts.map +1 -0
  38. package/dist/routes.js +543 -0
  39. package/dist/schema-core.d.ts +815 -0
  40. package/dist/schema-core.d.ts.map +1 -0
  41. package/dist/schema-core.js +98 -0
  42. package/dist/schema-itinerary.d.ts +239 -0
  43. package/dist/schema-itinerary.d.ts.map +1 -0
  44. package/dist/schema-itinerary.js +30 -0
  45. package/dist/schema-pricing.d.ts +385 -0
  46. package/dist/schema-pricing.d.ts.map +1 -0
  47. package/dist/schema-pricing.js +62 -0
  48. package/dist/schema-shared.d.ts +8 -0
  49. package/dist/schema-shared.d.ts.map +1 -0
  50. package/dist/schema-shared.js +37 -0
  51. package/dist/schema-sourced-content.d.ts +253 -0
  52. package/dist/schema-sourced-content.d.ts.map +1 -0
  53. package/dist/schema-sourced-content.js +44 -0
  54. package/dist/schema-yachts.d.ts +367 -0
  55. package/dist/schema-yachts.d.ts.map +1 -0
  56. package/dist/schema-yachts.js +30 -0
  57. package/dist/schema.d.ts +8 -0
  58. package/dist/schema.d.ts.map +1 -0
  59. package/dist/schema.js +7 -0
  60. package/dist/service-bookings-helpers.d.ts +20 -0
  61. package/dist/service-bookings-helpers.d.ts.map +1 -0
  62. package/dist/service-bookings-helpers.js +67 -0
  63. package/dist/service-bookings-local.d.ts +5 -0
  64. package/dist/service-bookings-local.d.ts.map +1 -0
  65. package/dist/service-bookings-local.js +177 -0
  66. package/dist/service-bookings-types.d.ts +88 -0
  67. package/dist/service-bookings-types.d.ts.map +1 -0
  68. package/dist/service-bookings-types.js +1 -0
  69. package/dist/service-bookings.d.ts +36 -0
  70. package/dist/service-bookings.d.ts.map +1 -0
  71. package/dist/service-bookings.js +267 -0
  72. package/dist/service-catalog-plane.d.ts +58 -0
  73. package/dist/service-catalog-plane.d.ts.map +1 -0
  74. package/dist/service-catalog-plane.js +145 -0
  75. package/dist/service-content-synthesizer.d.ts +42 -0
  76. package/dist/service-content-synthesizer.d.ts.map +1 -0
  77. package/dist/service-content-synthesizer.js +122 -0
  78. package/dist/service-content.d.ts +43 -0
  79. package/dist/service-content.d.ts.map +1 -0
  80. package/dist/service-content.js +248 -0
  81. package/dist/service-myba.d.ts +85 -0
  82. package/dist/service-myba.d.ts.map +1 -0
  83. package/dist/service-myba.js +88 -0
  84. package/dist/service-pricing.d.ts +64 -0
  85. package/dist/service-pricing.d.ts.map +1 -0
  86. package/dist/service-pricing.js +167 -0
  87. package/dist/service.d.ts +131 -0
  88. package/dist/service.d.ts.map +1 -0
  89. package/dist/service.js +279 -0
  90. package/dist/validation-core.d.ts +152 -0
  91. package/dist/validation-core.d.ts.map +1 -0
  92. package/dist/validation-core.js +66 -0
  93. package/dist/validation-itinerary.d.ts +43 -0
  94. package/dist/validation-itinerary.d.ts.map +1 -0
  95. package/dist/validation-itinerary.js +19 -0
  96. package/dist/validation-pricing.d.ts +103 -0
  97. package/dist/validation-pricing.d.ts.map +1 -0
  98. package/dist/validation-pricing.js +28 -0
  99. package/dist/validation-shared.d.ts +61 -0
  100. package/dist/validation-shared.d.ts.map +1 -0
  101. package/dist/validation-shared.js +60 -0
  102. package/dist/validation-yachts.d.ts +76 -0
  103. package/dist/validation-yachts.d.ts.map +1 -0
  104. package/dist/validation-yachts.js +36 -0
  105. package/dist/validation.d.ts +6 -0
  106. package/dist/validation.d.ts.map +1 -0
  107. package/dist/validation.js +5 -0
  108. 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
+ }