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.
- package/dist/index.js +279 -141
- package/dist/index.mjs +279 -141
- 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
|
-
|
|
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 = "
|
|
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
|
|
2464
|
-
if (!
|
|
2465
|
-
return {
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
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
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
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
|
-
|
|
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:
|
|
2487
|
-
// API accepts comma-separated list
|
|
2504
|
+
recipient: recipientStr,
|
|
2488
2505
|
sender_id: this.sender,
|
|
2489
2506
|
type: "plain",
|
|
2490
|
-
message: this.cleanMessage(args.
|
|
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"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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;/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
|
-
|
|
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
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
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
|
|
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
|
-
|
|
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 = "
|
|
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
|
|
2414
|
-
if (!
|
|
2415
|
-
return {
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
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
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
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
|
-
|
|
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:
|
|
2437
|
-
// API accepts comma-separated list
|
|
2454
|
+
recipient: recipientStr,
|
|
2438
2455
|
sender_id: this.sender,
|
|
2439
2456
|
type: "plain",
|
|
2440
|
-
message: this.cleanMessage(args.
|
|
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"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
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;/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
|
-
|
|
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
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
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
|
|
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
|