unismsgateway 1.5.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,7 @@ import { init, getSmsPlatform, reset, smsPlatform } from 'unismsgateway';
59
59
  | `timeout` | `number` | `nest` | Request deadline in milliseconds. The request is aborted with an `ETIMEDOUT` error if the server does not respond within this window. Default: `10000`. |
60
60
  | `maxSockets` | `number` | `nest` | Maximum concurrent sockets in the keep-alive pool. Default: `10`. |
61
61
  | `retries` | `number` | `nest` | Automatic retry attempts on transient socket errors (`ECONNRESET`, `ECONNABORTED`, `EPIPE`, `ETIMEDOUT`). Default: `1`. |
62
+ | `deliveryCallback` | `{ url: string; accept?: 'application/json' \| 'application/xml' }` | `nest` | Optional SMSOnlineGH [delivery push](https://dev.smsonlinegh.com/docs/v5/http/rest/messaging/delivery_push.html) webhook. When set, every `quickSend` and `send` includes a `callback` block in the send payload. `accept` defaults to `application/json`. Receiving webhook POSTs is your app's responsibility. |
62
63
 
63
64
 
64
65
  Validation runs in `smsPlatform` when the instance is constructed: missing required fields for the chosen `platformId` throw `Error` with a clear message.
@@ -85,7 +86,7 @@ Nothing is read from the environment unless **you** wire it. Required fields are
85
86
 
86
87
  | `platformId` | Required in `param` | Optional in `param` (defaults in this library) |
87
88
  | ------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
- | `nest` | `apiKey` | `host` (default `api.smsonlinegh.com`), `protocol` (default `https`), `debug`, `keepAlive` (default `true`), `timeout` (default `10000` ms), `maxSockets` (default `10`), `retries` (default `1`) |
89
+ | `nest` | `apiKey` | `host` (default `api.smsonlinegh.com`), `protocol` (default `https`), `debug`, `keepAlive` (default `true`), `timeout` (default `10000` ms), `maxSockets` (default `10`), `retries` (default `1`), `deliveryCallback` (optional delivery push webhook) |
89
90
  | `hubtel` | `clientId`, `clientSecret` | `debug` |
90
91
  | `route` | `username`, `password` | `host` (default `rslr.connectbind.com`), `protocol` (default `http`), `port` (default `8080`), `debug` |
91
92
 
@@ -98,6 +99,8 @@ Nothing is read from the environment unless **you** wire it. Required fields are
98
99
  | `NEST_API_KEY` | `apiKey` | `nest` |
99
100
  | `NEST_HOST` | `host` | optional for `nest` |
100
101
  | `NEST_PROTOCOL` | `protocol` | optional for `nest` |
102
+ | `NEST_DELIVERY_CALLBACK_URL` | `deliveryCallback.url` | optional for `nest` |
103
+ | `NEST_DELIVERY_CALLBACK_ACCEPT` | `deliveryCallback.accept` | optional for `nest` (`application/json` or `application/xml`; default `application/json`) |
101
104
  | `HUBTEL_CLIENT_ID` | `clientId` | `hubtel` |
102
105
  | `HUBTEL_CLIENT_SECRET` | `clientSecret` | `hubtel` |
103
106
  | `ROUTE_USERNAME` | `username` | `route` |
@@ -132,7 +135,13 @@ const paramByPlatform = {
132
135
  nest: {
133
136
  apiKey: process.env.NEST_API_KEY,
134
137
  host: process.env.NEST_HOST,
135
- protocol: process.env.NEST_PROTOCOL
138
+ protocol: process.env.NEST_PROTOCOL,
139
+ deliveryCallback: process.env.NEST_DELIVERY_CALLBACK_URL
140
+ ? {
141
+ url: process.env.NEST_DELIVERY_CALLBACK_URL,
142
+ accept: process.env.NEST_DELIVERY_CALLBACK_ACCEPT
143
+ }
144
+ : undefined
136
145
  }
137
146
  };
138
147
 
@@ -167,6 +176,8 @@ The script `scripts/test-live.ts` loads `.env` (copy from `.env.example`) and ex
167
176
  | `NEST_API_KEY` | **Yes** | Maps to `param.apiKey`. |
168
177
  | `NEST_HOST` | No | Overrides default host `api.smsonlinegh.com`. |
169
178
  | `NEST_PROTOCOL` | No | Overrides default `https`. |
179
+ | `NEST_DELIVERY_CALLBACK_URL` | No | Maps to `param.deliveryCallback.url`. When set, sends include SMSOnlineGH delivery push callback info. |
180
+ | `NEST_DELIVERY_CALLBACK_ACCEPT` | No | Maps to `param.deliveryCallback.accept` (`application/json` or `application/xml`; default `application/json`). |
170
181
 
171
182
 
172
183
  `**hubtel`**
@@ -193,12 +204,16 @@ The script `scripts/test-live.ts` loads `.env` (copy from `.env.example`) and ex
193
204
  #### Live send (optional; all gateways)
194
205
 
195
206
 
196
- | Variable | Required? | Purpose |
197
- | -------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------ |
198
- | `TEST_SEND` | No (default: do not send) | Set to `true` to call `quickSend()` and send a real SMS. If unset or not `true`, only init and balance checks run. |
199
- | `TEST_FROM` | **Yes** when `TEST_SEND=true` | `QuickSendParams.From`. |
200
- | `TEST_TO` | **Yes** when `TEST_SEND=true` | `QuickSendParams.To`. |
201
- | `TEST_CONTENT` | No | Message body; if omitted, the script uses a built-in default string. |
207
+ | Variable | Required? | Purpose |
208
+ | ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------ |
209
+ | `TEST_SEND` | No (default: do not send) | Set to `true` to run live send checks. If unset or not `true`, only init and balance checks run. |
210
+ | `TEST_SEND_METHOD` | No (default: `quickSend`) | `quickSend` — single recipient via `quickSend()`. `send` — multiple recipients via `send()` (`nest`, `route`). `both` — run both. `sendPersonalized` — personalised bulk via `sendPersonalized()` (`nest` only). |
211
+ | `TEST_FROM` | **Yes** when `TEST_SEND=true` | Sender ID (`From` / `from`). |
212
+ | `TEST_TO` | **Yes** when method is `quickSend` or `both` | Single recipient for `quickSend()`. Also used as a one-element array for `send()` when `TEST_TO_MULTI` is omitted. |
213
+ | `TEST_TO_MULTI` | Recommended for `send` / `both` | Comma-separated MSISDNs for `send()` (e.g. `233...,233...`). Required when `TEST_SEND_METHOD=send` unless `TEST_TO` is set. |
214
+ | `TEST_CONTENT` | No | Message body; if omitted, the script uses a built-in default string. |
215
+ | `TEST_PERSONALIZED_TEMPLATE` | No (default template used) | Message template for `sendPersonalized()` (e.g. `Hello {$name}. Your balance is ${$balance}.`). |
216
+ | `TEST_PERSONALIZED_DESTINATIONS` | **Yes** when `TEST_SEND_METHOD=sendPersonalized` | JSON array of `{ to, values }` objects, e.g. `[{"to":"233...","values":["Name",123]}]`. |
202
217
 
203
218
 
204
219
  ---
@@ -215,7 +230,7 @@ The script `scripts/test-live.ts` loads `.env` (copy from `.env.example`) and ex
215
230
  - Calls `createGateway()` to instantiate the underlying provider (`routeSms`, `HubtelSms`, or `NestSmsGateway`).
216
231
  3. `**getSmsPlatform(): smsPlatform | null`**: Returns the current singleton, or `null` if `reset()` was called and no new `init()` has run.
217
232
 
218
- There is **no async bootstrap**; after `init()` returns, `quickSend` is ready.
233
+ There is **no async bootstrap**; after `init()` returns, `quickSend`, `send`, and `sendPersonalized` are ready.
219
234
 
220
235
  ---
221
236
 
@@ -312,6 +327,7 @@ const gateway = unisms.init({
312
327
  | `timeout` | `10000` | Milliseconds before the request is aborted with `ETIMEDOUT`. |
313
328
  | `maxSockets` | `10` | Maximum sockets held open in the keep-alive pool. |
314
329
  | `retries` | `1` | Retry count for transient errors (`ECONNRESET`, `ECONNABORTED`, etc). |
330
+ | `deliveryCallback` | — | Optional delivery push webhook. When set, each send request includes `callback: { url, accept }` per [SMSOnlineGH delivery push docs](https://dev.smsonlinegh.com/docs/v5/http/rest/messaging/delivery_push.html). `accept` defaults to `application/json`. This library registers the URL with the provider only; your app must expose and handle the webhook endpoint. |
315
331
 
316
332
 
317
333
  Requests use `POST` to path `/v5/<endpoint>` (e.g. send: `message/sms/send`, balance: `account/balance`). Authorization header: `Authorization: key <apiKey>`.
@@ -330,6 +346,113 @@ const gateway = unisms.init({
330
346
  gateway.getGateway().destroy();
331
347
  ```
332
348
 
349
+ **Example with delivery push callback:**
350
+
351
+ ```javascript
352
+ const gateway = unisms.init({
353
+ platformId: 'nest',
354
+ param: {
355
+ apiKey: 'your-api-key',
356
+ deliveryCallback: {
357
+ url: 'https://your-app.example/sms/delivery',
358
+ accept: 'application/json' // optional; default application/json
359
+ }
360
+ }
361
+ });
362
+ ```
363
+
364
+ SMSOnlineGH POSTs delivery status to that URL asynchronously after the operator reports delivery. The initial `quickSend` response still only reflects submission status.
365
+
366
+ **Delivery push callback payload**
367
+
368
+ A delivery push notification uses the same response shape as a [Message Delivery Report](https://dev.smsonlinegh.com/docs/v5/http/rest/messaging/delivery_report.html), but applies to **one destination only**. Expect a single item in `data.destinations` (even when the original send had multiple recipients).
369
+
370
+ The `Content-Type` of the POST matches your configured `deliveryCallback.accept` (`application/json` by default, or `application/xml`).
371
+
372
+ **JSON** (`accept: 'application/json'`):
373
+
374
+ ```json
375
+ {
376
+ "handshake": {
377
+ "id": 0,
378
+ "label": "HSHK_OK"
379
+ },
380
+ "data": {
381
+ "batch": "cfa19ba67f94fbd6b19c067b0c87ed4f",
382
+ "delivery": true,
383
+ "category": "sms",
384
+ "text": "Hello world!",
385
+ "type": 0,
386
+ "sender": "Hello",
387
+ "personalised": false,
388
+ "destinationsCount": 2,
389
+ "destinations": [
390
+ {
391
+ "to": "233246314915",
392
+ "id": "093841e5-578a-41f4-5f5f-2f3910886c12",
393
+ "country": "Ghana",
394
+ "messageCount": 1,
395
+ "submitDateTime": "2021-09-29 21:57:44",
396
+ "reportDateTime": "2021-09-29 21:57:48",
397
+ "status": {
398
+ "id": 2110,
399
+ "label": "DS_DELIVERED"
400
+ }
401
+ }
402
+ ]
403
+ }
404
+ }
405
+ ```
406
+
407
+ **XML** (`accept: 'application/xml'`):
408
+
409
+ ```xml
410
+ <response>
411
+ <handshake>
412
+ <id>0</id>
413
+ <label>HSHK_OK</label>
414
+ </handshake>
415
+ <data>
416
+ <batch>cfa19ba67f94fbd6b19c067b0c87ed4f</batch>
417
+ <category>sms</category>
418
+ <delivery>true</delivery>
419
+ <text>Hello world!</text>
420
+ <type>0</type>
421
+ <sender>Hello</sender>
422
+ <personalised>false</personalised>
423
+ <destinationsCount>2</destinationsCount>
424
+ <destinations>
425
+ <item>
426
+ <to>233246314915</to>
427
+ <id>093841e5-578a-41f4-5f5f-2f3910886c12</id>
428
+ <country>Ghana</country>
429
+ <messageCount>1</messageCount>
430
+ <submitDateTime>2021-09-29 21:57:44</submitDateTime>
431
+ <reportDateTime>2021-09-29 21:57:48</reportDateTime>
432
+ <status>
433
+ <id>2110</id>
434
+ <label>DS_DELIVERED</label>
435
+ </status>
436
+ </item>
437
+ </destinations>
438
+ </data>
439
+ </response>
440
+ ```
441
+
442
+ Fields your webhook handler will typically use:
443
+
444
+ | Path | Description |
445
+ | ---- | ----------- |
446
+ | `handshake.label` | `HSHK_OK` when the payload is valid. |
447
+ | `data.batch` | Batch ID from the original send (matches `SendResult.messageId` / `data.batch` from `quickSend`). |
448
+ | `data.destinations[0].to` | Recipient phone number for this delivery event. |
449
+ | `data.destinations[0].id` | Per-destination message ID. |
450
+ | `data.destinations[0].status.id` / `status.label` | Delivery outcome (e.g. `2110` / `DS_DELIVERED`). |
451
+ | `data.destinations[0].submitDateTime` | When the message was submitted to the operator. |
452
+ | `data.destinations[0].reportDateTime` | When the operator reported this delivery status. |
453
+
454
+ Respond with HTTP `200` promptly so SMSOnlineGH does not retry the push. Persist and process the payload asynchronously if your handler does heavy work.
455
+
333
456
  **Example with performance tuning:**
334
457
 
335
458
  ```javascript
@@ -361,6 +484,8 @@ console.log(balance.balance, balance.model);
361
484
 
362
485
  ## Sending messages
363
486
 
487
+ Use **`quickSend()`** for a single recipient. Use **`send()`** when the active gateway supports batch delivery to multiple numbers in one request (`nest`, `route`). Use **`sendPersonalized()`** when each recipient needs a customised message from one template (`nest` only). **`hubtel`** does not support `send()` or `sendPersonalized()` — call `quickSend()` per recipient instead.
488
+
364
489
  ### `QuickSendParams`
365
490
 
366
491
 
@@ -392,7 +517,7 @@ Returns `Promise<SendResult>`. Optional `callback` is invoked with the same resu
392
517
 
393
518
  When `success` is `false`, always read `**error**` — it contains a human-readable reason (provider status codes, API handshake labels, network errors, and so on). For `**nest**`, if the API rejects the send but returns JSON, `**data**` is the **full parsed response body** (not only `response.data`), so you can inspect `handshake` and any provider fields. For HTTP errors, `data` may be the raw response body string. `**statusCode`** is set when the adapter knows the HTTP status (for example nest).
394
519
 
395
- **Debugging:** Set `param.debug: true` when calling `init()` to print request URLs, bodies, and responses to the console. The live test script enables debug for the `nest` platform so you can trace `quickSend` and `getBalance` without changing application code.
520
+ **Debugging:** Set `param.debug: true` when calling `init()` to print request URLs, bodies, and responses to the console. The live test script enables debug for the `nest` platform so you can trace `quickSend`, `send`, and `getBalance` without changing application code.
396
521
 
397
522
  **Example**
398
523
 
@@ -435,6 +560,102 @@ gateway.quickSend(
435
560
  );
436
561
  ```
437
562
 
563
+ ### `SendParams`
564
+
565
+ Same fields as `QuickSendParams`, except `To` is an array of recipients:
566
+
567
+
568
+ | Field | Type | Required | Description |
569
+ | --------- | ----------------------- | -------- | ---------------------------------------------------------------------- |
570
+ | `From` | `string` | yes | Sender ID or label. |
571
+ | `To` | `(string \| number)[]` | yes | One or more recipient MSISDNs or numbers. |
572
+ | `Content` | `string` | yes | Message body (same content to all recipients). |
573
+ | `Type` | `number` | no | Message type; **nest** maps this to request body `type` (default `0`). |
574
+
575
+
576
+ **camelCase:** Pass `{ from, to, content, type? }` where `to` is an array. See `normalizeSendParams`.
577
+
578
+ **Platform behaviour**
579
+
580
+ | Platform | `send()` support | Notes |
581
+ | -------- | ---------------- | ----- |
582
+ | `nest` | Yes | One HTTP request with `destinations: string[]`. `messageId` is the batch ID; per-recipient status is in `data.destinations`. |
583
+ | `route` | Yes | Passes `To` as `number[]` to Route Mobile. `success` is true only when every destination succeeds; partial failures set `error` with a summary. |
584
+ | `hubtel` | No | Throws — use `quickSend()` for each recipient. |
585
+
586
+ ### `send(params, callback?)`
587
+
588
+ Returns `Promise<SendResult>`. Same result shape and callback semantics as `quickSend`. Accepts `SendParams` (PascalCase) or `SendParamsCamel` (`{ from, to: [...], content, type? }`).
589
+
590
+ **Example (`nest`)**
591
+
592
+ ```javascript
593
+ const result = await gateway.send({
594
+ From: 'TEST',
595
+ To: ['0246314915', '0242053072'],
596
+ Content: 'Hello from unismsgateway!',
597
+ Type: 0
598
+ });
599
+
600
+ if (result.success) {
601
+ console.log('Batch:', result.messageId);
602
+ console.log('Destinations:', result.data?.destinations);
603
+ }
604
+ ```
605
+
606
+ ### Personalized bulk send
607
+
608
+ Use **`sendPersonalized()`** when each recipient should receive a **different** message body derived from one template. Placeholders in the template use SMSOnlineGH variable syntax (`{$name}`, `${$balance}`, etc.); per-recipient substitution values are passed as positional arrays in the same order variables appear in the template. See [SMSOnlineGH Personalised Messaging](https://dev.smsonlinegh.com/docs/v5/http/rest/messaging/sms_personalised.html?tabs=json%2Ctabid-1).
609
+
610
+ ### `PersonalizedSendParams`
611
+
612
+ | Field | Type | Required | Description |
613
+ | --------------- | ------------------------- | -------- | --------------------------------------------------------------------------- |
614
+ | `From` | `string` | yes | Sender ID or label. |
615
+ | `Content` | `string` | yes | Message template with `{$variable}` placeholders. |
616
+ | `Destinations` | `PersonalizedRecipient[]` | yes | One entry per recipient with `To` and positional `Values`. |
617
+ | `Type` | `number` | no | Message type; **nest** maps this to request body `type` (default `0`). |
618
+
619
+ Each `PersonalizedRecipient`:
620
+
621
+ | Field | Type | Required | Description |
622
+ | -------- | ----------------------- | -------- | ------------------------------------------------ |
623
+ | `To` | `string \| number` | yes | Recipient MSISDN or number. |
624
+ | `Values` | `(string \| number)[]` | yes | Substitution values in template variable order. |
625
+
626
+ **camelCase:** Pass `{ from, content, destinations: [{ to, values }], type? }`. See `normalizePersonalizedSendParams`.
627
+
628
+ **Platform behaviour**
629
+
630
+ | Platform | `sendPersonalized()` support | Notes |
631
+ | -------- | ---------------------------- | ----- |
632
+ | `nest` | Yes | One HTTP request with `destinations: [{ number, values }]`. Same endpoint as `send()`; `messageId` is the batch ID; per-recipient status is in `data.destinations`. |
633
+ | `route` | No | Throws — use `send()` with the same content for all recipients. |
634
+ | `hubtel` | No | Throws — use `quickSend()` per recipient. |
635
+
636
+ ### `sendPersonalized(params, callback?)`
637
+
638
+ Returns `Promise<SendResult>`. Same result shape and callback semantics as `send`. Accepts `PersonalizedSendParams` (PascalCase) or `PersonalizedSendParamsCamel`.
639
+
640
+ **Example (`nest`)**
641
+
642
+ ```javascript
643
+ const result = await gateway.sendPersonalized({
644
+ From: 'TEST',
645
+ Content: 'Hello {$name}. Your balance is ${$balance}.',
646
+ Destinations: [
647
+ { To: '0246314915', Values: ['Daniel', 560.45] },
648
+ { To: '0242053072', Values: ['Emmanuel', 348.56] }
649
+ ],
650
+ Type: 0
651
+ });
652
+
653
+ if (result.success) {
654
+ console.log('Batch:', result.messageId);
655
+ console.log('Destinations:', result.data?.destinations);
656
+ }
657
+ ```
658
+
438
659
  ---
439
660
 
440
661
  ## Testing (live integration)
@@ -457,7 +678,7 @@ npm run test:live
457
678
 
458
679
  1. **Init** — Builds `param` from your `.env`, calls `init()`, and checks configuration validation.
459
680
  2. **Balance** — For `nest` and `hubtel` only, calls `getBalance()` when the adapter supports it. `route` skips this step.
460
- 3. **Send** — **Opt-in.** By default no SMS is sent. Set `TEST_SEND=true` and the send-related variables listed in [Live integration test environment variables](#live-integration-test-environment-variables).
681
+ 3. **Send** — **Opt-in.** By default no SMS is sent. Set `TEST_SEND=true` and the send-related variables listed in [Live integration test environment variables](#live-integration-test-environment-variables). Use `TEST_SEND_METHOD` to choose `quickSend` (default), `send` (multi-destination on `nest`/`route`), `both`, or `sendPersonalized` (`nest` only). For `hubtel`, the script verifies that `send()` is rejected as expected; for `route`/`hubtel`, `sendPersonalized()` rejection is verified when that method is selected.
461
682
 
462
683
  Full variable reference (selection, per-gateway credentials, live send): [Live integration test environment variables](#live-integration-test-environment-variables). The script loads `.env` via `dotenv` (dev dependency). Exit code is `0` when all checks pass, non-zero if a step fails or no platform is selected.
463
684
 
@@ -475,6 +696,13 @@ Full variable reference (selection, per-gateway credentials, live send): [Live i
475
696
  | `QuickSendParamsInput` | Union: PascalCase `QuickSendParams` or camelCase `QuickSendParamsCamel`. |
476
697
  | `QuickSendParamsCamel` | `{ from, to, content, type? }` for `quickSend`. |
477
698
  | `normalizeQuickSendParams` | Maps input to canonical `QuickSendParams` (throws if body/sender missing). |
699
+ | `SendParamsInput` | Union: PascalCase `SendParams` or camelCase `SendParamsCamel`. |
700
+ | `SendParamsCamel` | `{ from, to: (string\|number)[], content, type? }` for `send`. |
701
+ | `normalizeSendParams` | Maps input to canonical `SendParams` (throws if body/sender/recipients missing). |
702
+ | `PersonalizedSendParamsInput` | Union: PascalCase `PersonalizedSendParams` or camelCase `PersonalizedSendParamsCamel`. |
703
+ | `PersonalizedSendParamsCamel` | `{ from, content, destinations: [{ to, values }], type? }` for `sendPersonalized`. |
704
+ | `PersonalizedRecipient` | `{ To, Values }` — one personalised destination. |
705
+ | `normalizePersonalizedSendParams` | Maps input to canonical `PersonalizedSendParams` (throws if template/sender/destinations missing). |
478
706
 
479
707
 
480
708
  `**smsPlatform` instance methods**
@@ -483,7 +711,9 @@ Full variable reference (selection, per-gateway credentials, live send): [Live i
483
711
  | Method | Returns | Description |
484
712
  | ------------------------------ | --------------------- | -------------------------------------------------------------------------------- |
485
713
  | `init()` | `ISmsGateway` | Returns `this` (facade). |
486
- | `quickSend(params, callback?)` | `Promise<SendResult>` | Normalizes PascalCase or camelCase params, then delegates to the active gateway. |
714
+ | `quickSend(params, callback?)` | `Promise<SendResult>` | Single recipient. Normalizes PascalCase or camelCase params. |
715
+ | `send(params, callback?)` | `Promise<SendResult>` | Multiple recipients (`To` array). Supported on `nest` and `route`; `hubtel` throws. |
716
+ | `sendPersonalized(params, callback?)` | `Promise<SendResult>` | Personalised bulk send (`Destinations` with per-recipient `Values`). Supported on `nest` only; `route` and `hubtel` throw. |
487
717
  | `getGateway()` | `ISmsGateway` | Underlying adapter (for nest: `getBalance()`). |
488
718
 
489
719
 
@@ -493,6 +723,8 @@ Full variable reference (selection, per-gateway credentials, live send): [Live i
493
723
 
494
724
  ### 1.6.0
495
725
 
726
+ - **New:** `sendPersonalized()` for personalised bulk SMS on **`nest`** (SMSOnlineGH template variables + per-destination `values`). **`route`** and **`hubtel`** throw — use `send()` or `quickSend()` instead.
727
+ - **New:** `send()` for multi-destination SMS on **`nest`** (SMSOnlineGH `destinations` array) and **`route`** (Route Mobile `number[]`). `To` is `(string | number)[]`. **`hubtel`** throws — use `quickSend()` per recipient.
496
728
  - **Performance (`nest`):** The `NestSmsGateway` now uses a persistent keep-alive connection pool (`https.Agent`) instead of opening a fresh TCP + TLS connection on every request. Subsequent sends to the same host reuse warm sockets, eliminating the per-call handshake overhead (~100–300 ms per request).
497
729
  - **Reliability (`nest`):** Stale-socket errors (`ECONNRESET`, `ECONNABORTED`, `EPIPE`, `ETIMEDOUT`) that can occur when a pooled socket is reused after the server has closed it are automatically retried on a fresh connection. The default retry count is `1`; configure via `param.retries`.
498
730
  - **Timeout support (`nest`):** Requests that stall mid-flight are now aborted after a configurable deadline (`param.timeout`, default `10 000 ms`) instead of hanging indefinitely.
@@ -1,4 +1,4 @@
1
- import { ISmsGatewayDelegate, QuickSendParams, SendResult } from './types';
1
+ import { ISmsGatewayDelegate, QuickSendParams, SendParams, PersonalizedSendParams, SendResult } from './types';
2
2
  export interface HubtelSmsGatewayConfig {
3
3
  clientId: string;
4
4
  clientSecret: string;
@@ -13,4 +13,6 @@ export declare class HubtelSmsGateway implements ISmsGatewayDelegate {
13
13
  constructor(config: HubtelSmsGatewayConfig);
14
14
  private log;
15
15
  quickSend(params: QuickSendParams, callback?: Function): Promise<SendResult>;
16
+ send(_params: SendParams, _callback?: Function): Promise<SendResult>;
17
+ sendPersonalized(_params: PersonalizedSendParams, _callback?: Function): Promise<SendResult>;
16
18
  }
@@ -67,5 +67,15 @@ class HubtelSmsGateway {
67
67
  return result;
68
68
  });
69
69
  }
70
+ send(_params, _callback) {
71
+ return __awaiter(this, void 0, void 0, function* () {
72
+ throw new Error('Hubtel does not support send() with multiple destinations. Use quickSend() for a single recipient.');
73
+ });
74
+ }
75
+ sendPersonalized(_params, _callback) {
76
+ return __awaiter(this, void 0, void 0, function* () {
77
+ throw new Error('Hubtel does not support sendPersonalized(). Use quickSend() for a single recipient.');
78
+ });
79
+ }
70
80
  }
71
81
  exports.HubtelSmsGateway = HubtelSmsGateway;
package/dist/lib/lib.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { smsPlatform, IgatewaySettings, IgatewayParam, PlatformId, QuickSendParams, QuickSendParamsInput, QuickSendParamsCamel, normalizeQuickSendParams, SendResult, ISmsGateway } from './platform';
1
+ import { smsPlatform, IgatewaySettings, IgatewayParam, PlatformId, QuickSendParams, QuickSendParamsInput, QuickSendParamsCamel, normalizeQuickSendParams, SendParams, SendParamsInput, SendParamsCamel, normalizeSendParams, PersonalizedSendParams, PersonalizedSendParamsInput, PersonalizedSendParamsCamel, PersonalizedRecipient, normalizePersonalizedSendParams, SendResult, ISmsGateway } from './platform';
2
2
  export declare function init(settings: IgatewaySettings): smsPlatform;
3
3
  export declare function getSmsPlatform(): smsPlatform | null;
4
4
  export declare function reset(): void;
5
- export { smsPlatform, IgatewaySettings, IgatewayParam, PlatformId, QuickSendParams, QuickSendParamsInput, QuickSendParamsCamel, normalizeQuickSendParams, SendResult, ISmsGateway };
5
+ export { smsPlatform, IgatewaySettings, IgatewayParam, PlatformId, QuickSendParams, QuickSendParamsInput, QuickSendParamsCamel, normalizeQuickSendParams, SendParams, SendParamsInput, SendParamsCamel, normalizeSendParams, PersonalizedSendParams, PersonalizedSendParamsInput, PersonalizedSendParamsCamel, PersonalizedRecipient, normalizePersonalizedSendParams, SendResult, ISmsGateway };
package/dist/lib/lib.js CHANGED
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeQuickSendParams = exports.smsPlatform = exports.reset = exports.getSmsPlatform = exports.init = void 0;
3
+ exports.normalizePersonalizedSendParams = exports.normalizeSendParams = exports.normalizeQuickSendParams = exports.smsPlatform = exports.reset = exports.getSmsPlatform = exports.init = void 0;
4
4
  const platform_1 = require("./platform");
5
5
  Object.defineProperty(exports, "smsPlatform", { enumerable: true, get: function () { return platform_1.smsPlatform; } });
6
6
  Object.defineProperty(exports, "normalizeQuickSendParams", { enumerable: true, get: function () { return platform_1.normalizeQuickSendParams; } });
7
+ Object.defineProperty(exports, "normalizeSendParams", { enumerable: true, get: function () { return platform_1.normalizeSendParams; } });
8
+ Object.defineProperty(exports, "normalizePersonalizedSendParams", { enumerable: true, get: function () { return platform_1.normalizePersonalizedSendParams; } });
7
9
  let smsPlatformInstance = null;
8
10
  function init(settings) {
9
11
  smsPlatformInstance = new platform_1.smsPlatform(settings);
@@ -1,4 +1,4 @@
1
- import { ISmsGateway, QuickSendParams, SendResult, NestSmsConfig } from './types';
1
+ import { ISmsGateway, QuickSendParams, SendParams, PersonalizedSendParams, SendResult, NestSmsConfig } from './types';
2
2
  export declare class NestSmsGateway implements ISmsGateway {
3
3
  private readonly _cfg;
4
4
  private _agent;
@@ -50,6 +50,12 @@ export declare class NestSmsGateway implements ISmsGateway {
50
50
  */
51
51
  private makeRequest;
52
52
  quickSend(params: QuickSendParams, callback?: Function): Promise<SendResult>;
53
+ send(params: SendParams, callback?: Function): Promise<SendResult>;
54
+ sendPersonalized(params: PersonalizedSendParams, callback?: Function): Promise<SendResult>;
55
+ private _buildMessageRequestBody;
56
+ private _buildPersonalizedMessageRequestBody;
57
+ private _parseNestSendResponse;
58
+ private _sendToDestinations;
53
59
  getBalance(): Promise<{
54
60
  balance: number;
55
61
  model: string;
@@ -58,7 +58,8 @@ class NestSmsGateway {
58
58
  timeout: (_a = config.timeout) !== null && _a !== void 0 ? _a : DEFAULT_TIMEOUT,
59
59
  maxSockets: (_b = config.maxSockets) !== null && _b !== void 0 ? _b : DEFAULT_MAX_SOCKETS,
60
60
  retries: (_c = config.retries) !== null && _c !== void 0 ? _c : DEFAULT_RETRIES,
61
- keepAlive: config.keepAlive !== false
61
+ keepAlive: config.keepAlive !== false,
62
+ deliveryCallback: config.deliveryCallback
62
63
  };
63
64
  this._agent = this._createAgent();
64
65
  }
@@ -211,48 +212,117 @@ class NestSmsGateway {
211
212
  });
212
213
  }
213
214
  quickSend(params, callback) {
214
- var _a, _b, _c, _d, _e;
215
215
  return __awaiter(this, void 0, void 0, function* () {
216
- const requestBody = {
217
- text: params.Content,
218
- type: (_a = params.Type) !== null && _a !== void 0 ? _a : 0,
219
- sender: params.From,
220
- destinations: [String(params.To)]
221
- };
222
- this.log('quickSend params:', JSON.stringify(params));
216
+ return this._sendToDestinations([String(params.To)], params.From, params.Content, params.Type, callback, 'quickSend', params);
217
+ });
218
+ }
219
+ send(params, callback) {
220
+ return __awaiter(this, void 0, void 0, function* () {
221
+ return this._sendToDestinations(params.To.map(String), params.From, params.Content, params.Type, callback, 'send', params);
222
+ });
223
+ }
224
+ sendPersonalized(params, callback) {
225
+ return __awaiter(this, void 0, void 0, function* () {
226
+ const requestBody = this._buildPersonalizedMessageRequestBody(params.Destinations, params.From, params.Content, params.Type);
227
+ this.log('sendPersonalized params:', JSON.stringify(params));
223
228
  let result;
224
229
  try {
225
230
  const { statusCode, body: response } = yield this.makeRequest('message/sms/send', requestBody);
226
- const handshakeId = (_b = response.handshake) === null || _b === void 0 ? void 0 : _b.id;
227
- const handshakeLabel = (_c = response.handshake) === null || _c === void 0 ? void 0 : _c.label;
228
- const handshakeOk = Number(handshakeId) === 0;
229
- const responseData = (_d = response.data) !== null && _d !== void 0 ? _d : null;
230
- const batchId = responseData && typeof responseData === 'object'
231
- ? responseData.batch
232
- : undefined;
233
- const firstDest = responseData && typeof responseData === 'object'
234
- ? (_e = responseData.destinations) === null || _e === void 0 ? void 0 : _e[0]
235
- : undefined;
236
- let errorMsg;
237
- if (!handshakeOk) {
238
- if (handshakeLabel) {
239
- errorMsg = `API Error [code ${handshakeId}]: ${handshakeLabel}`;
240
- }
241
- else if (handshakeId !== undefined && handshakeId !== null) {
242
- errorMsg = `API Error: handshake code=${handshakeId}`;
243
- }
244
- else {
245
- errorMsg = `Unexpected API response: ${JSON.stringify(response)}`;
246
- }
247
- }
231
+ result = this._parseNestSendResponse(statusCode, response);
232
+ }
233
+ catch (error) {
234
+ const httpErr = error;
235
+ const errorMessage = error instanceof Error ? error.message : String(error);
248
236
  result = {
249
- success: handshakeOk,
250
- messageId: batchId || (firstDest === null || firstDest === void 0 ? void 0 : firstDest.id),
251
- data: handshakeOk ? responseData : response,
252
- error: errorMsg,
253
- statusCode
237
+ success: false,
238
+ error: errorMessage,
239
+ statusCode: httpErr.statusCode,
240
+ data: httpErr.rawBody !== undefined ? httpErr.rawBody : null
254
241
  };
255
242
  }
243
+ this.log('sendPersonalized result:', JSON.stringify(result));
244
+ if (callback) {
245
+ callback(result);
246
+ }
247
+ return result;
248
+ });
249
+ }
250
+ _buildMessageRequestBody(destinations, from, content, type) {
251
+ var _a, _b;
252
+ const requestBody = {
253
+ text: content,
254
+ type: type !== null && type !== void 0 ? type : 0,
255
+ sender: from,
256
+ destinations
257
+ };
258
+ if ((_a = this._cfg.deliveryCallback) === null || _a === void 0 ? void 0 : _a.url) {
259
+ requestBody.callback = {
260
+ url: this._cfg.deliveryCallback.url,
261
+ accept: (_b = this._cfg.deliveryCallback.accept) !== null && _b !== void 0 ? _b : 'application/json'
262
+ };
263
+ }
264
+ return requestBody;
265
+ }
266
+ _buildPersonalizedMessageRequestBody(destinations, from, content, type) {
267
+ var _a, _b;
268
+ const requestBody = {
269
+ text: content,
270
+ type: type !== null && type !== void 0 ? type : 0,
271
+ sender: from,
272
+ destinations: destinations.map(d => ({
273
+ number: String(d.To),
274
+ values: d.Values
275
+ }))
276
+ };
277
+ if ((_a = this._cfg.deliveryCallback) === null || _a === void 0 ? void 0 : _a.url) {
278
+ requestBody.callback = {
279
+ url: this._cfg.deliveryCallback.url,
280
+ accept: (_b = this._cfg.deliveryCallback.accept) !== null && _b !== void 0 ? _b : 'application/json'
281
+ };
282
+ }
283
+ return requestBody;
284
+ }
285
+ _parseNestSendResponse(statusCode, response) {
286
+ var _a, _b, _c, _d;
287
+ const handshakeId = (_a = response.handshake) === null || _a === void 0 ? void 0 : _a.id;
288
+ const handshakeLabel = (_b = response.handshake) === null || _b === void 0 ? void 0 : _b.label;
289
+ const handshakeOk = Number(handshakeId) === 0;
290
+ const responseData = (_c = response.data) !== null && _c !== void 0 ? _c : null;
291
+ const batchId = responseData && typeof responseData === 'object'
292
+ ? responseData.batch
293
+ : undefined;
294
+ const firstDest = responseData && typeof responseData === 'object'
295
+ ? (_d = responseData.destinations) === null || _d === void 0 ? void 0 : _d[0]
296
+ : undefined;
297
+ let errorMsg;
298
+ if (!handshakeOk) {
299
+ if (handshakeLabel) {
300
+ errorMsg = `API Error [code ${handshakeId}]: ${handshakeLabel}`;
301
+ }
302
+ else if (handshakeId !== undefined && handshakeId !== null) {
303
+ errorMsg = `API Error: handshake code=${handshakeId}`;
304
+ }
305
+ else {
306
+ errorMsg = `Unexpected API response: ${JSON.stringify(response)}`;
307
+ }
308
+ }
309
+ return {
310
+ success: handshakeOk,
311
+ messageId: batchId || (firstDest === null || firstDest === void 0 ? void 0 : firstDest.id),
312
+ data: handshakeOk ? responseData : response,
313
+ error: errorMsg,
314
+ statusCode
315
+ };
316
+ }
317
+ _sendToDestinations(destinations, from, content, type, callback, logLabel, logParams) {
318
+ return __awaiter(this, void 0, void 0, function* () {
319
+ const requestBody = this._buildMessageRequestBody(destinations, from, content, type);
320
+ this.log(`${logLabel} params:`, JSON.stringify(logParams));
321
+ let result;
322
+ try {
323
+ const { statusCode, body: response } = yield this.makeRequest('message/sms/send', requestBody);
324
+ result = this._parseNestSendResponse(statusCode, response);
325
+ }
256
326
  catch (error) {
257
327
  const httpErr = error;
258
328
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -263,7 +333,7 @@ class NestSmsGateway {
263
333
  data: httpErr.rawBody !== undefined ? httpErr.rawBody : null
264
334
  };
265
335
  }
266
- this.log('quickSend result:', JSON.stringify(result));
336
+ this.log(`${logLabel} result:`, JSON.stringify(result));
267
337
  if (callback) {
268
338
  callback(result);
269
339
  }
@@ -1,13 +1,16 @@
1
- import { IgatewaySettings, IgatewayParam, ISmsGateway, ISmsGatewayDelegate, QuickSendParamsInput, SendResult } from './types';
1
+ import { IgatewaySettings, IgatewayParam, ISmsGateway, ISmsGatewayDelegate, QuickSendParamsInput, SendParamsInput, SendResult, PersonalizedSendParamsInput } from './types';
2
2
  export * from './types';
3
3
  export declare class smsPlatform implements ISmsGateway {
4
4
  private _settings;
5
5
  private _gateway;
6
6
  constructor(settings: IgatewaySettings);
7
7
  private validateSettings;
8
+ private validateNestDeliveryCallback;
8
9
  private createGateway;
9
10
  init(): ISmsGateway;
10
11
  quickSend(param: QuickSendParamsInput, callback?: Function): Promise<SendResult>;
12
+ send(param: SendParamsInput, callback?: Function): Promise<SendResult>;
13
+ sendPersonalized(param: PersonalizedSendParamsInput, callback?: Function): Promise<SendResult>;
11
14
  getGateway(): ISmsGatewayDelegate;
12
15
  }
13
16
  export { IgatewaySettings, IgatewayParam };
@@ -43,6 +43,21 @@ class smsPlatform {
43
43
  if (config.requiresUsernamePassword && (!param.username || !param.password)) {
44
44
  throw new Error(`Platform '${settings.platformId}' requires 'username' and 'password' in param`);
45
45
  }
46
+ if (settings.platformId === 'nest' && param.deliveryCallback) {
47
+ this.validateNestDeliveryCallback(param.deliveryCallback);
48
+ }
49
+ }
50
+ validateNestDeliveryCallback(deliveryCallback) {
51
+ const url = deliveryCallback.url == null ? '' : String(deliveryCallback.url).trim();
52
+ if (url === '') {
53
+ throw new Error("Platform 'nest': deliveryCallback.url must be a non-empty string");
54
+ }
55
+ const accept = deliveryCallback.accept;
56
+ if (accept !== undefined &&
57
+ accept !== 'application/json' &&
58
+ accept !== 'application/xml') {
59
+ throw new Error("Platform 'nest': deliveryCallback.accept must be 'application/json' or 'application/xml'");
60
+ }
46
61
  }
47
62
  createGateway() {
48
63
  const { platformId, param } = this._settings;
@@ -71,7 +86,13 @@ class smsPlatform {
71
86
  timeout: param.timeout,
72
87
  maxSockets: param.maxSockets,
73
88
  retries: param.retries,
74
- keepAlive: param.keepAlive
89
+ keepAlive: param.keepAlive,
90
+ deliveryCallback: param.deliveryCallback
91
+ ? {
92
+ url: param.deliveryCallback.url.trim(),
93
+ accept: param.deliveryCallback.accept
94
+ }
95
+ : undefined
75
96
  });
76
97
  default:
77
98
  throw new Error(`Unsupported platform: ${platformId}`);
@@ -87,6 +108,20 @@ class smsPlatform {
87
108
  const normalized = (0, types_1.normalizeQuickSendParams)(param);
88
109
  return this._gateway.quickSend(normalized, callback);
89
110
  }
111
+ send(param, callback) {
112
+ if (!this._gateway) {
113
+ throw new Error('Gateway not initialized. Call init() first.');
114
+ }
115
+ const normalized = (0, types_1.normalizeSendParams)(param);
116
+ return this._gateway.send(normalized, callback);
117
+ }
118
+ sendPersonalized(param, callback) {
119
+ if (!this._gateway) {
120
+ throw new Error('Gateway not initialized. Call init() first.');
121
+ }
122
+ const normalized = (0, types_1.normalizePersonalizedSendParams)(param);
123
+ return this._gateway.sendPersonalized(normalized, callback);
124
+ }
90
125
  getGateway() {
91
126
  return this._gateway;
92
127
  }
@@ -1,4 +1,4 @@
1
- import { ISmsGatewayDelegate, QuickSendParams, SendResult } from './types';
1
+ import { ISmsGatewayDelegate, QuickSendParams, SendParams, PersonalizedSendParams, SendResult } from './types';
2
2
  export interface RouteSmsGatewayConfig {
3
3
  host: string;
4
4
  username: string;
@@ -19,4 +19,7 @@ export declare class RouteSmsGateway implements ISmsGatewayDelegate {
19
19
  constructor(config: RouteSmsGatewayConfig);
20
20
  private log;
21
21
  quickSend(params: QuickSendParams, callback?: Function): Promise<SendResult>;
22
+ send(params: SendParams, callback?: Function): Promise<SendResult>;
23
+ private _send;
24
+ sendPersonalized(_params: PersonalizedSendParams, _callback?: Function): Promise<SendResult>;
22
25
  }
@@ -19,6 +19,9 @@ function toRouteDestination(to) {
19
19
  const n = Number(digits);
20
20
  return Number.isNaN(n) ? 0 : n;
21
21
  }
22
+ function toRouteDestinations(to) {
23
+ return to.map(toRouteDestination);
24
+ }
22
25
  /**
23
26
  * Adapts routemobilesms to {@link ISmsGatewayDelegate}.
24
27
  *
@@ -45,21 +48,31 @@ class RouteSmsGateway {
45
48
  }
46
49
  }
47
50
  quickSend(params, callback) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ return this._send([toRouteDestination(params.To)], params.From, params.Content, params.Type, callback, 'quickSend', params);
53
+ });
54
+ }
55
+ send(params, callback) {
56
+ return __awaiter(this, void 0, void 0, function* () {
57
+ return this._send(toRouteDestinations(params.To), params.From, params.Content, params.Type, callback, 'send', params);
58
+ });
59
+ }
60
+ _send(destinations, from, content, type, callback, logLabel, logParams) {
48
61
  var _a, _b;
49
62
  return __awaiter(this, void 0, void 0, function* () {
50
- this.log('quickSend params:', JSON.stringify(params));
63
+ this.log(`${logLabel} params:`, JSON.stringify(logParams));
51
64
  const sendParams = {
52
- From: params.From,
53
- To: toRouteDestination(params.To),
54
- Content: params.Content
65
+ From: from,
66
+ To: destinations.length === 1 ? destinations[0] : destinations,
67
+ Content: content
55
68
  };
56
- if (params.Type !== undefined) {
57
- sendParams.config = { type: params.Type, dlr: 0 };
69
+ if (type !== undefined) {
70
+ sendParams.config = { type, dlr: 0 };
58
71
  }
59
72
  let result;
60
73
  try {
61
74
  const raw = yield routemobilesms_1.routeSms.sendAsync(sendParams);
62
- this.log('quickSend raw response:', JSON.stringify(raw));
75
+ this.log(`${logLabel} raw response:`, JSON.stringify(raw));
63
76
  if (raw === undefined || raw === null) {
64
77
  result = {
65
78
  success: false,
@@ -68,15 +81,18 @@ class RouteSmsGateway {
68
81
  };
69
82
  }
70
83
  else if (Array.isArray(raw) && raw.length > 0) {
84
+ const allOk = raw.every(item => item.status === 'successful');
71
85
  const first = raw[0];
72
- const ok = first.status === 'successful';
86
+ const failed = raw.filter(item => item.status !== 'successful');
73
87
  result = {
74
- success: ok,
88
+ success: allOk,
75
89
  messageId: first.id,
76
90
  data: raw,
77
- error: ok
91
+ error: allOk
78
92
  ? undefined
79
- : `Route SMS Error [${(_a = first.code) !== null && _a !== void 0 ? _a : 'unknown'}]: ${(_b = first.message) !== null && _b !== void 0 ? _b : 'Send failed'}`
93
+ : failed.length === raw.length
94
+ ? `Route SMS Error [${(_a = first.code) !== null && _a !== void 0 ? _a : 'unknown'}]: ${(_b = first.message) !== null && _b !== void 0 ? _b : 'Send failed'}`
95
+ : `Route SMS: ${failed.length} of ${raw.length} destinations failed`
80
96
  };
81
97
  }
82
98
  else {
@@ -89,19 +105,24 @@ class RouteSmsGateway {
89
105
  }
90
106
  catch (error) {
91
107
  const errorMessage = error instanceof Error ? error.message : String(error);
92
- this.log('quickSend error:', errorMessage);
108
+ this.log(`${logLabel} error:`, errorMessage);
93
109
  result = {
94
110
  success: false,
95
111
  error: errorMessage,
96
112
  data: null
97
113
  };
98
114
  }
99
- this.log('quickSend result:', JSON.stringify(result));
115
+ this.log(`${logLabel} result:`, JSON.stringify(result));
100
116
  if (callback) {
101
117
  callback(result);
102
118
  }
103
119
  return result;
104
120
  });
105
121
  }
122
+ sendPersonalized(_params, _callback) {
123
+ return __awaiter(this, void 0, void 0, function* () {
124
+ throw new Error('Route Mobile does not support sendPersonalized(). Use send() with the same content for all recipients.');
125
+ });
126
+ }
106
127
  }
107
128
  exports.RouteSmsGateway = RouteSmsGateway;
@@ -17,11 +17,60 @@ export interface QuickSendParamsCamel {
17
17
  type?: number;
18
18
  }
19
19
  export declare type QuickSendParamsInput = QuickSendParams | QuickSendParamsCamel;
20
+ export interface SendParams {
21
+ From: string;
22
+ To: (string | number)[];
23
+ Content: string;
24
+ Type?: number;
25
+ }
26
+ /**
27
+ * camelCase variant of {@link SendParams}.
28
+ */
29
+ export interface SendParamsCamel {
30
+ from: string;
31
+ to: (string | number)[];
32
+ content: string;
33
+ type?: number;
34
+ }
35
+ export declare type SendParamsInput = SendParams | SendParamsCamel;
36
+ export interface PersonalizedRecipient {
37
+ To: string | number;
38
+ Values: (string | number)[];
39
+ }
40
+ export interface PersonalizedSendParams {
41
+ From: string;
42
+ Content: string;
43
+ Destinations: PersonalizedRecipient[];
44
+ Type?: number;
45
+ }
46
+ /**
47
+ * camelCase variant of {@link PersonalizedSendParams}.
48
+ */
49
+ export interface PersonalizedSendParamsCamel {
50
+ from: string;
51
+ content: string;
52
+ destinations: {
53
+ to: string | number;
54
+ values: (string | number)[];
55
+ }[];
56
+ type?: number;
57
+ }
58
+ export declare type PersonalizedSendParamsInput = PersonalizedSendParams | PersonalizedSendParamsCamel;
20
59
  /**
21
60
  * Maps PascalCase or camelCase quick-send fields to {@link QuickSendParams}.
22
61
  * PascalCase wins when both are present.
23
62
  */
24
63
  export declare function normalizeQuickSendParams(params: QuickSendParamsInput): QuickSendParams;
64
+ /**
65
+ * Maps PascalCase or camelCase multi-destination send fields to {@link SendParams}.
66
+ * PascalCase wins when both are present.
67
+ */
68
+ export declare function normalizeSendParams(params: SendParamsInput): SendParams;
69
+ /**
70
+ * Maps PascalCase or camelCase personalised bulk-send fields to
71
+ * {@link PersonalizedSendParams}. PascalCase wins when both are present.
72
+ */
73
+ export declare function normalizePersonalizedSendParams(params: PersonalizedSendParamsInput): PersonalizedSendParams;
25
74
  export interface SendResult {
26
75
  success: boolean;
27
76
  messageId?: string;
@@ -65,6 +114,13 @@ export interface IgatewayParam {
65
114
  * `retries` setting. Default: true.
66
115
  */
67
116
  keepAlive?: boolean;
117
+ /** nest only — SMSOnlineGH delivery push webhook */
118
+ deliveryCallback?: NestDeliveryCallbackConfig;
119
+ }
120
+ export declare type NestDeliveryCallbackAccept = 'application/json' | 'application/xml';
121
+ export interface NestDeliveryCallbackConfig {
122
+ url: string;
123
+ accept?: NestDeliveryCallbackAccept;
68
124
  }
69
125
  export interface IgatewaySettings {
70
126
  platformId: PlatformId;
@@ -73,6 +129,8 @@ export interface IgatewaySettings {
73
129
  /** Send surface used by the facade; third-party SDKs may omit `init()`. */
74
130
  export interface ISmsGatewayDelegate {
75
131
  quickSend(params: QuickSendParams, callback?: Function): Promise<SendResult>;
132
+ send(params: SendParams, callback?: Function): Promise<SendResult>;
133
+ sendPersonalized(params: PersonalizedSendParams, callback?: Function): Promise<SendResult>;
76
134
  getBalance?(): Promise<any>;
77
135
  }
78
136
  export interface ISmsGateway extends ISmsGatewayDelegate {
@@ -91,6 +149,8 @@ export interface NestSmsConfig {
91
149
  retries?: number;
92
150
  /** Enable HTTP keep-alive connection pooling. Default: true. */
93
151
  keepAlive?: boolean;
152
+ /** SMSOnlineGH delivery push webhook; included in every send when set. */
153
+ deliveryCallback?: NestDeliveryCallbackConfig;
94
154
  }
95
155
  export interface NestSendResponse {
96
156
  handshake: {
package/dist/lib/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeQuickSendParams = void 0;
3
+ exports.normalizePersonalizedSendParams = exports.normalizeSendParams = exports.normalizeQuickSendParams = void 0;
4
4
  /**
5
5
  * Maps PascalCase or camelCase quick-send fields to {@link QuickSendParams}.
6
6
  * PascalCase wins when both are present.
@@ -40,3 +40,115 @@ function normalizeQuickSendParams(params) {
40
40
  };
41
41
  }
42
42
  exports.normalizeQuickSendParams = normalizeQuickSendParams;
43
+ /**
44
+ * Maps PascalCase or camelCase multi-destination send fields to {@link SendParams}.
45
+ * PascalCase wins when both are present.
46
+ */
47
+ function normalizeSendParams(params) {
48
+ var _a, _b, _c, _d;
49
+ const p = params;
50
+ const from = (_a = p.From) !== null && _a !== void 0 ? _a : p.from;
51
+ const to = (_b = p.To) !== null && _b !== void 0 ? _b : p.to;
52
+ const content = (_c = p.Content) !== null && _c !== void 0 ? _c : p.content;
53
+ const type = (_d = p.Type) !== null && _d !== void 0 ? _d : p.type;
54
+ const contentStr = content == null ? '' : String(content);
55
+ const trimmedBody = contentStr.trim();
56
+ if (trimmedBody === '') {
57
+ throw new Error('send: message body is missing. Pass Content or content with a non-empty string.');
58
+ }
59
+ const fromStr = from == null ? '' : String(from).trim();
60
+ if (fromStr === '') {
61
+ throw new Error('send: sender is missing. Pass From or from with a non-empty string.');
62
+ }
63
+ if (!Array.isArray(to) || to.length === 0) {
64
+ throw new Error('send: at least one recipient is required. Pass To or to as a non-empty array.');
65
+ }
66
+ const recipients = [];
67
+ for (let i = 0; i < to.length; i++) {
68
+ const item = to[i];
69
+ if (item === null || item === undefined) {
70
+ throw new Error(`send: recipient at index ${i} is missing.`);
71
+ }
72
+ if (typeof item === 'number') {
73
+ recipients.push(item);
74
+ continue;
75
+ }
76
+ const trimmed = String(item).trim();
77
+ if (trimmed === '') {
78
+ throw new Error(`send: recipient at index ${i} is empty.`);
79
+ }
80
+ recipients.push(trimmed);
81
+ }
82
+ let typeNum;
83
+ if (type !== undefined && type !== null) {
84
+ const n = Number(type);
85
+ if (!Number.isNaN(n)) {
86
+ typeNum = n;
87
+ }
88
+ }
89
+ return {
90
+ From: fromStr,
91
+ To: recipients,
92
+ Content: trimmedBody,
93
+ Type: typeNum
94
+ };
95
+ }
96
+ exports.normalizeSendParams = normalizeSendParams;
97
+ /**
98
+ * Maps PascalCase or camelCase personalised bulk-send fields to
99
+ * {@link PersonalizedSendParams}. PascalCase wins when both are present.
100
+ */
101
+ function normalizePersonalizedSendParams(params) {
102
+ var _a, _b, _c, _d, _e, _f;
103
+ const p = params;
104
+ const from = (_a = p.From) !== null && _a !== void 0 ? _a : p.from;
105
+ const content = (_b = p.Content) !== null && _b !== void 0 ? _b : p.content;
106
+ const destinations = (_c = p.Destinations) !== null && _c !== void 0 ? _c : p.destinations;
107
+ const type = (_d = p.Type) !== null && _d !== void 0 ? _d : p.type;
108
+ const contentStr = content == null ? '' : String(content);
109
+ const trimmedBody = contentStr.trim();
110
+ if (trimmedBody === '') {
111
+ throw new Error('sendPersonalized: message template is missing. Pass Content or content with a non-empty string.');
112
+ }
113
+ const fromStr = from == null ? '' : String(from).trim();
114
+ if (fromStr === '') {
115
+ throw new Error('sendPersonalized: sender is missing. Pass From or from with a non-empty string.');
116
+ }
117
+ if (!Array.isArray(destinations) || destinations.length === 0) {
118
+ throw new Error('sendPersonalized: at least one destination is required. Pass Destinations or destinations as a non-empty array.');
119
+ }
120
+ const normalizedDestinations = [];
121
+ for (let i = 0; i < destinations.length; i++) {
122
+ const item = destinations[i];
123
+ const to = (_e = item.To) !== null && _e !== void 0 ? _e : item.to;
124
+ const values = (_f = item.Values) !== null && _f !== void 0 ? _f : item.values;
125
+ if (to === null || to === undefined) {
126
+ throw new Error(`sendPersonalized: destination at index ${i} is missing To or to.`);
127
+ }
128
+ const toVal = typeof to === 'number' ? to : String(to).trim();
129
+ if (typeof toVal === 'string' && toVal === '') {
130
+ throw new Error(`sendPersonalized: destination at index ${i} has an empty To or to.`);
131
+ }
132
+ if (!Array.isArray(values)) {
133
+ throw new Error(`sendPersonalized: destination at index ${i} requires Values or values as an array.`);
134
+ }
135
+ normalizedDestinations.push({
136
+ To: toVal,
137
+ Values: values
138
+ });
139
+ }
140
+ let typeNum;
141
+ if (type !== undefined && type !== null) {
142
+ const n = Number(type);
143
+ if (!Number.isNaN(n)) {
144
+ typeNum = n;
145
+ }
146
+ }
147
+ return {
148
+ From: fromStr,
149
+ Content: trimmedBody,
150
+ Destinations: normalizedDestinations,
151
+ Type: typeNum
152
+ };
153
+ }
154
+ exports.normalizePersonalizedSendParams = normalizePersonalizedSendParams;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unismsgateway",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "A unified SMS gateway library that brings access to multiple SMS gateways under a single API",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",