@voyant-travel/storefront 0.120.1

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 (126) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +231 -0
  3. package/dist/booking-intents.d.ts +42 -0
  4. package/dist/booking-intents.d.ts.map +1 -0
  5. package/dist/booking-intents.js +83 -0
  6. package/dist/customer-portal/index.d.ts +16 -0
  7. package/dist/customer-portal/index.d.ts.map +1 -0
  8. package/dist/customer-portal/index.js +23 -0
  9. package/dist/customer-portal/route-runtime.d.ts +16 -0
  10. package/dist/customer-portal/route-runtime.d.ts.map +1 -0
  11. package/dist/customer-portal/route-runtime.js +27 -0
  12. package/dist/customer-portal/routes-public.d.ts +1936 -0
  13. package/dist/customer-portal/routes-public.d.ts.map +1 -0
  14. package/dist/customer-portal/routes-public.js +165 -0
  15. package/dist/customer-portal/routes.d.ts +43 -0
  16. package/dist/customer-portal/routes.d.ts.map +1 -0
  17. package/dist/customer-portal/routes.js +17 -0
  18. package/dist/customer-portal/service-public-impl.d.ts +138 -0
  19. package/dist/customer-portal/service-public-impl.d.ts.map +1 -0
  20. package/dist/customer-portal/service-public-impl.js +1808 -0
  21. package/dist/customer-portal/service-public.d.ts +2 -0
  22. package/dist/customer-portal/service-public.d.ts.map +1 -0
  23. package/dist/customer-portal/service-public.js +1 -0
  24. package/dist/customer-portal/validation-public/bookings.d.ts +551 -0
  25. package/dist/customer-portal/validation-public/bookings.d.ts.map +1 -0
  26. package/dist/customer-portal/validation-public/bookings.js +132 -0
  27. package/dist/customer-portal/validation-public/common.d.ts +162 -0
  28. package/dist/customer-portal/validation-public/common.d.ts.map +1 -0
  29. package/dist/customer-portal/validation-public/common.js +139 -0
  30. package/dist/customer-portal/validation-public/profile.d.ts +749 -0
  31. package/dist/customer-portal/validation-public/profile.d.ts.map +1 -0
  32. package/dist/customer-portal/validation-public/profile.js +308 -0
  33. package/dist/customer-portal/validation-public.d.ts +3 -0
  34. package/dist/customer-portal/validation-public.d.ts.map +1 -0
  35. package/dist/customer-portal/validation-public.js +2 -0
  36. package/dist/guest-booking-guard.d.ts +24 -0
  37. package/dist/guest-booking-guard.d.ts.map +1 -0
  38. package/dist/guest-booking-guard.js +55 -0
  39. package/dist/index.d.ts +23 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +41 -0
  42. package/dist/product-extra-ref.d.ts +238 -0
  43. package/dist/product-extra-ref.d.ts.map +1 -0
  44. package/dist/product-extra-ref.js +22 -0
  45. package/dist/routes-admin.d.ts +220 -0
  46. package/dist/routes-admin.d.ts.map +1 -0
  47. package/dist/routes-admin.js +28 -0
  48. package/dist/routes-public.d.ts +1475 -0
  49. package/dist/routes-public.d.ts.map +1 -0
  50. package/dist/routes-public.js +362 -0
  51. package/dist/service-booking-session-bootstrap.d.ts +227 -0
  52. package/dist/service-booking-session-bootstrap.d.ts.map +1 -0
  53. package/dist/service-booking-session-bootstrap.js +287 -0
  54. package/dist/service-boundary-resource-sql.d.ts +18 -0
  55. package/dist/service-boundary-resource-sql.d.ts.map +1 -0
  56. package/dist/service-boundary-resource-sql.js +73 -0
  57. package/dist/service-boundary-sql.d.ts +103 -0
  58. package/dist/service-boundary-sql.d.ts.map +1 -0
  59. package/dist/service-boundary-sql.js +307 -0
  60. package/dist/service-departures-core.d.ts +41 -0
  61. package/dist/service-departures-core.d.ts.map +1 -0
  62. package/dist/service-departures-core.js +92 -0
  63. package/dist/service-departures-extensions.d.ts +46 -0
  64. package/dist/service-departures-extensions.d.ts.map +1 -0
  65. package/dist/service-departures-extensions.js +81 -0
  66. package/dist/service-departures-offers.d.ts +220 -0
  67. package/dist/service-departures-offers.d.ts.map +1 -0
  68. package/dist/service-departures-offers.js +177 -0
  69. package/dist/service-departures-price-preview.d.ts +306 -0
  70. package/dist/service-departures-price-preview.d.ts.map +1 -0
  71. package/dist/service-departures-price-preview.js +383 -0
  72. package/dist/service-departures-pricing-context.d.ts +115 -0
  73. package/dist/service-departures-pricing-context.d.ts.map +1 -0
  74. package/dist/service-departures-pricing-context.js +237 -0
  75. package/dist/service-departures-pricing.d.ts +5 -0
  76. package/dist/service-departures-pricing.d.ts.map +1 -0
  77. package/dist/service-departures-pricing.js +4 -0
  78. package/dist/service-departures.d.ts +192 -0
  79. package/dist/service-departures.d.ts.map +1 -0
  80. package/dist/service-departures.js +213 -0
  81. package/dist/service-intake.d.ts +130 -0
  82. package/dist/service-intake.d.ts.map +1 -0
  83. package/dist/service-intake.js +274 -0
  84. package/dist/service-transport-eligibility.d.ts +10 -0
  85. package/dist/service-transport-eligibility.d.ts.map +1 -0
  86. package/dist/service-transport-eligibility.js +198 -0
  87. package/dist/service.d.ts +1062 -0
  88. package/dist/service.d.ts.map +1 -0
  89. package/dist/service.js +332 -0
  90. package/dist/transport-eligibility.d.ts +4 -0
  91. package/dist/transport-eligibility.d.ts.map +1 -0
  92. package/dist/transport-eligibility.js +2 -0
  93. package/dist/validation/departures.d.ts +1669 -0
  94. package/dist/validation/departures.d.ts.map +1 -0
  95. package/dist/validation/departures.js +397 -0
  96. package/dist/validation/intake.d.ts +147 -0
  97. package/dist/validation/intake.d.ts.map +1 -0
  98. package/dist/validation/intake.js +69 -0
  99. package/dist/validation/offers.d.ts +340 -0
  100. package/dist/validation/offers.d.ts.map +1 -0
  101. package/dist/validation/offers.js +117 -0
  102. package/dist/validation-settings.d.ts +609 -0
  103. package/dist/validation-settings.d.ts.map +1 -0
  104. package/dist/validation-settings.js +235 -0
  105. package/dist/validation-transport-eligibility.d.ts +314 -0
  106. package/dist/validation-transport-eligibility.d.ts.map +1 -0
  107. package/dist/validation-transport-eligibility.js +97 -0
  108. package/dist/validation.d.ts +6 -0
  109. package/dist/validation.d.ts.map +1 -0
  110. package/dist/validation.js +4 -0
  111. package/dist/verification/index.d.ts +12 -0
  112. package/dist/verification/index.d.ts.map +1 -0
  113. package/dist/verification/index.js +18 -0
  114. package/dist/verification/routes-public.d.ts +121 -0
  115. package/dist/verification/routes-public.d.ts.map +1 -0
  116. package/dist/verification/routes-public.js +125 -0
  117. package/dist/verification/schema.d.ts +273 -0
  118. package/dist/verification/schema.d.ts.map +1 -0
  119. package/dist/verification/schema.js +50 -0
  120. package/dist/verification/service.d.ts +114 -0
  121. package/dist/verification/service.d.ts.map +1 -0
  122. package/dist/verification/service.js +283 -0
  123. package/dist/verification/validation.d.ts +98 -0
  124. package/dist/verification/validation.d.ts.map +1 -0
  125. package/dist/verification/validation.js +54 -0
  126. package/package.json +148 -0
