@infuro/cms-core 1.0.46 → 1.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +30 -16
  2. package/dist/admin.cjs +4568 -2716
  3. package/dist/admin.d.cts +18 -4
  4. package/dist/admin.d.ts +18 -4
  5. package/dist/admin.js +3475 -1625
  6. package/dist/api.cjs +38 -39
  7. package/dist/api.d.cts +9 -3
  8. package/dist/api.d.ts +9 -3
  9. package/dist/api.js +6 -7
  10. package/dist/auth.cjs +84 -51
  11. package/dist/auth.d.cts +105 -4
  12. package/dist/auth.d.ts +105 -4
  13. package/dist/auth.js +4 -3
  14. package/dist/chunk-2IAXTKQN.cjs +19 -0
  15. package/dist/chunk-3EOM4V2M.cjs +452 -0
  16. package/dist/{chunk-X6UQFV4X.cjs → chunk-77NUXO6A.cjs} +14 -12
  17. package/dist/{chunk-KFFB6EMZ.js → chunk-AYERBA7I.js} +3 -7
  18. package/dist/chunk-EIZKW2PF.js +13 -0
  19. package/dist/{chunk-CRYKVJTO.js → chunk-IPDHT2UV.js} +1420 -3
  20. package/dist/{chunk-UDVLFVEC.cjs → chunk-JCMOOPNX.cjs} +3820 -2907
  21. package/dist/{chunk-X47PBV4I.js → chunk-JE22VP6S.js} +1 -0
  22. package/dist/{chunk-DN65KDIA.js → chunk-JIWUVQ6B.js} +14 -13
  23. package/dist/{chunk-HT5DI3XW.cjs → chunk-KOTXDSQB.cjs} +1 -0
  24. package/dist/{chunk-UKC3HYXI.js → chunk-LT2WOPKA.js} +3733 -2813
  25. package/dist/chunk-RK5ETF2I.js +434 -0
  26. package/dist/{chunk-SPQFNIYJ.cjs → chunk-TJ2MIQUT.cjs} +3 -7
  27. package/dist/{chunk-TCSGFAWB.cjs → chunk-UUWAUHUW.cjs} +1451 -3
  28. package/dist/{chunk-57O3HZPG.js → chunk-WRKV6MHB.js} +415 -3
  29. package/dist/{chunk-FBKDMRQL.cjs → chunk-YV5PK4JW.cjs} +427 -7
  30. package/dist/cli.cjs +13 -22
  31. package/dist/cli.js +13 -22
  32. package/dist/{emit-order-notification-trigger-IVLWKKJL.js → emit-order-notification-trigger-6XX7LSC4.js} +1 -1
  33. package/dist/{emit-order-notification-trigger-D2N3KFHL.cjs → emit-order-notification-trigger-NASG7ZEO.cjs} +3 -3
  34. package/dist/{event-order-defaults-EMWBIGL3.cjs → event-order-defaults-DU7V3YND.cjs} +9 -9
  35. package/dist/{event-order-defaults-SDRNAZHC.js → event-order-defaults-H4RT7NMR.js} +1 -1
  36. package/dist/hooks.cjs +43 -0
  37. package/dist/hooks.d.cts +10 -1
  38. package/dist/hooks.d.ts +10 -1
  39. package/dist/hooks.js +39 -1
  40. package/dist/index.cjs +479 -339
  41. package/dist/index.d.cts +181 -20
  42. package/dist/index.d.ts +181 -20
  43. package/dist/index.js +115 -13
  44. package/dist/migrations/1782200000000-VendorTeamProfiles.ts +233 -0
  45. package/dist/migrations/1782300000000-AddTypeToOrderNotificationBindings.ts +75 -0
  46. package/dist/migrations/1782400000000-CreateUserDeviceTokens.ts +36 -0
  47. package/dist/migrations/1782500000000-AddPushToOrderNotificationBindingsChannelEnum.ts +15 -0
  48. package/dist/{order-notification-dispatcher-ZJZB2HUN.js → order-notification-dispatcher-3TA4ETYJ.js} +1 -2
  49. package/dist/{order-notification-dispatcher-SBQAMM5V.cjs → order-notification-dispatcher-ZEAZ5SHV.cjs} +7 -4
  50. package/dist/{rbac-debug-db-2SGNLANF.js → rbac-debug-db-6EMIVMGF.js} +1 -1
  51. package/dist/{rbac-debug-db-4Z6A425H.cjs → rbac-debug-db-6PWTLBEL.cjs} +2 -2
  52. package/dist/theme.cjs +6 -1
  53. package/dist/theme.d.cts +4 -1
  54. package/dist/theme.d.ts +4 -1
  55. package/dist/theme.js +3 -1
  56. package/package.json +1 -1
  57. package/src/admin/admin.css +7 -0
  58. package/dist/chunk-7PMHZRF3.cjs +0 -872
  59. package/dist/chunk-CQHXW4TR.js +0 -107
  60. package/dist/chunk-GUSHM5HN.js +0 -862
  61. package/dist/chunk-NBY4NVTY.cjs +0 -182
  62. package/dist/chunk-SF2XKMCI.js +0 -171
  63. package/dist/chunk-XMRMZ6NQ.cjs +0 -109
  64. package/dist/send-order-placed-emails-B5ZVJT7T.cjs +0 -15
  65. package/dist/send-order-placed-emails-WPI37KRZ.js +0 -6
@@ -1,9 +1,629 @@
1
+ import { notificationTriggerEmitter } from './chunk-HCIRL37O.js';
1
2
  import { queueEmail } from './chunk-L3525VXI.js';
2
3
  import { mergeEmailLayoutCompanyDetails } from './chunk-2KUCQVAQ.js';
4
+ import { applyTemplateVars } from './chunk-CRFV5WJK.js';
3
5
  import { __name } from './chunk-SHUYVCID.js';
4
6
  import { SESClient, SendRawEmailCommand, SendEmailCommand } from '@aws-sdk/client-ses';
5
7
  import { createRequire } from 'module';
6
8
  import nodemailer from 'nodemailer';
