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 +244 -12
- package/dist/lib/hubtel-gateway.d.ts +3 -1
- package/dist/lib/hubtel-gateway.js +10 -0
- package/dist/lib/lib.d.ts +2 -2
- package/dist/lib/lib.js +3 -1
- package/dist/lib/nest-gateway.d.ts +7 -1
- package/dist/lib/nest-gateway.js +107 -37
- package/dist/lib/platform.d.ts +4 -1
- package/dist/lib/platform.js +36 -1
- package/dist/lib/route-gateway.d.ts +4 -1
- package/dist/lib/route-gateway.js +34 -13
- package/dist/lib/types.d.ts +60 -0
- package/dist/lib/types.js +113 -1
- package/package.json +1 -1
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
|
|
197
|
-
|
|
|
198
|
-
| `TEST_SEND`
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
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`
|
|
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
|
|
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;
|
package/dist/lib/nest-gateway.js
CHANGED
|
@@ -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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
const
|
|
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:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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(
|
|
336
|
+
this.log(`${logLabel} result:`, JSON.stringify(result));
|
|
267
337
|
if (callback) {
|
|
268
338
|
callback(result);
|
|
269
339
|
}
|
package/dist/lib/platform.d.ts
CHANGED
|
@@ -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 };
|
package/dist/lib/platform.js
CHANGED
|
@@ -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(
|
|
63
|
+
this.log(`${logLabel} params:`, JSON.stringify(logParams));
|
|
51
64
|
const sendParams = {
|
|
52
|
-
From:
|
|
53
|
-
To:
|
|
54
|
-
Content:
|
|
65
|
+
From: from,
|
|
66
|
+
To: destinations.length === 1 ? destinations[0] : destinations,
|
|
67
|
+
Content: content
|
|
55
68
|
};
|
|
56
|
-
if (
|
|
57
|
-
sendParams.config = { type
|
|
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(
|
|
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
|
|
86
|
+
const failed = raw.filter(item => item.status !== 'successful');
|
|
73
87
|
result = {
|
|
74
|
-
success:
|
|
88
|
+
success: allOk,
|
|
75
89
|
messageId: first.id,
|
|
76
90
|
data: raw,
|
|
77
|
-
error:
|
|
91
|
+
error: allOk
|
|
78
92
|
? undefined
|
|
79
|
-
:
|
|
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(
|
|
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(
|
|
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;
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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