@voyantjs/storefront 0.47.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/routes-admin.d.ts +192 -0
- package/dist/routes-admin.d.ts.map +1 -0
- package/dist/routes-admin.js +28 -0
- package/dist/routes-public.d.ts +598 -10
- package/dist/routes-public.d.ts.map +1 -1
- package/dist/routes-public.js +104 -3
- package/dist/service-booking-session-bootstrap.d.ts +227 -0
- package/dist/service-booking-session-bootstrap.d.ts.map +1 -0
- package/dist/service-booking-session-bootstrap.js +297 -0
- package/dist/service-departures.d.ts +301 -2
- package/dist/service-departures.d.ts.map +1 -1
- package/dist/service-departures.js +406 -42
- package/dist/service-intake.d.ts +40 -0
- package/dist/service-intake.d.ts.map +1 -0
- package/dist/service-intake.js +231 -0
- package/dist/service.d.ts +634 -6
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +127 -2
- package/dist/validation-settings.d.ts +489 -0
- package/dist/validation-settings.d.ts.map +1 -0
- package/dist/validation-settings.js +205 -0
- package/dist/validation.d.ts +1458 -433
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +238 -124
- package/package.json +17 -9
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { crmService } from "@voyantjs/crm";
|
|
2
|
+
import { CUSTOMER_SIGNAL_CREATED_EVENT, emitCustomerSignalCreated } from "@voyantjs/crm/events";
|
|
3
|
+
import { customerSignals } from "@voyantjs/crm/schema";
|
|
4
|
+
import { and, eq } from "drizzle-orm";
|
|
5
|
+
export { CUSTOMER_SIGNAL_CREATED_EVENT };
|
|
6
|
+
function requireDb(context) {
|
|
7
|
+
if (!context.db) {
|
|
8
|
+
throw new Error("Storefront intake requires a request database");
|
|
9
|
+
}
|
|
10
|
+
return context.db;
|
|
11
|
+
}
|
|
12
|
+
function splitName(name) {
|
|
13
|
+
if (!name)
|
|
14
|
+
return {};
|
|
15
|
+
const parts = name.trim().split(/\s+/);
|
|
16
|
+
if (parts.length === 0)
|
|
17
|
+
return {};
|
|
18
|
+
if (parts.length === 1)
|
|
19
|
+
return { firstName: parts[0] };
|
|
20
|
+
return { firstName: parts[0], lastName: parts.slice(1).join(" ") };
|
|
21
|
+
}
|
|
22
|
+
function personNameFromContact(contact) {
|
|
23
|
+
const split = splitName(contact.name);
|
|
24
|
+
return {
|
|
25
|
+
firstName: contact.firstName ?? split.firstName ?? "Storefront",
|
|
26
|
+
lastName: contact.lastName ?? split.lastName ?? "Lead",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function personNameFromNewsletter(input) {
|
|
30
|
+
const split = splitName(input.name);
|
|
31
|
+
const emailLocalPart = input.email
|
|
32
|
+
.split("@")[0]
|
|
33
|
+
?.replace(/[._-]+/g, " ")
|
|
34
|
+
.trim();
|
|
35
|
+
return {
|
|
36
|
+
firstName: input.firstName ?? split.firstName ?? emailLocalPart ?? "Newsletter",
|
|
37
|
+
lastName: input.lastName ?? split.lastName ?? "Subscriber",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function normalizeEmail(email) {
|
|
41
|
+
return email.trim().toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
function defaultNewsletterSubmissionId(email) {
|
|
44
|
+
return `newsletter:${normalizeEmail(email)}`;
|
|
45
|
+
}
|
|
46
|
+
async function findExistingSignal(db, input) {
|
|
47
|
+
if (!input.sourceSubmissionId)
|
|
48
|
+
return null;
|
|
49
|
+
const [row] = await db
|
|
50
|
+
.select()
|
|
51
|
+
.from(customerSignals)
|
|
52
|
+
.where(and(eq(customerSignals.kind, input.kind), eq(customerSignals.sourceSubmissionId, input.sourceSubmissionId)))
|
|
53
|
+
.limit(1);
|
|
54
|
+
return row ?? null;
|
|
55
|
+
}
|
|
56
|
+
function leadResponse(signal, duplicate) {
|
|
57
|
+
return {
|
|
58
|
+
id: signal.id,
|
|
59
|
+
personId: signal.personId,
|
|
60
|
+
kind: signal.kind,
|
|
61
|
+
source: signal.source,
|
|
62
|
+
status: signal.status,
|
|
63
|
+
duplicate,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function newsletterDoubleOptInFromSignal(signal) {
|
|
67
|
+
const metadata = signal.metadata;
|
|
68
|
+
const newsletter = metadata && typeof metadata === "object" && "newsletter" in metadata
|
|
69
|
+
? metadata.newsletter
|
|
70
|
+
: null;
|
|
71
|
+
if (!newsletter || typeof newsletter !== "object" || !("doubleOptIn" in newsletter)) {
|
|
72
|
+
return "not_configured";
|
|
73
|
+
}
|
|
74
|
+
return newsletter.doubleOptIn === "requested" ? "requested" : "not_configured";
|
|
75
|
+
}
|
|
76
|
+
function newsletterSignalMetadata(input) {
|
|
77
|
+
return {
|
|
78
|
+
intake: { surface: "storefront", type: "newsletter" },
|
|
79
|
+
newsletter: { email: input.email, doubleOptIn: input.doubleOptIn },
|
|
80
|
+
payload: input.body.payload,
|
|
81
|
+
consent: input.body.consent,
|
|
82
|
+
source: {
|
|
83
|
+
url: input.body.sourceUrl ?? null,
|
|
84
|
+
locale: input.body.locale ?? null,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export async function createStorefrontLeadSignal(input) {
|
|
89
|
+
const db = requireDb(input.context);
|
|
90
|
+
const existing = await findExistingSignal(db, {
|
|
91
|
+
kind: input.body.kind,
|
|
92
|
+
sourceSubmissionId: input.body.sourceSubmissionId,
|
|
93
|
+
});
|
|
94
|
+
if (existing)
|
|
95
|
+
return leadResponse(existing, true);
|
|
96
|
+
const { firstName, lastName } = personNameFromContact(input.body.contact);
|
|
97
|
+
const person = await crmService.createPerson(db, {
|
|
98
|
+
firstName,
|
|
99
|
+
lastName,
|
|
100
|
+
status: "active",
|
|
101
|
+
website: null,
|
|
102
|
+
email: input.body.contact.email ? normalizeEmail(input.body.contact.email) : null,
|
|
103
|
+
phone: input.body.contact.phone ?? null,
|
|
104
|
+
source: "storefront",
|
|
105
|
+
sourceRef: input.body.sourceSubmissionId ?? null,
|
|
106
|
+
tags: input.body.tags,
|
|
107
|
+
});
|
|
108
|
+
if (!person)
|
|
109
|
+
throw new Error("Failed to create CRM person for storefront lead");
|
|
110
|
+
const signal = await crmService.createCustomerSignal(db, {
|
|
111
|
+
personId: person.id,
|
|
112
|
+
productId: input.body.productId ?? null,
|
|
113
|
+
optionUnitId: input.body.optionUnitId ?? null,
|
|
114
|
+
kind: input.body.kind,
|
|
115
|
+
source: input.body.source,
|
|
116
|
+
status: "new",
|
|
117
|
+
priority: "normal",
|
|
118
|
+
notes: input.body.notes ?? null,
|
|
119
|
+
tags: input.body.tags,
|
|
120
|
+
sourceSubmissionId: input.body.sourceSubmissionId ?? null,
|
|
121
|
+
metadata: {
|
|
122
|
+
intake: { surface: "storefront", type: "lead" },
|
|
123
|
+
payload: input.body.payload,
|
|
124
|
+
consent: input.body.consent,
|
|
125
|
+
source: {
|
|
126
|
+
url: input.body.sourceUrl ?? null,
|
|
127
|
+
locale: input.body.locale ?? null,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
if (!signal)
|
|
132
|
+
throw new Error("Failed to create CRM customer signal for storefront lead");
|
|
133
|
+
await emitCustomerSignalCreated(input.context.eventBus, {
|
|
134
|
+
id: signal.id,
|
|
135
|
+
personId: signal.personId,
|
|
136
|
+
kind: signal.kind,
|
|
137
|
+
source: signal.source,
|
|
138
|
+
status: signal.status,
|
|
139
|
+
productId: signal.productId,
|
|
140
|
+
optionUnitId: signal.optionUnitId,
|
|
141
|
+
sourceSubmissionId: signal.sourceSubmissionId,
|
|
142
|
+
intake: { surface: "storefront", type: "lead" },
|
|
143
|
+
}, "route");
|
|
144
|
+
return leadResponse(signal, false);
|
|
145
|
+
}
|
|
146
|
+
export async function subscribeStorefrontNewsletter(input) {
|
|
147
|
+
const db = requireDb(input.context);
|
|
148
|
+
const email = normalizeEmail(input.body.email);
|
|
149
|
+
const sourceSubmissionId = input.body.sourceSubmissionId ?? defaultNewsletterSubmissionId(input.body.email);
|
|
150
|
+
const existing = await findExistingSignal(db, {
|
|
151
|
+
kind: "notify",
|
|
152
|
+
sourceSubmissionId,
|
|
153
|
+
});
|
|
154
|
+
if (existing) {
|
|
155
|
+
return {
|
|
156
|
+
...leadResponse(existing, true),
|
|
157
|
+
doubleOptIn: newsletterDoubleOptInFromSignal(existing),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const { firstName, lastName } = personNameFromNewsletter(input.body);
|
|
161
|
+
const person = await crmService.createPerson(db, {
|
|
162
|
+
firstName,
|
|
163
|
+
lastName,
|
|
164
|
+
status: "active",
|
|
165
|
+
website: null,
|
|
166
|
+
email,
|
|
167
|
+
source: "storefront-newsletter",
|
|
168
|
+
sourceRef: sourceSubmissionId,
|
|
169
|
+
tags: input.body.tags,
|
|
170
|
+
});
|
|
171
|
+
if (!person)
|
|
172
|
+
throw new Error("Failed to create CRM person for newsletter subscription");
|
|
173
|
+
const doubleOptIn = input.requestDoubleOptIn ? "requested" : "not_configured";
|
|
174
|
+
let signal = await crmService.createCustomerSignal(db, {
|
|
175
|
+
personId: person.id,
|
|
176
|
+
kind: "notify",
|
|
177
|
+
source: input.body.source,
|
|
178
|
+
status: "new",
|
|
179
|
+
priority: "normal",
|
|
180
|
+
notes: "Newsletter subscription",
|
|
181
|
+
tags: input.body.tags,
|
|
182
|
+
sourceSubmissionId,
|
|
183
|
+
metadata: newsletterSignalMetadata({
|
|
184
|
+
email,
|
|
185
|
+
doubleOptIn: "not_configured",
|
|
186
|
+
body: input.body,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
if (!signal)
|
|
190
|
+
throw new Error("Failed to create CRM customer signal for newsletter subscription");
|
|
191
|
+
if (input.requestDoubleOptIn) {
|
|
192
|
+
try {
|
|
193
|
+
await input.requestDoubleOptIn({
|
|
194
|
+
email,
|
|
195
|
+
personId: person.id,
|
|
196
|
+
signalId: signal.id,
|
|
197
|
+
sourceSubmissionId,
|
|
198
|
+
body: input.body,
|
|
199
|
+
context: input.context,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
await crmService.deleteCustomerSignal(db, signal.id).catch(() => null);
|
|
204
|
+
await crmService.deletePerson(db, person.id).catch(() => null);
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
signal =
|
|
208
|
+
(await crmService.updateCustomerSignal(db, signal.id, {
|
|
209
|
+
metadata: newsletterSignalMetadata({
|
|
210
|
+
email,
|
|
211
|
+
doubleOptIn,
|
|
212
|
+
body: input.body,
|
|
213
|
+
}),
|
|
214
|
+
})) ?? signal;
|
|
215
|
+
}
|
|
216
|
+
await emitCustomerSignalCreated(input.context.eventBus, {
|
|
217
|
+
id: signal.id,
|
|
218
|
+
personId: signal.personId,
|
|
219
|
+
kind: signal.kind,
|
|
220
|
+
source: signal.source,
|
|
221
|
+
status: signal.status,
|
|
222
|
+
productId: signal.productId,
|
|
223
|
+
optionUnitId: signal.optionUnitId,
|
|
224
|
+
sourceSubmissionId: signal.sourceSubmissionId,
|
|
225
|
+
intake: { surface: "storefront", type: "newsletter", doubleOptIn },
|
|
226
|
+
}, "route");
|
|
227
|
+
return {
|
|
228
|
+
...leadResponse(signal, false),
|
|
229
|
+
doubleOptIn,
|
|
230
|
+
};
|
|
231
|
+
}
|