9
+ import * as crypto from 'crypto';
10
+
11
+ // src/lib/event-order-template-context.ts
12
+ function resolvePublicSiteUrl() {
13
+ if (typeof process !== "undefined") {
14
+ const u = (process.env.CMS_PUBLIC_URL || process.env.NEXT_PUBLIC_SITE_URL || "").trim();
15
+ if (u) return u.replace(/\/+$/, "");
16
+ const v = (process.env.VERCEL_URL || "").trim();
17
+ if (v) return v.startsWith("http") ? v.replace(/\/+$/, "") : `https://${v.replace(/\/+$/, "")}`;
18
+ }
19
+ return "";
20
+ }
21
+ __name(resolvePublicSiteUrl, "resolvePublicSiteUrl");
22
+ function buildOrderTrackUrl(qrToken, siteUrl, orderNumber) {
23
+ const token = qrToken?.trim() || orderNumber?.trim();
24
+ if (!token) return "";
25
+ const base = (siteUrl ?? resolvePublicSiteUrl()).replace(/\/+$/, "");
26
+ if (!base) return `/track/${token}`;
27
+ return `${base}/track/${token}`;
28
+ }
29
+ __name(buildOrderTrackUrl, "buildOrderTrackUrl");
30
+ function formatOrderDetailsText(input) {
31
+ const { orderNumber, total, currency, lines } = input;
32
+ const header = `Order: ${orderNumber}
33
+ Total: ${total} ${currency}`;
34
+ if (!lines.length) return header;
35
+ const items = lines.map((line) => `- ${line.name} \xD7 ${line.quantity} (${line.total} ${line.currency})`).join("\n");
36
+ return `${header}
37
+
38
+ Items:
39
+ ${items}`;
40
+ }
41
+ __name(formatOrderDetailsText, "formatOrderDetailsText");
42
+ function formatOrderDetailsHtml(input) {
43
+ const text = formatOrderDetailsText(input);
44
+ const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
45
+ return `<pre style="font-family:inherit;white-space:pre-wrap;margin:0">${escaped}</pre>`;
46
+ }
47
+ __name(formatOrderDetailsHtml, "formatOrderDetailsHtml");
48
+ function buildEventOrderTemplateVariables(input) {
49
+ const { event, order, contact, productNames, vendorName = "", orderLines = [], siteUrl } = input;
50
+ const trackUrl = buildOrderTrackUrl(order.qrToken, siteUrl, order.orderNumber);
51
+ const baseSite = (siteUrl ?? resolvePublicSiteUrl()).replace(/\/+$/, "");
52
+ const invoiceNumber = `INV-${order.orderNumber ?? ""}`;
53
+ const invoiceUrl = baseSite ? `${baseSite}/api/orders/${order.orderNumber ?? ""}/invoice` : `/api/orders/${order.orderNumber ?? ""}/invoice`;
54
+ const orderDetailsPayload = {
55
+ orderNumber: String(order.orderNumber ?? ""),
56
+ total: String(order.total ?? ""),
57
+ currency: String(order.currency ?? "INR"),
58
+ lines: orderLines
59
+ };
60
+ return {
61
+ eventName: event.name ?? productNames ?? "",
62
+ vendorName: vendorName.trim(),
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
+ invoiceNumber,
75
+ invoiceUrl,
76
+ orderDetails: formatOrderDetailsText(orderDetailsPayload),
77
+ orderDetailsHtml: formatOrderDetailsHtml(orderDetailsPayload)
78
+ };
79
+ }
80
+ __name(buildEventOrderTemplateVariables, "buildEventOrderTemplateVariables");
81
+ function formatEventDate(d) {
82
+ if (!d) return "";
83
+ try {
84
+ return new Date(d).toLocaleString(void 0, {
85
+ dateStyle: "medium",
86
+ timeStyle: "short"
87
+ });
88
+ } catch {
89
+ return String(d);
90
+ }
91
+ }
92
+ __name(formatEventDate, "formatEventDate");
93
+
94
+ // src/lib/order-notification-context.ts
95
+ async function loadOrderLines(dataSource, entityMap, orderId, currency) {
96
+ if (!entityMap.order_items) return [];
97
+ const itemRows = await dataSource.getRepository(entityMap.order_items).find({
98
+ where: {
99
+ orderId
100
+ },
101
+ relations: [
102
+ "product"
103
+ ],
104
+ order: {
105
+ id: "ASC"
106
+ }
107
+ });
108
+ return itemRows.map((item) => {
109
+ const row = item;
110
+ const name = row.product?.name || row.product?.title || `Product #${row.productId}`;
111
+ return {
112
+ name: String(name),
113
+ quantity: Number(row.quantity) || 1,
114
+ total: row.total,
115
+ currency
116
+ };
117
+ });
118
+ }
119
+ __name(loadOrderLines, "loadOrderLines");
120
+ async function loadPrimaryEvent(dataSource, entityMap, itemRows) {
121
+ if (!entityMap.event_products || !entityMap.events) {
122
+ return {
123
+ productNames: []
124
+ };
125
+ }
126
+ const epRepo = dataSource.getRepository(entityMap.event_products);
127
+ const eventRepo = dataSource.getRepository(entityMap.events);
128
+ const eventMap = /* @__PURE__ */ new Map();
129
+ for (const item of itemRows) {
130
+ const productId = item.productId;
131
+ const productName = item.product?.name || item.product?.title || `Product #${productId}`;
132
+ const eps = await epRepo.find({
133
+ where: {
134
+ productId,
135
+ isActive: true
136
+ }
137
+ });
138
+ for (const ep of eps) {
139
+ const eventId = ep.eventId;
140
+ const existing = eventMap.get(eventId);
141
+ if (existing) {
142
+ if (!existing.productNames.includes(String(productName))) {
143
+ existing.productNames.push(String(productName));
144
+ }
145
+ } else {
146
+ const event = await eventRepo.findOne({
147
+ where: {
148
+ id: eventId,
149
+ deleted: false
150
+ }
151
+ });
152
+ if (event) {
153
+ eventMap.set(eventId, {
154
+ event,
155
+ productNames: [
156
+ String(productName)
157
+ ]
158
+ });
159
+ }
160
+ }
161
+ }
162
+ }
163
+ const first = eventMap.values().next().value;
164
+ if (!first) return {
165
+ productNames: []
166
+ };
167
+ return {
168
+ event: first.event,
169
+ productNames: first.productNames
170
+ };
171
+ }
172
+ __name(loadPrimaryEvent, "loadPrimaryEvent");
173
+ async function isFeatureGroupEnabled(dataSource, entityMap, group) {
174
+ if (!entityMap.configs) return true;
175
+ try {
176
+ const rows = await dataSource.getRepository(entityMap.configs).find({
177
+ where: {
178
+ settings: group,
179
+ deleted: false
180
+ }
181
+ });
182
+ const settings = Object.fromEntries(rows.map((r) => [
183
+ r.key,
184
+ r.value
185
+ ]));
186
+ return settings.enabled !== "false" && settings.enabled !== "0";
187
+ } catch {
188
+ return true;
189
+ }
190
+ }
191
+ __name(isFeatureGroupEnabled, "isFeatureGroupEnabled");
192
+ async function loadOrderNotificationContexts(orderId, deps) {
193
+ if (!deps.entityMap.orders) return null;
194
+ const order = await deps.dataSource.getRepository(deps.entityMap.orders).findOne({
195
+ where: {
196
+ id: orderId,
197
+ deleted: false
198
+ },
199
+ relations: [
200
+ "contact"
201
+ ]
202
+ });
203
+ if (!order) return null;
204
+ const o = order;
205
+ const meta = o.metadata && typeof o.metadata === "object" && !Array.isArray(o.metadata) ? o.metadata : {};
206
+ const metaBuyerEmail = typeof meta.buyerEmail === "string" && meta.buyerEmail.includes("@") ? meta.buyerEmail.trim() : null;
207
+ const metaCheckoutEmail = typeof meta.checkoutEmail === "string" && meta.checkoutEmail.includes("@") ? meta.checkoutEmail.trim() : null;
208
+ const metaBuyerName = typeof meta.buyerName === "string" && meta.buyerName.trim() ? meta.buyerName.trim() : null;
209
+ const rawContact = o.contact ?? {};
210
+ const contact = {
211
+ name: metaBuyerName || rawContact.name,
212
+ email: metaBuyerEmail || metaCheckoutEmail || rawContact.email,
213
+ phone: rawContact.phone
214
+ };
215
+ const currency = String(o.currency ?? "INR");
216
+ const siteUrl = resolvePublicSiteUrl();
217
+ const itemRows = await deps.dataSource.getRepository(deps.entityMap.order_items).find({
218
+ where: {
219
+ orderId
220
+ },
221
+ relations: [
222
+ "product"
223
+ ],
224
+ order: {
225
+ id: "ASC"
226
+ }
227
+ });
228
+ const orderLines = await loadOrderLines(deps.dataSource, deps.entityMap, orderId, currency);
229
+ const { event, productNames: eventProductNames } = await loadPrimaryEvent(deps.dataSource, deps.entityMap, itemRows);
230
+ const allProductNames = orderLines.map((line) => line.name);
231
+ const productNames = eventProductNames.length > 0 ? eventProductNames.join(", ") : allProductNames.join(", ");
232
+ const [multiVendorEnabled, eventsEnabled] = await Promise.all([
233
+ isFeatureGroupEnabled(deps.dataSource, deps.entityMap, "multi_vendor"),
234
+ isFeatureGroupEnabled(deps.dataSource, deps.entityMap, "events")
235
+ ]);
236
+ let vendorName = "";
237
+ if (multiVendorEnabled) {
238
+ const vendorId = o.vendorId ?? event?.vendorId;
239
+ if (vendorId && deps.entityMap.vendors) {
240
+ try {
241
+ const vRow = await deps.dataSource.getRepository(deps.entityMap.vendors).findOne({
242
+ where: {
243
+ id: vendorId
244
+ }
245
+ });
246
+ if (vRow) {
247
+ const v = vRow;
248
+ vendorName = String(v.storeName?.trim() || v.name?.trim() || v.companyName?.trim() || "");
249
+ }
250
+ } catch {
251
+ }
252
+ }
253
+ }
254
+ const effectiveEvent = eventsEnabled ? event : void 0;
255
+ const variables = effectiveEvent ? buildEventOrderTemplateVariables({
256
+ event: effectiveEvent,
257
+ order: {
258
+ orderNumber: o.orderNumber,
259
+ total: o.total,
260
+ currency,
261
+ qrToken: o.qrToken
262
+ },
263
+ contact,
264
+ productNames,
265
+ vendorName,
266
+ orderLines,
267
+ siteUrl
268
+ }) : buildEventOrderTemplateVariables({
269
+ event: {
270
+ name: eventsEnabled ? productNames || "Your order" : "",
271
+ slug: "",
272
+ venue: "",
273
+ startDate: null
274
+ },
275
+ order: {
276
+ orderNumber: o.orderNumber,
277
+ total: o.total,
278
+ currency,
279
+ qrToken: o.qrToken
280
+ },
281
+ contact,
282
+ productNames,
283
+ vendorName,
284
+ orderLines,
285
+ siteUrl
286
+ });
287
+ return {
288
+ order: {
289
+ id: o.id,
290
+ orderNumber: String(o.orderNumber ?? ""),
291
+ total: o.total,
292
+ currency,
293
+ qrToken: o.qrToken
294
+ },
295
+ contact,
296
+ orderLines,
297
+ productNames,
298
+ event,
299
+ variables
300
+ };
301
+ }
302
+ __name(loadOrderNotificationContexts, "loadOrderNotificationContexts");
303
+
304
+ // src/plugins/whatsapp/whatsapp-queue.ts
305
+ var WHATSAPP_QUEUE_NAME = "whatsapp";
306
+ function maskPhone(raw) {
307
+ const digits = raw.replace(/\D/g, "");
308
+ if (digits.length <= 4) return "****";
309
+ return `${"*".repeat(Math.max(0, digits.length - 4))}${digits.slice(-4)}`;
310
+ }
311
+ __name(maskPhone, "maskPhone");
312
+ function registerWhatsAppQueueProcessor(cms) {
313
+ const queue = cms.getPlugin("queue");
314
+ const whatsapp = cms.getPlugin("whatsapp");
315
+ if (!queue) {
316
+ console.warn("[whatsapp] queue processor not registered: queue plugin missing");
317
+ return;
318
+ }
319
+ if (!whatsapp || typeof whatsapp.send !== "function") {
320
+ console.warn("[whatsapp] queue processor not registered: whatsapp plugin missing or not configured");
321
+ return;
322
+ }
323
+ queue.registerProcessor(WHATSAPP_QUEUE_NAME, async (data) => {
324
+ const payload = data;
325
+ if (!payload.to) {
326
+ console.warn("[whatsapp] queue job skipped: missing recipient");
327
+ return;
328
+ }
329
+ console.log("[whatsapp] queue job processing", {
330
+ to: maskPhone(payload.to),
331
+ templateKey: payload.templateKey ?? null,
332
+ externalTemplateRef: payload.externalTemplateRef ?? null
333
+ });
334
+ try {
335
+ const ok = await whatsapp.send(payload);
336
+ if (ok) {
337
+ console.log("[whatsapp] queue job completed", {
338
+ to: maskPhone(payload.to),
339
+ sent: true
340
+ });
341
+ } else {
342
+ console.warn("[whatsapp] queue job completed without delivery", {
343
+ to: maskPhone(payload.to),
344
+ sent: false
345
+ });
346
+ }
347
+ } catch (err) {
348
+ console.error("[whatsapp] queue job failed", {
349
+ to: maskPhone(payload.to),
350
+ error: err instanceof Error ? err.message : String(err)
351
+ });
352
+ }
353
+ });
354
+ console.log("[whatsapp] queue processor registered", {
355
+ queue: WHATSAPP_QUEUE_NAME
356
+ });
357
+ }
358
+ __name(registerWhatsAppQueueProcessor, "registerWhatsAppQueueProcessor");
359
+ async function queueWhatsApp(cms, payload) {
360
+ const queue = cms.getPlugin("queue");
361
+ if (queue) {
362
+ console.log("[whatsapp] enqueue", {
363
+ to: maskPhone(payload.to),
364
+ templateKey: payload.templateKey ?? null,
365
+ externalTemplateRef: payload.externalTemplateRef ?? null
366
+ });
367
+ await queue.add(WHATSAPP_QUEUE_NAME, payload);
368
+ return true;
369
+ }
370
+ const whatsapp = cms.getPlugin("whatsapp");
371
+ if (whatsapp && typeof whatsapp.send === "function") {
372
+ console.log("[whatsapp] no queue plugin \u2014 sending immediately", {
373
+ to: maskPhone(payload.to)
374
+ });
375
+ return whatsapp.send(payload);
376
+ }
377
+ console.warn("[whatsapp] send skipped: neither queue nor whatsapp plugin available", {
378
+ to: maskPhone(payload.to)
379
+ });
380
+ return false;
381
+ }
382
+ __name(queueWhatsApp, "queueWhatsApp");
383
+
384
+ // src/plugins/whatsapp/whatsapp-service.ts
385
+ function norm(v) {
386
+ const t = v?.trim();
387
+ return t || void 0;
388
+ }
389
+ __name(norm, "norm");
390
+ function pick(plugin, db, env, ...keys) {
391
+ for (const key of keys) {
392
+ const pk = key;
393
+ if (pk in plugin && plugin[pk] !== void 0 && String(plugin[pk]).trim() !== "") {
394
+ const v = norm(String(plugin[pk]));
395
+ if (v) return v;
396
+ }
397
+ if (norm(db[key])) return norm(db[key]);
398
+ if (norm(env[key])) return norm(env[key]);
399
+ }
400
+ return void 0;
401
+ }
402
+ __name(pick, "pick");
403
+ function mergeWhatsAppConfigLayers(env, db, plugin) {
404
+ return {
405
+ accessToken: pick(plugin, db, env, "accessToken", "WHATSAPP_ACCESS_TOKEN"),
406
+ phoneNumberId: pick(plugin, db, env, "phoneNumberId", "WHATSAPP_PHONE_NUMBER_ID"),
407
+ apiVersion: pick(plugin, db, env, "apiVersion", "WHATSAPP_API_VERSION") ?? "v21.0"
408
+ };
409
+ }
410
+ __name(mergeWhatsAppConfigLayers, "mergeWhatsAppConfigLayers");
411
+ function isWhatsAppPluginEnabled(db) {
412
+ const v = (db.enabled ?? "true").toLowerCase();
413
+ return v !== "false" && v !== "0";
414
+ }
415
+ __name(isWhatsAppPluginEnabled, "isWhatsAppPluginEnabled");
416
+ function whatsAppConfigured(config) {
417
+ return Boolean(config.accessToken?.trim() && config.phoneNumberId?.trim());
418
+ }
419
+ __name(whatsAppConfigured, "whatsAppConfigured");
420
+ function normalizeWhatsAppRecipient(to) {
421
+ const digits = to.replace(/\D/g, "");
422
+ return digits;
423
+ }
424
+ __name(normalizeWhatsAppRecipient, "normalizeWhatsAppRecipient");
425
+ function resolveMetaWhatsAppTemplateName(opts) {
426
+ const ref = opts.externalTemplateRef?.trim();
427
+ const rowName = opts.name?.trim();
428
+ if (ref && !/^\d+$/.test(ref)) return ref;
429
+ if (rowName) return rowName;
430
+ return ref || void 0;
431
+ }
432
+ __name(resolveMetaWhatsAppTemplateName, "resolveMetaWhatsAppTemplateName");
433
+ function maskPhone2(digits) {
434
+ if (digits.length <= 4) return "****";
435
+ return `${"*".repeat(Math.max(0, digits.length - 4))}${digits.slice(-4)}`;
436
+ }
437
+ __name(maskPhone2, "maskPhone");
438
+ function logWhatsApp(level, message, extra) {
439
+ const line = extra ? `${message} ${JSON.stringify(extra)}` : message;
440
+ if (level === "info") console.log(`[whatsapp] ${line}`);
441
+ else if (level === "warn") console.warn(`[whatsapp] ${line}`);
442
+ else console.error(`[whatsapp] ${line}`);
443
+ }
444
+ __name(logWhatsApp, "logWhatsApp");
445
+ var WhatsAppService = class {
446
+ static {
447
+ __name(this, "WhatsAppService");
448
+ }
449
+ getWhatsAppSettings;
450
+ config;
451
+ getMessageTemplateRow;
452
+ constructor(env, plugin = {}, getWhatsAppSettings, getMessageTemplateRow) {
453
+ this.getWhatsAppSettings = getWhatsAppSettings;
454
+ this.getMessageTemplateRow = getMessageTemplateRow;
455
+ this.config = mergeWhatsAppConfigLayers(env, {}, plugin);
456
+ }
457
+ async mergedConfig() {
458
+ let db = {};
459
+ try {
460
+ db = await this.getWhatsAppSettings?.() ?? {};
461
+ } catch {
462
+ }
463
+ return mergeWhatsAppConfigLayers((typeof process !== "undefined" ? process.env : {}) ?? {}, db, this.config);
464
+ }
465
+ async send(opts) {
466
+ const cfg = await this.mergedConfig();
467
+ if (!whatsAppConfigured(cfg)) {
468
+ logWhatsApp("warn", "send skipped: not configured (Plugins \u2192 WhatsApp or WHATSAPP_ACCESS_TOKEN + WHATSAPP_PHONE_NUMBER_ID)");
469
+ return false;
470
+ }
471
+ const to = normalizeWhatsAppRecipient(opts.to);
472
+ if (!to) {
473
+ logWhatsApp("warn", "send skipped: invalid recipient", {
474
+ rawTo: opts.to
475
+ });
476
+ return false;
477
+ }
478
+ logWhatsApp("info", "send started", {
479
+ to: maskPhone2(to),
480
+ templateKey: opts.templateKey ?? null,
481
+ externalTemplateRef: opts.externalTemplateRef ?? null,
482
+ phoneNumberId: cfg.phoneNumberId
483
+ });
484
+ let templateName = resolveMetaWhatsAppTemplateName({
485
+ externalTemplateRef: opts.externalTemplateRef
486
+ });
487
+ let templateLanguage = opts.templateLanguage?.trim();
488
+ let body = opts.body?.trim() ?? "";
489
+ const variables = opts.variables ?? {};
490
+ if (opts.templateKey?.trim() && this.getMessageTemplateRow) {
491
+ const { resolveEventOrderTemplate, EVENT_ORDER_TEMPLATE_KEY } = await import('./event-order-defaults-H4RT7NMR.js');
492
+ if (opts.templateKey.trim() === EVENT_ORDER_TEMPLATE_KEY) {
493
+ const resolved = await resolveEventOrderTemplate("whatsapp", async (ch, key) => {
494
+ const row = await this.getMessageTemplateRow(ch, key);
495
+ if (!row) return null;
496
+ return {
497
+ subject: null,
498
+ body: row.body,
499
+ externalTemplateRef: row.externalTemplateRef,
500
+ enabled: row.enabled
501
+ };
502
+ });
503
+ if (resolved && !resolved.enabled) {
504
+ logWhatsApp("warn", "send skipped: template disabled in message_templates", {
505
+ templateKey: opts.templateKey
506
+ });
507
+ return false;
508
+ }
509
+ if (resolved) {
510
+ body = resolved.body;
511
+ if (resolved.externalTemplateRef) templateName = resolved.externalTemplateRef;
512
+ }
513
+ }
514
+ }
515
+ const { applyTemplateVars: applyTemplateVars2 } = await import('./interpolate-WDUWDF3I.js');
516
+ if (body) body = applyTemplateVars2(body, variables);
517
+ if (templateName) {
518
+ const paramValues = opts.templateParamOrder?.length ? opts.templateParamOrder.map((key) => String(variables[key] ?? "").slice(0, 1024)) : Object.keys(variables).length ? Object.values(variables) : body ? [
519
+ body
520
+ ] : [];
521
+ logWhatsApp("info", "sending template message", {
522
+ to: maskPhone2(to),
523
+ templateName,
524
+ paramCount: paramValues.length,
525
+ paramOrder: opts.templateParamOrder ?? null
526
+ });
527
+ return this.sendTemplateMessage(cfg, to, templateName, paramValues, templateLanguage);
528
+ }
529
+ if (!body) {
530
+ logWhatsApp("warn", "send skipped: no template name and empty body", {
531
+ to: maskPhone2(to)
532
+ });
533
+ return false;
534
+ }
535
+ logWhatsApp("info", "sending text message", {
536
+ to: maskPhone2(to),
537
+ bodyLength: body.length
538
+ });
539
+ return this.sendTextMessage(cfg, to, body);
540
+ }
541
+ async sendTextMessage(cfg, to, body) {
542
+ const url = `https://graph.facebook.com/${cfg.apiVersion}/${cfg.phoneNumberId}/messages`;
543
+ const res = await fetch(url, {
544
+ method: "POST",
545
+ headers: {
546
+ Authorization: `Bearer ${cfg.accessToken}`,
547
+ "Content-Type": "application/json"
548
+ },
549
+ body: JSON.stringify({
550
+ messaging_product: "whatsapp",
551
+ to,
552
+ type: "text",
553
+ text: {
554
+ body
555
+ }
556
+ })
557
+ });
558
+ if (!res.ok) {
559
+ const errText = await res.text().catch(() => "");
560
+ logWhatsApp("error", "text send failed", {
561
+ status: res.status,
562
+ to: maskPhone2(to),
563
+ error: errText
564
+ });
565
+ return false;
566
+ }
567
+ const data = await res.json().catch(() => ({}));
568
+ logWhatsApp("info", "text send succeeded", {
569
+ to: maskPhone2(to),
570
+ messageId: data.messages?.[0]?.id ?? null
571
+ });
572
+ return true;
573
+ }
574
+ async sendTemplateMessage(cfg, to, templateName, paramValues, templateLanguage) {
575
+ const components = paramValues.length > 0 ? [
576
+ {
577
+ type: "body",
578
+ parameters: paramValues.map((text) => ({
579
+ type: "text",
580
+ text: String(text).slice(0, 1024)
581
+ }))
582
+ }
583
+ ] : void 0;
584
+ const url = `https://graph.facebook.com/${cfg.apiVersion}/${cfg.phoneNumberId}/messages`;
585
+ const payload = {
586
+ messaging_product: "whatsapp",
587
+ to,
588
+ type: "template",
589
+ template: {
590
+ name: templateName,
591
+ language: {
592
+ code: templateLanguage?.trim() || "en"
593
+ },
594
+ ...components ? {
595
+ components
596
+ } : {}
597
+ }
598
+ };
599
+ const res = await fetch(url, {
600
+ method: "POST",
601
+ headers: {
602
+ Authorization: `Bearer ${cfg.accessToken}`,
603
+ "Content-Type": "application/json"
604
+ },
605
+ body: JSON.stringify(payload)
606
+ });
607
+ if (!res.ok) {
608
+ const errText = await res.text().catch(() => "");
609
+ logWhatsApp("error", "template send failed", {
610
+ status: res.status,
611
+ to: maskPhone2(to),
612
+ templateName,
613
+ paramCount: paramValues.length,
614
+ error: errText
615
+ });
616
+ return false;
617
+ }
618
+ const data = await res.json().catch(() => ({}));
619
+ logWhatsApp("info", "template send succeeded", {
620
+ to: maskPhone2(to),
621
+ templateName,
622
+ messageId: data.messages?.[0]?.id ?? null
623
+ });
624
+ return true;
625
+ }
626
+ };
7
627
 
