av6-core 1.5.6 → 1.5.8

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 (3) hide show
  1. package/dist/index.js +279 -141
  2. package/dist/index.mjs +279 -141
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2451,167 +2451,121 @@ var SmsProvider = class {
2451
2451
  constructor(logger = console, args) {
2452
2452
  this.logger = logger;
2453
2453
  this.args = args;
2454
- this.logger.info(`[NotificationService] args for sms : ${JSON.stringify(args)}`);
2454
+ const safeArgs = { ...args, apiKey: args.apiKey ? "***" : "" };
2455
+ this.logger.info(`[NotificationService] args for sms : ${JSON.stringify(safeArgs)}`);
2455
2456
  this.url = args.apiUrl || "https://web.nsemfua.com/api/http/sms/send";
2456
- this.token = "13|R3XfH6wHx3zdflBzpGMlNiTWKuOUEkGutORyGcuS";
2457
+ this.token = (args.apiKey || "").trim();
2457
2458
  this.sender = args.senderId || "AlmaMedLab";
2459
+ this.countryCode = (args.countryCode || "+233").trim();
2458
2460
  }
2459
2461
  url;
2460
2462
  token;
2461
2463
  sender;
2464
+ countryCode;
2465
+ /**
2466
+ * Single recipient wrapper (still same API).
2467
+ */
2462
2468
  async send(args) {
2463
- const to = args.recipient;
2464
- if (!to) {
2465
- return {
2466
- ok: false,
2467
- provider: "SMS" /* SMS */,
2468
- error: "Missing recipient.phone"
2469
- };
2469
+ const phone = args?.recipient?.phone;
2470
+ if (!phone) {
2471
+ return { ok: false, provider: "SMS" /* SMS */, error: "Missing recipient.phone" };
2472
+ }
2473
+ const bulk = await this.sendBulk({
2474
+ phones: [phone],
2475
+ message: args.body,
2476
+ includeMaster: true
2477
+ });
2478
+ if (!bulk.ok) {
2479
+ return { ok: false, provider: "SMS" /* SMS */, error: bulk.error, meta: bulk.meta };
2470
2480
  }
2471
- const allRecipients = [to];
2472
- if (this.args.masterNumber) {
2473
- allRecipients.push({ phone: this.args.masterNumber });
2481
+ return { ok: true, provider: "SMS" /* SMS */, externalId: bulk.externalId, meta: bulk.meta };
2482
+ }
2483
+ /**
2484
+ * ✅ Bulk SMS in ONE HTTP call (like PHP: recipient: "988.., 977..")
2485
+ */
2486
+ async sendBulk(args) {
2487
+ if (!this.url) {
2488
+ return { ok: false, provider: "SMS" /* SMS */, recipients: [], error: "Missing sms apiUrl" };
2474
2489
  }
2475
- const recipients = this.normalizeRecipients(allRecipients);
2490
+ if (!this.token) {
2491
+ return { ok: false, provider: "SMS" /* SMS */, recipients: [], error: "Missing sms apiKey" };
2492
+ }
2493
+ const includeMaster = args.includeMaster ?? true;
2494
+ const phones = [...args.phones ?? []];
2495
+ if (includeMaster && this.args.masterNumber) phones.push(this.args.masterNumber);
2496
+ const recipients = this.normalizePhones(phones);
2476
2497
  if (!recipients.length) {
2477
- return {
2478
- ok: false,
2479
- provider: "SMS" /* SMS */,
2480
- error: "Missing recipient.phone"
2481
- };
2498
+ return { ok: false, provider: "SMS" /* SMS */, recipients: [], error: "No valid phone recipients" };
2482
2499
  }
2500
+ const recipientStr = recipients.join(", ");
2483
2501
  try {
2484
2502
  const payload = {
2485
2503
  api_token: this.token,
2486
- recipient: recipients.join(", "),
2487
- // API accepts comma-separated list
2504
+ recipient: recipientStr,
2488
2505
  sender_id: this.sender,
2489
2506
  type: "plain",
2490
- message: this.cleanMessage(args.body)
2491
- // schedule_time: "" // add if you need scheduling
2507
+ message: this.cleanMessage(args.message)
2492
2508
  };
2493
2509
  const res = await fetch(this.url, {
2494
2510
  method: "POST",
2495
- headers: {
2496
- "Content-Type": "application/json",
2497
- Accept: "application/json"
2498
- },
2511
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2499
2512
  body: JSON.stringify(payload)
2500
2513
  });
2501
2514
  const data = await res.json().catch(() => ({}));
2502
- const isError = data?.status === "error" || !res.ok;
2515
+ const isError = !res.ok || data?.status === "error";
2503
2516
  if (isError) {
2504
2517
  return {
2505
2518
  ok: false,
2506
2519
  provider: "SMS" /* SMS */,
2507
- error: data?.message || data?.error || `Gateway error (${res.status} ${res.statusText})`
2520
+ recipients,
2521
+ error: data?.message || data?.error || `Gateway error (${res.status} ${res.statusText})`,
2522
+ meta: data
2508
2523
  };
2509
2524
  }
2510
- const externalId = data?.id || data?.data?.id || data?.message_id || data?.data?.message_id || void 0;
2525
+ const externalId = data?.id || data?.data?.id || data?.message_id || data?.data?.message_id;
2511
2526
  return {
2512
2527
  ok: true,
2513
2528
  provider: "SMS" /* SMS */,
2529
+ recipients,
2514
2530
  externalId,
2515
- meta: {
2516
- gatewayStatus: data?.status,
2517
- recipient: payload.recipient
2518
- }
2531
+ meta: data
2519
2532
  };
2520
2533
  } catch (err) {
2521
2534
  return {
2522
2535
  ok: false,
2523
2536
  provider: "SMS" /* SMS */,
2537
+ recipients,
2524
2538
  error: err?.message ?? String(err)
2525
2539
  };
2526
2540
  }
2527
2541
  }
2528
- /** Accepts single/multiple Recipient objects and returns sanitized numbers */
2529
- normalizeRecipients(input) {
2530
- return input.map((r) => r?.phone || "").map((p) => this.stripCountryCode(p)).map((p) => this.onlyDialable(p)).filter(Boolean);
2531
- }
2532
- /** Match PHP: remove leading +233 (Ghana) only */
2533
- stripCountryCode(phone) {
2534
- return (phone || "").trim().replace(this.args.countryCode || "+233", "");
2535
- }
2536
- /** Remove spaces, dashes, parentheses; keep numbers only (and commas handled elsewhere) */
2537
- onlyDialable(phone) {
2538
- return phone.replace(/[^\d]/g, "");
2542
+ // ---------------- helpers ----------------
2543
+ /**
2544
+ * Normalize phones:
2545
+ * - trim
2546
+ * - strip leading country code digits if present (e.g., +233 or 233)
2547
+ * - keep digits only
2548
+ * - dedupe
2549
+ */
2550
+ normalizePhones(input) {
2551
+ const ccDigits = this.countryCode.replace(/[^\d]/g, "");
2552
+ const normalized = (input ?? []).map((x) => String(x ?? "").trim()).filter(Boolean).map((x) => x.replace(/[^\d]/g, "")).map((digits) => {
2553
+ if (ccDigits && digits.startsWith(ccDigits) && digits.length > ccDigits.length) {
2554
+ return digits.slice(ccDigits.length);
2555
+ }
2556
+ return digits;
2557
+ }).filter((x) => x.length >= 6);
2558
+ return Array.from(new Set(normalized));
2539
2559
  }
2540
2560
  /** Mirror PHP cleanup (strip tags, collapse nbsp, trim) */
2541
2561
  cleanMessage(msg) {
2542
- const noTags = msg.replace(/<\/?[^>]+(>|$)/g, "");
2562
+ const noTags = String(msg ?? "").replace(/<\/?[^>]+(>|$)/g, "");
2543
2563
  return noTags.replace(/&nbsp;|&amp;nbsp;/g, " ").trim();
2544
2564
  }
2545
2565
  };
2546
2566
 
2547
2567
  // src/providers/whatsapp.provider.ts
2548
2568
  var import_axios2 = __toESM(require("axios"));
2549
- var WhatsAppProvider = class {
2550
- constructor(args, logger) {
2551
- this.args = args;
2552
- this.logger = logger;
2553
- this.logger.info(`[NotificationService] args for whatsapp : ${JSON.stringify(args)}`);
2554
- this.apiUrl = args.apiUrl || "https://api.interakt.ai/v1/public/message/";
2555
- this.apiKey = "RW9FTlFWM3h3aTdGbmJhVFFRU0RQdzVUdERyQl84VkU2RFRJWVdhcW8xZzo=";
2556
- }
2557
- apiKey;
2558
- apiUrl;
2559
- async send(inp) {
2560
- const to = inp.recipient;
2561
- if (!to) {
2562
- return {
2563
- ok: false,
2564
- provider: "WHATSAPP" /* WHATSAPP */,
2565
- error: "Missing recipient.whatsapp/phone"
2566
- };
2567
- }
2568
- const allRecipients = [to];
2569
- if (this.args.masterNumber) {
2570
- allRecipients.push(this.args.masterNumber);
2571
- }
2572
- try {
2573
- for (const recipient of allRecipients) {
2574
- const requestBody = {
2575
- countryCode: this.args.countryCode ?? "+233",
2576
- phoneNumber: recipient.replace(this.args.countryCode ?? "+233", ""),
2577
- callbackData: this.args.callbackData || "default-callback",
2578
- type: "Template",
2579
- template: {
2580
- name: this.args.templateName,
2581
- languageCode: this.args.languageCode,
2582
- bodyValues: inp.bodyValues ?? []
2583
- }
2584
- };
2585
- if (inp.fileUrl) {
2586
- requestBody.template.headerValues = [inp.fileUrl];
2587
- if (inp.fileName) requestBody.template.fileName = inp.fileName;
2588
- }
2589
- const headers = {
2590
- Authorization: `Basic ${this.apiKey}`,
2591
- "Content-Type": "application/json"
2592
- };
2593
- const response = await import_axios2.default.post(this.apiUrl, requestBody, { headers });
2594
- const data = response.data;
2595
- return {
2596
- ok: true,
2597
- provider: "WHATSAPP" /* WHATSAPP */,
2598
- externalId: data?.message_id || "unknown",
2599
- meta: data
2600
- };
2601
- }
2602
- return {
2603
- ok: true,
2604
- provider: "WHATSAPP" /* WHATSAPP */
2605
- };
2606
- } catch (err) {
2607
- return {
2608
- ok: false,
2609
- provider: "WHATSAPP" /* WHATSAPP */,
2610
- error: err?.response?.data || err?.message || String(err)
2611
- };
2612
- }
2613
- }
2614
- };
2615
2569
 
2616
2570
  // src/utils/notification.utils.ts
2617
2571
  function defaultValueField(t) {
@@ -2884,6 +2838,177 @@ async function resolveAllRecipients(args) {
2884
2838
  }
2885
2839
  return res;
2886
2840
  }
2841
+ async function mapWithConcurrency(items, limit, fn) {
2842
+ const results = new Array(items.length);
2843
+ let idx = 0;
2844
+ const workers = new Array(limit).fill(null).map(async () => {
2845
+ while (idx < items.length) {
2846
+ const cur = idx++;
2847
+ results[cur] = await fn(items[cur]);
2848
+ }
2849
+ });
2850
+ await Promise.all(workers);
2851
+ return results;
2852
+ }
2853
+
2854
+ // src/providers/whatsapp.provider.ts
2855
+ var WhatsAppProvider = class {
2856
+ constructor(args, logger) {
2857
+ this.args = args;
2858
+ this.logger = logger;
2859
+ const safeArgs = { ...args, apiKey: args.apiKey ? "***" : "" };
2860
+ this.logger.info(`[NotificationService] args for whatsapp : ${JSON.stringify(safeArgs)}`);
2861
+ this.apiUrl = args.apiUrl ?? "";
2862
+ this.apiKey = args.apiKey;
2863
+ }
2864
+ apiKey;
2865
+ apiUrl;
2866
+ /**
2867
+ * Existing single-recipient send (kept as-is, but fixed phone normalization a bit).
2868
+ * NOTE: This method sends to inp.recipient ONLY (no master). sendBulk handles master inclusion.
2869
+ */
2870
+ async send(inp) {
2871
+ const to = inp.recipient;
2872
+ if (!to) {
2873
+ return { ok: false, provider: "WHATSAPP" /* WHATSAPP */, error: "Missing recipient" };
2874
+ }
2875
+ if (!this.apiUrl) {
2876
+ return { ok: false, provider: "WHATSAPP" /* WHATSAPP */, error: "Missing WhatsApp apiUrl" };
2877
+ }
2878
+ if (!this.apiKey) {
2879
+ return { ok: false, provider: "WHATSAPP" /* WHATSAPP */, error: "Missing WhatsApp apiKey" };
2880
+ }
2881
+ try {
2882
+ const { requestBody, headers } = this.buildRequest(to, inp);
2883
+ const response = await import_axios2.default.post(this.apiUrl, requestBody, { headers });
2884
+ const data = response.data;
2885
+ return {
2886
+ ok: true,
2887
+ provider: "WHATSAPP" /* WHATSAPP */,
2888
+ externalId: data?.message_id || "unknown",
2889
+ meta: data
2890
+ };
2891
+ } catch (err) {
2892
+ return {
2893
+ ok: false,
2894
+ provider: "WHATSAPP" /* WHATSAPP */,
2895
+ error: err?.response?.data || err?.message || String(err)
2896
+ };
2897
+ }
2898
+ }
2899
+ /**
2900
+ * Proper bulk send:
2901
+ * - Accepts list of recipients
2902
+ * - Optionally includes masterNumber
2903
+ * - Dedupe recipients
2904
+ * - Concurrency-limited sending
2905
+ * - Returns per-recipient results + overall ok
2906
+ */
2907
+ async sendBulk(args) {
2908
+ if (!this.apiUrl) {
2909
+ return {
2910
+ ok: false,
2911
+ provider: "WHATSAPP" /* WHATSAPP */,
2912
+ items: [{ recipient: "", ok: false, error: "Missing WhatsApp apiUrl" }]
2913
+ };
2914
+ }
2915
+ if (!this.apiKey) {
2916
+ return {
2917
+ ok: false,
2918
+ provider: "WHATSAPP" /* WHATSAPP */,
2919
+ items: [{ recipient: "", ok: false, error: "Missing WhatsApp apiKey" }]
2920
+ };
2921
+ }
2922
+ const includeMaster = args.includeMaster ?? true;
2923
+ const concurrency = Math.max(1, Math.min(args.concurrency ?? 5, 25));
2924
+ const cleaned = (args.recipients ?? []).map((x) => String(x ?? "").trim()).filter(Boolean);
2925
+ const uniq = new Set(cleaned);
2926
+ if (includeMaster && this.args.masterNumber) {
2927
+ uniq.add(String(this.args.masterNumber).trim());
2928
+ }
2929
+ const recipients = Array.from(uniq);
2930
+ if (!recipients.length) {
2931
+ return { ok: true, provider: "WHATSAPP" /* WHATSAPP */, items: [] };
2932
+ }
2933
+ const baseInp = {
2934
+ bodyValues: args.bodyValues ?? [],
2935
+ fileUrl: args.fileUrl,
2936
+ fileName: args.fileName
2937
+ };
2938
+ const items = await mapWithConcurrency(recipients, concurrency, async (recipient) => {
2939
+ try {
2940
+ const { requestBody, headers } = this.buildRequest(recipient, baseInp);
2941
+ const response = await import_axios2.default.post(this.apiUrl, requestBody, { headers });
2942
+ const data = response.data;
2943
+ return {
2944
+ recipient,
2945
+ ok: true,
2946
+ externalId: data?.message_id || "unknown",
2947
+ meta: data
2948
+ };
2949
+ } catch (err) {
2950
+ const errorPayload = err?.response?.data || err?.message || String(err);
2951
+ return {
2952
+ recipient,
2953
+ ok: false,
2954
+ error: typeof errorPayload === "string" ? errorPayload : JSON.stringify(errorPayload),
2955
+ meta: err?.response?.data
2956
+ };
2957
+ }
2958
+ });
2959
+ const ok = items.every((x) => x.ok);
2960
+ return {
2961
+ ok,
2962
+ provider: "WHATSAPP" /* WHATSAPP */,
2963
+ items
2964
+ };
2965
+ }
2966
+ // ------------------------- helpers -------------------------
2967
+ buildRequest(recipient, inp) {
2968
+ const countryCode = (this.args.countryCode ?? "+233").trim();
2969
+ const languageCode = (this.args.languageCode ?? "en").trim();
2970
+ const callbackData = (this.args.callbackData ?? "default-callback").trim();
2971
+ const phoneNumber = this.normalizePhoneNumber(recipient, countryCode);
2972
+ const requestBody = {
2973
+ countryCode,
2974
+ phoneNumber,
2975
+ callbackData,
2976
+ type: "Template",
2977
+ template: {
2978
+ name: this.args.templateName,
2979
+ languageCode,
2980
+ bodyValues: inp.bodyValues ?? []
2981
+ }
2982
+ };
2983
+ if (inp.fileUrl) {
2984
+ requestBody.template.headerValues = [inp.fileUrl];
2985
+ if (inp.fileName) requestBody.template.fileName = inp.fileName;
2986
+ }
2987
+ const headers = {
2988
+ Authorization: `Basic ${this.apiKey}`,
2989
+ "Content-Type": "application/json"
2990
+ };
2991
+ return { requestBody, headers };
2992
+ }
2993
+ /**
2994
+ * Normalize a phone number into Interakt's expected format:
2995
+ * - requestBody.countryCode = "+91"
2996
+ * - requestBody.phoneNumber = "9883984351" (digits, without country code)
2997
+ *
2998
+ * Handles input formats:
2999
+ * - "9883984351"
3000
+ * - "+919883984351"
3001
+ * - "91 98839 84351"
3002
+ */
3003
+ normalizePhoneNumber(input, countryCode) {
3004
+ const ccDigits = countryCode.replace(/[^\d]/g, "");
3005
+ const digits = String(input ?? "").replace(/[^\d]/g, "");
3006
+ if (ccDigits && digits.startsWith(ccDigits) && digits.length > ccDigits.length) {
3007
+ return digits.slice(ccDigits.length);
3008
+ }
3009
+ return digits;
3010
+ }
3011
+ };
2887
3012
 
2888
3013
  // src/utils/renderer.ts
2889
3014
  var import_handlebars = __toESM(require("handlebars"));
@@ -3046,32 +3171,57 @@ var NotificationService = class {
3046
3171
  const { cfg, evt, deliveryId } = args;
3047
3172
  const tpl = await this.prisma.template.findUnique({ where: { id: cfg.smsTemplateId } });
3048
3173
  if (!tpl) return;
3049
- const recipients = args.recipients;
3174
+ const recipients = args.recipients ?? [];
3050
3175
  if (!recipients.length) {
3051
3176
  this.logger.info("[NotificationService] SMS: no recipients resolved");
3052
3177
  return;
3053
3178
  }
3054
3179
  const body = renderTemplate(tpl.bodyText ?? "", evt.data ?? {});
3055
3180
  const provider = new SmsProvider(this.logger, {
3056
- apiUrl: cfg.serviceEvent.smsApiUrl,
3181
+ apiUrl: cfg.serviceEvent.smsApiUrl ?? void 0,
3182
+ apiKey: cfg.serviceEvent.smsApiKey ?? void 0,
3057
3183
  countryCode: cfg.serviceEvent.countryCode ?? "+91",
3058
- senderId: cfg.serviceEvent.smsSenderId ?? void 0
3059
- // apiKey: cfg.serviceEvent.smsApiKey ?? undefined, // if your SmsProvider supports it
3184
+ senderId: cfg.serviceEvent.smsSenderId ?? void 0,
3185
+ masterNumber: cfg.serviceEvent.masterPhone ?? void 0
3186
+ // if you want master included here too
3060
3187
  });
3061
- await this.sendPerRecipient({
3062
- deliveryId,
3063
- notificationType: "SMS" /* SMS */,
3064
- recipients,
3065
- messageContent: body,
3066
- sendOne: async (recipient) => {
3067
- const res = await provider.send({
3068
- recipient: { phone: recipient },
3069
- body,
3070
- priority: evt.priority
3071
- });
3072
- return this.mapProviderResult(res);
3073
- }
3188
+ const bulk = await provider.sendBulk({
3189
+ phones: recipients,
3190
+ message: body,
3191
+ includeMaster: true
3074
3192
  });
3193
+ const now = /* @__PURE__ */ new Date();
3194
+ const auditRecipients = (bulk.recipients?.length ? bulk.recipients : recipients).filter(Boolean);
3195
+ if (bulk.ok) {
3196
+ await this.prisma.eventDeliveryItem.createMany({
3197
+ data: auditRecipients.map((r) => ({
3198
+ deliveryId,
3199
+ notificationType: "SMS" /* SMS */,
3200
+ recipient: r,
3201
+ messageContent: body,
3202
+ thirdPartyResponse: bulk.meta ?? void 0,
3203
+ isSent: true,
3204
+ sentAt: now,
3205
+ externalId: bulk.externalId ?? null,
3206
+ error: null
3207
+ }))
3208
+ });
3209
+ } else {
3210
+ const errMsg = bulk.error ?? "SMS bulk send failed";
3211
+ await this.prisma.eventDeliveryItem.createMany({
3212
+ data: auditRecipients.map((r) => ({
3213
+ deliveryId,
3214
+ notificationType: "SMS" /* SMS */,
3215
+ recipient: r,
3216
+ messageContent: body,
3217
+ thirdPartyResponse: bulk.meta ?? void 0,
3218
+ isSent: false,
3219
+ sentAt: null,
3220
+ externalId: bulk.externalId ?? null,
3221
+ error: errMsg
3222
+ }))
3223
+ });
3224
+ }
3075
3225
  }
3076
3226
  async processAppChannel(args) {
3077
3227
  const { cfg, evt, deliveryId } = args;
@@ -3249,7 +3399,7 @@ var NotificationService = class {
3249
3399
  this.logger.error("[NotificationService] WhatsApp sendBulk failed, falling back", e);
3250
3400
  }
3251
3401
  }
3252
- const items = await this.mapWithConcurrency(
3402
+ const items = await mapWithConcurrency(
3253
3403
  uniq,
3254
3404
  Math.max(1, args.concurrency),
3255
3405
  async (recipient) => {
@@ -3271,18 +3421,6 @@ var NotificationService = class {
3271
3421
  const ok = items.length > 0 && items.every((x) => x.ok);
3272
3422
  return { ok, items };
3273
3423
  }
3274
- async mapWithConcurrency(items, limit, fn) {
3275
- const results = new Array(items.length);
3276
- let idx = 0;
3277
- const workers = new Array(limit).fill(null).map(async () => {
3278
- while (idx < items.length) {
3279
- const cur = idx++;
3280
- results[cur] = await fn(items[cur]);
3281
- }
3282
- });
3283
- await Promise.all(workers);
3284
- return results;
3285
- }
3286
3424
  };
3287
3425
 
3288
3426
  // src/events/eventEmitter.ts
package/dist/index.mjs CHANGED
@@ -2401,167 +2401,121 @@ var SmsProvider = class {
2401
2401
  constructor(logger = console, args) {
2402
2402
  this.logger = logger;
2403
2403
  this.args = args;
2404
- this.logger.info(`[NotificationService] args for sms : ${JSON.stringify(args)}`);
2404
+ const safeArgs = { ...args, apiKey: args.apiKey ? "***" : "" };
2405
+ this.logger.info(`[NotificationService] args for sms : ${JSON.stringify(safeArgs)}`);
2405
2406
  this.url = args.apiUrl || "https://web.nsemfua.com/api/http/sms/send";
2406
- this.token = "13|R3XfH6wHx3zdflBzpGMlNiTWKuOUEkGutORyGcuS";
2407
+ this.token = (args.apiKey || "").trim();
2407
2408
  this.sender = args.senderId || "AlmaMedLab";
2409
+ this.countryCode = (args.countryCode || "+233").trim();
2408
2410
  }
2409
2411
  url;
2410
2412
  token;
2411
2413
  sender;
2414
+ countryCode;
2415
+ /**
2416
+ * Single recipient wrapper (still same API).
2417
+ */
2412
2418
  async send(args) {
2413
- const to = args.recipient;
2414
- if (!to) {
2415
- return {
2416
- ok: false,
2417
- provider: "SMS" /* SMS */,
2418
- error: "Missing recipient.phone"
2419
- };
2419
+ const phone = args?.recipient?.phone;
2420
+ if (!phone) {
2421
+ return { ok: false, provider: "SMS" /* SMS */, error: "Missing recipient.phone" };
2422
+ }
2423
+ const bulk = await this.sendBulk({
2424
+ phones: [phone],
2425
+ message: args.body,
2426
+ includeMaster: true
2427
+ });
2428
+ if (!bulk.ok) {
2429
+ return { ok: false, provider: "SMS" /* SMS */, error: bulk.error, meta: bulk.meta };
2420
2430
  }
2421
- const allRecipients = [to];
2422
- if (this.args.masterNumber) {
2423
- allRecipients.push({ phone: this.args.masterNumber });
2431
+ return { ok: true, provider: "SMS" /* SMS */, externalId: bulk.externalId, meta: bulk.meta };
2432
+ }
2433
+ /**
2434
+ * ✅ Bulk SMS in ONE HTTP call (like PHP: recipient: "988.., 977..")
2435
+ */
2436
+ async sendBulk(args) {
2437
+ if (!this.url) {
2438
+ return { ok: false, provider: "SMS" /* SMS */, recipients: [], error: "Missing sms apiUrl" };
2424
2439
  }
2425
- const recipients = this.normalizeRecipients(allRecipients);
2440
+ if (!this.token) {
2441
+ return { ok: false, provider: "SMS" /* SMS */, recipients: [], error: "Missing sms apiKey" };
2442
+ }
2443
+ const includeMaster = args.includeMaster ?? true;
2444
+ const phones = [...args.phones ?? []];
2445
+ if (includeMaster && this.args.masterNumber) phones.push(this.args.masterNumber);
2446
+ const recipients = this.normalizePhones(phones);
2426
2447
  if (!recipients.length) {
2427
- return {
2428
- ok: false,
2429
- provider: "SMS" /* SMS */,
2430
- error: "Missing recipient.phone"
2431
- };
2448
+ return { ok: false, provider: "SMS" /* SMS */, recipients: [], error: "No valid phone recipients" };
2432
2449
  }
2450
+ const recipientStr = recipients.join(", ");
2433
2451
  try {
2434
2452
  const payload = {
2435
2453
  api_token: this.token,
2436
- recipient: recipients.join(", "),
2437
- // API accepts comma-separated list
2454
+ recipient: recipientStr,
2438
2455
  sender_id: this.sender,
2439
2456
  type: "plain",
2440
- message: this.cleanMessage(args.body)
2441
- // schedule_time: "" // add if you need scheduling
2457
+ message: this.cleanMessage(args.message)
2442
2458
  };
2443
2459
  const res = await fetch(this.url, {
2444
2460
  method: "POST",
2445
- headers: {
2446
- "Content-Type": "application/json",
2447
- Accept: "application/json"
2448
- },
2461
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2449
2462
  body: JSON.stringify(payload)
2450
2463
  });
2451
2464
  const data = await res.json().catch(() => ({}));
2452
- const isError = data?.status === "error" || !res.ok;
2465
+ const isError = !res.ok || data?.status === "error";
2453
2466
  if (isError) {
2454
2467
  return {
2455
2468
  ok: false,
2456
2469
  provider: "SMS" /* SMS */,
2457
- error: data?.message || data?.error || `Gateway error (${res.status} ${res.statusText})`
2470
+ recipients,
2471
+ error: data?.message || data?.error || `Gateway error (${res.status} ${res.statusText})`,
2472
+ meta: data
2458
2473
  };
2459
2474
  }
2460
- const externalId = data?.id || data?.data?.id || data?.message_id || data?.data?.message_id || void 0;
2475
+ const externalId = data?.id || data?.data?.id || data?.message_id || data?.data?.message_id;
2461
2476
  return {
2462
2477
  ok: true,
2463
2478
  provider: "SMS" /* SMS */,
2479
+ recipients,
2464
2480
  externalId,
2465
- meta: {
2466
- gatewayStatus: data?.status,
2467
- recipient: payload.recipient
2468
- }
2481
+ meta: data
2469
2482
  };
2470
2483
  } catch (err) {
2471
2484
  return {
2472
2485
  ok: false,
2473
2486
  provider: "SMS" /* SMS */,
2487
+ recipients,
2474
2488
  error: err?.message ?? String(err)
2475
2489
  };
2476
2490
  }
2477
2491
  }
2478
- /** Accepts single/multiple Recipient objects and returns sanitized numbers */
2479
- normalizeRecipients(input) {
2480
- return input.map((r) => r?.phone || "").map((p) => this.stripCountryCode(p)).map((p) => this.onlyDialable(p)).filter(Boolean);
2481
- }
2482
- /** Match PHP: remove leading +233 (Ghana) only */
2483
- stripCountryCode(phone) {
2484
- return (phone || "").trim().replace(this.args.countryCode || "+233", "");
2485
- }
2486
- /** Remove spaces, dashes, parentheses; keep numbers only (and commas handled elsewhere) */
2487
- onlyDialable(phone) {
2488
- return phone.replace(/[^\d]/g, "");
2492
+ // ---------------- helpers ----------------
2493
+ /**
2494
+ * Normalize phones:
2495
+ * - trim
2496
+ * - strip leading country code digits if present (e.g., +233 or 233)
2497
+ * - keep digits only
2498
+ * - dedupe
2499
+ */
2500
+ normalizePhones(input) {
2501
+ const ccDigits = this.countryCode.replace(/[^\d]/g, "");
2502
+ const normalized = (input ?? []).map((x) => String(x ?? "").trim()).filter(Boolean).map((x) => x.replace(/[^\d]/g, "")).map((digits) => {
2503
+ if (ccDigits && digits.startsWith(ccDigits) && digits.length > ccDigits.length) {
2504
+ return digits.slice(ccDigits.length);
2505
+ }
2506
+ return digits;
2507
+ }).filter((x) => x.length >= 6);
2508
+ return Array.from(new Set(normalized));
2489
2509
  }
2490
2510
  /** Mirror PHP cleanup (strip tags, collapse nbsp, trim) */
2491
2511
  cleanMessage(msg) {
2492
- const noTags = msg.replace(/<\/?[^>]+(>|$)/g, "");
2512
+ const noTags = String(msg ?? "").replace(/<\/?[^>]+(>|$)/g, "");
2493
2513
  return noTags.replace(/&nbsp;|&amp;nbsp;/g, " ").trim();
2494
2514
  }
2495
2515
  };
2496
2516
 
2497
2517
  // src/providers/whatsapp.provider.ts
2498
2518
  import axios2 from "axios";
2499
- var WhatsAppProvider = class {
2500
- constructor(args, logger) {
2501
- this.args = args;
2502
- this.logger = logger;
2503
- this.logger.info(`[NotificationService] args for whatsapp : ${JSON.stringify(args)}`);
2504
- this.apiUrl = args.apiUrl || "https://api.interakt.ai/v1/public/message/";
2505
- this.apiKey = "RW9FTlFWM3h3aTdGbmJhVFFRU0RQdzVUdERyQl84VkU2RFRJWVdhcW8xZzo=";
2506
- }
2507
- apiKey;
2508
- apiUrl;
2509
- async send(inp) {
2510
- const to = inp.recipient;
2511
- if (!to) {
2512
- return {
2513
- ok: false,
2514
- provider: "WHATSAPP" /* WHATSAPP */,
2515
- error: "Missing recipient.whatsapp/phone"
2516
- };
2517
- }
2518
- const allRecipients = [to];
2519
- if (this.args.masterNumber) {
2520
- allRecipients.push(this.args.masterNumber);
2521
- }
2522
- try {
2523
- for (const recipient of allRecipients) {
2524
- const requestBody = {
2525
- countryCode: this.args.countryCode ?? "+233",
2526
- phoneNumber: recipient.replace(this.args.countryCode ?? "+233", ""),
2527
- callbackData: this.args.callbackData || "default-callback",
2528
- type: "Template",
2529
- template: {
2530
- name: this.args.templateName,
2531
- languageCode: this.args.languageCode,
2532
- bodyValues: inp.bodyValues ?? []
2533
- }
2534
- };
2535
- if (inp.fileUrl) {
2536
- requestBody.template.headerValues = [inp.fileUrl];
2537
- if (inp.fileName) requestBody.template.fileName = inp.fileName;
2538
- }
2539
- const headers = {
2540
- Authorization: `Basic ${this.apiKey}`,
2541
- "Content-Type": "application/json"
2542
- };
2543
- const response = await axios2.post(this.apiUrl, requestBody, { headers });
2544
- const data = response.data;
2545
- return {
2546
- ok: true,
2547
- provider: "WHATSAPP" /* WHATSAPP */,
2548
- externalId: data?.message_id || "unknown",
2549
- meta: data
2550
- };
2551
- }
2552
- return {
2553
- ok: true,
2554
- provider: "WHATSAPP" /* WHATSAPP */
2555
- };
2556
- } catch (err) {
2557
- return {
2558
- ok: false,
2559
- provider: "WHATSAPP" /* WHATSAPP */,
2560
- error: err?.response?.data || err?.message || String(err)
2561
- };
2562
- }
2563
- }
2564
- };
2565
2519
 
2566
2520
  // src/utils/notification.utils.ts
2567
2521
  function defaultValueField(t) {
@@ -2834,6 +2788,177 @@ async function resolveAllRecipients(args) {
2834
2788
  }
2835
2789
  return res;
2836
2790
  }
2791
+ async function mapWithConcurrency(items, limit, fn) {
2792
+ const results = new Array(items.length);
2793
+ let idx = 0;
2794
+ const workers = new Array(limit).fill(null).map(async () => {
2795
+ while (idx < items.length) {
2796
+ const cur = idx++;
2797
+ results[cur] = await fn(items[cur]);
2798
+ }
2799
+ });
2800
+ await Promise.all(workers);
2801
+ return results;
2802
+ }
2803
+
2804
+ // src/providers/whatsapp.provider.ts
2805
+ var WhatsAppProvider = class {
2806
+ constructor(args, logger) {
2807
+ this.args = args;
2808
+ this.logger = logger;
2809
+ const safeArgs = { ...args, apiKey: args.apiKey ? "***" : "" };
2810
+ this.logger.info(`[NotificationService] args for whatsapp : ${JSON.stringify(safeArgs)}`);
2811
+ this.apiUrl = args.apiUrl ?? "";
2812
+ this.apiKey = args.apiKey;
2813
+ }
2814
+ apiKey;
2815
+ apiUrl;
2816
+ /**
2817
+ * Existing single-recipient send (kept as-is, but fixed phone normalization a bit).
2818
+ * NOTE: This method sends to inp.recipient ONLY (no master). sendBulk handles master inclusion.
2819
+ */
2820
+ async send(inp) {
2821
+ const to = inp.recipient;
2822
+ if (!to) {
2823
+ return { ok: false, provider: "WHATSAPP" /* WHATSAPP */, error: "Missing recipient" };
2824
+ }
2825
+ if (!this.apiUrl) {
2826
+ return { ok: false, provider: "WHATSAPP" /* WHATSAPP */, error: "Missing WhatsApp apiUrl" };
2827
+ }
2828
+ if (!this.apiKey) {
2829
+ return { ok: false, provider: "WHATSAPP" /* WHATSAPP */, error: "Missing WhatsApp apiKey" };
2830
+ }
2831
+ try {
2832
+ const { requestBody, headers } = this.buildRequest(to, inp);
2833
+ const response = await axios2.post(this.apiUrl, requestBody, { headers });
2834
+ const data = response.data;
2835
+ return {
2836
+ ok: true,
2837
+ provider: "WHATSAPP" /* WHATSAPP */,
2838
+ externalId: data?.message_id || "unknown",
2839
+ meta: data
2840
+ };
2841
+ } catch (err) {
2842
+ return {
2843
+ ok: false,
2844
+ provider: "WHATSAPP" /* WHATSAPP */,
2845
+ error: err?.response?.data || err?.message || String(err)
2846
+ };
2847
+ }
2848
+ }
2849
+ /**
2850
+ * Proper bulk send:
2851
+ * - Accepts list of recipients
2852
+ * - Optionally includes masterNumber
2853
+ * - Dedupe recipients
2854
+ * - Concurrency-limited sending
2855
+ * - Returns per-recipient results + overall ok
2856
+ */
2857
+ async sendBulk(args) {
2858
+ if (!this.apiUrl) {
2859
+ return {
2860
+ ok: false,
2861
+ provider: "WHATSAPP" /* WHATSAPP */,
2862
+ items: [{ recipient: "", ok: false, error: "Missing WhatsApp apiUrl" }]
2863
+ };
2864
+ }
2865
+ if (!this.apiKey) {
2866
+ return {
2867
+ ok: false,
2868
+ provider: "WHATSAPP" /* WHATSAPP */,
2869
+ items: [{ recipient: "", ok: false, error: "Missing WhatsApp apiKey" }]
2870
+ };
2871
+ }
2872
+ const includeMaster = args.includeMaster ?? true;
2873
+ const concurrency = Math.max(1, Math.min(args.concurrency ?? 5, 25));
2874
+ const cleaned = (args.recipients ?? []).map((x) => String(x ?? "").trim()).filter(Boolean);
2875
+ const uniq = new Set(cleaned);
2876
+ if (includeMaster && this.args.masterNumber) {
2877
+ uniq.add(String(this.args.masterNumber).trim());
2878
+ }
2879
+ const recipients = Array.from(uniq);
2880
+ if (!recipients.length) {
2881
+ return { ok: true, provider: "WHATSAPP" /* WHATSAPP */, items: [] };
2882
+ }
2883
+ const baseInp = {
2884
+ bodyValues: args.bodyValues ?? [],
2885
+ fileUrl: args.fileUrl,
2886
+ fileName: args.fileName
2887
+ };
2888
+ const items = await mapWithConcurrency(recipients, concurrency, async (recipient) => {
2889
+ try {
2890
+ const { requestBody, headers } = this.buildRequest(recipient, baseInp);
2891
+ const response = await axios2.post(this.apiUrl, requestBody, { headers });
2892
+ const data = response.data;
2893
+ return {
2894
+ recipient,
2895
+ ok: true,
2896
+ externalId: data?.message_id || "unknown",
2897
+ meta: data
2898
+ };
2899
+ } catch (err) {
2900
+ const errorPayload = err?.response?.data || err?.message || String(err);
2901
+ return {
2902
+ recipient,
2903
+ ok: false,
2904
+ error: typeof errorPayload === "string" ? errorPayload : JSON.stringify(errorPayload),
2905
+ meta: err?.response?.data
2906
+ };
2907
+ }
2908
+ });
2909
+ const ok = items.every((x) => x.ok);
2910
+ return {
2911
+ ok,
2912
+ provider: "WHATSAPP" /* WHATSAPP */,
2913
+ items
2914
+ };
2915
+ }
2916
+ // ------------------------- helpers -------------------------
2917
+ buildRequest(recipient, inp) {
2918
+ const countryCode = (this.args.countryCode ?? "+233").trim();
2919
+ const languageCode = (this.args.languageCode ?? "en").trim();
2920
+ const callbackData = (this.args.callbackData ?? "default-callback").trim();
2921
+ const phoneNumber = this.normalizePhoneNumber(recipient, countryCode);
2922
+ const requestBody = {
2923
+ countryCode,
2924
+ phoneNumber,
2925
+ callbackData,
2926
+ type: "Template",
2927
+ template: {
2928
+ name: this.args.templateName,
2929
+ languageCode,
2930
+ bodyValues: inp.bodyValues ?? []
2931
+ }
2932
+ };
2933
+ if (inp.fileUrl) {
2934
+ requestBody.template.headerValues = [inp.fileUrl];
2935
+ if (inp.fileName) requestBody.template.fileName = inp.fileName;
2936
+ }
2937
+ const headers = {
2938
+ Authorization: `Basic ${this.apiKey}`,
2939
+ "Content-Type": "application/json"
2940
+ };
2941
+ return { requestBody, headers };
2942
+ }
2943
+ /**
2944
+ * Normalize a phone number into Interakt's expected format:
2945
+ * - requestBody.countryCode = "+91"
2946
+ * - requestBody.phoneNumber = "9883984351" (digits, without country code)
2947
+ *
2948
+ * Handles input formats:
2949
+ * - "9883984351"
2950
+ * - "+919883984351"
2951
+ * - "91 98839 84351"
2952
+ */
2953
+ normalizePhoneNumber(input, countryCode) {
2954
+ const ccDigits = countryCode.replace(/[^\d]/g, "");
2955
+ const digits = String(input ?? "").replace(/[^\d]/g, "");
2956
+ if (ccDigits && digits.startsWith(ccDigits) && digits.length > ccDigits.length) {
2957
+ return digits.slice(ccDigits.length);
2958
+ }
2959
+ return digits;
2960
+ }
2961
+ };
2837
2962
 
2838
2963
  // src/utils/renderer.ts
2839
2964
  import Handlebars from "handlebars";
@@ -2996,32 +3121,57 @@ var NotificationService = class {
2996
3121
  const { cfg, evt, deliveryId } = args;
2997
3122
  const tpl = await this.prisma.template.findUnique({ where: { id: cfg.smsTemplateId } });
2998
3123
  if (!tpl) return;
2999
- const recipients = args.recipients;
3124
+ const recipients = args.recipients ?? [];
3000
3125
  if (!recipients.length) {
3001
3126
  this.logger.info("[NotificationService] SMS: no recipients resolved");
3002
3127
  return;
3003
3128
  }
3004
3129
  const body = renderTemplate(tpl.bodyText ?? "", evt.data ?? {});
3005
3130
  const provider = new SmsProvider(this.logger, {
3006
- apiUrl: cfg.serviceEvent.smsApiUrl,
3131
+ apiUrl: cfg.serviceEvent.smsApiUrl ?? void 0,
3132
+ apiKey: cfg.serviceEvent.smsApiKey ?? void 0,
3007
3133
  countryCode: cfg.serviceEvent.countryCode ?? "+91",
3008
- senderId: cfg.serviceEvent.smsSenderId ?? void 0
3009
- // apiKey: cfg.serviceEvent.smsApiKey ?? undefined, // if your SmsProvider supports it
3134
+ senderId: cfg.serviceEvent.smsSenderId ?? void 0,
3135
+ masterNumber: cfg.serviceEvent.masterPhone ?? void 0
3136
+ // if you want master included here too
3010
3137
  });
3011
- await this.sendPerRecipient({
3012
- deliveryId,
3013
- notificationType: "SMS" /* SMS */,
3014
- recipients,
3015
- messageContent: body,
3016
- sendOne: async (recipient) => {
3017
- const res = await provider.send({
3018
- recipient: { phone: recipient },
3019
- body,
3020
- priority: evt.priority
3021
- });
3022
- return this.mapProviderResult(res);
3023
- }
3138
+ const bulk = await provider.sendBulk({
3139
+ phones: recipients,
3140
+ message: body,
3141
+ includeMaster: true
3024
3142
  });
3143
+ const now = /* @__PURE__ */ new Date();
3144
+ const auditRecipients = (bulk.recipients?.length ? bulk.recipients : recipients).filter(Boolean);
3145
+ if (bulk.ok) {
3146
+ await this.prisma.eventDeliveryItem.createMany({
3147
+ data: auditRecipients.map((r) => ({
3148
+ deliveryId,
3149
+ notificationType: "SMS" /* SMS */,
3150
+ recipient: r,
3151
+ messageContent: body,
3152
+ thirdPartyResponse: bulk.meta ?? void 0,
3153
+ isSent: true,
3154
+ sentAt: now,
3155
+ externalId: bulk.externalId ?? null,
3156
+ error: null
3157
+ }))
3158
+ });
3159
+ } else {
3160
+ const errMsg = bulk.error ?? "SMS bulk send failed";
3161
+ await this.prisma.eventDeliveryItem.createMany({
3162
+ data: auditRecipients.map((r) => ({
3163
+ deliveryId,
3164
+ notificationType: "SMS" /* SMS */,
3165
+ recipient: r,
3166
+ messageContent: body,
3167
+ thirdPartyResponse: bulk.meta ?? void 0,
3168
+ isSent: false,
3169
+ sentAt: null,
3170
+ externalId: bulk.externalId ?? null,
3171
+ error: errMsg
3172
+ }))
3173
+ });
3174
+ }
3025
3175
  }
3026
3176
  async processAppChannel(args) {
3027
3177
  const { cfg, evt, deliveryId } = args;
@@ -3199,7 +3349,7 @@ var NotificationService = class {
3199
3349
  this.logger.error("[NotificationService] WhatsApp sendBulk failed, falling back", e);
3200
3350
  }
3201
3351
  }
3202
- const items = await this.mapWithConcurrency(
3352
+ const items = await mapWithConcurrency(
3203
3353
  uniq,
3204
3354
  Math.max(1, args.concurrency),
3205
3355
  async (recipient) => {
@@ -3221,18 +3371,6 @@ var NotificationService = class {
3221
3371
  const ok = items.length > 0 && items.every((x) => x.ok);
3222
3372
  return { ok, items };
3223
3373
  }
3224
- async mapWithConcurrency(items, limit, fn) {
3225
- const results = new Array(items.length);
3226
- let idx = 0;
3227
- const workers = new Array(limit).fill(null).map(async () => {
3228
- while (idx < items.length) {
3229
- const cur = idx++;
3230
- results[cur] = await fn(items[cur]);
3231
- }
3232
- });
3233
- await Promise.all(workers);
3234
- return results;
3235
- }
3236
3374
  };
3237
3375
 
3238
3376
  // src/events/eventEmitter.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "av6-core",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",