@infuro/cms-core 1.0.34 → 1.0.36
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 +13 -0
- package/dist/OrderTrackPage-W3J6WIG2.js +426 -0
- package/dist/OrderTrackPage-Z6FCJQYY.cjs +428 -0
- package/dist/admin.cjs +35254 -21613
- package/dist/admin.d.cts +192 -9
- package/dist/admin.d.ts +192 -9
- package/dist/admin.js +34301 -20755
- package/dist/api.cjs +137 -17531
- package/dist/api.d.cts +633 -3
- package/dist/api.d.ts +633 -3
- package/dist/api.js +15 -17528
- package/dist/auth.cjs +192 -1034
- package/dist/auth.d.cts +197 -54
- package/dist/auth.d.ts +197 -54
- package/dist/auth.js +4 -966
- package/dist/chunk-2AEVFOQP.cjs +30436 -0
- package/dist/chunk-3DZRIZOD.cjs +52 -0
- package/dist/chunk-5ELSW2UP.js +150 -0
- package/dist/chunk-7NMT3GBR.cjs +29 -0
- package/dist/chunk-COBORIJH.cjs +2373 -0
- package/dist/chunk-CRFV5WJK.js +16 -0
- package/dist/chunk-D5FBNKQM.cjs +104 -0
- package/dist/chunk-FG6I64HP.js +604 -0
- package/dist/chunk-GO7PPYNU.js +76 -0
- package/dist/chunk-HCIRL37O.js +21 -0
- package/dist/chunk-HF3L2GIT.cjs +194 -0
- package/dist/chunk-HT5DI3XW.cjs +82 -0
- package/dist/chunk-JC6DLWTE.js +21 -0
- package/dist/chunk-JS2W7XZ4.js +30312 -0
- package/dist/chunk-JXF23MPG.js +25 -0
- package/dist/chunk-KFFB6EMZ.js +123 -0
- package/dist/chunk-KPKFNLQQ.cjs +648 -0
- package/dist/chunk-LI2IXVAY.js +171 -0
- package/dist/chunk-MQBT33IV.js +48 -0
- package/dist/chunk-OOTLNH63.cjs +18 -0
- package/dist/chunk-PNOKNSG2.cjs +222 -0
- package/dist/chunk-SHUYVCID.js +4 -0
- package/dist/chunk-SPQFNIYJ.cjs +132 -0
- package/dist/chunk-SYBOCAWB.js +97 -0
- package/dist/chunk-TDV6EJHV.js +192 -0
- package/dist/chunk-UCKN4BBY.cjs +24 -0
- package/dist/chunk-UF3BYUXY.js +616 -0
- package/dist/chunk-UPLVMVRX.cjs +23 -0
- package/dist/chunk-USNT2KNT.cjs +6 -0
- package/dist/chunk-VMY2S32H.js +2342 -0
- package/dist/chunk-WEMDMVHQ.js +216 -0
- package/dist/chunk-WO2DW4AU.cjs +182 -0
- package/dist/chunk-X47PBV4I.js +79 -0
- package/dist/chunk-X6H5VBQ2.cjs +620 -0
- package/dist/chunk-XQ3QUHWR.cjs +83 -0
- package/dist/chunk-XW5ATPRW.cjs +155 -0
- package/dist/cli.cjs +107 -86
- package/dist/cli.js +66 -29
- package/dist/email-queue-CGZBVVAS.js +2 -0
- package/dist/email-queue-TPZWLTIV.cjs +23 -0
- package/dist/emit-order-notification-trigger-D2N3KFHL.cjs +16 -0
- package/dist/emit-order-notification-trigger-IVLWKKJL.js +3 -0
- package/dist/erp-config-enabled-CUMQ6N2V.cjs +11 -0
- package/dist/erp-config-enabled-T37X7T34.js +2 -0
- package/dist/erp-order-invoice-3YISSOWW.js +3 -0
- package/dist/erp-order-invoice-ZR5F2KTA.cjs +12 -0
- package/dist/event-order-defaults-EMWBIGL3.cjs +39 -0
- package/dist/event-order-defaults-SDRNAZHC.js +2 -0
- package/dist/hooks.cjs +259 -132
- package/dist/hooks.d.cts +42 -1
- package/dist/hooks.d.ts +42 -1
- package/dist/hooks.js +234 -91
- package/dist/index.cjs +3679 -22356
- package/dist/index.d.cts +2173 -643
- package/dist/index.d.ts +2173 -643
- package/dist/index.js +2587 -22119
- package/dist/interpolate-WAKPN56R.cjs +11 -0
- package/dist/interpolate-WDUWDF3I.js +2 -0
- package/dist/migrations/1779400000000-ConvertStatusToEnum.ts +142 -0
- package/dist/migrations/1779400000000-SeedDefaultCurrencies.ts +33 -0
- package/dist/migrations/1779500000000-VendorRolesAndPermissions.ts +102 -0
- package/dist/migrations/1779600000000-VendorRegistrationProfile.ts +52 -0
- package/dist/migrations/1779700000000-CollectionIsCatalog.ts +22 -0
- package/dist/migrations/1779710000000-CollectionPlatformCatalog.ts +60 -0
- package/dist/migrations/1779720000000-ProductCategoryPlatformCatalog.ts +157 -0
- package/dist/migrations/1779800000000-EventsAndEventProducts.ts +173 -0
- package/dist/migrations/1779810000000-BackfillVendorRoleEventsPermissions.ts +38 -0
- package/dist/migrations/1779820000000-FixProductsCategoryForeignKey.ts +70 -0
- package/dist/migrations/1779830000000-ProductTitleColumn.ts +32 -0
- package/dist/migrations/1779840000000-EventAboutAndVisibilityFields.ts +34 -0
- package/dist/migrations/1779850000000-EventCityCountrySplit.ts +43 -0
- package/dist/migrations/1779860000000-CreateCombos.ts +140 -0
- package/dist/migrations/1780100000000-OrderAttendeesEventId.ts +55 -0
- package/dist/migrations/1780110000000-EventContactFormId.ts +49 -0
- package/dist/migrations/1780130000000-ProductVariants.ts +54 -0
- package/dist/migrations/1780140000000-EventManagementEnhancements.ts +44 -0
- package/dist/migrations/1780160000000-EventRegistrationGstProductContactForm.ts +79 -0
- package/dist/migrations/1780170000000-ComboCurrencyPrices.ts +20 -0
- package/dist/migrations/1781110000000-VendorOwnerCombosPermission.ts +38 -0
- package/dist/migrations/1781120000000-RefundPoliciesAndRequests.ts +225 -0
- package/dist/migrations/1781130000000-CreateAttendees.ts +44 -0
- package/dist/migrations/1781140000000-EventOrderNotifications.ts +29 -0
- package/dist/migrations/1781150000000-EventNamedLists.ts +28 -0
- package/dist/migrations/1781160000000-RefundPolicyEventTiers.ts +27 -0
- package/dist/migrations/1781170000000-EventRefundPolicy.ts +42 -0
- package/dist/migrations/1781180000000-RemoveCartItemsUniqueConstraint.ts +15 -0
- package/dist/migrations/1781190000000-RenameProductTaxesToProductConfig.ts +84 -0
- package/dist/migrations/1781200000000-VendorOwnerProductConfigPermissions.ts +51 -0
- package/dist/migrations/1781210000000-VendorRoleProductConfigPermissions.ts +46 -0
- package/dist/migrations/1781220000000-AddWhatsappTriggerBindings.ts +23 -0
- package/dist/migrations/1781230000000-AddOrderNotificationTriggers.ts +46 -0
- package/dist/migrations/1781240000000-DropEventOrderNotificationColumns.ts +29 -0
- package/dist/migrations/1781250000000-RemoveOrderRefundedTrigger.ts +36 -0
- package/dist/migrations/helpers/vendor-role-seed.ts +243 -0
- package/dist/paid-order-erp-3USEG6WL.js +3 -0
- package/dist/paid-order-erp-FSQ3GSXE.cjs +12 -0
- package/dist/pg-boss-service-4XF2TQ2E.js +2 -0
- package/dist/pg-boss-service-L6MTF4EQ.cjs +15 -0
- package/dist/razorpay-KLO2KNCB.js +49 -0
- package/dist/razorpay-U23LGY72.cjs +56 -0
- package/dist/rbac-debug-db-HWP65A4O.cjs +35 -0
- package/dist/rbac-debug-db-QJH3AD34.js +33 -0
- package/dist/sms-defaults-2NL24H57.js +2 -0
- package/dist/sms-defaults-KCOPKQBA.cjs +19 -0
- package/dist/stripe-4FD27QVJ.js +51 -0
- package/dist/stripe-VU6OEG2D.cjs +57 -0
- package/dist/theme.cjs +31 -59
- package/dist/theme.d.cts +78 -3
- package/dist/theme.d.ts +78 -3
- package/dist/theme.js +23 -20
- package/dist/vendor-invite-status-77O7ZQ3U.cjs +19 -0
- package/dist/vendor-invite-status-PWJ3DE76.js +2 -0
- package/dist/whatsapp-trigger-dispatcher-ONHXI7UY.cjs +19 -0
- package/dist/whatsapp-trigger-dispatcher-PWFI4FKP.js +6 -0
- package/package.json +147 -142
- package/dist/admin.cjs.map +0 -1
- package/dist/admin.js.map +0 -1
- package/dist/api.cjs.map +0 -1
- package/dist/api.js.map +0 -1
- package/dist/auth.cjs.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config-DJ5CmQvS.d.cts +0 -8
- package/dist/config-DJ5CmQvS.d.ts +0 -8
- package/dist/hooks.cjs.map +0 -1
- package/dist/hooks.js.map +0 -1
- package/dist/index-Bf5GO8fu.d.cts +0 -583
- package/dist/index-DOiJuMQA.d.ts +0 -583
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/theme.cjs.map +0 -1
- package/dist/theme.js.map +0 -1
- package/dist/types-DCVaXlPV.d.cts +0 -78
- package/dist/types-DCVaXlPV.d.ts +0 -78
- package/dist/vendor-scope-DTYhJk_I.d.cts +0 -129
- package/dist/vendor-scope-DTYhJk_I.d.ts +0 -129
|
@@ -0,0 +1,2342 @@
|
|
|
1
|
+
import { notificationTriggerEmitter } from './chunk-HCIRL37O.js';
|
|
2
|
+
import { queueEmail } from './chunk-5ELSW2UP.js';
|
|
3
|
+
import { applyTemplateVars } from './chunk-CRFV5WJK.js';
|
|
4
|
+
import { resolveEventOrderTemplate, EVENT_ORDER_WHATSAPP_PARAM_KEYS } from './chunk-KFFB6EMZ.js';
|
|
5
|
+
import { __name } from './chunk-SHUYVCID.js';
|
|
6
|
+
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
|
|
7
|
+
import nodemailer from 'nodemailer';
|
|
8
|
+
|
|
9
|
+
// src/lib/event-order-template-context.ts
|
|
10
|
+
function resolvePublicSiteUrl() {
|
|
11
|
+
if (typeof process !== "undefined") {
|
|
12
|
+
const u = (process.env.CMS_PUBLIC_URL || process.env.NEXT_PUBLIC_SITE_URL || "").trim();
|
|
13
|
+
if (u) return u.replace(/\/+$/, "");
|
|
14
|
+
const v = (process.env.VERCEL_URL || "").trim();
|
|
15
|
+
if (v) return v.startsWith("http") ? v.replace(/\/+$/, "") : `https://${v.replace(/\/+$/, "")}`;
|
|
16
|
+
}
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
__name(resolvePublicSiteUrl, "resolvePublicSiteUrl");
|
|
20
|
+
function buildOrderTrackUrl(qrToken, siteUrl) {
|
|
21
|
+
const token = qrToken?.trim();
|
|
22
|
+
if (!token) return "";
|
|
23
|
+
const base = (siteUrl ?? resolvePublicSiteUrl()).replace(/\/+$/, "");
|
|
24
|
+
if (!base) return `/track/${token}`;
|
|
25
|
+
return `${base}/track/${token}`;
|
|
26
|
+
}
|
|
27
|
+
__name(buildOrderTrackUrl, "buildOrderTrackUrl");
|
|
28
|
+
function buildOrderQrImageUrl(trackUrl, size = 200) {
|
|
29
|
+
if (!trackUrl.trim()) return "";
|
|
30
|
+
return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(trackUrl)}`;
|
|
31
|
+
}
|
|
32
|
+
__name(buildOrderQrImageUrl, "buildOrderQrImageUrl");
|
|
33
|
+
function formatOrderDetailsText(input) {
|
|
34
|
+
const { orderNumber, total, currency, lines } = input;
|
|
35
|
+
const header = `Order: ${orderNumber}
|
|
36
|
+
Total: ${total} ${currency}`;
|
|
37
|
+
if (!lines.length) return header;
|
|
38
|
+
const items = lines.map((line) => `- ${line.name} \xD7 ${line.quantity} (${line.total} ${line.currency})`).join("\n");
|
|
39
|
+
return `${header}
|
|
40
|
+
|
|
41
|
+
Items:
|
|
42
|
+
${items}`;
|
|
43
|
+
}
|
|
44
|
+
__name(formatOrderDetailsText, "formatOrderDetailsText");
|
|
45
|
+
function formatOrderDetailsHtml(input) {
|
|
46
|
+
const text = formatOrderDetailsText(input);
|
|
47
|
+
const escaped = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
48
|
+
return `<pre style="font-family:inherit;white-space:pre-wrap;margin:0">${escaped}</pre>`;
|
|
49
|
+
}
|
|
50
|
+
__name(formatOrderDetailsHtml, "formatOrderDetailsHtml");
|
|
51
|
+
function buildEventOrderTemplateVariables(input) {
|
|
52
|
+
const { event, order, contact, productNames, orderLines = [], siteUrl } = input;
|
|
53
|
+
const trackUrl = buildOrderTrackUrl(order.qrToken, siteUrl);
|
|
54
|
+
const qrImageUrl = buildOrderQrImageUrl(trackUrl);
|
|
55
|
+
const orderDetailsPayload = {
|
|
56
|
+
orderNumber: String(order.orderNumber ?? ""),
|
|
57
|
+
total: String(order.total ?? ""),
|
|
58
|
+
currency: String(order.currency ?? "INR"),
|
|
59
|
+
lines: orderLines
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
eventName: event.name ?? "",
|
|
63
|
+
eventSlug: event.slug ?? "",
|
|
64
|
+
venue: event.venue ?? "",
|
|
65
|
+
startDate: formatEventDate(event.startDate),
|
|
66
|
+
orderNumber: String(order.orderNumber ?? ""),
|
|
67
|
+
orderTotal: String(order.total ?? ""),
|
|
68
|
+
currency: String(order.currency ?? "INR"),
|
|
69
|
+
customerName: contact.name?.trim() || contact.email?.split("@")[0] || "Guest",
|
|
70
|
+
customerEmail: contact.email?.trim() || "",
|
|
71
|
+
customerPhone: contact.phone?.trim() || "",
|
|
72
|
+
productNames,
|
|
73
|
+
trackUrl,
|
|
74
|
+
qrImageUrl,
|
|
75
|
+
orderDetails: formatOrderDetailsText(orderDetailsPayload),
|
|
76
|
+
orderDetailsHtml: formatOrderDetailsHtml(orderDetailsPayload)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
__name(buildEventOrderTemplateVariables, "buildEventOrderTemplateVariables");
|
|
80
|
+
function formatEventDate(d) {
|
|
81
|
+
if (!d) return "";
|
|
82
|
+
try {
|
|
83
|
+
return new Date(d).toLocaleString(void 0, {
|
|
84
|
+
dateStyle: "medium",
|
|
85
|
+
timeStyle: "short"
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
return String(d);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
__name(formatEventDate, "formatEventDate");
|
|
92
|
+
|
|
93
|
+
// src/lib/order-notification-context.ts
|
|
94
|
+
async function loadOrderLines(dataSource, entityMap, orderId, currency) {
|
|
95
|
+
if (!entityMap.order_items) return [];
|
|
96
|
+
const itemRows = await dataSource.getRepository(entityMap.order_items).find({
|
|
97
|
+
where: {
|
|
98
|
+
orderId
|
|
99
|
+
},
|
|
100
|
+
relations: [
|
|
101
|
+
"product"
|
|
102
|
+
],
|
|
103
|
+
order: {
|
|
104
|
+
id: "ASC"
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return itemRows.map((item) => {
|
|
108
|
+
const row = item;
|
|
109
|
+
const name = row.product?.name || row.product?.title || `Product #${row.productId}`;
|
|
110
|
+
return {
|
|
111
|
+
name: String(name),
|
|
112
|
+
quantity: Number(row.quantity) || 1,
|
|
113
|
+
total: row.total,
|
|
114
|
+
currency
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
__name(loadOrderLines, "loadOrderLines");
|
|
119
|
+
async function loadPrimaryEvent(dataSource, entityMap, itemRows) {
|
|
120
|
+
if (!entityMap.event_products || !entityMap.events) {
|
|
121
|
+
return {
|
|
122
|
+
productNames: []
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const epRepo = dataSource.getRepository(entityMap.event_products);
|
|
126
|
+
const eventRepo = dataSource.getRepository(entityMap.events);
|
|
127
|
+
const eventMap = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const item of itemRows) {
|
|
129
|
+
const productId = item.productId;
|
|
130
|
+
const productName = item.product?.name || item.product?.title || `Product #${productId}`;
|
|
131
|
+
const eps = await epRepo.find({
|
|
132
|
+
where: {
|
|
133
|
+
productId,
|
|
134
|
+
isActive: true
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
for (const ep of eps) {
|
|
138
|
+
const eventId = ep.eventId;
|
|
139
|
+
const existing = eventMap.get(eventId);
|
|
140
|
+
if (existing) {
|
|
141
|
+
if (!existing.productNames.includes(String(productName))) {
|
|
142
|
+
existing.productNames.push(String(productName));
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
const event = await eventRepo.findOne({
|
|
146
|
+
where: {
|
|
147
|
+
id: eventId,
|
|
148
|
+
deleted: false
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
if (event) {
|
|
152
|
+
eventMap.set(eventId, {
|
|
153
|
+
event,
|
|
154
|
+
productNames: [
|
|
155
|
+
String(productName)
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const first = eventMap.values().next().value;
|
|
163
|
+
if (!first) return {
|
|
164
|
+
productNames: []
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
event: first.event,
|
|
168
|
+
productNames: first.productNames
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
__name(loadPrimaryEvent, "loadPrimaryEvent");
|
|
172
|
+
async function loadOrderNotificationContexts(orderId, deps) {
|
|
173
|
+
if (!deps.entityMap.orders) return null;
|
|
174
|
+
const order = await deps.dataSource.getRepository(deps.entityMap.orders).findOne({
|
|
175
|
+
where: {
|
|
176
|
+
id: orderId,
|
|
177
|
+
deleted: false
|
|
178
|
+
},
|
|
179
|
+
relations: [
|
|
180
|
+
"contact"
|
|
181
|
+
]
|
|
182
|
+
});
|
|
183
|
+
if (!order) return null;
|
|
184
|
+
const o = order;
|
|
185
|
+
const contact = o.contact ?? {};
|
|
186
|
+
const currency = String(o.currency ?? "INR");
|
|
187
|
+
const siteUrl = resolvePublicSiteUrl();
|
|
188
|
+
const itemRows = await deps.dataSource.getRepository(deps.entityMap.order_items).find({
|
|
189
|
+
where: {
|
|
190
|
+
orderId
|
|
191
|
+
},
|
|
192
|
+
relations: [
|
|
193
|
+
"product"
|
|
194
|
+
],
|
|
195
|
+
order: {
|
|
196
|
+
id: "ASC"
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const orderLines = await loadOrderLines(deps.dataSource, deps.entityMap, orderId, currency);
|
|
200
|
+
const { event, productNames: eventProductNames } = await loadPrimaryEvent(deps.dataSource, deps.entityMap, itemRows);
|
|
201
|
+
const allProductNames = orderLines.map((line) => line.name);
|
|
202
|
+
const productNames = eventProductNames.length > 0 ? eventProductNames.join(", ") : allProductNames.join(", ");
|
|
203
|
+
const variables = event ? buildEventOrderTemplateVariables({
|
|
204
|
+
event,
|
|
205
|
+
order: {
|
|
206
|
+
orderNumber: o.orderNumber,
|
|
207
|
+
total: o.total,
|
|
208
|
+
currency,
|
|
209
|
+
qrToken: o.qrToken
|
|
210
|
+
},
|
|
211
|
+
contact,
|
|
212
|
+
productNames,
|
|
213
|
+
orderLines,
|
|
214
|
+
siteUrl
|
|
215
|
+
}) : buildEventOrderTemplateVariables({
|
|
216
|
+
event: {
|
|
217
|
+
name: productNames || "Your order",
|
|
218
|
+
slug: "",
|
|
219
|
+
venue: "",
|
|
220
|
+
startDate: null
|
|
221
|
+
},
|
|
222
|
+
order: {
|
|
223
|
+
orderNumber: o.orderNumber,
|
|
224
|
+
total: o.total,
|
|
225
|
+
currency,
|
|
226
|
+
qrToken: o.qrToken
|
|
227
|
+
},
|
|
228
|
+
contact,
|
|
229
|
+
productNames,
|
|
230
|
+
orderLines,
|
|
231
|
+
siteUrl
|
|
232
|
+
});
|
|
233
|
+
return {
|
|
234
|
+
order: {
|
|
235
|
+
id: o.id,
|
|
236
|
+
orderNumber: String(o.orderNumber ?? ""),
|
|
237
|
+
total: o.total,
|
|
238
|
+
currency,
|
|
239
|
+
qrToken: o.qrToken
|
|
240
|
+
},
|
|
241
|
+
contact,
|
|
242
|
+
orderLines,
|
|
243
|
+
productNames,
|
|
244
|
+
event,
|
|
245
|
+
variables
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
__name(loadOrderNotificationContexts, "loadOrderNotificationContexts");
|
|
249
|
+
|
|
250
|
+
// src/plugins/whatsapp/whatsapp-queue.ts
|
|
251
|
+
var WHATSAPP_QUEUE_NAME = "whatsapp";
|
|
252
|
+
function maskPhone(raw) {
|
|
253
|
+
const digits = raw.replace(/\D/g, "");
|
|
254
|
+
if (digits.length <= 4) return "****";
|
|
255
|
+
return `${"*".repeat(Math.max(0, digits.length - 4))}${digits.slice(-4)}`;
|
|
256
|
+
}
|
|
257
|
+
__name(maskPhone, "maskPhone");
|
|
258
|
+
function registerWhatsAppQueueProcessor(cms) {
|
|
259
|
+
const queue = cms.getPlugin("queue");
|
|
260
|
+
const whatsapp = cms.getPlugin("whatsapp");
|
|
261
|
+
if (!queue) {
|
|
262
|
+
console.warn("[whatsapp] queue processor not registered: queue plugin missing");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!whatsapp || typeof whatsapp.send !== "function") {
|
|
266
|
+
console.warn("[whatsapp] queue processor not registered: whatsapp plugin missing or not configured");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
queue.registerProcessor(WHATSAPP_QUEUE_NAME, async (data) => {
|
|
270
|
+
const payload = data;
|
|
271
|
+
if (!payload.to) {
|
|
272
|
+
console.warn("[whatsapp] queue job skipped: missing recipient");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
console.log("[whatsapp] queue job processing", {
|
|
276
|
+
to: maskPhone(payload.to),
|
|
277
|
+
templateKey: payload.templateKey ?? null,
|
|
278
|
+
externalTemplateRef: payload.externalTemplateRef ?? null
|
|
279
|
+
});
|
|
280
|
+
try {
|
|
281
|
+
const ok = await whatsapp.send(payload);
|
|
282
|
+
if (ok) {
|
|
283
|
+
console.log("[whatsapp] queue job completed", {
|
|
284
|
+
to: maskPhone(payload.to),
|
|
285
|
+
sent: true
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
console.warn("[whatsapp] queue job completed without delivery", {
|
|
289
|
+
to: maskPhone(payload.to),
|
|
290
|
+
sent: false
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error("[whatsapp] queue job failed", {
|
|
295
|
+
to: maskPhone(payload.to),
|
|
296
|
+
error: err instanceof Error ? err.message : String(err)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
console.log("[whatsapp] queue processor registered", {
|
|
301
|
+
queue: WHATSAPP_QUEUE_NAME
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
__name(registerWhatsAppQueueProcessor, "registerWhatsAppQueueProcessor");
|
|
305
|
+
async function queueWhatsApp(cms, payload) {
|
|
306
|
+
const queue = cms.getPlugin("queue");
|
|
307
|
+
if (queue) {
|
|
308
|
+
console.log("[whatsapp] enqueue", {
|
|
309
|
+
to: maskPhone(payload.to),
|
|
310
|
+
templateKey: payload.templateKey ?? null,
|
|
311
|
+
externalTemplateRef: payload.externalTemplateRef ?? null
|
|
312
|
+
});
|
|
313
|
+
await queue.add(WHATSAPP_QUEUE_NAME, payload);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
const whatsapp = cms.getPlugin("whatsapp");
|
|
317
|
+
if (whatsapp && typeof whatsapp.send === "function") {
|
|
318
|
+
console.log("[whatsapp] no queue plugin \u2014 sending immediately", {
|
|
319
|
+
to: maskPhone(payload.to)
|
|
320
|
+
});
|
|
321
|
+
return whatsapp.send(payload);
|
|
322
|
+
}
|
|
323
|
+
console.warn("[whatsapp] send skipped: neither queue nor whatsapp plugin available", {
|
|
324
|
+
to: maskPhone(payload.to)
|
|
325
|
+
});
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
__name(queueWhatsApp, "queueWhatsApp");
|
|
329
|
+
|
|
330
|
+
// src/plugins/whatsapp/whatsapp-service.ts
|
|
331
|
+
function norm(v) {
|
|
332
|
+
const t = v?.trim();
|
|
333
|
+
return t || void 0;
|
|
334
|
+
}
|
|
335
|
+
__name(norm, "norm");
|
|
336
|
+
function pick(plugin, db, env, ...keys) {
|
|
337
|
+
for (const key of keys) {
|
|
338
|
+
const pk = key;
|
|
339
|
+
if (pk in plugin && plugin[pk] !== void 0 && String(plugin[pk]).trim() !== "") {
|
|
340
|
+
const v = norm(String(plugin[pk]));
|
|
341
|
+
if (v) return v;
|
|
342
|
+
}
|
|
343
|
+
if (norm(db[key])) return norm(db[key]);
|
|
344
|
+
if (norm(env[key])) return norm(env[key]);
|
|
345
|
+
}
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
__name(pick, "pick");
|
|
349
|
+
function mergeWhatsAppConfigLayers(env, db, plugin) {
|
|
350
|
+
return {
|
|
351
|
+
accessToken: pick(plugin, db, env, "accessToken", "WHATSAPP_ACCESS_TOKEN"),
|
|
352
|
+
phoneNumberId: pick(plugin, db, env, "phoneNumberId", "WHATSAPP_PHONE_NUMBER_ID"),
|
|
353
|
+
apiVersion: pick(plugin, db, env, "apiVersion", "WHATSAPP_API_VERSION") ?? "v21.0"
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
__name(mergeWhatsAppConfigLayers, "mergeWhatsAppConfigLayers");
|
|
357
|
+
function isWhatsAppPluginEnabled(db) {
|
|
358
|
+
const v = (db.enabled ?? "true").toLowerCase();
|
|
359
|
+
return v !== "false" && v !== "0";
|
|
360
|
+
}
|
|
361
|
+
__name(isWhatsAppPluginEnabled, "isWhatsAppPluginEnabled");
|
|
362
|
+
function whatsAppConfigured(config) {
|
|
363
|
+
return Boolean(config.accessToken?.trim() && config.phoneNumberId?.trim());
|
|
364
|
+
}
|
|
365
|
+
__name(whatsAppConfigured, "whatsAppConfigured");
|
|
366
|
+
function normalizeWhatsAppRecipient(to) {
|
|
367
|
+
const digits = to.replace(/\D/g, "");
|
|
368
|
+
return digits;
|
|
369
|
+
}
|
|
370
|
+
__name(normalizeWhatsAppRecipient, "normalizeWhatsAppRecipient");
|
|
371
|
+
function resolveMetaWhatsAppTemplateName(opts) {
|
|
372
|
+
const ref = opts.externalTemplateRef?.trim();
|
|
373
|
+
const rowName = opts.name?.trim();
|
|
374
|
+
if (ref && !/^\d+$/.test(ref)) return ref;
|
|
375
|
+
if (rowName) return rowName;
|
|
376
|
+
return ref || void 0;
|
|
377
|
+
}
|
|
378
|
+
__name(resolveMetaWhatsAppTemplateName, "resolveMetaWhatsAppTemplateName");
|
|
379
|
+
function maskPhone2(digits) {
|
|
380
|
+
if (digits.length <= 4) return "****";
|
|
381
|
+
return `${"*".repeat(Math.max(0, digits.length - 4))}${digits.slice(-4)}`;
|
|
382
|
+
}
|
|
383
|
+
__name(maskPhone2, "maskPhone");
|
|
384
|
+
function logWhatsApp(level, message, extra) {
|
|
385
|
+
const line = extra ? `${message} ${JSON.stringify(extra)}` : message;
|
|
386
|
+
if (level === "info") console.log(`[whatsapp] ${line}`);
|
|
387
|
+
else if (level === "warn") console.warn(`[whatsapp] ${line}`);
|
|
388
|
+
else console.error(`[whatsapp] ${line}`);
|
|
389
|
+
}
|
|
390
|
+
__name(logWhatsApp, "logWhatsApp");
|
|
391
|
+
var WhatsAppService = class {
|
|
392
|
+
static {
|
|
393
|
+
__name(this, "WhatsAppService");
|
|
394
|
+
}
|
|
395
|
+
getWhatsAppSettings;
|
|
396
|
+
config;
|
|
397
|
+
getMessageTemplateRow;
|
|
398
|
+
constructor(env, plugin = {}, getWhatsAppSettings, getMessageTemplateRow) {
|
|
399
|
+
this.getWhatsAppSettings = getWhatsAppSettings;
|
|
400
|
+
this.getMessageTemplateRow = getMessageTemplateRow;
|
|
401
|
+
this.config = mergeWhatsAppConfigLayers(env, {}, plugin);
|
|
402
|
+
}
|
|
403
|
+
async mergedConfig() {
|
|
404
|
+
let db = {};
|
|
405
|
+
try {
|
|
406
|
+
db = await this.getWhatsAppSettings?.() ?? {};
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
return mergeWhatsAppConfigLayers((typeof process !== "undefined" ? process.env : {}) ?? {}, db, this.config);
|
|
410
|
+
}
|
|
411
|
+
async send(opts) {
|
|
412
|
+
const cfg = await this.mergedConfig();
|
|
413
|
+
if (!whatsAppConfigured(cfg)) {
|
|
414
|
+
logWhatsApp("warn", "send skipped: not configured (Plugins \u2192 WhatsApp or WHATSAPP_ACCESS_TOKEN + WHATSAPP_PHONE_NUMBER_ID)");
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const to = normalizeWhatsAppRecipient(opts.to);
|
|
418
|
+
if (!to) {
|
|
419
|
+
logWhatsApp("warn", "send skipped: invalid recipient", {
|
|
420
|
+
rawTo: opts.to
|
|
421
|
+
});
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
logWhatsApp("info", "send started", {
|
|
425
|
+
to: maskPhone2(to),
|
|
426
|
+
templateKey: opts.templateKey ?? null,
|
|
427
|
+
externalTemplateRef: opts.externalTemplateRef ?? null,
|
|
428
|
+
phoneNumberId: cfg.phoneNumberId
|
|
429
|
+
});
|
|
430
|
+
let templateName = resolveMetaWhatsAppTemplateName({
|
|
431
|
+
externalTemplateRef: opts.externalTemplateRef
|
|
432
|
+
});
|
|
433
|
+
let templateLanguage = opts.templateLanguage?.trim();
|
|
434
|
+
let body = opts.body?.trim() ?? "";
|
|
435
|
+
const variables = opts.variables ?? {};
|
|
436
|
+
if (opts.templateKey?.trim() && this.getMessageTemplateRow) {
|
|
437
|
+
const { resolveEventOrderTemplate: resolveEventOrderTemplate2, EVENT_ORDER_TEMPLATE_KEY } = await import('./event-order-defaults-SDRNAZHC.js');
|
|
438
|
+
if (opts.templateKey.trim() === EVENT_ORDER_TEMPLATE_KEY) {
|
|
439
|
+
const resolved = await resolveEventOrderTemplate2("whatsapp", async (ch, key) => {
|
|
440
|
+
const row = await this.getMessageTemplateRow(ch, key);
|
|
441
|
+
if (!row) return null;
|
|
442
|
+
return {
|
|
443
|
+
subject: null,
|
|
444
|
+
body: row.body,
|
|
445
|
+
externalTemplateRef: row.externalTemplateRef,
|
|
446
|
+
enabled: row.enabled
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
if (resolved && !resolved.enabled) {
|
|
450
|
+
logWhatsApp("warn", "send skipped: template disabled in message_templates", {
|
|
451
|
+
templateKey: opts.templateKey
|
|
452
|
+
});
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
if (resolved) {
|
|
456
|
+
body = resolved.body;
|
|
457
|
+
if (resolved.externalTemplateRef) templateName = resolved.externalTemplateRef;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const { applyTemplateVars: applyTemplateVars2 } = await import('./interpolate-WDUWDF3I.js');
|
|
462
|
+
if (body) body = applyTemplateVars2(body, variables);
|
|
463
|
+
if (templateName) {
|
|
464
|
+
const paramValues = opts.templateParamOrder?.length ? opts.templateParamOrder.map((key) => String(variables[key] ?? "").slice(0, 1024)) : Object.keys(variables).length ? Object.values(variables) : body ? [
|
|
465
|
+
body
|
|
466
|
+
] : [];
|
|
467
|
+
logWhatsApp("info", "sending template message", {
|
|
468
|
+
to: maskPhone2(to),
|
|
469
|
+
templateName,
|
|
470
|
+
paramCount: paramValues.length,
|
|
471
|
+
paramOrder: opts.templateParamOrder ?? null
|
|
472
|
+
});
|
|
473
|
+
return this.sendTemplateMessage(cfg, to, templateName, paramValues, templateLanguage);
|
|
474
|
+
}
|
|
475
|
+
if (!body) {
|
|
476
|
+
logWhatsApp("warn", "send skipped: no template name and empty body", {
|
|
477
|
+
to: maskPhone2(to)
|
|
478
|
+
});
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
logWhatsApp("info", "sending text message", {
|
|
482
|
+
to: maskPhone2(to),
|
|
483
|
+
bodyLength: body.length
|
|
484
|
+
});
|
|
485
|
+
return this.sendTextMessage(cfg, to, body);
|
|
486
|
+
}
|
|
487
|
+
async sendTextMessage(cfg, to, body) {
|
|
488
|
+
const url = `https://graph.facebook.com/${cfg.apiVersion}/${cfg.phoneNumberId}/messages`;
|
|
489
|
+
const res = await fetch(url, {
|
|
490
|
+
method: "POST",
|
|
491
|
+
headers: {
|
|
492
|
+
Authorization: `Bearer ${cfg.accessToken}`,
|
|
493
|
+
"Content-Type": "application/json"
|
|
494
|
+
},
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
messaging_product: "whatsapp",
|
|
497
|
+
to,
|
|
498
|
+
type: "text",
|
|
499
|
+
text: {
|
|
500
|
+
body
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
});
|
|
504
|
+
if (!res.ok) {
|
|
505
|
+
const errText = await res.text().catch(() => "");
|
|
506
|
+
logWhatsApp("error", "text send failed", {
|
|
507
|
+
status: res.status,
|
|
508
|
+
to: maskPhone2(to),
|
|
509
|
+
error: errText
|
|
510
|
+
});
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
const data = await res.json().catch(() => ({}));
|
|
514
|
+
logWhatsApp("info", "text send succeeded", {
|
|
515
|
+
to: maskPhone2(to),
|
|
516
|
+
messageId: data.messages?.[0]?.id ?? null
|
|
517
|
+
});
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
async sendTemplateMessage(cfg, to, templateName, paramValues, templateLanguage) {
|
|
521
|
+
const components = paramValues.length > 0 ? [
|
|
522
|
+
{
|
|
523
|
+
type: "body",
|
|
524
|
+
parameters: paramValues.map((text) => ({
|
|
525
|
+
type: "text",
|
|
526
|
+
text: String(text).slice(0, 1024)
|
|
527
|
+
}))
|
|
528
|
+
}
|
|
529
|
+
] : void 0;
|
|
530
|
+
const url = `https://graph.facebook.com/${cfg.apiVersion}/${cfg.phoneNumberId}/messages`;
|
|
531
|
+
const payload = {
|
|
532
|
+
messaging_product: "whatsapp",
|
|
533
|
+
to,
|
|
534
|
+
type: "template",
|
|
535
|
+
template: {
|
|
536
|
+
name: templateName,
|
|
537
|
+
language: {
|
|
538
|
+
code: templateLanguage?.trim() || "en"
|
|
539
|
+
},
|
|
540
|
+
...components ? {
|
|
541
|
+
components
|
|
542
|
+
} : {}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
const res = await fetch(url, {
|
|
546
|
+
method: "POST",
|
|
547
|
+
headers: {
|
|
548
|
+
Authorization: `Bearer ${cfg.accessToken}`,
|
|
549
|
+
"Content-Type": "application/json"
|
|
550
|
+
},
|
|
551
|
+
body: JSON.stringify(payload)
|
|
552
|
+
});
|
|
553
|
+
if (!res.ok) {
|
|
554
|
+
const errText = await res.text().catch(() => "");
|
|
555
|
+
logWhatsApp("error", "template send failed", {
|
|
556
|
+
status: res.status,
|
|
557
|
+
to: maskPhone2(to),
|
|
558
|
+
templateName,
|
|
559
|
+
paramCount: paramValues.length,
|
|
560
|
+
error: errText
|
|
561
|
+
});
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
const data = await res.json().catch(() => ({}));
|
|
565
|
+
logWhatsApp("info", "template send succeeded", {
|
|
566
|
+
to: maskPhone2(to),
|
|
567
|
+
templateName,
|
|
568
|
+
messageId: data.messages?.[0]?.id ?? null
|
|
569
|
+
});
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// src/plugins/email/templates/layout.ts
|
|
575
|
+
function escapeHtml(s) {
|
|
576
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
577
|
+
}
|
|
578
|
+
__name(escapeHtml, "escapeHtml");
|
|
579
|
+
function socialLinkInnerHtml(s) {
|
|
580
|
+
if (s.iconUrl?.trim()) {
|
|
581
|
+
return `<img src="${escapeHtml(s.iconUrl.trim())}" alt="" width="20" height="20" style="display:inline-block;vertical-align:middle;border:0;" />`;
|
|
582
|
+
}
|
|
583
|
+
const label = s.icon && s.icon.trim() || "\u{1F517}";
|
|
584
|
+
return escapeHtml(label);
|
|
585
|
+
}
|
|
586
|
+
__name(socialLinkInnerHtml, "socialLinkInnerHtml");
|
|
587
|
+
function supportLinesHtml(supportEmail, supportPhone, opts) {
|
|
588
|
+
const parts = [];
|
|
589
|
+
const top = opts?.tightTop ? "margin-top:0;" : "margin-top:4px;";
|
|
590
|
+
if (supportEmail) {
|
|
591
|
+
parts.push(`<div style="font-size:13px;color:#444;${top}">📧 ${escapeHtml(supportEmail)}</div>`);
|
|
592
|
+
}
|
|
593
|
+
if (supportPhone) {
|
|
594
|
+
parts.push(`<div style="font-size:13px;color:#444;margin-top:2px;">📞 ${escapeHtml(supportPhone)}</div>`);
|
|
595
|
+
}
|
|
596
|
+
return parts.join("");
|
|
597
|
+
}
|
|
598
|
+
__name(supportLinesHtml, "supportLinesHtml");
|
|
599
|
+
function renderLayout(options) {
|
|
600
|
+
const { bodyHtml, companyDetails } = options;
|
|
601
|
+
const { logoUrl, companyName, supportEmail, supportPhone, socialLinks, footerDisclaimer, followUsTitle = "Follow Us" } = companyDetails;
|
|
602
|
+
const supportFooterHtml = supportLinesHtml(supportEmail, supportPhone, {
|
|
603
|
+
tightTop: true
|
|
604
|
+
});
|
|
605
|
+
const hasSocial = Boolean(socialLinks?.length);
|
|
606
|
+
const brandFooterInner = [];
|
|
607
|
+
if (logoUrl) {
|
|
608
|
+
brandFooterInner.push(`<img src="${escapeHtml(logoUrl)}" alt="" style="max-height:28px;max-width:160px;height:auto;width:auto;display:inline-block;vertical-align:middle;margin-right:10px;border:0;" />`);
|
|
609
|
+
}
|
|
610
|
+
if (companyName) {
|
|
611
|
+
brandFooterInner.push(`<span style="font-size:14px;font-weight:600;color:#111;vertical-align:middle;">${escapeHtml(companyName)}</span>`);
|
|
612
|
+
}
|
|
613
|
+
const brandFooterHtml = brandFooterInner.length ? `<div style="line-height:1.4;">${brandFooterInner.join("")}</div>` : "";
|
|
614
|
+
const followTitleHtml = hasSocial && followUsTitle ? `<div class="footer-follow-title" style="font-size:13px;font-weight:600;color:#555;margin:0;">${escapeHtml(followUsTitle)}</div>` : "";
|
|
615
|
+
const socialRowHtml = hasSocial && socialLinks ? `<div class="footer-social-icons" style="margin-top:6px;text-align:left;font-size:0;line-height:0;">${socialLinks.map((s) => `<a href="${escapeHtml(s.url)}" style="display:inline-block;margin-right:12px;text-decoration:none;color:#333;font-size:18px;line-height:1;vertical-align:middle;">${socialLinkInnerHtml(s)}</a>`).join("")}</div>` : "";
|
|
616
|
+
const rowSupport = supportFooterHtml ? `<tr><td class="footer-support-cell" colspan="2" valign="top" style="padding:0 0 10px 0;">${supportFooterHtml}</td></tr>` : "";
|
|
617
|
+
let footerBlock = "";
|
|
618
|
+
if (brandFooterHtml || followTitleHtml || socialRowHtml || footerDisclaimer || rowSupport) {
|
|
619
|
+
const rowBrand = brandFooterHtml ? `<tr><td class="footer-brand-cell" colspan="2" valign="top" style="padding:0 0 10px 0;">${brandFooterHtml}</td></tr>` : "";
|
|
620
|
+
const rowFollowTitle = followTitleHtml ? `<tr><td class="footer-follow-cell" colspan="2" valign="top" style="padding:14px 0 0 0;text-align:left;">${followTitleHtml}</td></tr>` : "";
|
|
621
|
+
const rowSocial = socialRowHtml ? `<tr><td class="footer-social-cell" colspan="2" style="padding:0;text-align:left;">${socialRowHtml}</td></tr>` : "";
|
|
622
|
+
footerBlock = `<table role="presentation" class="footer-main" width="100%" cellpadding="0" cellspacing="0" style="margin-top:28px;padding-top:16px;">${rowBrand}${rowSupport}${rowFollowTitle}${rowSocial}</table>`;
|
|
623
|
+
}
|
|
624
|
+
const disclaimerBlock = footerDisclaimer ? `<div style="margin-top:20px;padding-top:12px;font-size:11px;line-height:1.5;color:#888;">${escapeHtml(footerDisclaimer).replace(/\n/g, "<br/>")}</div>` : "";
|
|
625
|
+
const responsiveCss = `
|
|
626
|
+
@media only screen and (max-width: 600px) {
|
|
627
|
+
.email-wrap { padding-left: 12px !important; padding-right: 12px !important; }
|
|
628
|
+
.footer-follow-cell { padding-top: 12px !important; }
|
|
629
|
+
}
|
|
630
|
+
`;
|
|
631
|
+
return `<!DOCTYPE html>
|
|
632
|
+
<html>
|
|
633
|
+
<head>
|
|
634
|
+
<meta charset="utf-8">
|
|
635
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
636
|
+
<style type="text/css">${responsiveCss}</style>
|
|
637
|
+
</head>
|
|
638
|
+
<body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;font-size:14px;line-height:1.5;color:#333;background:#fff;">
|
|
639
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#fff;">
|
|
640
|
+
<tr><td class="email-wrap" style="padding:16px;">
|
|
641
|
+
<div>
|
|
642
|
+
${bodyHtml}
|
|
643
|
+
</div>
|
|
644
|
+
${footerBlock}
|
|
645
|
+
${disclaimerBlock}
|
|
646
|
+
</td></tr>
|
|
647
|
+
</table>
|
|
648
|
+
</body>
|
|
649
|
+
</html>`;
|
|
650
|
+
}
|
|
651
|
+
__name(renderLayout, "renderLayout");
|
|
652
|
+
|
|
653
|
+
// src/plugins/email/templates/types.ts
|
|
654
|
+
function normalizeSocialLinkItem(o) {
|
|
655
|
+
const url = String(o.url ?? "").trim();
|
|
656
|
+
if (!url) return null;
|
|
657
|
+
let iconUrl = String(o.iconUrl ?? o.icon_image ?? "").trim();
|
|
658
|
+
let icon = String(o.icon ?? "").trim();
|
|
659
|
+
if (!iconUrl && /^https?:\/\//i.test(icon)) {
|
|
660
|
+
iconUrl = icon;
|
|
661
|
+
icon = "";
|
|
662
|
+
}
|
|
663
|
+
const item = {
|
|
664
|
+
url
|
|
665
|
+
};
|
|
666
|
+
if (iconUrl) item.iconUrl = iconUrl;
|
|
667
|
+
if (icon) item.icon = icon;
|
|
668
|
+
return item;
|
|
669
|
+
}
|
|
670
|
+
__name(normalizeSocialLinkItem, "normalizeSocialLinkItem");
|
|
671
|
+
function parseSocialLinksJson(raw) {
|
|
672
|
+
if (raw == null || raw.trim() === "") return void 0;
|
|
673
|
+
try {
|
|
674
|
+
const parsed = JSON.parse(raw);
|
|
675
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
676
|
+
const out = [];
|
|
677
|
+
for (const item of parsed) {
|
|
678
|
+
if (item && typeof item === "object" && "url" in item) {
|
|
679
|
+
const n = normalizeSocialLinkItem(item);
|
|
680
|
+
if (n) out.push(n);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return out.length ? out : void 0;
|
|
684
|
+
} catch {
|
|
685
|
+
return void 0;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
__name(parseSocialLinksJson, "parseSocialLinksJson");
|
|
689
|
+
function mergeEmailLayoutCompanyDetails(branding, emailSettings) {
|
|
690
|
+
const fromBranding = getCompanyDetailsFromSettings(branding);
|
|
691
|
+
const pick2 = /* @__PURE__ */ __name((emailVal, fallback) => {
|
|
692
|
+
const t = emailVal?.trim();
|
|
693
|
+
return t || fallback?.trim() || void 0;
|
|
694
|
+
}, "pick");
|
|
695
|
+
const logoUrl = pick2(emailSettings.logoUrl ?? emailSettings.emailLogoUrl, fromBranding.logoUrl);
|
|
696
|
+
const companyName = pick2(emailSettings.companyName ?? emailSettings.emailCompanyName, fromBranding.companyName);
|
|
697
|
+
const supportEmail = pick2(emailSettings.supportEmail ?? emailSettings.emailSupportEmail, fromBranding.supportEmail);
|
|
698
|
+
const supportPhone = pick2(emailSettings.supportPhone, void 0);
|
|
699
|
+
const footerDisclaimer = pick2(emailSettings.footerDisclaimer, void 0);
|
|
700
|
+
const followUsTitle = pick2(emailSettings.followUsTitle, "Follow Us") || "Follow Us";
|
|
701
|
+
const socialFromEmail = parseSocialLinksJson(emailSettings.socialLinks);
|
|
702
|
+
const socialLinks = socialFromEmail?.length ? socialFromEmail : fromBranding.socialLinks;
|
|
703
|
+
return {
|
|
704
|
+
logoUrl,
|
|
705
|
+
companyName,
|
|
706
|
+
supportEmail,
|
|
707
|
+
supportPhone,
|
|
708
|
+
socialLinks,
|
|
709
|
+
footerDisclaimer,
|
|
710
|
+
followUsTitle
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
__name(mergeEmailLayoutCompanyDetails, "mergeEmailLayoutCompanyDetails");
|
|
714
|
+
function getCompanyDetailsFromSettings(settingsGroup) {
|
|
715
|
+
const logoUrl = settingsGroup.logo ?? settingsGroup.logoUrl ?? "";
|
|
716
|
+
const companyName = settingsGroup.companyName ?? settingsGroup.company_name ?? "";
|
|
717
|
+
const supportEmail = settingsGroup.supportEmail ?? settingsGroup.support_email ?? "";
|
|
718
|
+
let socialLinks = [];
|
|
719
|
+
const raw = settingsGroup.socialLinks ?? settingsGroup.social_links;
|
|
720
|
+
if (typeof raw === "string") {
|
|
721
|
+
try {
|
|
722
|
+
const arr = JSON.parse(raw);
|
|
723
|
+
if (Array.isArray(arr)) {
|
|
724
|
+
for (const item of arr) {
|
|
725
|
+
if (item && typeof item === "object") {
|
|
726
|
+
const n = normalizeSocialLinkItem(item);
|
|
727
|
+
if (n) socialLinks.push(n);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
logoUrl: logoUrl || void 0,
|
|
736
|
+
companyName: companyName || void 0,
|
|
737
|
+
supportEmail: supportEmail || void 0,
|
|
738
|
+
socialLinks: socialLinks.length ? socialLinks : void 0
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
__name(getCompanyDetailsFromSettings, "getCompanyDetailsFromSettings");
|
|
742
|
+
|
|
743
|
+
// src/plugins/email/templates/inline-cta.ts
|
|
744
|
+
function escapeHtml2(s) {
|
|
745
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
746
|
+
}
|
|
747
|
+
__name(escapeHtml2, "escapeHtml");
|
|
748
|
+
function escapeAttr(s) {
|
|
749
|
+
return escapeHtml2(s);
|
|
750
|
+
}
|
|
751
|
+
__name(escapeAttr, "escapeAttr");
|
|
752
|
+
function primaryCtaButton(href, label) {
|
|
753
|
+
return `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:20px 0;">
|
|
754
|
+
<tr><td style="border-radius:6px;background:#1a1a1a;">
|
|
755
|
+
<a href="${escapeAttr(href)}" style="display:inline-block;padding:12px 22px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;">${escapeHtml2(label)}</a>
|
|
756
|
+
</td></tr>
|
|
757
|
+
</table>`;
|
|
758
|
+
}
|
|
759
|
+
__name(primaryCtaButton, "primaryCtaButton");
|
|
760
|
+
|
|
761
|
+
// src/plugins/email/templates/signup.ts
|
|
762
|
+
function render(ctx) {
|
|
763
|
+
const { name, loginUrl, verifyEmailUrl, companyDetails } = ctx;
|
|
764
|
+
if (verifyEmailUrl) {
|
|
765
|
+
const subject2 = "Confirm your email";
|
|
766
|
+
const greeting = name && name.trim() ? `Hi ${escapeHtml2(name.trim())},` : "Hi,";
|
|
767
|
+
const bodyHtml2 = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${greeting}</p>
|
|
768
|
+
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Thanks for signing up. Please confirm your email address to activate your account. Until you confirm, you won\u2019t be able to sign in.</p>
|
|
769
|
+
${primaryCtaButton(verifyEmailUrl, "Confirm email address")}
|
|
770
|
+
<p style="margin:16px 0 0 0;font-size:12px;line-height:1.5;color:#888;">This link expires in a few days. If you didn\u2019t create an account, you can ignore this message.<br/><span style="word-break:break-all;">${escapeHtml2(verifyEmailUrl)}</span></p>`;
|
|
771
|
+
const text2 = [
|
|
772
|
+
greeting,
|
|
773
|
+
"",
|
|
774
|
+
"Confirm your email to activate your account:",
|
|
775
|
+
verifyEmailUrl,
|
|
776
|
+
"",
|
|
777
|
+
"You cannot sign in until your email is confirmed."
|
|
778
|
+
].join("\n");
|
|
779
|
+
const html2 = renderLayout({
|
|
780
|
+
bodyHtml: bodyHtml2,
|
|
781
|
+
companyDetails
|
|
782
|
+
});
|
|
783
|
+
return {
|
|
784
|
+
subject: subject2,
|
|
785
|
+
html: html2,
|
|
786
|
+
text: text2
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
const subject = "Welcome";
|
|
790
|
+
const bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Welcome${name ? `, ${escapeHtml2(name)}` : ""}.</p><p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Your account has been created.</p>${loginUrl ? `${primaryCtaButton(loginUrl, "Sign in")}` : ""}`;
|
|
791
|
+
const text = `Welcome${name ? `, ${name}` : ""}. Your account has been created.${loginUrl ? ` Sign in: ${loginUrl}` : ""}`;
|
|
792
|
+
const html = renderLayout({
|
|
793
|
+
bodyHtml,
|
|
794
|
+
companyDetails
|
|
795
|
+
});
|
|
796
|
+
return {
|
|
797
|
+
subject,
|
|
798
|
+
html,
|
|
799
|
+
text
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
__name(render, "render");
|
|
803
|
+
|
|
804
|
+
// src/plugins/email/templates/passwordReset.ts
|
|
805
|
+
function render2(ctx) {
|
|
806
|
+
const { resetLink, companyDetails } = ctx;
|
|
807
|
+
const subject = "Reset your password";
|
|
808
|
+
const bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Hello,</p>
|
|
809
|
+
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">We received a request to reset the password for your account. If you made this request, use the button below to choose a new password.</p>
|
|
810
|
+
<p style="margin:0 0 8px 0;font-size:14px;line-height:1.5;color:#555;">For your security, this link will stop working after a short time. If it expires, request a new reset from the sign-in page.</p>
|
|
811
|
+
${primaryCtaButton(resetLink, "Reset password")}
|
|
812
|
+
<p style="margin:16px 0 0 0;font-size:12px;line-height:1.5;color:#888;">If you did not request a password reset, you can safely ignore this email\u2014your password will stay the same.</p>
|
|
813
|
+
<p style="margin:12px 0 0 0;font-size:12px;line-height:1.5;color:#888;">If the button does not work, copy and paste this URL into your browser:<br/><span style="word-break:break-all;">${escapeHtml2(resetLink)}</span></p>`;
|
|
814
|
+
const text = [
|
|
815
|
+
"Hello,",
|
|
816
|
+
"",
|
|
817
|
+
"We received a request to reset your password. Open the link below to set a new password:",
|
|
818
|
+
resetLink,
|
|
819
|
+
"",
|
|
820
|
+
"This link expires after a limited time.",
|
|
821
|
+
"",
|
|
822
|
+
"If you did not request this, you can ignore this email."
|
|
823
|
+
].join("\n");
|
|
824
|
+
const html = renderLayout({
|
|
825
|
+
bodyHtml,
|
|
826
|
+
companyDetails
|
|
827
|
+
});
|
|
828
|
+
return {
|
|
829
|
+
subject,
|
|
830
|
+
html,
|
|
831
|
+
text
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
__name(render2, "render");
|
|
835
|
+
|
|
836
|
+
// src/plugins/email/templates/passwordChange.ts
|
|
837
|
+
function render3(ctx) {
|
|
838
|
+
const { name, companyDetails } = ctx;
|
|
839
|
+
const subject = "Password changed";
|
|
840
|
+
const bodyHtml = `<h2>Password changed</h2><p>Your password has been updated successfully${name ? `, ${escapeHtml3(name)}` : ""}.</p>`;
|
|
841
|
+
const text = `Your password has been updated successfully${name ? `, ${name}` : ""}.`;
|
|
842
|
+
const html = renderLayout({
|
|
843
|
+
bodyHtml,
|
|
844
|
+
companyDetails
|
|
845
|
+
});
|
|
846
|
+
return {
|
|
847
|
+
subject,
|
|
848
|
+
html,
|
|
849
|
+
text
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
__name(render3, "render");
|
|
853
|
+
function escapeHtml3(s) {
|
|
854
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
855
|
+
}
|
|
856
|
+
__name(escapeHtml3, "escapeHtml");
|
|
857
|
+
|
|
858
|
+
// src/plugins/email/templates/orderPlaced.ts
|
|
859
|
+
function formatMoney(amount, currency) {
|
|
860
|
+
const v = typeof amount === "string" ? parseFloat(amount) : amount;
|
|
861
|
+
const num = Number.isFinite(v) ? Number(v).toFixed(2) : String(amount);
|
|
862
|
+
return currency ? `${num} ${currency}` : num;
|
|
863
|
+
}
|
|
864
|
+
__name(formatMoney, "formatMoney");
|
|
865
|
+
function renderLineItemsHtml(items, currency) {
|
|
866
|
+
if (!items.length) {
|
|
867
|
+
return '<p style="margin:12px 0 0 0;font-size:14px;color:#666;">No line items.</p>';
|
|
868
|
+
}
|
|
869
|
+
const rows = items.map((it) => {
|
|
870
|
+
const name = escapeHtml2(it.productName);
|
|
871
|
+
const sku = it.sku && String(it.sku).trim() ? `<span style="font-size:12px;color:#888;"> (${escapeHtml2(String(it.sku).trim())})</span>` : "";
|
|
872
|
+
const hsn = it.hsn && String(it.hsn).trim() ? `<br/><span style="font-size:11px;color:#888;">HSN: ${escapeHtml2(String(it.hsn).trim())}</span>` : "";
|
|
873
|
+
const taxNote = it.tax != null && String(it.tax).trim() && Number(it.tax) !== 0 ? `<br/><span style="font-size:11px;color:#888;">Tax: ${escapeHtml2(formatMoney(it.tax, currency))}</span>` : "";
|
|
874
|
+
return `<tr>
|
|
875
|
+
<td style="padding:10px 8px 10px 0;border-bottom:1px solid #eee;vertical-align:top;font-size:14px;color:#333;">${name}${sku}${hsn}${taxNote}</td>
|
|
876
|
+
<td align="right" style="padding:10px 8px;border-bottom:1px solid #eee;vertical-align:top;font-size:14px;color:#333;white-space:nowrap;">${escapeHtml2(String(it.quantity))}</td>
|
|
877
|
+
<td align="right" style="padding:10px 8px;border-bottom:1px solid #eee;vertical-align:top;font-size:14px;color:#333;white-space:nowrap;">${escapeHtml2(formatMoney(it.unitPrice, currency))}</td>
|
|
878
|
+
<td align="right" style="padding:10px 0 10px 8px;border-bottom:1px solid #eee;vertical-align:top;font-size:14px;color:#333;white-space:nowrap;">${escapeHtml2(formatMoney(it.lineTotal, currency))}</td>
|
|
879
|
+
</tr>`;
|
|
880
|
+
}).join("");
|
|
881
|
+
return `<p style="margin:16px 0 8px 0;font-size:14px;font-weight:600;color:#111;">Order items</p>
|
|
882
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
|
|
883
|
+
<tr>
|
|
884
|
+
<td style="padding:0 8px 8px 0;font-size:12px;font-weight:600;color:#555;text-transform:uppercase;letter-spacing:0.02em;">Item</td>
|
|
885
|
+
<td align="right" style="padding:0 8px 8px 8px;font-size:12px;font-weight:600;color:#555;text-transform:uppercase;">Qty</td>
|
|
886
|
+
<td align="right" style="padding:0 8px 8px 8px;font-size:12px;font-weight:600;color:#555;text-transform:uppercase;">Price</td>
|
|
887
|
+
<td align="right" style="padding:0 0 8px 8px;font-size:12px;font-weight:600;color:#555;text-transform:uppercase;">Total</td>
|
|
888
|
+
</tr>
|
|
889
|
+
${rows}
|
|
890
|
+
</table>`;
|
|
891
|
+
}
|
|
892
|
+
__name(renderLineItemsHtml, "renderLineItemsHtml");
|
|
893
|
+
function renderLineItemsText(items, currency) {
|
|
894
|
+
if (!items.length) return "";
|
|
895
|
+
const lines = items.map((it) => {
|
|
896
|
+
let s = `- ${it.productName} \xD7 ${it.quantity} @ ${formatMoney(it.unitPrice, currency)} = ${formatMoney(it.lineTotal, currency)}`;
|
|
897
|
+
if (it.sku) s += ` [${it.sku}]`;
|
|
898
|
+
if (it.hsn && String(it.hsn).trim()) s += ` HSN:${it.hsn}`;
|
|
899
|
+
if (it.tax != null && Number(it.tax) !== 0) s += ` tax:${formatMoney(it.tax, currency)}`;
|
|
900
|
+
return s;
|
|
901
|
+
});
|
|
902
|
+
return [
|
|
903
|
+
"Items:",
|
|
904
|
+
...lines
|
|
905
|
+
].join("\n");
|
|
906
|
+
}
|
|
907
|
+
__name(renderLineItemsText, "renderLineItemsText");
|
|
908
|
+
function formatAddressLines(addr) {
|
|
909
|
+
if (!addr || typeof addr !== "object") return [];
|
|
910
|
+
const parts = [
|
|
911
|
+
addr.line1,
|
|
912
|
+
addr.line2,
|
|
913
|
+
[
|
|
914
|
+
addr.city,
|
|
915
|
+
addr.state
|
|
916
|
+
].filter(Boolean).join(", "),
|
|
917
|
+
addr.postalCode,
|
|
918
|
+
addr.country
|
|
919
|
+
].filter((x) => x != null && String(x).trim() !== "");
|
|
920
|
+
return parts.map((p) => String(p).trim()).filter(Boolean);
|
|
921
|
+
}
|
|
922
|
+
__name(formatAddressLines, "formatAddressLines");
|
|
923
|
+
function renderAddressesHtml(billing, shipping) {
|
|
924
|
+
const bLines = formatAddressLines(billing);
|
|
925
|
+
const sLines = formatAddressLines(shipping);
|
|
926
|
+
if (!bLines.length && !sLines.length) return "";
|
|
927
|
+
let html = '<p style="margin:16px 0 8px 0;font-size:14px;font-weight:600;color:#111;">Addresses</p>';
|
|
928
|
+
if (bLines.length) {
|
|
929
|
+
html += `<p style="margin:0 0 4px 0;font-size:12px;font-weight:600;color:#555;">Billing</p><p style="margin:0 0 12px 0;font-size:14px;line-height:1.5;color:#333;">${bLines.map((l) => escapeHtml2(l)).join("<br/>")}</p>`;
|
|
930
|
+
}
|
|
931
|
+
if (sLines.length) {
|
|
932
|
+
html += `<p style="margin:0 0 4px 0;font-size:12px;font-weight:600;color:#555;">Shipping</p><p style="margin:0 0 0 0;font-size:14px;line-height:1.5;color:#333;">${sLines.map((l) => escapeHtml2(l)).join("<br/>")}</p>`;
|
|
933
|
+
}
|
|
934
|
+
return html;
|
|
935
|
+
}
|
|
936
|
+
__name(renderAddressesHtml, "renderAddressesHtml");
|
|
937
|
+
function renderAddressesText(billing, shipping) {
|
|
938
|
+
const bLines = formatAddressLines(billing);
|
|
939
|
+
const sLines = formatAddressLines(shipping);
|
|
940
|
+
const out = [];
|
|
941
|
+
if (bLines.length) out.push("Billing:", ...bLines.map((l) => ` ${l}`));
|
|
942
|
+
if (sLines.length) out.push("Shipping:", ...sLines.map((l) => ` ${l}`));
|
|
943
|
+
return out.length ? out.join("\n") : "";
|
|
944
|
+
}
|
|
945
|
+
__name(renderAddressesText, "renderAddressesText");
|
|
946
|
+
function render4(ctx) {
|
|
947
|
+
const { orderNumber, total, subtotal, tax, currency, customerName, companyDetails, audience = "customer", internalCustomerEmail, lineItems = [], billingAddress, shippingAddress } = ctx;
|
|
948
|
+
const itemsHtml = renderLineItemsHtml(lineItems, currency);
|
|
949
|
+
const itemsText = renderLineItemsText(lineItems, currency);
|
|
950
|
+
const addrHtml = renderAddressesHtml(billingAddress, shippingAddress);
|
|
951
|
+
const addrText = renderAddressesText(billingAddress, shippingAddress);
|
|
952
|
+
const subtotalLine = subtotal != null && String(subtotal).trim() !== "" ? `<p style="margin:8px 0 0 0;font-size:14px;line-height:1.5;color:#333;"><strong>Subtotal:</strong> ${escapeHtml2(String(subtotal))}${currency ? ` ${escapeHtml2(currency)}` : ""}</p>` : "";
|
|
953
|
+
const taxLine = tax != null && String(tax).trim() !== "" && Number(tax) !== 0 ? `<p style="margin:4px 0 0 0;font-size:14px;line-height:1.5;color:#333;"><strong>Tax:</strong> ${escapeHtml2(String(tax))}${currency ? ` ${escapeHtml2(currency)}` : ""}</p>` : "";
|
|
954
|
+
const totalLine = total != null && String(total).trim() !== "" ? `<p style="margin:12px 0 0 0;font-size:15px;line-height:1.5;color:#333;"><strong>Order total:</strong> ${escapeHtml2(String(total))}${currency ? ` ${escapeHtml2(currency)}` : ""}</p>` : "";
|
|
955
|
+
let subject;
|
|
956
|
+
let bodyHtml;
|
|
957
|
+
let text;
|
|
958
|
+
if (audience === "sales") {
|
|
959
|
+
subject = `New order #${orderNumber}`;
|
|
960
|
+
const who = customerName || internalCustomerEmail ? `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;"><strong>Customer:</strong> ${escapeHtml2(customerName || "\u2014")}${internalCustomerEmail ? ` <span style="color:#555;">(${escapeHtml2(internalCustomerEmail)})</span>` : ""}</p>` : "";
|
|
961
|
+
bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">A new order has been placed and payment completed.</p>
|
|
962
|
+
<p style="margin:0 0 8px 0;font-size:15px;line-height:1.5;color:#333;"><strong>Order number:</strong> ${escapeHtml2(orderNumber)}</p>
|
|
963
|
+
${who}
|
|
964
|
+
${addrHtml}
|
|
965
|
+
${itemsHtml}
|
|
966
|
+
${subtotalLine}${taxLine}${totalLine}`;
|
|
967
|
+
text = [
|
|
968
|
+
`New order #${orderNumber}`,
|
|
969
|
+
customerName ? `Customer: ${customerName}` : "",
|
|
970
|
+
internalCustomerEmail ? `Email: ${internalCustomerEmail}` : "",
|
|
971
|
+
addrText,
|
|
972
|
+
itemsText,
|
|
973
|
+
subtotal != null ? `Subtotal: ${subtotal}${currency ? ` ${currency}` : ""}` : "",
|
|
974
|
+
tax != null && Number(tax) !== 0 ? `Tax: ${tax}${currency ? ` ${currency}` : ""}` : "",
|
|
975
|
+
total != null ? `Order total: ${total}${currency ? ` ${currency}` : ""}` : ""
|
|
976
|
+
].filter(Boolean).join("\n\n");
|
|
977
|
+
} else {
|
|
978
|
+
subject = `Order confirmed #${orderNumber}`;
|
|
979
|
+
const thanksPlain = customerName && customerName.trim() ? `Thank you, ${customerName.trim()}.` : "Thank you for your order.";
|
|
980
|
+
const thanksHtml = `${escapeHtml2(thanksPlain)} We\u2019ve received your order and will process it shortly.`;
|
|
981
|
+
bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${thanksHtml}</p>
|
|
982
|
+
<p style="margin:0 0 8px 0;font-size:15px;line-height:1.5;color:#333;"><strong>Order number:</strong> ${escapeHtml2(orderNumber)}</p>
|
|
983
|
+
${addrHtml}
|
|
984
|
+
${itemsHtml}
|
|
985
|
+
${subtotalLine}${taxLine}${totalLine}
|
|
986
|
+
<p style="margin:16px 0 0 0;font-size:13px;line-height:1.5;color:#666;">If you have questions, reply to this email or contact us using the details below.</p>`;
|
|
987
|
+
text = [
|
|
988
|
+
`Order confirmed #${orderNumber}`,
|
|
989
|
+
`${thanksPlain} We\u2019ve received your order and will process it shortly.`,
|
|
990
|
+
"",
|
|
991
|
+
addrText,
|
|
992
|
+
itemsText,
|
|
993
|
+
subtotal != null ? `Subtotal: ${subtotal}${currency ? ` ${currency}` : ""}` : "",
|
|
994
|
+
tax != null && Number(tax) !== 0 ? `Tax: ${tax}${currency ? ` ${currency}` : ""}` : "",
|
|
995
|
+
total != null ? `Order total: ${total}${currency ? ` ${currency}` : ""}` : "",
|
|
996
|
+
"",
|
|
997
|
+
"We will process your order shortly."
|
|
998
|
+
].filter((line, i, arr) => !(line === "" && arr[i - 1] === "")).join("\n");
|
|
999
|
+
}
|
|
1000
|
+
const html = renderLayout({
|
|
1001
|
+
bodyHtml,
|
|
1002
|
+
companyDetails
|
|
1003
|
+
});
|
|
1004
|
+
return {
|
|
1005
|
+
subject,
|
|
1006
|
+
html,
|
|
1007
|
+
text
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
__name(render4, "render");
|
|
1011
|
+
|
|
1012
|
+
// src/plugins/email/templates/returnInitiated.ts
|
|
1013
|
+
function render5(ctx) {
|
|
1014
|
+
const { returnId, orderNumber, companyDetails } = ctx;
|
|
1015
|
+
const subject = "Return initiated";
|
|
1016
|
+
const bodyHtml = `<h2>Return initiated</h2><p>Your return request has been received.</p>${orderNumber ? `<p><strong>Order:</strong> ${escapeHtml4(orderNumber)}</p>` : ""}${returnId ? `<p><strong>Return ID:</strong> ${escapeHtml4(returnId)}</p>` : ""}`;
|
|
1017
|
+
const text = `Return initiated.${orderNumber ? ` Order: ${orderNumber}` : ""}${returnId ? ` Return ID: ${returnId}` : ""}`;
|
|
1018
|
+
const html = renderLayout({
|
|
1019
|
+
bodyHtml,
|
|
1020
|
+
companyDetails
|
|
1021
|
+
});
|
|
1022
|
+
return {
|
|
1023
|
+
subject,
|
|
1024
|
+
html,
|
|
1025
|
+
text
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
__name(render5, "render");
|
|
1029
|
+
function escapeHtml4(s) {
|
|
1030
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1031
|
+
}
|
|
1032
|
+
__name(escapeHtml4, "escapeHtml");
|
|
1033
|
+
|
|
1034
|
+
// src/plugins/email/templates/shippingUpdate.ts
|
|
1035
|
+
function render6(ctx) {
|
|
1036
|
+
const { orderNumber, status, trackingUrl, companyDetails } = ctx;
|
|
1037
|
+
const subject = status ? `Shipping update: ${status}` : "Shipping update";
|
|
1038
|
+
const bodyHtml = `<h2>Shipping update</h2>${status ? `<p><strong>Status:</strong> ${escapeHtml5(status)}</p>` : ""}${orderNumber ? `<p><strong>Order:</strong> ${escapeHtml5(orderNumber)}</p>` : ""}${trackingUrl ? `<p><a href="${escapeHtml5(trackingUrl)}">Track your order</a></p>` : ""}`;
|
|
1039
|
+
const text = `Shipping update.${status ? ` Status: ${status}` : ""}${orderNumber ? ` Order: ${orderNumber}` : ""}${trackingUrl ? ` Track: ${trackingUrl}` : ""}`;
|
|
1040
|
+
const html = renderLayout({
|
|
1041
|
+
bodyHtml,
|
|
1042
|
+
companyDetails
|
|
1043
|
+
});
|
|
1044
|
+
return {
|
|
1045
|
+
subject,
|
|
1046
|
+
html,
|
|
1047
|
+
text
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
__name(render6, "render");
|
|
1051
|
+
function escapeHtml5(s) {
|
|
1052
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1053
|
+
}
|
|
1054
|
+
__name(escapeHtml5, "escapeHtml");
|
|
1055
|
+
|
|
1056
|
+
// src/plugins/email/templates/invite.ts
|
|
1057
|
+
function render7(ctx) {
|
|
1058
|
+
const { inviteLink, email, inviteeName, companyDetails } = ctx;
|
|
1059
|
+
const subject = "You're invited";
|
|
1060
|
+
const greeting = inviteeName && inviteeName.trim() ? `Hello ${escapeHtml2(inviteeName.trim())},` : "Hello,";
|
|
1061
|
+
const bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${greeting}</p>
|
|
1062
|
+
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">You have been invited to create your account for <strong>${escapeHtml2(email)}</strong>. Use the secure link below to choose your password and activate access.</p>
|
|
1063
|
+
<p style="margin:0 0 8px 0;font-size:14px;line-height:1.5;color:#555;">This link is personal to you. If you did not expect this invitation, you can ignore this message.</p>
|
|
1064
|
+
${primaryCtaButton(inviteLink, "Accept invitation & set password")}
|
|
1065
|
+
<p style="margin:16px 0 0 0;font-size:12px;line-height:1.5;color:#888;">If the button does not work, copy and paste this URL into your browser:<br/><span style="word-break:break-all;">${escapeHtml2(inviteLink)}</span></p>`;
|
|
1066
|
+
const text = [
|
|
1067
|
+
inviteeName?.trim() ? `Hello ${inviteeName.trim()},` : "Hello,",
|
|
1068
|
+
"",
|
|
1069
|
+
`You have been invited to create your account (${email}).`,
|
|
1070
|
+
"Open this link to set your password:",
|
|
1071
|
+
inviteLink,
|
|
1072
|
+
"",
|
|
1073
|
+
"If you did not expect this invitation, you can ignore this email."
|
|
1074
|
+
].join("\n");
|
|
1075
|
+
const html = renderLayout({
|
|
1076
|
+
bodyHtml,
|
|
1077
|
+
companyDetails
|
|
1078
|
+
});
|
|
1079
|
+
return {
|
|
1080
|
+
subject,
|
|
1081
|
+
html,
|
|
1082
|
+
text
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
__name(render7, "render");
|
|
1086
|
+
|
|
1087
|
+
// src/plugins/email/templates/formSubmission.ts
|
|
1088
|
+
function render8(ctx) {
|
|
1089
|
+
const { formName, contactName, contactEmail, formData, formFieldRows, companyDetails } = ctx;
|
|
1090
|
+
const subject = `New Form Submission: ${formName}`;
|
|
1091
|
+
const fieldsBlock = formFieldRows && formFieldRows.length > 0 ? formFieldRows.map((r) => `<p style="margin:10px 0 0 0;line-height:1.5;"><strong>${escapeHtml6(r.label)}</strong><br/>${escapeHtml6(r.value)}</p>`).join("") : `<p style="margin:10px 0 0 0;font-size:13px;white-space:pre-wrap;">${escapeHtml6(JSON.stringify(formData, null, 2))}</p>`;
|
|
1092
|
+
const bodyHtml = `<p style="margin:0 0 6px 0;font-size:18px;font-weight:600;color:#111;">New Form Submission</p>
|
|
1093
|
+
<p style="margin:0 0 4px 0;"><strong>Form:</strong> ${escapeHtml6(formName)}</p>
|
|
1094
|
+
<p style="margin:0 0 8px 0;"><strong>Contact:</strong> ${escapeHtml6(contactName)}${contactEmail ? ` (${escapeHtml6(contactEmail)})` : ""}</p>
|
|
1095
|
+
${fieldsBlock}`;
|
|
1096
|
+
const textLines = [
|
|
1097
|
+
"New Form Submission",
|
|
1098
|
+
`Form: ${formName}`,
|
|
1099
|
+
`Contact: ${contactName}${contactEmail ? ` (${contactEmail})` : ""}`,
|
|
1100
|
+
"",
|
|
1101
|
+
...formFieldRows?.length ? formFieldRows.map((r) => `${r.label}: ${r.value}`) : [
|
|
1102
|
+
JSON.stringify(formData, null, 2)
|
|
1103
|
+
]
|
|
1104
|
+
];
|
|
1105
|
+
const text = textLines.join("\n");
|
|
1106
|
+
const html = renderLayout({
|
|
1107
|
+
bodyHtml,
|
|
1108
|
+
companyDetails
|
|
1109
|
+
});
|
|
1110
|
+
return {
|
|
1111
|
+
subject,
|
|
1112
|
+
html,
|
|
1113
|
+
text
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
__name(render8, "render");
|
|
1117
|
+
function escapeHtml6(s) {
|
|
1118
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1119
|
+
}
|
|
1120
|
+
__name(escapeHtml6, "escapeHtml");
|
|
1121
|
+
|
|
1122
|
+
// src/plugins/email/templates/otp.ts
|
|
1123
|
+
function render9(ctx) {
|
|
1124
|
+
const { code, companyDetails } = ctx;
|
|
1125
|
+
const name = companyDetails.companyName?.trim() || "Our store";
|
|
1126
|
+
const bodyHtml = `
|
|
1127
|
+
<p>Your verification code is:</p>
|
|
1128
|
+
<p style="font-size:24px;font-weight:bold;letter-spacing:4px;">${escapeHtml7(code)}</p>
|
|
1129
|
+
<p style="color:#666;font-size:14px;">This code expires in 10 minutes. If you did not request it, you can ignore this email.</p>
|
|
1130
|
+
`;
|
|
1131
|
+
const html = renderLayout({
|
|
1132
|
+
bodyHtml,
|
|
1133
|
+
companyDetails
|
|
1134
|
+
});
|
|
1135
|
+
return {
|
|
1136
|
+
subject: `${name}: your sign-in code`,
|
|
1137
|
+
html,
|
|
1138
|
+
text: `Your code is ${code}. It expires in 10 minutes.`
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
__name(render9, "render");
|
|
1142
|
+
function escapeHtml7(s) {
|
|
1143
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1144
|
+
}
|
|
1145
|
+
__name(escapeHtml7, "escapeHtml");
|
|
1146
|
+
|
|
1147
|
+
// src/plugins/email/templates/chatLead.ts
|
|
1148
|
+
function render10(ctx) {
|
|
1149
|
+
const { contactName, contactEmail, contactPhone, conversationId, latestMessage, intentCode, intentReason, transcript, companyDetails } = ctx;
|
|
1150
|
+
const intentDisplay = intentCode?.trim() ? `${intentCode.trim()}${intentReason?.trim() ? ` \u2014 ${intentReason.trim()}` : ""}` : intentReason;
|
|
1151
|
+
const subject = `Chat lead${intentCode?.trim() ? ` [${intentCode.trim()}]` : ""}: ${contactName || contactEmail || "Visitor"}`;
|
|
1152
|
+
const transcriptBlock = transcript?.trim() ? `<div style="margin-top:16px;padding:12px;background:#f9fafb;border-radius:6px;border:1px solid #e5e7eb;">
|
|
1153
|
+
<p style="margin:0 0 8px 0;font-size:13px;font-weight:600;color:#374151;">Recent conversation</p>
|
|
1154
|
+
<pre style="margin:0;font-size:12px;white-space:pre-wrap;font-family:inherit;color:#4b5563;">${escapeHtml8(transcript.trim())}</pre>
|
|
1155
|
+
</div>` : "";
|
|
1156
|
+
const bodyHtml = `<p style="margin:0 0 6px 0;font-size:18px;font-weight:600;color:#111;">New chat lead</p>
|
|
1157
|
+
<p style="margin:0 0 12px 0;font-size:14px;color:#374151;">A visitor showed interest based on your intent rules.</p>
|
|
1158
|
+
<table style="width:100%;border-collapse:collapse;font-size:14px;">
|
|
1159
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Name</td><td>${escapeHtml8(contactName || "\u2014")}</td></tr>
|
|
1160
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Email</td><td><a href="mailto:${escapeHtml8(contactEmail)}">${escapeHtml8(contactEmail)}</a></td></tr>
|
|
1161
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Phone</td><td>${escapeHtml8(contactPhone?.trim() || "\u2014")}</td></tr>
|
|
1162
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Conversation</td><td>#${conversationId}</td></tr>
|
|
1163
|
+
<tr><td style="padding:4px 8px 4px 0;color:#6b7280;vertical-align:top;">Intent</td><td>${escapeHtml8(intentReason)}</td></tr>
|
|
1164
|
+
</table>
|
|
1165
|
+
<p style="margin:16px 0 6px 0;font-size:13px;font-weight:600;color:#374151;">Latest message</p>
|
|
1166
|
+
<p style="margin:0;font-size:14px;white-space:pre-wrap;">${escapeHtml8(latestMessage)}</p>
|
|
1167
|
+
${transcriptBlock}`;
|
|
1168
|
+
const text = [
|
|
1169
|
+
"New chat lead",
|
|
1170
|
+
`Name: ${contactName || "\u2014"}`,
|
|
1171
|
+
`Email: ${contactEmail}`,
|
|
1172
|
+
`Phone: ${contactPhone?.trim() || "\u2014"}`,
|
|
1173
|
+
`Conversation: #${conversationId}`,
|
|
1174
|
+
`Intent: ${intentDisplay}`,
|
|
1175
|
+
"",
|
|
1176
|
+
"Latest message:",
|
|
1177
|
+
latestMessage,
|
|
1178
|
+
transcript?.trim() ? `
|
|
1179
|
+
Recent conversation:
|
|
1180
|
+
${transcript.trim()}` : ""
|
|
1181
|
+
].join("\n");
|
|
1182
|
+
const html = renderLayout({
|
|
1183
|
+
bodyHtml,
|
|
1184
|
+
companyDetails
|
|
1185
|
+
});
|
|
1186
|
+
return {
|
|
1187
|
+
subject,
|
|
1188
|
+
html,
|
|
1189
|
+
text
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
__name(render10, "render");
|
|
1193
|
+
function escapeHtml8(s) {
|
|
1194
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1195
|
+
}
|
|
1196
|
+
__name(escapeHtml8, "escapeHtml");
|
|
1197
|
+
|
|
1198
|
+
// src/plugins/email/templates/vendorOnboarded.ts
|
|
1199
|
+
function render11(ctx) {
|
|
1200
|
+
const { audience, activation, kind = "vendor_onboard", vendorName, vendorSlug, ownerName, ownerEmail, inviteLink, signInLink, companyDetails } = ctx;
|
|
1201
|
+
const storeLabel = vendorName.trim() || "your store";
|
|
1202
|
+
const greeting = ownerName && ownerName.trim() ? `Hello ${escapeHtml2(ownerName.trim())},` : "Hello,";
|
|
1203
|
+
const slugNote = vendorSlug && vendorSlug.trim() ? `<p style="margin:0 0 8px 0;font-size:14px;line-height:1.5;color:#555;"><strong>Store URL slug:</strong> ${escapeHtml2(vendorSlug.trim())}</p>` : "";
|
|
1204
|
+
let subject;
|
|
1205
|
+
let bodyHtml;
|
|
1206
|
+
let text;
|
|
1207
|
+
if (audience === "internal") {
|
|
1208
|
+
const action = kind === "team_invite" ? "A team member was invited to a vendor store." : activation === "invite" ? "A new vendor store was created and the owner was invited." : "A new vendor store was created with an active owner account.";
|
|
1209
|
+
subject = kind === "team_invite" ? `Team invite: ${storeLabel}` : `New vendor: ${storeLabel}`;
|
|
1210
|
+
bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${action}</p>
|
|
1211
|
+
<p style="margin:0 0 8px 0;font-size:15px;line-height:1.5;color:#333;"><strong>Store:</strong> ${escapeHtml2(storeLabel)}</p>
|
|
1212
|
+
${slugNote}
|
|
1213
|
+
<p style="margin:0 0 8px 0;font-size:15px;line-height:1.5;color:#333;"><strong>Owner / invitee:</strong> ${escapeHtml2(ownerName?.trim() || "\u2014")} <span style="color:#555;">(${escapeHtml2(ownerEmail)})</span></p>
|
|
1214
|
+
<p style="margin:0 0 0 0;font-size:14px;line-height:1.5;color:#555;"><strong>Activation:</strong> ${escapeHtml2(activation === "invite" ? "Invitation" : "Password set")}</p>`;
|
|
1215
|
+
text = [
|
|
1216
|
+
subject,
|
|
1217
|
+
"",
|
|
1218
|
+
action,
|
|
1219
|
+
`Store: ${storeLabel}`,
|
|
1220
|
+
vendorSlug ? `Slug: ${vendorSlug}` : "",
|
|
1221
|
+
`Owner / invitee: ${ownerName?.trim() || "\u2014"} (${ownerEmail})`,
|
|
1222
|
+
`Activation: ${activation === "invite" ? "Invitation" : "Password set"}`
|
|
1223
|
+
].filter(Boolean).join("\n");
|
|
1224
|
+
} else if (kind === "team_invite" && activation === "invite" && inviteLink) {
|
|
1225
|
+
subject = `You're invited to ${storeLabel}`;
|
|
1226
|
+
bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${greeting}</p>
|
|
1227
|
+
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">You have been invited to join the vendor team for <strong>${escapeHtml2(storeLabel)}</strong>. Use the secure link below to set your password and activate your account.</p>
|
|
1228
|
+
${primaryCtaButton(inviteLink, "Accept invitation & set password")}
|
|
1229
|
+
<p style="margin:16px 0 0 0;font-size:12px;line-height:1.5;color:#888;">If the button does not work, copy and paste this URL into your browser:<br/><span style="word-break:break-all;">${escapeHtml2(inviteLink)}</span></p>`;
|
|
1230
|
+
text = [
|
|
1231
|
+
ownerName?.trim() ? `Hello ${ownerName.trim()},` : "Hello,",
|
|
1232
|
+
"",
|
|
1233
|
+
`You have been invited to join ${storeLabel}.`,
|
|
1234
|
+
"Open this link to set your password:",
|
|
1235
|
+
inviteLink
|
|
1236
|
+
].join("\n");
|
|
1237
|
+
} else if (activation === "invite" && inviteLink) {
|
|
1238
|
+
subject = `Your vendor store "${storeLabel}" is ready`;
|
|
1239
|
+
bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${greeting}</p>
|
|
1240
|
+
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Your vendor store <strong>${escapeHtml2(storeLabel)}</strong> has been created. Accept the invitation below to set your password and access the admin dashboard.</p>
|
|
1241
|
+
${slugNote}
|
|
1242
|
+
${primaryCtaButton(inviteLink, "Accept invitation & set password")}
|
|
1243
|
+
<p style="margin:16px 0 0 0;font-size:12px;line-height:1.5;color:#888;">If the button does not work, copy and paste this URL into your browser:<br/><span style="word-break:break-all;">${escapeHtml2(inviteLink)}</span></p>`;
|
|
1244
|
+
text = [
|
|
1245
|
+
ownerName?.trim() ? `Hello ${ownerName.trim()},` : "Hello,",
|
|
1246
|
+
"",
|
|
1247
|
+
`Your vendor store "${storeLabel}" has been created.`,
|
|
1248
|
+
"Open this link to set your password:",
|
|
1249
|
+
inviteLink
|
|
1250
|
+
].join("\n");
|
|
1251
|
+
} else {
|
|
1252
|
+
subject = `Welcome to ${storeLabel}`;
|
|
1253
|
+
const loginUrl = signInLink?.trim() || inviteLink?.trim() || "";
|
|
1254
|
+
bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${greeting}</p>
|
|
1255
|
+
<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">Your vendor store <strong>${escapeHtml2(storeLabel)}</strong> has been created and your owner account is ready. Sign in to the admin dashboard to manage your store.</p>
|
|
1256
|
+
${slugNote}
|
|
1257
|
+
${loginUrl ? primaryCtaButton(loginUrl, "Sign in to admin") : ""}
|
|
1258
|
+
${loginUrl ? `<p style="margin:16px 0 0 0;font-size:12px;line-height:1.5;color:#888;">Sign-in URL:<br/><span style="word-break:break-all;">${escapeHtml2(loginUrl)}</span></p>` : ""}`;
|
|
1259
|
+
text = [
|
|
1260
|
+
ownerName?.trim() ? `Hello ${ownerName.trim()},` : "Hello,",
|
|
1261
|
+
"",
|
|
1262
|
+
`Your vendor store "${storeLabel}" has been created.`,
|
|
1263
|
+
loginUrl ? `Sign in: ${loginUrl}` : ""
|
|
1264
|
+
].filter(Boolean).join("\n");
|
|
1265
|
+
}
|
|
1266
|
+
const html = renderLayout({
|
|
1267
|
+
bodyHtml,
|
|
1268
|
+
companyDetails
|
|
1269
|
+
});
|
|
1270
|
+
return {
|
|
1271
|
+
subject,
|
|
1272
|
+
html,
|
|
1273
|
+
text
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
__name(render11, "render");
|
|
1277
|
+
|
|
1278
|
+
// src/plugins/email/templates/index.ts
|
|
1279
|
+
var templateRenderMap = {
|
|
1280
|
+
signup: render,
|
|
1281
|
+
passwordReset: render2,
|
|
1282
|
+
passwordChange: render3,
|
|
1283
|
+
orderPlaced: render4,
|
|
1284
|
+
returnInitiated: render5,
|
|
1285
|
+
shippingUpdate: render6,
|
|
1286
|
+
invite: render7,
|
|
1287
|
+
formSubmission: render8,
|
|
1288
|
+
otp: render9,
|
|
1289
|
+
chatLead: render10,
|
|
1290
|
+
vendorOnboarded: render11
|
|
1291
|
+
};
|
|
1292
|
+
function getTemplateRenderer(name) {
|
|
1293
|
+
return templateRenderMap[name];
|
|
1294
|
+
}
|
|
1295
|
+
__name(getTemplateRenderer, "getTemplateRenderer");
|
|
1296
|
+
|
|
1297
|
+
// src/plugins/email/renderer.ts
|
|
1298
|
+
function renderEmail(templateName, ctx, options) {
|
|
1299
|
+
const layoutFn = options?.renderLayout ?? renderLayout;
|
|
1300
|
+
const getBody = options?.getBody;
|
|
1301
|
+
if (getBody) {
|
|
1302
|
+
const custom = getBody(templateName, ctx);
|
|
1303
|
+
if (custom != null) {
|
|
1304
|
+
const html = layoutFn({
|
|
1305
|
+
bodyHtml: custom.bodyHtml,
|
|
1306
|
+
companyDetails: ctx.companyDetails
|
|
1307
|
+
});
|
|
1308
|
+
return {
|
|
1309
|
+
subject: custom.subject,
|
|
1310
|
+
html,
|
|
1311
|
+
text: custom.text
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const render12 = getTemplateRenderer(templateName);
|
|
1316
|
+
if (!render12) {
|
|
1317
|
+
throw new Error(`Unknown email template: ${templateName}`);
|
|
1318
|
+
}
|
|
1319
|
+
return render12(ctx);
|
|
1320
|
+
}
|
|
1321
|
+
__name(renderEmail, "renderEmail");
|
|
1322
|
+
|
|
1323
|
+
// src/plugins/email/email-service.ts
|
|
1324
|
+
var SES_REGIONS_WITHOUT_SMTP = /* @__PURE__ */ new Set([
|
|
1325
|
+
"af-south-1",
|
|
1326
|
+
"ap-south-2",
|
|
1327
|
+
"ap-southeast-3",
|
|
1328
|
+
"ap-southeast-5",
|
|
1329
|
+
"ca-west-1",
|
|
1330
|
+
"eu-central-2",
|
|
1331
|
+
"eu-south-1",
|
|
1332
|
+
"il-central-1",
|
|
1333
|
+
"me-central-1",
|
|
1334
|
+
"me-south-1"
|
|
1335
|
+
]);
|
|
1336
|
+
function sesSmtpHost(region) {
|
|
1337
|
+
return `email-smtp.${region}.amazonaws.com`;
|
|
1338
|
+
}
|
|
1339
|
+
__name(sesSmtpHost, "sesSmtpHost");
|
|
1340
|
+
function sesRegionSupportsSmtp(region) {
|
|
1341
|
+
return !SES_REGIONS_WITHOUT_SMTP.has(region.trim());
|
|
1342
|
+
}
|
|
1343
|
+
__name(sesRegionSupportsSmtp, "sesRegionSupportsSmtp");
|
|
1344
|
+
var EmailService = class {
|
|
1345
|
+
static {
|
|
1346
|
+
__name(this, "EmailService");
|
|
1347
|
+
}
|
|
1348
|
+
config;
|
|
1349
|
+
templateOptions;
|
|
1350
|
+
sesClient;
|
|
1351
|
+
transporter;
|
|
1352
|
+
/** AWS via SMTP (nodemailer) vs SES API SDK. */
|
|
1353
|
+
awsViaSmtp = false;
|
|
1354
|
+
constructor(config) {
|
|
1355
|
+
this.config = config;
|
|
1356
|
+
this.templateOptions = config.templateOptions;
|
|
1357
|
+
if (config.type === "AWS") {
|
|
1358
|
+
if (!config.region) {
|
|
1359
|
+
throw new Error("AWS SES configuration incomplete: AWS_REGION is required");
|
|
1360
|
+
}
|
|
1361
|
+
if (config.user && config.password) {
|
|
1362
|
+
this.awsViaSmtp = true;
|
|
1363
|
+
const host = config.host || sesSmtpHost(config.region);
|
|
1364
|
+
const port = config.port ?? 587;
|
|
1365
|
+
const secure = config.secure ?? false;
|
|
1366
|
+
this.transporter = nodemailer.createTransport({
|
|
1367
|
+
host,
|
|
1368
|
+
port,
|
|
1369
|
+
secure,
|
|
1370
|
+
auth: {
|
|
1371
|
+
user: config.user,
|
|
1372
|
+
pass: config.password
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
} else if (config.accessKeyId && config.secretAccessKey) {
|
|
1376
|
+
this.sesClient = new SESClient({
|
|
1377
|
+
region: config.region,
|
|
1378
|
+
credentials: {
|
|
1379
|
+
accessKeyId: config.accessKeyId,
|
|
1380
|
+
secretAccessKey: config.secretAccessKey
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
} else {
|
|
1384
|
+
throw new Error("AWS SES configuration incomplete: set SMTP_USER and SMTP_PASSWORD (SES SMTP) or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (SES API)");
|
|
1385
|
+
}
|
|
1386
|
+
} else if (config.type === "SMTP" || config.type === "GMAIL") {
|
|
1387
|
+
if (!config.user || !config.password) throw new Error("SMTP configuration incomplete");
|
|
1388
|
+
const host = config.type === "GMAIL" ? "smtp.gmail.com" : config.host;
|
|
1389
|
+
if (!host) {
|
|
1390
|
+
throw new Error("SMTP configuration incomplete: SMTP_HOST is required when SMTP_TYPE=SMTP (nodemailer otherwise connects to 127.0.0.1)");
|
|
1391
|
+
}
|
|
1392
|
+
const port = config.port ?? 587;
|
|
1393
|
+
const secure = config.secure ?? false;
|
|
1394
|
+
this.transporter = nodemailer.createTransport({
|
|
1395
|
+
host,
|
|
1396
|
+
port,
|
|
1397
|
+
secure,
|
|
1398
|
+
auth: {
|
|
1399
|
+
user: config.user,
|
|
1400
|
+
pass: config.password
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
} else {
|
|
1404
|
+
throw new Error(`Unsupported email type: ${config.type}`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
async send(emailData) {
|
|
1408
|
+
try {
|
|
1409
|
+
if (this.config.type === "AWS" && this.sesClient) {
|
|
1410
|
+
await this.sesClient.send(new SendEmailCommand({
|
|
1411
|
+
Source: emailData.from || this.config.from,
|
|
1412
|
+
Destination: {
|
|
1413
|
+
ToAddresses: [
|
|
1414
|
+
emailData.to || this.config.to
|
|
1415
|
+
]
|
|
1416
|
+
},
|
|
1417
|
+
Message: {
|
|
1418
|
+
Subject: {
|
|
1419
|
+
Data: emailData.subject,
|
|
1420
|
+
Charset: "UTF-8"
|
|
1421
|
+
},
|
|
1422
|
+
Body: {
|
|
1423
|
+
Html: {
|
|
1424
|
+
Data: emailData.html,
|
|
1425
|
+
Charset: "UTF-8"
|
|
1426
|
+
},
|
|
1427
|
+
...emailData.text && {
|
|
1428
|
+
Text: {
|
|
1429
|
+
Data: emailData.text,
|
|
1430
|
+
Charset: "UTF-8"
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}));
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1438
|
+
if (this.transporter && (this.config.type === "SMTP" || this.config.type === "GMAIL" || this.config.type === "AWS" && this.awsViaSmtp)) {
|
|
1439
|
+
await this.transporter.sendMail({
|
|
1440
|
+
from: emailData.from || this.config.from,
|
|
1441
|
+
to: emailData.to || this.config.to,
|
|
1442
|
+
subject: emailData.subject,
|
|
1443
|
+
html: emailData.html,
|
|
1444
|
+
text: emailData.text
|
|
1445
|
+
});
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
return false;
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
console.error("Email sending failed:", error);
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
renderTemplate(templateName, ctx) {
|
|
1455
|
+
return renderEmail(templateName, ctx, this.templateOptions);
|
|
1456
|
+
}
|
|
1457
|
+
/** Default notification recipient from plugin config (SMTP_TO / plugin `to`). */
|
|
1458
|
+
getDefaultTo() {
|
|
1459
|
+
return this.config.to;
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
var emailTemplates = {
|
|
1463
|
+
formSubmission: /* @__PURE__ */ __name((data) => ({
|
|
1464
|
+
subject: `New Form Submission: ${data.formName}`,
|
|
1465
|
+
html: `<h2>New Form Submission</h2><p><strong>Form:</strong> ${data.formName}</p><p><strong>Contact:</strong> ${data.contactName} (${data.contactEmail})</p><pre>${JSON.stringify(data.formData, null, 2)}</pre>`,
|
|
1466
|
+
text: `New Form Submission
|
|
1467
|
+
Form: ${data.formName}
|
|
1468
|
+
Contact: ${data.contactName} (${data.contactEmail})
|
|
1469
|
+
${JSON.stringify(data.formData, null, 2)}`
|
|
1470
|
+
}), "formSubmission"),
|
|
1471
|
+
contactSubmission: /* @__PURE__ */ __name((data) => ({
|
|
1472
|
+
subject: `New Contact Form Submission from ${data.name}`,
|
|
1473
|
+
html: `<h2>New Contact Form Submission</h2><p><strong>Name:</strong> ${data.name}</p><p><strong>Email:</strong> ${data.email}</p>${data.phone ? `<p><strong>Phone:</strong> ${data.phone}</p>` : ""}${data.message ? `<p><strong>Message:</strong></p><p>${data.message}</p>` : ""}`,
|
|
1474
|
+
text: `New Contact Form Submission
|
|
1475
|
+
Name: ${data.name}
|
|
1476
|
+
Email: ${data.email}
|
|
1477
|
+
${data.phone ? `Phone: ${data.phone}
|
|
1478
|
+
` : ""}${data.message ? `Message: ${data.message}` : ""}`
|
|
1479
|
+
}), "contactSubmission"),
|
|
1480
|
+
passwordReset: /* @__PURE__ */ __name((data) => ({
|
|
1481
|
+
subject: "Reset your password",
|
|
1482
|
+
html: `<h2>Reset your password</h2><p>Click the link below to set a new password. This link expires in 1 hour.</p><p><a href="${data.resetLink}">${data.resetLink}</a></p>`,
|
|
1483
|
+
text: `Reset your password: ${data.resetLink}
|
|
1484
|
+
|
|
1485
|
+
This link expires in 1 hour.`
|
|
1486
|
+
}), "passwordReset")
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
// src/lib/email-recipients.ts
|
|
1490
|
+
function parseEmailRecipientsFromConfig(raw) {
|
|
1491
|
+
if (raw == null || raw === "") return [];
|
|
1492
|
+
const trimmed = raw.trim();
|
|
1493
|
+
if (trimmed.startsWith("[")) {
|
|
1494
|
+
try {
|
|
1495
|
+
const parsed = JSON.parse(trimmed);
|
|
1496
|
+
if (Array.isArray(parsed)) {
|
|
1497
|
+
return parsed.map((e) => String(e).trim()).filter(Boolean);
|
|
1498
|
+
}
|
|
1499
|
+
} catch {
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return trimmed.split(/[,;]+/).map((s) => s.trim()).filter(Boolean);
|
|
1503
|
+
}
|
|
1504
|
+
__name(parseEmailRecipientsFromConfig, "parseEmailRecipientsFromConfig");
|
|
1505
|
+
function serializeEmailRecipients(emails) {
|
|
1506
|
+
return JSON.stringify(emails);
|
|
1507
|
+
}
|
|
1508
|
+
__name(serializeEmailRecipients, "serializeEmailRecipients");
|
|
1509
|
+
function joinRecipientsForSend(emails) {
|
|
1510
|
+
if (!emails.length) return null;
|
|
1511
|
+
return emails.join(", ");
|
|
1512
|
+
}
|
|
1513
|
+
__name(joinRecipientsForSend, "joinRecipientsForSend");
|
|
1514
|
+
|
|
1515
|
+
// src/plugins/llm/chat-email-intent.ts
|
|
1516
|
+
var NONE_INTENT = "NONE";
|
|
1517
|
+
var CHAT_EMAIL_LOG = "[chat-email-tool]";
|
|
1518
|
+
function logChatEmail(step, data) {
|
|
1519
|
+
if (data && Object.keys(data).length > 0) {
|
|
1520
|
+
console.info(CHAT_EMAIL_LOG, step, data);
|
|
1521
|
+
} else {
|
|
1522
|
+
console.info(CHAT_EMAIL_LOG, step);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
__name(logChatEmail, "logChatEmail");
|
|
1526
|
+
function normalizeIntentKey(raw) {
|
|
1527
|
+
return raw.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1528
|
+
}
|
|
1529
|
+
__name(normalizeIntentKey, "normalizeIntentKey");
|
|
1530
|
+
function parseIntentsJson(raw) {
|
|
1531
|
+
if (!raw?.trim()) return null;
|
|
1532
|
+
try {
|
|
1533
|
+
const parsed = JSON.parse(raw);
|
|
1534
|
+
if (!Array.isArray(parsed)) return null;
|
|
1535
|
+
const out = [];
|
|
1536
|
+
for (const row of parsed) {
|
|
1537
|
+
if (!row || typeof row !== "object") continue;
|
|
1538
|
+
const o = row;
|
|
1539
|
+
const intent = normalizeIntentKey(String(o.intent ?? o.id ?? ""));
|
|
1540
|
+
const description = String(o.description ?? "").trim();
|
|
1541
|
+
const emailTo = String(o.emailTo ?? o.email ?? "").trim();
|
|
1542
|
+
if (!intent || !description || !emailTo) continue;
|
|
1543
|
+
out.push({
|
|
1544
|
+
intent,
|
|
1545
|
+
description,
|
|
1546
|
+
emailTo
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
return out.length > 0 ? out : null;
|
|
1550
|
+
} catch {
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
__name(parseIntentsJson, "parseIntentsJson");
|
|
1555
|
+
function dedupeIntents(intents) {
|
|
1556
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1557
|
+
const out = [];
|
|
1558
|
+
for (const row of intents) {
|
|
1559
|
+
const key = normalizeIntentKey(row.intent);
|
|
1560
|
+
if (!key || seen.has(key)) continue;
|
|
1561
|
+
seen.add(key);
|
|
1562
|
+
out.push({
|
|
1563
|
+
intent: key,
|
|
1564
|
+
description: row.description.trim(),
|
|
1565
|
+
emailTo: row.emailTo.trim()
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
return out;
|
|
1569
|
+
}
|
|
1570
|
+
__name(dedupeIntents, "dedupeIntents");
|
|
1571
|
+
function parseChatEmailToolSettings(map) {
|
|
1572
|
+
const fromJson = parseIntentsJson(map.emailIntents);
|
|
1573
|
+
const intents = dedupeIntents(fromJson ?? []);
|
|
1574
|
+
const legacyPositive = map.emailIntentPrompt?.trim() ?? "";
|
|
1575
|
+
const legacyNegative = map.emailNegativeIntentPrompt?.trim() ?? "";
|
|
1576
|
+
let classifierInstructions = map.emailClassifierInstructions?.trim() ?? "";
|
|
1577
|
+
if (!classifierInstructions && (legacyPositive || legacyNegative)) {
|
|
1578
|
+
const parts = [];
|
|
1579
|
+
if (legacyPositive) parts.push(`Legacy positive signals:
|
|
1580
|
+
${legacyPositive}`);
|
|
1581
|
+
if (legacyNegative) parts.push(`Do NOT assign an intent when:
|
|
1582
|
+
${legacyNegative}`);
|
|
1583
|
+
classifierInstructions = parts.join("\n\n");
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
enabled: map.emailToolEnabled === "true",
|
|
1587
|
+
intents,
|
|
1588
|
+
classifierInstructions,
|
|
1589
|
+
toolPrompt: map.emailToolPrompt ?? ""
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
__name(parseChatEmailToolSettings, "parseChatEmailToolSettings");
|
|
1593
|
+
var EMAIL_TOOL_VALIDATION_KEY = "emailTool";
|
|
1594
|
+
function parseEmailToolObject(raw) {
|
|
1595
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
1596
|
+
const o = raw;
|
|
1597
|
+
const intentsRaw = o.intents;
|
|
1598
|
+
let intents;
|
|
1599
|
+
if (Array.isArray(intentsRaw)) {
|
|
1600
|
+
const parsed = parseIntentsJson(JSON.stringify(intentsRaw));
|
|
1601
|
+
if (parsed?.length) intents = parsed;
|
|
1602
|
+
}
|
|
1603
|
+
return {
|
|
1604
|
+
enabled: o.enabled === true,
|
|
1605
|
+
classifierInstructions: typeof o.classifierInstructions === "string" ? o.classifierInstructions.trim() : void 0,
|
|
1606
|
+
toolPrompt: typeof o.toolPrompt === "string" ? o.toolPrompt.trim() : void 0,
|
|
1607
|
+
intents
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
__name(parseEmailToolObject, "parseEmailToolObject");
|
|
1611
|
+
function parseEmailToolFromAgentValidationRules(validationRulesText) {
|
|
1612
|
+
const raw = validationRulesText?.trim();
|
|
1613
|
+
if (!raw) return null;
|
|
1614
|
+
try {
|
|
1615
|
+
const parsed = JSON.parse(raw);
|
|
1616
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
1617
|
+
const emailTool = parsed[EMAIL_TOOL_VALIDATION_KEY];
|
|
1618
|
+
return parseEmailToolObject(emailTool);
|
|
1619
|
+
} catch {
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
__name(parseEmailToolFromAgentValidationRules, "parseEmailToolFromAgentValidationRules");
|
|
1624
|
+
function resolveChatEmailToolSettings(configMap, sources) {
|
|
1625
|
+
const fromConfig = parseChatEmailToolSettings(configMap);
|
|
1626
|
+
const resolved = typeof sources === "string" || sources == null ? {
|
|
1627
|
+
chatbotValidationRules: typeof sources === "string" ? sources : null
|
|
1628
|
+
} : sources;
|
|
1629
|
+
const fromChatbot = parseEmailToolFromAgentValidationRules(resolved.chatbotValidationRules);
|
|
1630
|
+
const fromNotify = resolved.notifyAgent ? parseEmailToolFromAgentValidationRules(resolved.notifyAgent.validationRules) : null;
|
|
1631
|
+
const notifySystem = resolved.notifyAgent?.systemInstruction?.trim() ?? "";
|
|
1632
|
+
const intents = fromNotify?.intents?.length ? fromNotify.intents : fromChatbot?.intents?.length ? fromChatbot.intents : fromConfig.intents;
|
|
1633
|
+
const classifierParts = [
|
|
1634
|
+
notifySystem,
|
|
1635
|
+
fromNotify?.classifierInstructions?.trim(),
|
|
1636
|
+
fromChatbot?.classifierInstructions?.trim(),
|
|
1637
|
+
fromConfig.classifierInstructions.trim()
|
|
1638
|
+
].filter(Boolean);
|
|
1639
|
+
const toolPrompt = fromNotify?.toolPrompt?.trim() || fromChatbot?.toolPrompt?.trim() || fromConfig.toolPrompt?.trim() || "";
|
|
1640
|
+
return {
|
|
1641
|
+
enabled: fromConfig.enabled,
|
|
1642
|
+
intents,
|
|
1643
|
+
classifierInstructions: classifierParts.join("\n\n"),
|
|
1644
|
+
toolPrompt
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
__name(resolveChatEmailToolSettings, "resolveChatEmailToolSettings");
|
|
1648
|
+
function intentByKey(intents, key) {
|
|
1649
|
+
if (!key) return null;
|
|
1650
|
+
const norm2 = normalizeIntentKey(key);
|
|
1651
|
+
return intents.find((i) => i.intent === norm2) ?? null;
|
|
1652
|
+
}
|
|
1653
|
+
__name(intentByKey, "intentByKey");
|
|
1654
|
+
function buildIntentTableForPrompt(intents) {
|
|
1655
|
+
return intents.map((i) => `- ${i.intent}: ${i.description} \u2192 notify ${i.emailTo}`).join("\n");
|
|
1656
|
+
}
|
|
1657
|
+
__name(buildIntentTableForPrompt, "buildIntentTableForPrompt");
|
|
1658
|
+
function buildIntentClassifierSystem(config, agentClassifierInstructions) {
|
|
1659
|
+
const table = buildIntentTableForPrompt(config.intents);
|
|
1660
|
+
const extra = [
|
|
1661
|
+
agentClassifierInstructions?.trim(),
|
|
1662
|
+
config.classifierInstructions.trim()
|
|
1663
|
+
].filter(Boolean).join("\n\n");
|
|
1664
|
+
const intentKeys = config.intents.map((i) => i.intent).join(", ");
|
|
1665
|
+
return `You classify a chat visitor into exactly one lead intent for email routing.
|
|
1666
|
+
|
|
1667
|
+
Available intents (pick the best match):
|
|
1668
|
+
${table}
|
|
1669
|
+
|
|
1670
|
+
If the visitor is not ready for a lead email (general FAQ, greetings only, spam, off-topic, no clear business need), use intent "${NONE_INTENT}".
|
|
1671
|
+
|
|
1672
|
+
Allowed intent values: ${intentKeys}, or ${NONE_INTENT}.
|
|
1673
|
+
|
|
1674
|
+
${extra ? `Additional instructions:
|
|
1675
|
+
${extra}
|
|
1676
|
+
` : ""}
|
|
1677
|
+
Reply with ONLY valid JSON, no markdown:
|
|
1678
|
+
{"intent":"INTENT_KEY","reason":"short explanation"}
|
|
1679
|
+
Use "${NONE_INTENT}" when no intent applies.`;
|
|
1680
|
+
}
|
|
1681
|
+
__name(buildIntentClassifierSystem, "buildIntentClassifierSystem");
|
|
1682
|
+
function buildChatEmailAgentContext(settings) {
|
|
1683
|
+
if (!settings.enabled) return "";
|
|
1684
|
+
const classifier = settings.classifierInstructions.trim();
|
|
1685
|
+
const extra = settings.toolPrompt?.trim() ?? "";
|
|
1686
|
+
const parts = [];
|
|
1687
|
+
if (settings.intents.length > 0) {
|
|
1688
|
+
parts.push("## Lead email routing", "When a visitor message clearly matches an intent below, the system sends one team notification email (per conversation). Continue the conversation normally in all cases.");
|
|
1689
|
+
if (classifier) parts.push(`### Routing rules
|
|
1690
|
+
${classifier}`);
|
|
1691
|
+
parts.push(`### Intent catalog
|
|
1692
|
+
${buildIntentTableForPrompt(settings.intents)}`);
|
|
1693
|
+
} else if (classifier) {
|
|
1694
|
+
parts.push(`## Lead email routing
|
|
1695
|
+
${classifier}`);
|
|
1696
|
+
}
|
|
1697
|
+
if (extra) parts.push(`### Assistant behavior
|
|
1698
|
+
${extra}`);
|
|
1699
|
+
return parts.join("\n\n");
|
|
1700
|
+
}
|
|
1701
|
+
__name(buildChatEmailAgentContext, "buildChatEmailAgentContext");
|
|
1702
|
+
function buildChatbotSystemPromptWithEmailTool(baseSystemInstruction, guardrailsForPrompt, emailSettings) {
|
|
1703
|
+
let prompt = (baseSystemInstruction ?? "").trim();
|
|
1704
|
+
const guard = guardrailsForPrompt?.trim();
|
|
1705
|
+
if (guard) prompt = [
|
|
1706
|
+
prompt,
|
|
1707
|
+
guard
|
|
1708
|
+
].filter(Boolean).join("\n\n");
|
|
1709
|
+
if (emailSettings.enabled) {
|
|
1710
|
+
const emailCtx = buildChatEmailAgentContext(emailSettings);
|
|
1711
|
+
if (emailCtx) prompt = [
|
|
1712
|
+
prompt,
|
|
1713
|
+
emailCtx
|
|
1714
|
+
].filter(Boolean).join("\n\n");
|
|
1715
|
+
}
|
|
1716
|
+
return prompt;
|
|
1717
|
+
}
|
|
1718
|
+
__name(buildChatbotSystemPromptWithEmailTool, "buildChatbotSystemPromptWithEmailTool");
|
|
1719
|
+
function formatTranscript(history, latestMessage, maxChars = 4e3) {
|
|
1720
|
+
const lines = [];
|
|
1721
|
+
for (const m of history) {
|
|
1722
|
+
const role = m.role === "assistant" ? "Assistant" : m.role === "user" ? "Visitor" : "System";
|
|
1723
|
+
lines.push(`${role}: ${m.content}`);
|
|
1724
|
+
}
|
|
1725
|
+
const last = history[history.length - 1];
|
|
1726
|
+
if (!last || last.role !== "user" || last.content !== latestMessage) {
|
|
1727
|
+
lines.push(`Visitor: ${latestMessage}`);
|
|
1728
|
+
}
|
|
1729
|
+
let text = lines.join("\n");
|
|
1730
|
+
if (text.length > maxChars) text = text.slice(-maxChars);
|
|
1731
|
+
return text;
|
|
1732
|
+
}
|
|
1733
|
+
__name(formatTranscript, "formatTranscript");
|
|
1734
|
+
function parseIntentJson(raw, intents) {
|
|
1735
|
+
const trimmed = raw.trim();
|
|
1736
|
+
const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1737
|
+
const candidate = (fence?.[1] ?? trimmed).trim();
|
|
1738
|
+
const tryParse = /* @__PURE__ */ __name((s) => {
|
|
1739
|
+
const o = JSON.parse(s);
|
|
1740
|
+
let intentRaw = null;
|
|
1741
|
+
if (typeof o.intent === "string") intentRaw = o.intent;
|
|
1742
|
+
else if (o.interested === false) intentRaw = NONE_INTENT;
|
|
1743
|
+
else if (o.interested === true && intents[0]) intentRaw = intents[0].intent;
|
|
1744
|
+
if (intentRaw == null) return null;
|
|
1745
|
+
const reason = typeof o.reason === "string" && o.reason.trim() ? o.reason.trim() : "Classified from conversation";
|
|
1746
|
+
return {
|
|
1747
|
+
intent: normalizeIntentKey(intentRaw),
|
|
1748
|
+
reason
|
|
1749
|
+
};
|
|
1750
|
+
}, "tryParse");
|
|
1751
|
+
try {
|
|
1752
|
+
return tryParse(candidate);
|
|
1753
|
+
} catch {
|
|
1754
|
+
const m = candidate.match(/\{[\s\S]*\}/);
|
|
1755
|
+
if (!m) return null;
|
|
1756
|
+
try {
|
|
1757
|
+
return tryParse(m[0]);
|
|
1758
|
+
} catch {
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
__name(parseIntentJson, "parseIntentJson");
|
|
1764
|
+
async function detectChatLeadIntent(llm, params) {
|
|
1765
|
+
if (!params.settings.intents.length) {
|
|
1766
|
+
logChatEmail("classify skipped", {
|
|
1767
|
+
reason: "no_intents_configured"
|
|
1768
|
+
});
|
|
1769
|
+
return {
|
|
1770
|
+
intent: null,
|
|
1771
|
+
intentLabel: "NONE",
|
|
1772
|
+
reason: "No lead intents configured",
|
|
1773
|
+
emailTo: null
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
logChatEmail("classify start", {
|
|
1777
|
+
intentCount: params.settings.intents.length,
|
|
1778
|
+
intentKeys: params.settings.intents.map((i) => i.intent),
|
|
1779
|
+
model: params.model?.trim() || "(gateway default)",
|
|
1780
|
+
messageChars: params.message.length,
|
|
1781
|
+
historyTurns: params.history.length,
|
|
1782
|
+
hasClassifierInstructions: Boolean(params.agentClassifierInstructions?.trim() || params.settings.classifierInstructions.trim())
|
|
1783
|
+
});
|
|
1784
|
+
const transcript = formatTranscript(params.history, params.message);
|
|
1785
|
+
const contactLines = [
|
|
1786
|
+
params.contact.name?.trim() ? `Name: ${params.contact.name.trim()}` : null,
|
|
1787
|
+
params.contact.email?.trim() ? `Email: ${params.contact.email.trim()}` : null,
|
|
1788
|
+
params.contact.phone?.trim() ? `Phone: ${params.contact.phone.trim()}` : null
|
|
1789
|
+
].filter(Boolean).join("\n");
|
|
1790
|
+
const userPrompt = `Customer:
|
|
1791
|
+
${contactLines || "(unknown)"}
|
|
1792
|
+
|
|
1793
|
+
Conversation:
|
|
1794
|
+
${transcript}
|
|
1795
|
+
|
|
1796
|
+
Pick the single best intent for the visitor's latest message.`;
|
|
1797
|
+
const res = await llm.chatAgent({
|
|
1798
|
+
systemPrompt: buildIntentClassifierSystem(params.settings, params.agentClassifierInstructions),
|
|
1799
|
+
userPrompt,
|
|
1800
|
+
temperature: 0.1,
|
|
1801
|
+
max_tokens: 256,
|
|
1802
|
+
...params.model?.trim() ? {
|
|
1803
|
+
model: params.model.trim()
|
|
1804
|
+
} : {}
|
|
1805
|
+
});
|
|
1806
|
+
const rawContent = res.content ?? "";
|
|
1807
|
+
logChatEmail("classify llm response", {
|
|
1808
|
+
responseChars: rawContent.length,
|
|
1809
|
+
responsePreview: rawContent.slice(0, 280) + (rawContent.length > 280 ? "\u2026" : "")
|
|
1810
|
+
});
|
|
1811
|
+
const parsed = parseIntentJson(rawContent, params.settings.intents);
|
|
1812
|
+
if (!parsed) {
|
|
1813
|
+
logChatEmail("classify result", {
|
|
1814
|
+
matched: false,
|
|
1815
|
+
outcome: "parse_failed",
|
|
1816
|
+
emailWillSend: false
|
|
1817
|
+
});
|
|
1818
|
+
return {
|
|
1819
|
+
intent: null,
|
|
1820
|
+
intentLabel: "Unclassified",
|
|
1821
|
+
reason: "Could not parse intent classifier response",
|
|
1822
|
+
emailTo: null
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
if (parsed.intent === NONE_INTENT) {
|
|
1826
|
+
logChatEmail("classify result", {
|
|
1827
|
+
matched: false,
|
|
1828
|
+
outcome: NONE_INTENT,
|
|
1829
|
+
reason: parsed.reason,
|
|
1830
|
+
emailWillSend: false
|
|
1831
|
+
});
|
|
1832
|
+
return {
|
|
1833
|
+
intent: null,
|
|
1834
|
+
intentLabel: NONE_INTENT,
|
|
1835
|
+
reason: parsed.reason,
|
|
1836
|
+
emailTo: null
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
const matched = intentByKey(params.settings.intents, parsed.intent);
|
|
1840
|
+
if (!matched) {
|
|
1841
|
+
logChatEmail("classify result", {
|
|
1842
|
+
matched: false,
|
|
1843
|
+
outcome: "unknown_intent",
|
|
1844
|
+
parsedIntent: parsed.intent,
|
|
1845
|
+
reason: parsed.reason,
|
|
1846
|
+
emailWillSend: false
|
|
1847
|
+
});
|
|
1848
|
+
return {
|
|
1849
|
+
intent: null,
|
|
1850
|
+
intentLabel: parsed.intent,
|
|
1851
|
+
reason: `Unknown intent "${parsed.intent}": ${parsed.reason}`,
|
|
1852
|
+
emailTo: null
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
logChatEmail("classify result", {
|
|
1856
|
+
matched: true,
|
|
1857
|
+
outcome: "intent_found",
|
|
1858
|
+
intent: matched.intent,
|
|
1859
|
+
emailTo: matched.emailTo,
|
|
1860
|
+
reason: parsed.reason,
|
|
1861
|
+
emailWillSend: true
|
|
1862
|
+
});
|
|
1863
|
+
return {
|
|
1864
|
+
intent: matched.intent,
|
|
1865
|
+
intentLabel: `${matched.intent} \u2014 ${matched.description}`,
|
|
1866
|
+
reason: parsed.reason,
|
|
1867
|
+
emailTo: matched.emailTo
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
__name(detectChatLeadIntent, "detectChatLeadIntent");
|
|
1871
|
+
function buildTranscriptForLeadEmail(history, latestMessage) {
|
|
1872
|
+
return formatTranscript(history, latestMessage);
|
|
1873
|
+
}
|
|
1874
|
+
__name(buildTranscriptForLeadEmail, "buildTranscriptForLeadEmail");
|
|
1875
|
+
function parseIntentRecipientEmails(emailTo) {
|
|
1876
|
+
return emailTo.split(/[,;]+/).map((s) => s.trim()).filter((s) => s.length > 0 && s.includes("@"));
|
|
1877
|
+
}
|
|
1878
|
+
__name(parseIntentRecipientEmails, "parseIntentRecipientEmails");
|
|
1879
|
+
|
|
1880
|
+
// src/plugins/email/chat-lead-email.ts
|
|
1881
|
+
function resolveLeadRecipients(emailPlugin2, emailSettings) {
|
|
1882
|
+
const fromCrm = parseEmailRecipientsFromConfig(emailSettings.crmEmails ?? emailSettings.crmEmail ?? "");
|
|
1883
|
+
if (fromCrm.length > 0) return fromCrm;
|
|
1884
|
+
const fallback = emailPlugin2.getDefaultTo?.() ?? "";
|
|
1885
|
+
if (fallback.trim()) return [
|
|
1886
|
+
fallback.trim()
|
|
1887
|
+
];
|
|
1888
|
+
return [];
|
|
1889
|
+
}
|
|
1890
|
+
__name(resolveLeadRecipients, "resolveLeadRecipients");
|
|
1891
|
+
async function sendChatLeadEmail(cms, emailSettings, brandingSettings, input) {
|
|
1892
|
+
console.info(CHAT_EMAIL_LOG, "send start", {
|
|
1893
|
+
conversationId: input.conversationId,
|
|
1894
|
+
intentCode: input.intentCode ?? null,
|
|
1895
|
+
contactEmail: input.contactEmail,
|
|
1896
|
+
contactName: input.contactName,
|
|
1897
|
+
explicitRecipients: input.recipients?.length ?? 0
|
|
1898
|
+
});
|
|
1899
|
+
const email = cms.getPlugin("email");
|
|
1900
|
+
if (!email?.send || !email.renderTemplate) {
|
|
1901
|
+
console.warn(CHAT_EMAIL_LOG, "send failed", {
|
|
1902
|
+
conversationId: input.conversationId,
|
|
1903
|
+
reason: "email_plugin_disabled"
|
|
1904
|
+
});
|
|
1905
|
+
return {
|
|
1906
|
+
sent: false,
|
|
1907
|
+
recipients: [],
|
|
1908
|
+
error: "Email plugin is not enabled"
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
const recipients = input.recipients?.filter((r) => r.trim().includes("@")).map((r) => r.trim()) ?? resolveLeadRecipients(email, emailSettings);
|
|
1912
|
+
if (recipients.length === 0) {
|
|
1913
|
+
console.warn(CHAT_EMAIL_LOG, "send failed", {
|
|
1914
|
+
conversationId: input.conversationId,
|
|
1915
|
+
reason: "no_recipients"
|
|
1916
|
+
});
|
|
1917
|
+
return {
|
|
1918
|
+
sent: false,
|
|
1919
|
+
recipients: [],
|
|
1920
|
+
error: "No lead email recipients (configure intent Email To, CRM emails, or SMTP default To)"
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
console.info(CHAT_EMAIL_LOG, "send recipients resolved", {
|
|
1924
|
+
conversationId: input.conversationId,
|
|
1925
|
+
recipients,
|
|
1926
|
+
source: input.recipients?.length ? "intent_email_to" : "crm_or_smtp_fallback"
|
|
1927
|
+
});
|
|
1928
|
+
const companyDetails = mergeEmailLayoutCompanyDetails(brandingSettings, emailSettings);
|
|
1929
|
+
const ctx = {
|
|
1930
|
+
...input,
|
|
1931
|
+
companyDetails: input.companyDetails ?? companyDetails
|
|
1932
|
+
};
|
|
1933
|
+
let anySent = false;
|
|
1934
|
+
for (const to of recipients) {
|
|
1935
|
+
await queueEmail(cms, {
|
|
1936
|
+
to,
|
|
1937
|
+
templateName: "chatLead",
|
|
1938
|
+
ctx
|
|
1939
|
+
});
|
|
1940
|
+
console.info(CHAT_EMAIL_LOG, "send queued", {
|
|
1941
|
+
conversationId: input.conversationId,
|
|
1942
|
+
to,
|
|
1943
|
+
template: "chatLead"
|
|
1944
|
+
});
|
|
1945
|
+
anySent = true;
|
|
1946
|
+
}
|
|
1947
|
+
console.info(CHAT_EMAIL_LOG, "send complete", {
|
|
1948
|
+
conversationId: input.conversationId,
|
|
1949
|
+
sent: anySent,
|
|
1950
|
+
recipientCount: recipients.length
|
|
1951
|
+
});
|
|
1952
|
+
return {
|
|
1953
|
+
sent: anySent,
|
|
1954
|
+
recipients
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
__name(sendChatLeadEmail, "sendChatLeadEmail");
|
|
1958
|
+
|
|
1959
|
+
// src/plugins/email/index.ts
|
|
1960
|
+
var EMAIL_TYPES = [
|
|
1961
|
+
"AWS",
|
|
1962
|
+
"SMTP",
|
|
1963
|
+
"GMAIL",
|
|
1964
|
+
"SENDGRID"
|
|
1965
|
+
];
|
|
1966
|
+
function normalizeEmailType(raw, fallback) {
|
|
1967
|
+
const v = String(raw ?? fallback).trim().toUpperCase();
|
|
1968
|
+
return EMAIL_TYPES.includes(v) ? v : fallback;
|
|
1969
|
+
}
|
|
1970
|
+
__name(normalizeEmailType, "normalizeEmailType");
|
|
1971
|
+
function isLocalSmtpHost(host) {
|
|
1972
|
+
if (!host) return false;
|
|
1973
|
+
const h = host.trim().toLowerCase();
|
|
1974
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1";
|
|
1975
|
+
}
|
|
1976
|
+
__name(isLocalSmtpHost, "isLocalSmtpHost");
|
|
1977
|
+
function emailPlugin(config) {
|
|
1978
|
+
return {
|
|
1979
|
+
name: "email",
|
|
1980
|
+
version: "1.0.0",
|
|
1981
|
+
async init(context) {
|
|
1982
|
+
const from = config.from || context.config.SMTP_FROM || "no-reply@example.com";
|
|
1983
|
+
const to = config.to || context.config.SMTP_TO || "info@example.com";
|
|
1984
|
+
const type = normalizeEmailType(context.config.SMTP_TYPE ?? config.type, "SMTP");
|
|
1985
|
+
const portEnv = context.config.SMTP_PORT;
|
|
1986
|
+
const portParsed = portEnv ? parseInt(portEnv, 10) : void 0;
|
|
1987
|
+
const region = config.region ?? context.config.AWS_SES_REGION ?? context.config.AWS_REGION;
|
|
1988
|
+
let host = config.host ?? context.config.SMTP_HOST;
|
|
1989
|
+
if (type === "AWS" && isLocalSmtpHost(host)) {
|
|
1990
|
+
host = void 0;
|
|
1991
|
+
}
|
|
1992
|
+
const merged = {
|
|
1993
|
+
...config,
|
|
1994
|
+
from,
|
|
1995
|
+
to,
|
|
1996
|
+
type,
|
|
1997
|
+
user: config.user ?? context.config.SMTP_USER,
|
|
1998
|
+
password: config.password ?? context.config.SMTP_PASSWORD ?? context.config.SMTP_PASS,
|
|
1999
|
+
host,
|
|
2000
|
+
port: config.port ?? (Number.isFinite(portParsed) ? portParsed : void 0),
|
|
2001
|
+
secure: config.secure ?? context.config.SMTP_SECURE === "true",
|
|
2002
|
+
region,
|
|
2003
|
+
accessKeyId: config.accessKeyId ?? context.config.AWS_ACCESS_KEY_ID,
|
|
2004
|
+
secretAccessKey: config.secretAccessKey ?? context.config.AWS_SECRET_ACCESS_KEY
|
|
2005
|
+
};
|
|
2006
|
+
if (type === "AWS" && !merged.region) {
|
|
2007
|
+
context.logger.warn("Email plugin skipped: AWS SES requires AWS_SES_REGION or AWS_REGION");
|
|
2008
|
+
return void 0;
|
|
2009
|
+
}
|
|
2010
|
+
if (type === "AWS" && merged.region && merged.user && merged.password && !sesRegionSupportsSmtp(merged.region)) {
|
|
2011
|
+
context.logger.warn(`Email plugin: region ${merged.region} has no SES SMTP endpoint (ENOTFOUND). Set AWS_SES_REGION=ap-south-1 and create SMTP credentials in that SES region.`);
|
|
2012
|
+
}
|
|
2013
|
+
if (type === "AWS" && !(merged.user && merged.password || merged.accessKeyId && merged.secretAccessKey)) {
|
|
2014
|
+
context.logger.warn("Email plugin skipped: AWS SES requires SMTP_USER and SMTP_PASSWORD (SES SMTP) or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (SES API)");
|
|
2015
|
+
return void 0;
|
|
2016
|
+
}
|
|
2017
|
+
if ((type === "SMTP" || type === "GMAIL") && (!merged.user || !merged.password)) {
|
|
2018
|
+
context.logger.warn("Email plugin skipped: SMTP credentials not configured");
|
|
2019
|
+
return void 0;
|
|
2020
|
+
}
|
|
2021
|
+
if (type === "SMTP" && !merged.host) {
|
|
2022
|
+
context.logger.warn("Email plugin skipped: SMTP_TYPE=SMTP requires SMTP_HOST (or use SMTP_TYPE=AWS with AWS_REGION and SES SMTP credentials)");
|
|
2023
|
+
return void 0;
|
|
2024
|
+
}
|
|
2025
|
+
const resolvedHost = type === "AWS" && merged.region ? merged.host || sesSmtpHost(merged.region) : merged.host;
|
|
2026
|
+
context.logger.info("Email plugin initialized", {
|
|
2027
|
+
type,
|
|
2028
|
+
host: resolvedHost,
|
|
2029
|
+
region: merged.region
|
|
2030
|
+
});
|
|
2031
|
+
return new EmailService(merged);
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
__name(emailPlugin, "emailPlugin");
|
|
2036
|
+
|
|
2037
|
+
// src/lib/whatsapp-trigger-dispatcher.ts
|
|
2038
|
+
var initialized = false;
|
|
2039
|
+
function maskPhone3(raw) {
|
|
2040
|
+
const digits = String(raw ?? "").replace(/\D/g, "");
|
|
2041
|
+
if (digits.length <= 4) return "(none)";
|
|
2042
|
+
return `***${digits.slice(-4)}`;
|
|
2043
|
+
}
|
|
2044
|
+
__name(maskPhone3, "maskPhone");
|
|
2045
|
+
async function getSettingsGroup(dataSource, entityMap, group) {
|
|
2046
|
+
if (!entityMap.configs) return {};
|
|
2047
|
+
const rows = await dataSource.getRepository(entityMap.configs).find({
|
|
2048
|
+
where: {
|
|
2049
|
+
settings: group,
|
|
2050
|
+
deleted: false
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
return Object.fromEntries(rows.map((r) => [
|
|
2054
|
+
r.key,
|
|
2055
|
+
r.value
|
|
2056
|
+
]));
|
|
2057
|
+
}
|
|
2058
|
+
__name(getSettingsGroup, "getSettingsGroup");
|
|
2059
|
+
async function isOrderNotificationsEnabled(dataSource, entityMap) {
|
|
2060
|
+
const settings = await getSettingsGroup(dataSource, entityMap, "event_notifications");
|
|
2061
|
+
return settings.enabled !== "false" && settings.enabled !== "0";
|
|
2062
|
+
}
|
|
2063
|
+
__name(isOrderNotificationsEnabled, "isOrderNotificationsEnabled");
|
|
2064
|
+
async function isTriggerCatalogEnabled(triggerKey, dataSource, entityMap) {
|
|
2065
|
+
if (!entityMap.order_notification_triggers) {
|
|
2066
|
+
console.warn("[order-notifications] order_notification_triggers missing \u2014 run migration 1781230000000-AddOrderNotificationTriggers");
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
try {
|
|
2070
|
+
const row = await dataSource.getRepository(entityMap.order_notification_triggers).findOne({
|
|
2071
|
+
where: {
|
|
2072
|
+
triggerKey,
|
|
2073
|
+
enabled: true
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
if (!row) {
|
|
2077
|
+
console.warn("[order-notifications] trigger disabled or not in DB", {
|
|
2078
|
+
triggerKey
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
return Boolean(row);
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
console.error("[order-notifications] failed to load trigger catalog", {
|
|
2084
|
+
triggerKey,
|
|
2085
|
+
err
|
|
2086
|
+
});
|
|
2087
|
+
return false;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
__name(isTriggerCatalogEnabled, "isTriggerCatalogEnabled");
|
|
2091
|
+
async function loadBoundWhatsAppTemplate(triggerKey, dataSource, entityMap) {
|
|
2092
|
+
if (!entityMap.whatsapp_trigger_bindings || !entityMap.message_templates) {
|
|
2093
|
+
console.warn("[order-notifications] bindings or message_templates missing from entityMap");
|
|
2094
|
+
return null;
|
|
2095
|
+
}
|
|
2096
|
+
const binding = await dataSource.getRepository(entityMap.whatsapp_trigger_bindings).findOne({
|
|
2097
|
+
where: {
|
|
2098
|
+
triggerKey,
|
|
2099
|
+
enabled: true
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
if (!binding) {
|
|
2103
|
+
console.warn("[order-notifications] no whatsapp_trigger_bindings row", {
|
|
2104
|
+
triggerKey
|
|
2105
|
+
});
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
const templateId = binding.messageTemplateId;
|
|
2109
|
+
if (templateId == null) {
|
|
2110
|
+
console.warn("[order-notifications] binding exists but no template selected", {
|
|
2111
|
+
triggerKey
|
|
2112
|
+
});
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
const template = await dataSource.getRepository(entityMap.message_templates).findOne({
|
|
2116
|
+
where: {
|
|
2117
|
+
id: templateId,
|
|
2118
|
+
channel: "whatsapp",
|
|
2119
|
+
deleted: false,
|
|
2120
|
+
enabled: true
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
if (!template) {
|
|
2124
|
+
console.warn("[order-notifications] bound WhatsApp template not found or disabled", {
|
|
2125
|
+
triggerKey,
|
|
2126
|
+
templateId
|
|
2127
|
+
});
|
|
2128
|
+
return null;
|
|
2129
|
+
}
|
|
2130
|
+
return template;
|
|
2131
|
+
}
|
|
2132
|
+
__name(loadBoundWhatsAppTemplate, "loadBoundWhatsAppTemplate");
|
|
2133
|
+
function mergeTriggerVariables(payload, rich) {
|
|
2134
|
+
const basic = {
|
|
2135
|
+
orderId: String(payload.orderId ?? ""),
|
|
2136
|
+
orderNumber: payload.orderNumber,
|
|
2137
|
+
customerName: payload.customerName,
|
|
2138
|
+
customerPhone: payload.customerPhone ?? "",
|
|
2139
|
+
total: String(payload.total),
|
|
2140
|
+
orderTotal: String(payload.total),
|
|
2141
|
+
currency: payload.currency,
|
|
2142
|
+
vendorId: String(payload.vendorId ?? ""),
|
|
2143
|
+
...payload.refundAmount != null ? {
|
|
2144
|
+
refundAmount: String(payload.refundAmount)
|
|
2145
|
+
} : {}
|
|
2146
|
+
};
|
|
2147
|
+
return {
|
|
2148
|
+
...basic,
|
|
2149
|
+
...rich ?? {}
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
__name(mergeTriggerVariables, "mergeTriggerVariables");
|
|
2153
|
+
async function sendOrderPlacedEmail(orderId, dataSource, entityMap, getCms, variables, contactEmail) {
|
|
2154
|
+
if (!entityMap.message_templates) return;
|
|
2155
|
+
const templateRepo = dataSource.getRepository(entityMap.message_templates);
|
|
2156
|
+
const getTemplateRow = /* @__PURE__ */ __name(async (channel, templateKey) => {
|
|
2157
|
+
const row = await templateRepo.findOne({
|
|
2158
|
+
where: {
|
|
2159
|
+
channel,
|
|
2160
|
+
templateKey,
|
|
2161
|
+
deleted: false
|
|
2162
|
+
}
|
|
2163
|
+
});
|
|
2164
|
+
if (!row) return null;
|
|
2165
|
+
const r = row;
|
|
2166
|
+
return {
|
|
2167
|
+
subject: r.subject,
|
|
2168
|
+
body: r.body,
|
|
2169
|
+
externalTemplateRef: r.externalTemplateRef,
|
|
2170
|
+
name: r.name,
|
|
2171
|
+
language: r.providerMeta?.language ?? null,
|
|
2172
|
+
paramOrder: r.providerMeta?.paramOrder ?? null,
|
|
2173
|
+
enabled: r.enabled
|
|
2174
|
+
};
|
|
2175
|
+
}, "getTemplateRow");
|
|
2176
|
+
const resolved = await resolveEventOrderTemplate("email", getTemplateRow, null);
|
|
2177
|
+
if (!resolved?.enabled) {
|
|
2178
|
+
console.warn("[order-notifications] email template disabled or missing for order_placed", {
|
|
2179
|
+
orderId
|
|
2180
|
+
});
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
const [branding, emailSettings] = await Promise.all([
|
|
2184
|
+
getSettingsGroup(dataSource, entityMap, "branding"),
|
|
2185
|
+
getSettingsGroup(dataSource, entityMap, "email")
|
|
2186
|
+
]);
|
|
2187
|
+
const companyDetails = mergeEmailLayoutCompanyDetails(branding, emailSettings);
|
|
2188
|
+
const subject = applyTemplateVars(resolved.subject ?? "Order confirmed", variables);
|
|
2189
|
+
const bodyHtml = applyTemplateVars(resolved.body, variables);
|
|
2190
|
+
const html = renderLayout({
|
|
2191
|
+
bodyHtml,
|
|
2192
|
+
companyDetails
|
|
2193
|
+
});
|
|
2194
|
+
const cms = await getCms();
|
|
2195
|
+
await queueEmail(cms, {
|
|
2196
|
+
to: contactEmail,
|
|
2197
|
+
subject,
|
|
2198
|
+
html,
|
|
2199
|
+
text: bodyHtml.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
|
|
2200
|
+
});
|
|
2201
|
+
console.log("[order-notifications] email queued", {
|
|
2202
|
+
orderId,
|
|
2203
|
+
to: contactEmail
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
__name(sendOrderPlacedEmail, "sendOrderPlacedEmail");
|
|
2207
|
+
async function sendWhatsAppForTrigger(triggerKey, orderId, phone, variables, dataSource, entityMap, getCms) {
|
|
2208
|
+
const template = await loadBoundWhatsAppTemplate(triggerKey, dataSource, entityMap);
|
|
2209
|
+
if (!template) return;
|
|
2210
|
+
const paramOrder = template.providerMeta?.paramOrder?.length ? template.providerMeta.paramOrder : EVENT_ORDER_WHATSAPP_PARAM_KEYS;
|
|
2211
|
+
const cms = await getCms();
|
|
2212
|
+
const metaTemplateName = resolveMetaWhatsAppTemplateName({
|
|
2213
|
+
externalTemplateRef: template.externalTemplateRef,
|
|
2214
|
+
name: template.name
|
|
2215
|
+
});
|
|
2216
|
+
if (!metaTemplateName) {
|
|
2217
|
+
console.warn("[order-notifications] bound WhatsApp template has no Meta template name", {
|
|
2218
|
+
triggerKey,
|
|
2219
|
+
templateId: template.id
|
|
2220
|
+
});
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
const sent = await queueWhatsApp(cms, {
|
|
2224
|
+
to: phone.trim(),
|
|
2225
|
+
templateKey: triggerKey,
|
|
2226
|
+
externalTemplateRef: metaTemplateName,
|
|
2227
|
+
templateLanguage: template.providerMeta?.language ?? void 0,
|
|
2228
|
+
body: template.body,
|
|
2229
|
+
variables,
|
|
2230
|
+
templateParamOrder: paramOrder
|
|
2231
|
+
});
|
|
2232
|
+
if (!sent) {
|
|
2233
|
+
console.warn("[order-notifications] WhatsApp send failed", {
|
|
2234
|
+
triggerKey,
|
|
2235
|
+
orderId,
|
|
2236
|
+
hint: "Register whatsappPlugin() + queuePlugin() in getCms(), or set WHATSAPP_ACCESS_TOKEN and WHATSAPP_PHONE_NUMBER_ID"
|
|
2237
|
+
});
|
|
2238
|
+
} else {
|
|
2239
|
+
console.log("[order-notifications] WhatsApp queued/sent", {
|
|
2240
|
+
triggerKey,
|
|
2241
|
+
orderId,
|
|
2242
|
+
phone: maskPhone3(phone)
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
__name(sendWhatsAppForTrigger, "sendWhatsAppForTrigger");
|
|
2247
|
+
async function handleTrigger(triggerKey, payload, dataSource, entityMap, getCms) {
|
|
2248
|
+
console.log("[order-notifications] received", {
|
|
2249
|
+
triggerKey,
|
|
2250
|
+
orderId: payload.orderId,
|
|
2251
|
+
phone: maskPhone3(payload.customerPhone)
|
|
2252
|
+
});
|
|
2253
|
+
const pluginEnabled = await isOrderNotificationsEnabled(dataSource, entityMap);
|
|
2254
|
+
if (!pluginEnabled) {
|
|
2255
|
+
console.warn("[order-notifications] skip: Plugins \u2192 Order notifications disabled");
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const triggerEnabled = await isTriggerCatalogEnabled(triggerKey, dataSource, entityMap);
|
|
2259
|
+
if (!triggerEnabled) return;
|
|
2260
|
+
const context = await loadOrderNotificationContexts(payload.orderId, {
|
|
2261
|
+
dataSource,
|
|
2262
|
+
entityMap
|
|
2263
|
+
});
|
|
2264
|
+
const variables = mergeTriggerVariables(payload, context?.variables);
|
|
2265
|
+
const contactEmail = context?.contact.email?.trim() || "";
|
|
2266
|
+
const contactPhone = context?.contact.phone?.trim() || payload.customerPhone?.trim() || "";
|
|
2267
|
+
if (triggerKey === "order_placed" && contactEmail) {
|
|
2268
|
+
await sendOrderPlacedEmail(payload.orderId, dataSource, entityMap, getCms, variables, contactEmail);
|
|
2269
|
+
} else if (triggerKey === "order_placed") {
|
|
2270
|
+
console.warn("[order-notifications] skip email: no customer email", {
|
|
2271
|
+
orderId: payload.orderId
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
if (!contactPhone) {
|
|
2275
|
+
console.warn("[order-notifications] skip WhatsApp: no customer phone", {
|
|
2276
|
+
triggerKey,
|
|
2277
|
+
orderId: payload.orderId
|
|
2278
|
+
});
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
await sendWhatsAppForTrigger(triggerKey, payload.orderId, contactPhone, variables, dataSource, entityMap, getCms);
|
|
2282
|
+
}
|
|
2283
|
+
__name(handleTrigger, "handleTrigger");
|
|
2284
|
+
function initWhatsappTriggerDispatcher(deps) {
|
|
2285
|
+
if (initialized) return;
|
|
2286
|
+
initialized = true;
|
|
2287
|
+
const { dataSource, entityMap, getCms } = deps;
|
|
2288
|
+
notificationTriggerEmitter.onAnyTrigger((triggerKey, payload) => {
|
|
2289
|
+
void handleTrigger(triggerKey, payload, dataSource, entityMap, getCms).catch((err) => {
|
|
2290
|
+
console.error("[order-notifications]", triggerKey, err);
|
|
2291
|
+
});
|
|
2292
|
+
});
|
|
2293
|
+
console.log("[order-notifications] dispatcher initialized (email + WhatsApp)");
|
|
2294
|
+
}
|
|
2295
|
+
__name(initWhatsappTriggerDispatcher, "initWhatsappTriggerDispatcher");
|
|
2296
|
+
async function resendOrderNotification(triggerKey, orderId, deps) {
|
|
2297
|
+
if (!deps.entityMap.orders) return {
|
|
2298
|
+
ok: false,
|
|
2299
|
+
error: "orders entity missing"
|
|
2300
|
+
};
|
|
2301
|
+
const pluginEnabled = await isOrderNotificationsEnabled(deps.dataSource, deps.entityMap);
|
|
2302
|
+
if (!pluginEnabled) return {
|
|
2303
|
+
ok: false,
|
|
2304
|
+
error: "Order notifications plugin is disabled"
|
|
2305
|
+
};
|
|
2306
|
+
const triggerEnabled = await isTriggerCatalogEnabled(triggerKey, deps.dataSource, deps.entityMap);
|
|
2307
|
+
if (!triggerEnabled) {
|
|
2308
|
+
return {
|
|
2309
|
+
ok: false,
|
|
2310
|
+
error: `Trigger "${triggerKey}" is disabled or migration 1781230000000 not applied`
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
const order = await deps.dataSource.getRepository(deps.entityMap.orders).findOne({
|
|
2314
|
+
where: {
|
|
2315
|
+
id: orderId,
|
|
2316
|
+
deleted: false
|
|
2317
|
+
},
|
|
2318
|
+
relations: [
|
|
2319
|
+
"contact"
|
|
2320
|
+
]
|
|
2321
|
+
});
|
|
2322
|
+
if (!order) return {
|
|
2323
|
+
ok: false,
|
|
2324
|
+
error: "Order not found"
|
|
2325
|
+
};
|
|
2326
|
+
const o = order;
|
|
2327
|
+
await handleTrigger(triggerKey, {
|
|
2328
|
+
orderId,
|
|
2329
|
+
orderNumber: String(o.orderNumber ?? ""),
|
|
2330
|
+
customerName: o.contact?.name ?? "",
|
|
2331
|
+
customerPhone: o.contact?.phone ?? null,
|
|
2332
|
+
total: o.total ?? 0,
|
|
2333
|
+
currency: String(o.currency ?? "INR"),
|
|
2334
|
+
vendorId: o.vendorId ?? null
|
|
2335
|
+
}, deps.dataSource, deps.entityMap, deps.getCms);
|
|
2336
|
+
return {
|
|
2337
|
+
ok: true
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
__name(resendOrderNotification, "resendOrderNotification");
|
|
2341
|
+
|
|
2342
|
+
export { CHAT_EMAIL_LOG, EmailService, WhatsAppService, buildChatbotSystemPromptWithEmailTool, buildEventOrderTemplateVariables, buildTranscriptForLeadEmail, detectChatLeadIntent, emailPlugin, emailTemplates, getCompanyDetailsFromSettings, initWhatsappTriggerDispatcher, isWhatsAppPluginEnabled, joinRecipientsForSend, mergeEmailLayoutCompanyDetails, mergeWhatsAppConfigLayers, parseEmailRecipientsFromConfig, parseIntentRecipientEmails, queueWhatsApp, registerWhatsAppQueueProcessor, renderEmail, renderLayout, resendOrderNotification, resolveChatEmailToolSettings, sendChatLeadEmail, serializeEmailRecipients, whatsAppConfigured };
|