8
628
  // src/plugins/email/templates/layout.ts
9
629
  function escapeHtml(s) {
@@ -567,6 +1187,19 @@ ${slugNote}
567
1187
  `Owner / invitee: ${ownerName?.trim() || "\u2014"} (${ownerEmail})`,
568
1188
  `Activation: ${activation === "invite" ? "Invitation" : "Password set"}`
569
1189
  ].filter(Boolean).join("\n");
1190
+ } else if (kind === "team_invite" && activation === "membership" && inviteLink) {
1191
+ subject = `You're invited to ${storeLabel}`;
1192
+ bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${greeting}</p>
1193
+ <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>. You already have an account \u2014 accept below to join this store. You will not need to set a new password.</p>
1194
+ ${primaryCtaButton(inviteLink, "Accept invitation")}
1195
+ <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>`;
1196
+ text = [
1197
+ ownerName?.trim() ? `Hello ${ownerName.trim()},` : "Hello,",
1198
+ "",
1199
+ `You have been invited to join ${storeLabel}.`,
1200
+ "Open this link to accept (no new password required):",
1201
+ inviteLink
1202
+ ].join("\n");
570
1203
  } else if (kind === "team_invite" && activation === "invite" && inviteLink) {
571
1204
  subject = `You're invited to ${storeLabel}`;
572
1205
  bodyHtml = `<p style="margin:0 0 12px 0;font-size:15px;line-height:1.5;color:#333;">${greeting}</p>
@@ -665,6 +1298,8 @@ function renderEmail(templateName, ctx, options) {
665
1298
  return render12(ctx);
666
1299
  }
667
1300
  __name(renderEmail, "renderEmail");
1301
+
1302
+ // src/plugins/email/email-service.ts
668
1303
  var require2 = createRequire(import.meta.url);
669
1304
  var MailComposer = require2("nodemailer/lib/mail-composer");
670
1305
  var SES_REGIONS_WITHOUT_SMTP = /* @__PURE__ */ new Set([
@@ -1031,8 +1666,8 @@ function resolveChatEmailToolSettings(configMap, sources) {
1031
1666
  __name(resolveChatEmailToolSettings, "resolveChatEmailToolSettings");
1032
1667
  function intentByKey(intents, key) {
1033
1668
  if (!key) return null;
1034
- const norm = normalizeIntentKey(key);
1035
- return intents.find((i) => i.intent === norm) ?? null;
1669
+ const norm2 = normalizeIntentKey(key);
1670
+ return intents.find((i) => i.intent === norm2) ?? null;
1036
1671
  }
1037
1672
  __name(intentByKey, "intentByKey");
1038
1673
  function buildIntentTableForPrompt(intents) {
@@ -1417,5 +2052,787 @@ function emailPlugin(config) {
1417
2052
  };
1418
2053
  }
1419
2054
  __name(emailPlugin, "emailPlugin");
2055
+ function base64UrlEncode(str) {
2056
+ const buf = typeof str === "string" ? Buffer.from(str, "utf8") : str;
2057
+ return buf.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
2058
+ }
2059
+ __name(base64UrlEncode, "base64UrlEncode");
2060
+ async function getGoogleAccessToken(clientEmail, privateKey) {
2061
+ const now = Math.floor(Date.now() / 1e3);
2062
+ const header = {
2063
+ alg: "RS256",
2064
+ typ: "JWT"
2065
+ };
2066
+ const claimSet = {
2067
+ iss: clientEmail,
2068
+ scope: "https://www.googleapis.com/auth/firebase.messaging",
2069
+ aud: "https://oauth2.googleapis.com/token",
2070
+ exp: now + 3600,
2071
+ iat: now
2072
+ };
2073
+ const encodedHeader = base64UrlEncode(JSON.stringify(header));
2074
+ const encodedClaimSet = base64UrlEncode(JSON.stringify(claimSet));
2075
+ const signatureInput = `${encodedHeader}.${encodedClaimSet}`;
2076
+ const formattedKey = privateKey.replace(/\\n/g, "\n");
2077
+ const signer = crypto.createSign("RSA-SHA256");
2078
+ signer.update(signatureInput);
2079
+ const signature = signer.sign(formattedKey, "base64");
2080
+ const jwt = `${signatureInput}.${signature.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_")}`;
2081
+ const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
2082
+ method: "POST",
2083
+ headers: {
2084
+ "Content-Type": "application/x-www-form-urlencoded"
2085
+ },
2086
+ body: new URLSearchParams({
2087
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
2088
+ assertion: jwt
2089
+ }).toString()
2090
+ });
2091
+ if (!tokenRes.ok) {
2092
+ const errText = await tokenRes.text();
2093
+ throw new Error(`Failed to obtain Google access token: ${errText}`);
2094
+ }
2095
+ const tokenData = await tokenRes.json();
2096
+ if (!tokenData.access_token) {
2097
+ throw new Error("Google OAuth token response missing access_token");
2098
+ }
2099
+ return tokenData.access_token;
2100
+ }
2101
+ __name(getGoogleAccessToken, "getGoogleAccessToken");
2102
+ async function resolveFcmServiceAccount(dataSource, entityMap) {
2103
+ const pushSettings = await getSettingsGroup(dataSource, entityMap, "push");
2104
+ let projectId = pushSettings.firebase_project_id?.trim();
2105
+ let clientEmail = pushSettings.firebase_client_email?.trim();
2106
+ let privateKey = pushSettings.firebase_private_key?.trim();
2107
+ if (pushSettings.firebase_service_account_json?.trim()) {
2108
+ try {
2109
+ const parsed = JSON.parse(pushSettings.firebase_service_account_json.trim());
2110
+ if (parsed.project_id) projectId = parsed.project_id;
2111
+ if (parsed.client_email) clientEmail = parsed.client_email;
2112
+ if (parsed.private_key) privateKey = parsed.private_key;
2113
+ } catch {
2114
+ }
2115
+ }
2116
+ if (!projectId || !clientEmail || !privateKey) {
2117
+ return {
2118
+ ok: false,
2119
+ error: "Firebase Service Account JSON / credentials are not configured"
2120
+ };
2121
+ }
2122
+ return {
2123
+ ok: true,
2124
+ account: {
2125
+ projectId,
2126
+ clientEmail,
2127
+ privateKey
2128
+ }
2129
+ };
2130
+ }
2131
+ __name(resolveFcmServiceAccount, "resolveFcmServiceAccount");
2132
+ async function validateFcmCredentials(dataSource, entityMap) {
2133
+ try {
2134
+ const resolved = await resolveFcmServiceAccount(dataSource, entityMap);
2135
+ if (!resolved.ok) {
2136
+ return {
2137
+ success: false,
2138
+ error: resolved.error
2139
+ };
2140
+ }
2141
+ const { projectId, clientEmail, privateKey } = resolved.account;
2142
+ await getGoogleAccessToken(clientEmail, privateKey);
2143
+ return {
2144
+ success: true,
2145
+ projectId,
2146
+ clientEmail
2147
+ };
2148
+ } catch (err) {
2149
+ const errorMsg = err instanceof Error ? err.message : "Unknown credential validation error";
2150
+ console.error("[fcm-push] Exception in validateFcmCredentials", errorMsg);
2151
+ return {
2152
+ success: false,
2153
+ error: errorMsg
2154
+ };
2155
+ }
2156
+ }
2157
+ __name(validateFcmCredentials, "validateFcmCredentials");
2158
+ async function sendFcmPushNotification(dataSource, entityMap, msg) {
2159
+ try {
2160
+ const resolved = await resolveFcmServiceAccount(dataSource, entityMap);
2161
+ if (!resolved.ok) {
2162
+ return {
2163
+ success: false,
2164
+ error: resolved.error
2165
+ };
2166
+ }
2167
+ const { projectId, clientEmail, privateKey } = resolved.account;
2168
+ const accessToken = await getGoogleAccessToken(clientEmail, privateKey);
2169
+ const fcmEndpoint = `https://fcm.googleapis.com/v1/projects/${projectId}/messages:send`;
2170
+ const fcmPayload = {
2171
+ message: {
2172
+ token: msg.token,
2173
+ notification: {
2174
+ title: msg.title,
2175
+ body: msg.body
2176
+ },
2177
+ data: msg.data || {}
2178
+ }
2179
+ };
2180
+ const response = await fetch(fcmEndpoint, {
2181
+ method: "POST",
2182
+ headers: {
2183
+ "Authorization": `Bearer ${accessToken}`,
2184
+ "Content-Type": "application/json"
2185
+ },
2186
+ body: JSON.stringify(fcmPayload)
2187
+ });
2188
+ const resData = await response.json();
2189
+ if (!response.ok) {
2190
+ const errorMsg = resData.error?.message || "FCM request failed";
2191
+ console.error("[fcm-push] Error sending push notification", errorMsg);
2192
+ return {
2193
+ success: false,
2194
+ error: errorMsg
2195
+ };
2196
+ }
2197
+ return {
2198
+ success: true,
2199
+ messageId: resData.name
2200
+ };
2201
+ } catch (err) {
2202
+ const errorMsg = err instanceof Error ? err.message : "Unknown push error";
2203
+ console.error("[fcm-push] Exception in sendFcmPushNotification", errorMsg);
2204
+ return {
2205
+ success: false,
2206
+ error: errorMsg
2207
+ };
2208
+ }
2209
+ }
2210
+ __name(sendFcmPushNotification, "sendFcmPushNotification");
2211
+
2212
+ // src/lib/order-notification-dispatcher.ts
2213
+ var initialized = false;
2214
+ function maskPhone3(raw) {
2215
+ const digits = String(raw ?? "").replace(/\D/g, "");
2216
+ if (digits.length <= 4) return "(none)";
2217
+ return `***${digits.slice(-4)}`;
2218
+ }
2219
+ __name(maskPhone3, "maskPhone");
2220
+ async function getSettingsGroup(dataSource, entityMap, group) {
2221
+ if (!entityMap.configs) return {};
2222
+ const rows = await dataSource.getRepository(entityMap.configs).find({
2223
+ where: {
2224
+ settings: group,
2225
+ deleted: false
2226
+ }
2227
+ });
2228
+ return Object.fromEntries(rows.map((r) => [
2229
+ r.key,
2230
+ r.value
2231
+ ]));
2232
+ }
2233
+ __name(getSettingsGroup, "getSettingsGroup");
2234
+ async function isEmailNotificationsEnabled(dataSource, entityMap) {
2235
+ const settings = await getSettingsGroup(dataSource, entityMap, "event_notifications");
2236
+ if (settings.enabled === "false" || settings.enabled === "0") return false;
2237
+ return settings.email_enabled !== "false" && settings.email_enabled !== "0";
2238
+ }
2239
+ __name(isEmailNotificationsEnabled, "isEmailNotificationsEnabled");
2240
+ async function isWhatsappNotificationsEnabled(dataSource, entityMap) {
2241
+ const settings = await getSettingsGroup(dataSource, entityMap, "event_notifications");
2242
+ if (settings.enabled === "false" || settings.enabled === "0") return false;
2243
+ return settings.whatsapp_enabled !== "false" && settings.whatsapp_enabled !== "0";
2244
+ }
2245
+ __name(isWhatsappNotificationsEnabled, "isWhatsappNotificationsEnabled");
2246
+ async function isTriggerCatalogEnabled(triggerKey, dataSource, entityMap) {
2247
+ if (!entityMap.order_notification_triggers) {
2248
+ console.warn("[order-notifications] order_notification_triggers missing \u2014 run migration 1781230000000-AddOrderNotificationTriggers");
2249
+ return false;
2250
+ }
2251
+ try {
2252
+ const row = await dataSource.getRepository(entityMap.order_notification_triggers).findOne({
2253
+ where: {
2254
+ triggerKey,
2255
+ enabled: true
2256
+ }
2257
+ });
2258
+ if (!row) {
2259
+ console.warn("[order-notifications] trigger disabled or not in DB", {
2260
+ triggerKey
2261
+ });
2262
+ }
2263
+ return Boolean(row);
2264
+ } catch (err) {
2265
+ console.error("[order-notifications] failed to load trigger catalog", {
2266
+ triggerKey,
2267
+ err
2268
+ });
2269
+ return false;
2270
+ }
2271
+ }
2272
+ __name(isTriggerCatalogEnabled, "isTriggerCatalogEnabled");
2273
+ async function loadBoundTemplate(triggerKey, channel, audienceType = "customers", dataSource, entityMap) {
2274
+ if (!entityMap.order_notification_bindings || !entityMap.message_templates) {
2275
+ console.warn("[order-notifications] order_notification_bindings or message_templates missing from entityMap");
2276
+ return null;
2277
+ }
2278
+ const bindingRepo = dataSource.getRepository(entityMap.order_notification_bindings);
2279
+ let binding = await bindingRepo.findOne({
2280
+ where: {
2281
+ triggerKey,
2282
+ channel,
2283
+ type: audienceType,
2284
+ enabled: true
2285
+ }
2286
+ });
2287
+ if (!binding && audienceType === "customers") {
2288
+ binding = await bindingRepo.findOne({
2289
+ where: {
2290
+ triggerKey,
2291
+ channel,
2292
+ enabled: true
2293
+ }
2294
+ });
2295
+ }
2296
+ if (!binding) {
2297
+ console.warn("[order-notifications] no order_notification_bindings row", {
2298
+ triggerKey,
2299
+ channel,
2300
+ audienceType
2301
+ });
2302
+ return null;
2303
+ }
2304
+ const templateId = binding.messageTemplateId;
2305
+ if (templateId == null) {
2306
+ console.warn("[order-notifications] binding exists but no template selected", {
2307
+ triggerKey,
2308
+ channel,
2309
+ audienceType
2310
+ });
2311
+ return null;
2312
+ }
2313
+ const template = await dataSource.getRepository(entityMap.message_templates).findOne({
2314
+ where: {
2315
+ id: templateId,
2316
+ channel,
2317
+ deleted: false,
2318
+ enabled: true
2319
+ }
2320
+ });
2321
+ if (!template) {
2322
+ console.warn("[order-notifications] bound template not found or disabled", {
2323
+ triggerKey,
2324
+ channel,
2325
+ audienceType,
2326
+ templateId
2327
+ });
2328
+ return null;
2329
+ }
2330
+ return template;
2331
+ }
2332
+ __name(loadBoundTemplate, "loadBoundTemplate");
2333
+ function mergeTriggerVariables(payload, rich) {
2334
+ const basic = {
2335
+ orderId: String(payload.orderId ?? ""),
2336
+ orderNumber: payload.orderNumber,
2337
+ customerName: payload.customerName,
2338
+ customerPhone: payload.customerPhone ?? "",
2339
+ total: String(payload.total),
2340
+ orderTotal: String(payload.total),
2341
+ currency: payload.currency,
2342
+ vendorId: String(payload.vendorId ?? ""),
2343
+ ...payload.refundAmount != null ? {
2344
+ refundAmount: String(payload.refundAmount)
2345
+ } : {}
2346
+ };
2347
+ return {
2348
+ ...basic,
2349
+ ...rich ?? {}
2350
+ };
2351
+ }
2352
+ __name(mergeTriggerVariables, "mergeTriggerVariables");
2353
+ async function isAudienceEmailEnabled(dataSource, entityMap, audience) {
2354
+ const settings = await getSettingsGroup(dataSource, entityMap, "event_notifications");
2355
+ if (settings.enabled === "false" || settings.enabled === "0") return false;
2356
+ if (settings.email_enabled === "false" || settings.email_enabled === "0") return false;
2357
+ const key = `email_${audience}_enabled`;
2358
+ return settings[key] !== "false" && settings[key] !== "0";
2359
+ }
2360
+ __name(isAudienceEmailEnabled, "isAudienceEmailEnabled");
2361
+ async function isPushNotificationsEnabled(dataSource, entityMap) {
2362
+ const settings = await getSettingsGroup(dataSource, entityMap, "event_notifications");
2363
+ if (settings.enabled === "false" || settings.enabled === "0") return false;
2364
+ return settings.push_enabled !== "false" && settings.push_enabled !== "0";
2365
+ }
2366
+ __name(isPushNotificationsEnabled, "isPushNotificationsEnabled");
2367
+ async function isAudiencePushEnabled(dataSource, entityMap, audience) {
2368
+ const settings = await getSettingsGroup(dataSource, entityMap, "event_notifications");
2369
+ if (settings.enabled === "false" || settings.enabled === "0") return false;
2370
+ if (settings.push_enabled === "false" || settings.push_enabled === "0") return false;
2371
+ const key = `push_${audience}_enabled`;
2372
+ return settings[key] !== "false" && settings[key] !== "0";
2373
+ }
2374
+ __name(isAudiencePushEnabled, "isAudiencePushEnabled");
2375
+ async function sendPushNotificationForTrigger(triggerKey, orderId, audienceType, targetId, variables, dataSource, entityMap) {
2376
+ let template = await loadBoundTemplate(triggerKey, "mobile", audienceType, dataSource, entityMap);
2377
+ if (!template) {
2378
+ template = await loadBoundTemplate(triggerKey, "push", audienceType, dataSource, entityMap);
2379
+ }
2380
+ if (!template || !template.body) return;
2381
+ const rawTitle = template.subject?.trim() || "Order Notification";
2382
+ const title = applyTemplateVars(rawTitle, variables);
2383
+ const body = applyTemplateVars(template.body, variables);
2384
+ if (!entityMap.user_device_tokens) return;
2385
+ const tokenRepo = dataSource.getRepository(entityMap.user_device_tokens);
2386
+ let targetTokens = [];
2387
+ try {
2388
+ if (audienceType === "customers") {
2389
+ targetTokens = await tokenRepo.find({
2390
+ where: {
2391
+ userId: targetId,
2392
+ isActive: true
2393
+ },
2394
+ select: [
2395
+ "token"
2396
+ ]
2397
+ });
2398
+ } else if (audienceType === "vendors") {
2399
+ targetTokens = await tokenRepo.find({
2400
+ where: {
2401
+ vendorId: targetId,
2402
+ isActive: true
2403
+ },
2404
+ select: [
2405
+ "token"
2406
+ ]
2407
+ });
2408
+ } else if (audienceType === "admin") {
2409
+ targetTokens = await tokenRepo.find({
2410
+ where: {
2411
+ isActive: true
2412
+ },
2413
+ select: [
2414
+ "token"
2415
+ ]
2416
+ });
2417
+ }
2418
+ } catch (err) {
2419
+ console.error("[order-notifications] Error querying device tokens for push", err);
2420
+ return;
2421
+ }
2422
+ for (const t of targetTokens) {
2423
+ if (t.token?.trim()) {
2424
+ await sendFcmPushNotification(dataSource, entityMap, {
2425
+ token: t.token.trim(),
2426
+ title,
2427
+ body,
2428
+ data: {
2429
+ orderId: String(orderId),
2430
+ triggerKey
2431
+ }
2432
+ });
2433
+ }
2434
+ }
2435
+ }
2436
+ __name(sendPushNotificationForTrigger, "sendPushNotificationForTrigger");
2437
+ async function getOrderVendorEmails(orderId, orderVendorId, dataSource, entityMap) {
2438
+ const emails = /* @__PURE__ */ new Set();
2439
+ if (!entityMap.vendors) return [];
2440
+ const vendorRepo = dataSource.getRepository(entityMap.vendors);
2441
+ if (orderVendorId) {
2442
+ try {
2443
+ const v = await vendorRepo.findOne({
2444
+ where: {
2445
+ id: orderVendorId
2446
+ }
2447
+ });
2448
+ if (v) {
2449
+ const row = v;
2450
+ if (row.email?.trim()) emails.add(row.email.trim());
2451
+ if (row.supportEmail?.trim()) emails.add(row.supportEmail.trim());
2452
+ }
2453
+ } catch {
2454
+ }
2455
+ }
2456
+ if (entityMap.order_items) {
2457
+ try {
2458
+ const items = await dataSource.getRepository(entityMap.order_items).find({
2459
+ where: {
2460
+ orderId
2461
+ },
2462
+ relations: [
2463
+ "product"
2464
+ ]
2465
+ });
2466
+ for (const item of items) {
2467
+ const row = item;
2468
+ const vId = row.product?.vendorId;
2469
+ if (vId && vId !== orderVendorId) {
2470
+ const v = await vendorRepo.findOne({
2471
+ where: {
2472
+ id: vId
2473
+ }
2474
+ });
2475
+ if (v) {
2476
+ const vRow = v;
2477
+ if (vRow.email?.trim()) emails.add(vRow.email.trim());
2478
+ if (vRow.supportEmail?.trim()) emails.add(vRow.supportEmail.trim());
2479
+ }
2480
+ }
2481
+ }
2482
+ } catch {
2483
+ }
2484
+ }
2485
+ return Array.from(emails);
2486
+ }
2487
+ __name(getOrderVendorEmails, "getOrderVendorEmails");
2488
+ async function getAdminRecipientEmails(dataSource, entityMap) {
2489
+ const emails = /* @__PURE__ */ new Set();
2490
+ const emailSettings = await getSettingsGroup(dataSource, entityMap, "email");
2491
+ const rawRecipients = [
2492
+ emailSettings.salesTeamEmails,
2493
+ emailSettings.salesTeamEmail,
2494
+ emailSettings.fulfilmentTeamEmails,
2495
+ emailSettings.fulfilmentTeamEmail,
2496
+ emailSettings.adminEmail
2497
+ ];
2498
+ for (const raw of rawRecipients) {
2499
+ if (!raw) continue;
2500
+ const parts = String(raw).split(/[,;]/);
2501
+ for (const p of parts) {
2502
+ const trimmed = p.trim();
2503
+ if (trimmed && trimmed.includes("@")) emails.add(trimmed.toLowerCase());
2504
+ }
2505
+ }
2506
+ if (entityMap.users) {
2507
+ try {
2508
+ const userRepo = dataSource.getRepository(entityMap.users);
2509
+ const adminUsers = await userRepo.find({
2510
+ where: [
2511
+ {
2512
+ groupId: 1,
2513
+ blocked: false
2514
+ },
2515
+ {
2516
+ adminAccess: true,
2517
+ blocked: false
2518
+ }
2519
+ ]
2520
+ });
2521
+ for (const u of adminUsers) {
2522
+ const em = u.email?.trim().toLowerCase();
2523
+ if (em && em.includes("@")) {
2524
+ emails.add(em);
2525
+ }
2526
+ }
2527
+ } catch {
2528
+ }
2529
+ }
2530
+ try {
2531
+ const rows = await dataSource.query(`SELECT DISTINCT LOWER(TRIM(email)) AS email
2532
+ FROM users
2533
+ WHERE ("groupId" = 1 OR "adminAccess" = true)
2534
+ AND COALESCE(blocked, false) = false
2535
+ AND COALESCE(deleted, false) = false
2536
+ AND "deletedAt" IS NULL`);
2537
+ for (const r of rows) {
2538
+ const em = r.email?.trim().toLowerCase();
2539
+ if (em && em.includes("@")) {
2540
+ emails.add(em);
2541
+ }
2542
+ }
2543
+ } catch {
2544
+ }
2545
+ return Array.from(emails);
2546
+ }
2547
+ __name(getAdminRecipientEmails, "getAdminRecipientEmails");
2548
+ function formatBodyForEmailHtml(body) {
2549
+ if (!body) return "";
2550
+ if (/^\s*<(html|body|table|section)[\s>]/i.test(body.trim())) {
2551
+ return body;
2552
+ }
2553
+ const withBreaks = body.replace(/\r\n/g, "\n").replace(/\n/g, "<br />");
2554
+ return `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 15px; line-height: 1.6; color: #1f2937;">${withBreaks}</div>`;
2555
+ }
2556
+ __name(formatBodyForEmailHtml, "formatBodyForEmailHtml");
2557
+ async function sendEmailForTrigger(triggerKey, orderId, audienceType, variables, contactEmail, dataSource, entityMap, getCms) {
2558
+ const template = await loadBoundTemplate(triggerKey, "email", audienceType, dataSource, entityMap);
2559
+ if (!template) return;
2560
+ const [branding, emailSettings] = await Promise.all([
2561
+ getSettingsGroup(dataSource, entityMap, "branding"),
2562
+ getSettingsGroup(dataSource, entityMap, "email")
2563
+ ]);
2564
+ const companyDetails = mergeEmailLayoutCompanyDetails(branding, emailSettings);
2565
+ const subject = applyTemplateVars(template.subject ?? "Order update", variables);
2566
+ const rawBody = applyTemplateVars(template.body, variables);
2567
+ const bodyHtml = formatBodyForEmailHtml(rawBody);
2568
+ const html = renderLayout({
2569
+ bodyHtml,
2570
+ companyDetails
2571
+ });
2572
+ const cms = await getCms();
2573
+ const isCustomer = audienceType === "customers";
2574
+ await queueEmail(cms, {
2575
+ to: contactEmail,
2576
+ subject,
2577
+ html,
2578
+ text: rawBody.replace(/<[^>]+>/g, "").trim(),
2579
+ ...isCustomer ? {
2580
+ orderId,
2581
+ attachInvoice: true
2582
+ } : {}
2583
+ });
2584
+ console.log("[order-notifications] email queued", {
2585
+ triggerKey,
2586
+ audienceType,
2587
+ orderId,
2588
+ to: contactEmail,
2589
+ attachInvoice: isCustomer
2590
+ });
2591
+ }
2592
+ __name(sendEmailForTrigger, "sendEmailForTrigger");
2593
+ async function sendWhatsAppForTrigger(triggerKey, orderId, phone, variables, dataSource, entityMap, getCms) {
2594
+ const template = await loadBoundTemplate(triggerKey, "whatsapp", "customers", dataSource, entityMap);
2595
+ if (!template) return;
2596
+ const paramOrder = template.providerMeta?.paramOrder ?? [];
2597
+ if (paramOrder.length === 0) {
2598
+ console.warn("[order-notifications] WhatsApp template has no paramOrder set \u2014 variables may send blank/wrong", {
2599
+ triggerKey
2600
+ });
2601
+ }
2602
+ const cms = await getCms();
2603
+ const metaTemplateName = resolveMetaWhatsAppTemplateName({
2604
+ externalTemplateRef: template.externalTemplateRef,
2605
+ name: template.name
2606
+ });
2607
+ if (!metaTemplateName) {
2608
+ console.warn("[order-notifications] bound WhatsApp template has no Meta template name", {
2609
+ triggerKey,
2610
+ templateId: template.id
2611
+ });
2612
+ return;
2613
+ }
2614
+ const sent = await queueWhatsApp(cms, {
2615
+ to: phone.trim(),
2616
+ templateKey: triggerKey,
2617
+ externalTemplateRef: metaTemplateName,
2618
+ templateLanguage: template.providerMeta?.language ?? void 0,
2619
+ body: template.body,
2620
+ variables,
2621
+ templateParamOrder: paramOrder
2622
+ });
2623
+ if (!sent) {
2624
+ console.warn("[order-notifications] WhatsApp send failed", {
2625
+ triggerKey,
2626
+ orderId,
2627
+ hint: "Register whatsappPlugin() + queuePlugin() in getCms(), or set WHATSAPP_ACCESS_TOKEN and WHATSAPP_PHONE_NUMBER_ID"
2628
+ });
2629
+ } else {
2630
+ console.log("[order-notifications] WhatsApp queued/sent", {
2631
+ triggerKey,
2632
+ orderId,
2633
+ phone: maskPhone3(phone)
2634
+ });
2635
+ }
2636
+ }
2637
+ __name(sendWhatsAppForTrigger, "sendWhatsAppForTrigger");
2638
+ async function isOrderPaymentConfirmed(orderId, dataSource, entityMap) {
2639
+ if (!entityMap.orders) return true;
2640
+ try {
2641
+ const orderRepo = dataSource.getRepository(entityMap.orders);
2642
+ const order = await orderRepo.findOne({
2643
+ where: {
2644
+ id: orderId
2645
+ }
2646
+ });
2647
+ if (!order) return false;
2648
+ const o = order;
2649
+ const status = String(o.status ?? "").toLowerCase().trim();
2650
+ const total = Number(o.total ?? 0);
2651
+ if (total <= 0) return true;
2652
+ const confirmedStatuses = [
2653
+ "confirmed",
2654
+ "completed",
2655
+ "processing",
2656
+ "paid",
2657
+ "fulfilled",
2658
+ "shipped"
2659
+ ];
2660
+ if (confirmedStatuses.includes(status)) {
2661
+ return true;
2662
+ }
2663
+ if (entityMap.payments) {
2664
+ const paymentRepo = dataSource.getRepository(entityMap.payments);
2665
+ const payment = await paymentRepo.findOne({
2666
+ where: {
2667
+ orderId
2668
+ }
2669
+ });
2670
+ if (payment) {
2671
+ const pStatus = String(payment.status ?? "").toLowerCase().trim();
2672
+ if (pStatus === "paid" || pStatus === "captured" || pStatus === "completed") {
2673
+ return true;
2674
+ }
2675
+ }
2676
+ }
2677
+ return false;
2678
+ } catch (err) {
2679
+ console.error("[order-notifications] failed to check order payment confirmation", err);
2680
+ return false;
2681
+ }
2682
+ }
2683
+ __name(isOrderPaymentConfirmed, "isOrderPaymentConfirmed");
2684
+ async function handleTrigger(triggerKey, payload, dataSource, entityMap, getCms) {
2685
+ console.log("[order-notifications] received", {
2686
+ triggerKey,
2687
+ orderId: payload.orderId,
2688
+ phone: maskPhone3(payload.customerPhone)
2689
+ });
2690
+ if (triggerKey === "order_placed") {
2691
+ const isConfirmed = await isOrderPaymentConfirmed(payload.orderId, dataSource, entityMap);
2692
+ if (!isConfirmed) {
2693
+ console.log("[order-notifications] skip order_placed: order payment not confirmed yet", {
2694
+ orderId: payload.orderId
2695
+ });
2696
+ return;
2697
+ }
2698
+ }
2699
+ const [emailChannelEnabled, whatsappChannelEnabled, pushChannelEnabled] = await Promise.all([
2700
+ isEmailNotificationsEnabled(dataSource, entityMap),
2701
+ isWhatsappNotificationsEnabled(dataSource, entityMap),
2702
+ isPushNotificationsEnabled(dataSource, entityMap)
2703
+ ]);
2704
+ if (!emailChannelEnabled && !whatsappChannelEnabled && !pushChannelEnabled) {
2705
+ console.warn("[order-notifications] skip: Email, WhatsApp, and Mobile Push notifications are disabled");
2706
+ return;
2707
+ }
2708
+ const triggerEnabled = await isTriggerCatalogEnabled(triggerKey, dataSource, entityMap);
2709
+ if (!triggerEnabled) return;
2710
+ const context = await loadOrderNotificationContexts(payload.orderId, {
2711
+ dataSource,
2712
+ entityMap
2713
+ });
2714
+ const variables = mergeTriggerVariables(payload, context?.variables);
2715
+ const contactEmail = context?.contact.email?.trim() || "";
2716
+ const contactPhone = context?.contact.phone?.trim() || payload.customerPhone?.trim() || "";
2717
+ if (emailChannelEnabled) {
2718
+ const [customerEnabled, vendorEnabled, adminEnabled] = await Promise.all([
2719
+ isAudienceEmailEnabled(dataSource, entityMap, "customer"),
2720
+ isAudienceEmailEnabled(dataSource, entityMap, "vendor"),
2721
+ isAudienceEmailEnabled(dataSource, entityMap, "admin")
2722
+ ]);
2723
+ if (customerEnabled && contactEmail) {
2724
+ await sendEmailForTrigger(triggerKey, payload.orderId, "customers", variables, contactEmail, dataSource, entityMap, getCms);
2725
+ }
2726
+ if (vendorEnabled) {
2727
+ const multiVendorSettings = await getSettingsGroup(dataSource, entityMap, "multi_vendor");
2728
+ if (multiVendorSettings.enabled !== "false" && multiVendorSettings.enabled !== "0") {
2729
+ const vendorEmails = await getOrderVendorEmails(payload.orderId, payload.vendorId, dataSource, entityMap);
2730
+ for (const vEmail of vendorEmails) {
2731
+ await sendEmailForTrigger(triggerKey, payload.orderId, "vendors", variables, vEmail, dataSource, entityMap, getCms);
2732
+ }
2733
+ }
2734
+ }
2735
+ if (adminEnabled) {
2736
+ const adminEmails = await getAdminRecipientEmails(dataSource, entityMap);
2737
+ for (const aEmail of adminEmails) {
2738
+ await sendEmailForTrigger(triggerKey, payload.orderId, "admin", variables, aEmail, dataSource, entityMap, getCms);
2739
+ }
2740
+ }
2741
+ } else {
2742
+ console.log("[order-notifications] skip email: Email notifications channel is disabled");
2743
+ }
2744
+ if (pushChannelEnabled) {
2745
+ const [pushCustomerEnabled, pushVendorEnabled, pushAdminEnabled] = await Promise.all([
2746
+ isAudiencePushEnabled(dataSource, entityMap, "customer"),
2747
+ isAudiencePushEnabled(dataSource, entityMap, "vendor"),
2748
+ isAudiencePushEnabled(dataSource, entityMap, "admin")
2749
+ ]);
2750
+ const targetCustomerId = context?.order?.customerContactId || context?.order?.contactId || context?.order?.userId || context?.order?.id;
2751
+ if (pushCustomerEnabled && targetCustomerId) {
2752
+ await sendPushNotificationForTrigger(triggerKey, payload.orderId, "customers", String(targetCustomerId), variables, dataSource, entityMap);
2753
+ }
2754
+ if (pushVendorEnabled && payload.vendorId) {
2755
+ await sendPushNotificationForTrigger(triggerKey, payload.orderId, "vendors", String(payload.vendorId), variables, dataSource, entityMap);
2756
+ }
2757
+ if (pushAdminEnabled) {
2758
+ await sendPushNotificationForTrigger(triggerKey, payload.orderId, "admin", "admin", variables, dataSource, entityMap);
2759
+ }
2760
+ }
2761
+ if (whatsappChannelEnabled) {
2762
+ if (contactPhone) {
2763
+ await sendWhatsAppForTrigger(triggerKey, payload.orderId, contactPhone, variables, dataSource, entityMap, getCms);
2764
+ } else {
2765
+ console.warn("[order-notifications] skip WhatsApp: no customer phone", {
2766
+ triggerKey,
2767
+ orderId: payload.orderId
2768
+ });
2769
+ }
2770
+ } else {
2771
+ console.log("[order-notifications] skip WhatsApp: WhatsApp notifications channel is disabled");
2772
+ }
2773
+ }
2774
+ __name(handleTrigger, "handleTrigger");
2775
+ function initWhatsappTriggerDispatcher(deps) {
2776
+ if (initialized) return;
2777
+ initialized = true;
2778
+ const { dataSource, entityMap, getCms } = deps;
2779
+ notificationTriggerEmitter.onAnyTrigger((triggerKey, payload) => {
2780
+ void handleTrigger(triggerKey, payload, dataSource, entityMap, getCms).catch((err) => {
2781
+ console.error("[order-notifications]", triggerKey, err);
2782
+ });
2783
+ });
2784
+ console.log("[order-notifications] dispatcher initialized (email + WhatsApp, unified via order_notification_bindings)");
2785
+ }
2786
+ __name(initWhatsappTriggerDispatcher, "initWhatsappTriggerDispatcher");
2787
+ async function resendOrderNotification(triggerKey, orderId, deps) {
2788
+ if (!deps.entityMap.orders) return {
2789
+ ok: false,
2790
+ error: "orders entity missing"
2791
+ };
2792
+ const [emailEnabled, whatsappEnabled] = await Promise.all([
2793
+ isEmailNotificationsEnabled(deps.dataSource, deps.entityMap),
2794
+ isWhatsappNotificationsEnabled(deps.dataSource, deps.entityMap)
2795
+ ]);
2796
+ if (!emailEnabled && !whatsappEnabled) {
2797
+ return {
2798
+ ok: false,
2799
+ error: "Both Email and WhatsApp notifications are disabled"
2800
+ };
2801
+ }
2802
+ const triggerEnabled = await isTriggerCatalogEnabled(triggerKey, deps.dataSource, deps.entityMap);
2803
+ if (!triggerEnabled) {
2804
+ return {
2805
+ ok: false,
2806
+ error: `Trigger "${triggerKey}" is disabled or migration 1781230000000 not applied`
2807
+ };
2808
+ }
2809
+ const order = await deps.dataSource.getRepository(deps.entityMap.orders).findOne({
2810
+ where: {
2811
+ id: orderId,
2812
+ deleted: false
2813
+ },
2814
+ relations: [
2815
+ "contact"
2816
+ ]
2817
+ });
2818
+ if (!order) return {
2819
+ ok: false,
2820
+ error: "Order not found"
2821
+ };
2822
+ const o = order;
2823
+ await handleTrigger(triggerKey, {
2824
+ orderId,
2825
+ orderNumber: String(o.orderNumber ?? ""),
2826
+ customerName: o.contact?.name ?? "",
2827
+ customerPhone: o.contact?.phone ?? null,
2828
+ total: o.total ?? 0,
2829
+ currency: String(o.currency ?? "INR"),
2830
+ vendorId: o.vendorId ?? null
2831
+ }, deps.dataSource, deps.entityMap, deps.getCms);
2832
+ return {
2833
+ ok: true
2834
+ };
2835
+ }
2836
+ __name(resendOrderNotification, "resendOrderNotification");
1420
2837
 
1421
- export { CHAT_EMAIL_LOG, EmailService, buildChatbotSystemPromptWithEmailTool, buildTranscriptForLeadEmail, detectChatLeadIntent, emailPlugin, emailTemplates, joinRecipientsForSend, parseEmailRecipientsFromConfig, parseIntentRecipientEmails, renderEmail, renderLayout, resolveChatEmailToolSettings, sendChatLeadEmail, serializeEmailRecipients };
2838
+ export { CHAT_EMAIL_LOG, EmailService, WhatsAppService, buildChatbotSystemPromptWithEmailTool, buildEventOrderTemplateVariables, buildTranscriptForLeadEmail, detectChatLeadIntent, emailPlugin, emailTemplates, getSettingsGroup, initWhatsappTriggerDispatcher, isWhatsAppPluginEnabled, joinRecipientsForSend, mergeWhatsAppConfigLayers, parseEmailRecipientsFromConfig, parseIntentRecipientEmails, queueWhatsApp, registerWhatsAppQueueProcessor, renderEmail, renderLayout, resendOrderNotification, resolveChatEmailToolSettings, sendChatLeadEmail, sendFcmPushNotification, serializeEmailRecipients, validateFcmCredentials, whatsAppConfigured };