@@ -0,0 +1,213 @@
1
+ import { getStorefrontSlotsResourceAvailability, listItineraryDayMedia, listItineraryDayServices, listItineraryDays, } from "./service-boundary-sql.js";
2
+ import { buildAvailabilityState, buildResourceManifest, countSlots, listDefaultItineraryIdsByProductIds, listMeetingPointsByProductIds, listSlots, normalizeIso, normalizeLocalDate, summarizeProductAvailability, todayLocalDate, } from "./service-departures-core.js";
3
+ import { buildDepartureStatus, buildRatePlans, resolvePricingContext, } from "./service-departures-pricing.js";
4
+ export { buildTravelerRequestedUnits, getStorefrontProductExtensions, previewStorefrontDeparturePrice, } from "./service-departures-pricing.js";
5
+ async function buildDeparture(db, slot, defaultItineraryByProduct, meetingPointByProduct, resourceAvailability) {
6
+ const context = await resolvePricingContext(db, slot.productId, slot.optionId, slot.id);
7
+ const itineraryId = slot.itineraryId ?? defaultItineraryByProduct.get(slot.productId) ?? null;
8
+ const resources = resourceAvailability ?? [];
9
+ return {
10
+ id: slot.id,
11
+ productId: slot.productId,
12
+ itineraryId: itineraryId ?? slot.id,
13
+ optionId: slot.optionId,
14
+ dateLocal: normalizeLocalDate(slot.dateLocal),
15
+ startAt: normalizeIso(slot.startsAt),
16
+ endAt: normalizeIso(slot.endsAt),
17
+ timezone: slot.timezone,
18
+ startTime: slot.startTimeId == null
19
+ ? null
20
+ : {
21
+ id: slot.startTimeId,
22
+ label: slot.startTimeLabel,
23
+ startTimeLocal: slot.startTimeLocal ?? "00:00",
24
+ durationMinutes: slot.durationMinutes,
25
+ },
26
+ meetingPoint: meetingPointByProduct?.get(slot.productId) ?? null,
27
+ capacity: slot.unlimited ? null : (slot.initialPax ?? slot.remainingPax ?? null),
28
+ remaining: slot.remainingPax ?? slot.remainingResources ?? null,
29
+ departureStatus: buildDepartureStatus(slot, context),
30
+ nights: slot.nights,
31
+ days: slot.days,
32
+ ratePlans: buildRatePlans(context),
33
+ resourceManifest: buildResourceManifest(resources),
34
+ };
35
+ }
36
+ export async function getStorefrontDeparture(db, departureId) {
37
+ const [slot] = await listSlots(db, { slotId: departureId, limit: 1 });
38
+ if (!slot) {
39
+ return null;
40
+ }
41
+ const [meetingPointByProduct, defaultItineraryByProduct, resourceAvailability] = await Promise.all([
42
+ listMeetingPointsByProductIds(db, [slot.productId]),
43
+ listDefaultItineraryIdsByProductIds(db, [slot.productId]),
44
+ getStorefrontSlotsResourceAvailability(db, [slot.id]),
45
+ ]);
46
+ return buildDeparture(db, slot, defaultItineraryByProduct, meetingPointByProduct, resourceAvailability.get(slot.id) ?? []);
47
+ }
48
+ export async function listStorefrontProductDepartures(db, productId, query) {
49
+ const filters = {
50
+ productId,
51
+ optionId: query.optionId,
52
+ status: query.status,
53
+ dateFrom: query.dateFrom,
54
+ dateTo: query.dateTo,
55
+ };
56
+ const [slots, total] = await Promise.all([
57
+ listSlots(db, {
58
+ ...filters,
59
+ limit: query.limit,
60
+ offset: query.offset,
61
+ }),
62
+ countSlots(db, filters),
63
+ ]);
64
+ const [meetingPointByProduct, defaultItineraryByProduct, resourceAvailability] = await Promise.all([
65
+ listMeetingPointsByProductIds(db, [productId]),
66
+ listDefaultItineraryIdsByProductIds(db, [productId]),
67
+ getStorefrontSlotsResourceAvailability(db, slots.map((slot) => slot.id)),
68
+ ]);
69
+ const data = await Promise.all(slots.map((slot) => buildDeparture(db, slot, defaultItineraryByProduct, meetingPointByProduct, resourceAvailability.get(slot.id) ?? [])));
70
+ return {
71
+ data,
72
+ total,
73
+ limit: query.limit,
74
+ offset: query.offset,
75
+ };
76
+ }
77
+ export async function getStorefrontProductAvailabilitySummary(db, productId, query) {
78
+ const requestedStatus = query.status;
79
+ const persistedStatus = requestedStatus === "on_request" ? "open" : requestedStatus;
80
+ const filters = {
81
+ productId,
82
+ optionId: query.optionId,
83
+ status: persistedStatus,
84
+ dateFrom: query.dateFrom ?? todayLocalDate(),
85
+ dateTo: query.dateTo,
86
+ includeCancelled: true,
87
+ };
88
+ const [slots, total] = await Promise.all([
89
+ listSlots(db, {
90
+ ...filters,
91
+ limit: query.limit,
92
+ offset: query.offset,
93
+ }),
94
+ countSlots(db, filters),
95
+ ]);
96
+ const [meetingPointByProduct, defaultItineraryByProduct] = await Promise.all([
97
+ listMeetingPointsByProductIds(db, [productId]),
98
+ listDefaultItineraryIdsByProductIds(db, [productId]),
99
+ ]);
100
+ const departures = (await Promise.all(slots.map(async (slot) => {
101
+ const departure = await buildDeparture(db, slot, defaultItineraryByProduct, meetingPointByProduct);
102
+ const availabilityState = buildAvailabilityState({
103
+ status: departure.departureStatus,
104
+ remaining: departure.remaining,
105
+ capacity: departure.capacity,
106
+ pastCutoff: slot.pastCutoff,
107
+ tooEarly: slot.tooEarly,
108
+ });
109
+ return {
110
+ id: departure.id,
111
+ productId: departure.productId,
112
+ optionId: departure.optionId,
113
+ dateLocal: departure.dateLocal,
114
+ startAt: departure.startAt,
115
+ endAt: departure.endAt,
116
+ timezone: departure.timezone,
117
+ status: departure.departureStatus,
118
+ availabilityState,
119
+ capacity: departure.capacity,
120
+ remaining: departure.remaining,
121
+ pastCutoff: slot.pastCutoff,
122
+ tooEarly: slot.tooEarly,
123
+ };
124
+ }))).filter((departure) => !requestedStatus || departure.status === requestedStatus);
125
+ const counts = departures.reduce((acc, departure) => {
126
+ acc.total += 1;
127
+ if (departure.status === "open")
128
+ acc.open += 1;
129
+ if (departure.status === "closed")
130
+ acc.closed += 1;
131
+ if (departure.status === "sold_out")
132
+ acc.soldOut += 1;
133
+ if (departure.status === "cancelled")
134
+ acc.cancelled += 1;
135
+ if (departure.status === "on_request")
136
+ acc.onRequest += 1;
137
+ if (departure.availabilityState === "past_cutoff")
138
+ acc.pastCutoff += 1;
139
+ if (departure.availabilityState === "too_early")
140
+ acc.tooEarly += 1;
141
+ if (departure.availabilityState === "available")
142
+ acc.available += 1;
143
+ return acc;
144
+ }, {
145
+ total: 0,
146
+ open: 0,
147
+ closed: 0,
148
+ soldOut: 0,
149
+ cancelled: 0,
150
+ onRequest: 0,
151
+ pastCutoff: 0,
152
+ tooEarly: 0,
153
+ available: 0,
154
+ });
155
+ return {
156
+ productId,
157
+ availabilityState: summarizeProductAvailability(departures),
158
+ counts,
159
+ departures,
160
+ total: requestedStatus === "on_request" ? departures.length : total,
161
+ limit: query.limit,
162
+ offset: query.offset,
163
+ };
164
+ }
165
+ export async function getStorefrontDepartureItinerary(db, input) {
166
+ const [slot] = await listSlots(db, {
167
+ productId: input.productId,
168
+ slotId: input.departureId,
169
+ limit: 1,
170
+ });
171
+ const defaultItineraryByProduct = await listDefaultItineraryIdsByProductIds(db, [input.productId]);
172
+ const itineraryId = slot?.itineraryId ?? defaultItineraryByProduct.get(input.productId);
173
+ if (!itineraryId) {
174
+ return null;
175
+ }
176
+ const days = await listItineraryDays(db, itineraryId);
177
+ if (days.length === 0) {
178
+ return null;
179
+ }
180
+ const dayIds = days.map((day) => day.id);
181
+ const [services, dayMedia] = await Promise.all([
182
+ listItineraryDayServices(db, dayIds),
183
+ listItineraryDayMedia(db, { productId: input.productId, dayIds }),
184
+ ]);
185
+ const servicesByDay = new Map();
186
+ for (const service of services) {
187
+ const existing = servicesByDay.get(service.dayId) ?? [];
188
+ existing.push(service);
189
+ servicesByDay.set(service.dayId, existing);
190
+ }
191
+ const mediaByDay = new Map();
192
+ for (const media of dayMedia) {
193
+ if (!media.dayId || mediaByDay.has(media.dayId)) {
194
+ continue;
195
+ }
196
+ mediaByDay.set(media.dayId, media);
197
+ }
198
+ return {
199
+ id: input.departureId,
200
+ itineraryId,
201
+ days: days.map((day) => ({
202
+ id: day.id,
203
+ title: day.title ?? `Day ${day.dayNumber}`,
204
+ description: day.description ?? null,
205
+ thumbnail: mediaByDay.get(day.id) ? { url: mediaByDay.get(day.id)?.url ?? "" } : null,
206
+ segments: (servicesByDay.get(day.id) ?? []).map((service) => ({
207
+ id: service.id,
208
+ title: service.name,
209
+ description: service.description ?? null,
210
+ })),
211
+ })),
212
+ };
213
+ }
@@ -0,0 +1,130 @@
1
+ import type { EventBus, EventSource } from "@voyant-travel/core";
2
+ import type { StorefrontRequestContext } from "./service.js";
3
+ import type { StorefrontIntakeResponse, StorefrontLeadIntakeInput, StorefrontNewsletterSubscribeInput, StorefrontNewsletterSubscribeResponse } from "./validation.js";
4
+ export declare const CUSTOMER_SIGNAL_CREATED_EVENT: "customer.signal.created";
5
+ export interface StorefrontCustomerSignalCreatedEvent {
6
+ id: string;
7
+ personId: string;
8
+ kind: StorefrontIntakeSignal["kind"];
9
+ source: StorefrontIntakeSignal["source"];
10
+ status: StorefrontIntakeSignal["status"];
11
+ productId?: string | null;
12
+ optionUnitId?: string | null;
13
+ sourceSubmissionId?: string | null;
14
+ intake?: {
15
+ surface: "storefront";
16
+ type: "lead";
17
+ } | {
18
+ surface: "storefront";
19
+ type: "newsletter";
20
+ doubleOptIn: "not_configured" | "requested";
21
+ };
22
+ }
23
+ export declare function emitCustomerSignalCreated(eventBus: EventBus | undefined, payload: StorefrontCustomerSignalCreatedEvent, source?: EventSource): Promise<void>;
24
+ export interface StorefrontIntakeGuardDecision {
25
+ allowed: boolean;
26
+ status?: 400 | 403 | 429;
27
+ error?: string;
28
+ }
29
+ export type StorefrontIntakeGuard = (input: {
30
+ kind: "lead";
31
+ body: StorefrontLeadIntakeInput;
32
+ context: StorefrontRequestContext;
33
+ } | {
34
+ kind: "newsletter";
35
+ body: StorefrontNewsletterSubscribeInput;
36
+ context: StorefrontRequestContext;
37
+ }) => Promise<StorefrontIntakeGuardDecision | undefined> | StorefrontIntakeGuardDecision | undefined;
38
+ export type StorefrontNewsletterDoubleOptInHook = (input: {
39
+ email: string;
40
+ personId: string;
41
+ signalId: string;
42
+ sourceSubmissionId: string;
43
+ body: StorefrontNewsletterSubscribeInput;
44
+ context: StorefrontRequestContext;
45
+ }) => Promise<void> | void;
46
+ export interface StorefrontIntakeSignal {
47
+ id: string;
48
+ personId: string;
49
+ kind: "wishlist" | "notify" | "inquiry" | "request_offer" | "referral";
50
+ source: "form" | "phone" | "admin" | "abandoned_cart" | "website" | "booking";
51
+ status: "new" | "contacted" | "qualified" | "converted" | "lost" | "expired";
52
+ productId?: string | null;
53
+ optionUnitId?: string | null;
54
+ sourceSubmissionId?: string | null;
55
+ metadata?: Record<string, unknown> | null;
56
+ }
57
+ export interface StorefrontIntakePerson {
58
+ id: string;
59
+ }
60
+ export interface StorefrontIntakePersistence {
61
+ findSignal(input: {
62
+ context: StorefrontRequestContext;
63
+ kind: StorefrontIntakeSignal["kind"];
64
+ sourceSubmissionId: string;
65
+ }): Promise<StorefrontIntakeSignal | null> | StorefrontIntakeSignal | null;
66
+ createPerson(input: {
67
+ context: StorefrontRequestContext;
68
+ data: {
69
+ firstName: string;
70
+ lastName: string;
71
+ status: "active";
72
+ website: string | null;
73
+ email?: string | null;
74
+ phone?: string | null;
75
+ source: string;
76
+ sourceRef: string;
77
+ tags: string[];
78
+ };
79
+ }): Promise<StorefrontIntakePerson | null> | StorefrontIntakePerson | null;
80
+ createCustomerSignal(input: {
81
+ context: StorefrontRequestContext;
82
+ data: {
83
+ personId: string;
84
+ productId?: string | null;
85
+ optionUnitId?: string | null;
86
+ kind: StorefrontIntakeSignal["kind"];
87
+ source: StorefrontIntakeSignal["source"];
88
+ status: "new";
89
+ priority: "normal";
90
+ notes?: string | null;
91
+ tags: string[];
92
+ sourceSubmissionId: string;
93
+ metadata: Record<string, unknown>;
94
+ };
95
+ }): Promise<StorefrontIntakeSignal | null> | StorefrontIntakeSignal | null;
96
+ updateCustomerSignal(input: {
97
+ context: StorefrontRequestContext;
98
+ id: string;
99
+ data: {
100
+ metadata: Record<string, unknown>;
101
+ };
102
+ }): Promise<StorefrontIntakeSignal | null> | StorefrontIntakeSignal | null;
103
+ deleteCustomerSignal(input: {
104
+ context: StorefrontRequestContext;
105
+ id: string;
106
+ }): Promise<unknown> | unknown;
107
+ deletePerson(input: {
108
+ context: StorefrontRequestContext;
109
+ id: string;
110
+ }): Promise<unknown> | unknown;
111
+ }
112
+ export type StorefrontIntakePersistenceResolver = (context: StorefrontRequestContext) => Promise<StorefrontIntakePersistence | null | undefined> | StorefrontIntakePersistence | null | undefined;
113
+ export interface StorefrontIntakeOptions {
114
+ guard?: StorefrontIntakeGuard;
115
+ persistence?: StorefrontIntakePersistence;
116
+ resolvePersistence?: StorefrontIntakePersistenceResolver;
117
+ requestNewsletterDoubleOptIn?: StorefrontNewsletterDoubleOptInHook;
118
+ }
119
+ export declare function createStorefrontLeadSignal(input: {
120
+ body: StorefrontLeadIntakeInput;
121
+ context: StorefrontRequestContext;
122
+ intake?: StorefrontIntakeOptions;
123
+ }): Promise<StorefrontIntakeResponse>;
124
+ export declare function subscribeStorefrontNewsletter(input: {
125
+ body: StorefrontNewsletterSubscribeInput;
126
+ context: StorefrontRequestContext;
127
+ intake?: StorefrontIntakeOptions;
128
+ requestDoubleOptIn?: StorefrontNewsletterDoubleOptInHook;
129
+ }): Promise<StorefrontNewsletterSubscribeResponse>;
130
+ //# sourceMappingURL=service-intake.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-intake.d.ts","sourceRoot":"","sources":["../src/service-intake.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,274 @@
1
+ export const CUSTOMER_SIGNAL_CREATED_EVENT = "customer.signal.created";
2
+ export async function emitCustomerSignalCreated(eventBus, payload, source = "service") {
3
+ if (!eventBus)
4
+ return;
5
+ await eventBus.emit(CUSTOMER_SIGNAL_CREATED_EVENT, payload, {
6
+ category: "domain",
7
+ source,
8
+ });
9
+ }
10
+ async function requirePersistence(options, context) {
11
+ const persistence = (await options?.resolvePersistence?.(context)) ?? options?.persistence ?? null;
12
+ if (!persistence) {
13
+ throw new Error("Storefront intake persistence is not configured");
14
+ }
15
+ return persistence;
16
+ }
17
+ function splitName(name) {
18
+ if (!name)
19
+ return {};
20
+ const parts = name.trim().split(/\s+/);
21
+ if (parts.length === 0)
22
+ return {};
23
+ if (parts.length === 1)
24
+ return { firstName: parts[0] };
25
+ return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
26
+ }
27
+ function personNameFromContact(contact) {
28
+ const split = splitName(contact.name);
29
+ return {
30
+ firstName: contact.firstName ?? split.firstName ?? "Storefront",
31
+ lastName: contact.lastName ?? split.lastName ?? "Lead",
32
+ };
33
+ }
34
+ function personNameFromNewsletter(input) {
35
+ const split = splitName(input.name);
36
+ const emailLocalPart = input.email
37
+ .split("@")[0]
38
+ ?.replace(/[._-]+/g, " ")
39
+ .trim();
40
+ return {
41
+ firstName: input.firstName ?? split.firstName ?? emailLocalPart ?? "Newsletter",
42
+ lastName: input.lastName ?? split.lastName ?? "Subscriber",
43
+ };
44
+ }
45
+ function normalizeEmail(email) {
46
+ return email.trim().toLowerCase();
47
+ }
48
+ function defaultNewsletterSubmissionId(email) {
49
+ return `newsletter:${normalizeEmail(email)}`;
50
+ }
51
+ function normalizePhone(phone) {
52
+ return phone?.replace(/[^\d+]/g, "").toLowerCase();
53
+ }
54
+ function defaultLeadSubmissionId(input) {
55
+ const contactKey = input.contact.email
56
+ ? `email:${normalizeEmail(input.contact.email)}`
57
+ : `phone:${normalizePhone(input.contact.phone) ?? "unknown"}`;
58
+ return [
59
+ "lead",
60
+ input.kind,
61
+ input.source,
62
+ input.productId ?? "-",
63
+ input.optionUnitId ?? "-",
64
+ contactKey,
65
+ ].join(":");
66
+ }
67
+ async function findExistingSignal(persistence, context, input) {
68
+ if (!input.sourceSubmissionId)
69
+ return null;
70
+ return await persistence.findSignal({
71
+ context,
72
+ kind: input.kind,
73
+ sourceSubmissionId: input.sourceSubmissionId,
74
+ });
75
+ }
76
+ function leadResponse(signal, duplicate) {
77
+ return {
78
+ id: signal.id,
79
+ personId: signal.personId,
80
+ kind: signal.kind,
81
+ source: signal.source,
82
+ status: signal.status,
83
+ duplicate,
84
+ };
85
+ }
86
+ function newsletterDoubleOptInFromSignal(signal) {
87
+ const metadata = signal.metadata;
88
+ const newsletter = metadata && typeof metadata === "object" && "newsletter" in metadata
89
+ ? metadata.newsletter
90
+ : null;
91
+ if (!newsletter || typeof newsletter !== "object" || !("doubleOptIn" in newsletter)) {
92
+ return "not_configured";
93
+ }
94
+ return newsletter.doubleOptIn === "requested" ? "requested" : "not_configured";
95
+ }
96
+ function newsletterSignalMetadata(input) {
97
+ return {
98
+ intake: { surface: "storefront", type: "newsletter" },
99
+ newsletter: { email: input.email, doubleOptIn: input.doubleOptIn },
100
+ payload: input.body.payload,
101
+ consent: input.body.consent,
102
+ source: {
103
+ url: input.body.sourceUrl ?? null,
104
+ locale: input.body.locale ?? null,
105
+ },
106
+ };
107
+ }
108
+ export async function createStorefrontLeadSignal(input) {
109
+ const persistence = await requirePersistence(input.intake, input.context);
110
+ const sourceSubmissionId = input.body.sourceSubmissionId ?? defaultLeadSubmissionId(input.body);
111
+ const existing = await findExistingSignal(persistence, input.context, {
112
+ kind: input.body.kind,
113
+ sourceSubmissionId,
114
+ });
115
+ if (existing)
116
+ return leadResponse(existing, true);
117
+ const { firstName, lastName } = personNameFromContact(input.body.contact);
118
+ const person = await persistence.createPerson({
119
+ context: input.context,
120
+ data: {
121
+ firstName,
122
+ lastName,
123
+ status: "active",
124
+ website: null,
125
+ email: input.body.contact.email ? normalizeEmail(input.body.contact.email) : null,
126
+ phone: input.body.contact.phone ?? null,
127
+ source: "storefront",
128
+ sourceRef: sourceSubmissionId,
129
+ tags: input.body.tags,
130
+ },
131
+ });
132
+ if (!person)
133
+ throw new Error("Failed to create intake person for storefront lead");
134
+ const signal = await persistence.createCustomerSignal({
135
+ context: input.context,
136
+ data: {
137
+ personId: person.id,
138
+ productId: input.body.productId ?? null,
139
+ optionUnitId: input.body.optionUnitId ?? null,
140
+ kind: input.body.kind,
141
+ source: input.body.source,
142
+ status: "new",
143
+ priority: "normal",
144
+ notes: input.body.notes ?? null,
145
+ tags: input.body.tags,
146
+ sourceSubmissionId,
147
+ metadata: {
148
+ intake: { surface: "storefront", type: "lead" },
149
+ payload: input.body.payload,
150
+ consent: input.body.consent,
151
+ source: {
152
+ url: input.body.sourceUrl ?? null,
153
+ locale: input.body.locale ?? null,
154
+ },
155
+ },
156
+ },
157
+ });
158
+ if (!signal)
159
+ throw new Error("Failed to create customer signal for storefront lead");
160
+ await emitCustomerSignalCreated(input.context.eventBus, {
161
+ id: signal.id,
162
+ personId: signal.personId,
163
+ kind: signal.kind,
164
+ source: signal.source,
165
+ status: signal.status,
166
+ productId: signal.productId,
167
+ optionUnitId: signal.optionUnitId,
168
+ sourceSubmissionId: signal.sourceSubmissionId,
169
+ intake: { surface: "storefront", type: "lead" },
170
+ }, "route");
171
+ return leadResponse(signal, false);
172
+ }
173
+ export async function subscribeStorefrontNewsletter(input) {
174
+ const persistence = await requirePersistence(input.intake, input.context);
175
+ const email = normalizeEmail(input.body.email);
176
+ const sourceSubmissionId = input.body.sourceSubmissionId ?? defaultNewsletterSubmissionId(input.body.email);
177
+ const existing = await findExistingSignal(persistence, input.context, {
178
+ kind: "notify",
179
+ sourceSubmissionId,
180
+ });
181
+ if (existing) {
182
+ return {
183
+ ...leadResponse(existing, true),
184
+ doubleOptIn: newsletterDoubleOptInFromSignal(existing),
185
+ };
186
+ }
187
+ const { firstName, lastName } = personNameFromNewsletter(input.body);
188
+ const person = await persistence.createPerson({
189
+ context: input.context,
190
+ data: {
191
+ firstName,
192
+ lastName,
193
+ status: "active",
194
+ website: null,
195
+ email,
196
+ source: "storefront-newsletter",
197
+ sourceRef: sourceSubmissionId,
198
+ tags: input.body.tags,
199
+ },
200
+ });
201
+ if (!person)
202
+ throw new Error("Failed to create intake person for newsletter subscription");
203
+ const doubleOptIn = input.requestDoubleOptIn ? "requested" : "not_configured";
204
+ let signal = await persistence.createCustomerSignal({
205
+ context: input.context,
206
+ data: {
207
+ personId: person.id,
208
+ kind: "notify",
209
+ source: input.body.source,
210
+ status: "new",
211
+ priority: "normal",
212
+ notes: "Newsletter subscription",
213
+ tags: input.body.tags,
214
+ sourceSubmissionId,
215
+ metadata: newsletterSignalMetadata({
216
+ email,
217
+ doubleOptIn: "not_configured",
218
+ body: input.body,
219
+ }),
220
+ },
221
+ });
222
+ if (!signal)
223
+ throw new Error("Failed to create customer signal for newsletter subscription");
224
+ if (input.requestDoubleOptIn) {
225
+ try {
226
+ await input.requestDoubleOptIn({
227
+ email,
228
+ personId: person.id,
229
+ signalId: signal.id,
230
+ sourceSubmissionId,
231
+ body: input.body,
232
+ context: input.context,
233
+ });
234
+ }
235
+ catch (error) {
236
+ await Promise.resolve(persistence.deleteCustomerSignal({
237
+ context: input.context,
238
+ id: signal.id,
239
+ })).catch(() => null);
240
+ await Promise.resolve(persistence.deletePerson({
241
+ context: input.context,
242
+ id: person.id,
243
+ })).catch(() => null);
244
+ throw error;
245
+ }
246
+ signal =
247
+ (await persistence.updateCustomerSignal({
248
+ context: input.context,
249
+ id: signal.id,
250
+ data: {
251
+ metadata: newsletterSignalMetadata({
252
+ email,
253
+ doubleOptIn,
254
+ body: input.body,
255
+ }),
256
+ },
257
+ })) ?? signal;
258
+ }
259
+ await emitCustomerSignalCreated(input.context.eventBus, {
260
+ id: signal.id,
261
+ personId: signal.personId,
262
+ kind: signal.kind,
263
+ source: signal.source,
264
+ status: signal.status,
265
+ productId: signal.productId,
266
+ optionUnitId: signal.optionUnitId,
267
+ sourceSubmissionId: signal.sourceSubmissionId,
268
+ intake: { surface: "storefront", type: "newsletter", doubleOptIn },
269
+ }, "route");
270
+ return {
271
+ ...leadResponse(signal, false),
272
+ doubleOptIn,
273
+ };
274
+ }
@@ -0,0 +1,10 @@
1
+ import type { StorefrontTransportEligibilityInput, StorefrontTransportEligibilityResult, StorefrontTransportEligibilityRuleInput } from "./validation-transport-eligibility.js";
2
+ export declare function evaluateStorefrontTransportEligibility(input: {
3
+ departureId: string;
4
+ productId?: string | null;
5
+ travelStartsOn?: string | null;
6
+ travelEndsOn?: string | null;
7
+ travelers: StorefrontTransportEligibilityInput["travelers"];
8
+ rules: StorefrontTransportEligibilityRuleInput[];
9
+ }): StorefrontTransportEligibilityResult;
10
+ //# sourceMappingURL=service-transport-eligibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-transport-eligibility.d.ts","sourceRoot":"","sources":["../src/service-transport-eligibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mCAAmC,EAEnC,oCAAoC,EAEpC,uCAAuC,EACxC,MAAM,uCAAuC,CAAA;AAoR9C,wBAAgB,sCAAsC,CAAC,KAAK,EAAE;IAC5D,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,mCAAmC,CAAC,WAAW,CAAC,CAAA;IAC3D,KAAK,EAAE,uCAAuC,EAAE,CAAA;CACjD,GAAG,oCAAoC,CA0CvC"}