townkrier-termii 1.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +296 -0
  3. package/__tests__/termii.channel.spec.ts +64 -0
  4. package/dist/core/index.d.ts +2 -0
  5. package/dist/core/index.d.ts.map +1 -0
  6. package/dist/core/index.js +18 -0
  7. package/dist/core/index.js.map +1 -0
  8. package/dist/core/termii-channel.d.ts +13 -0
  9. package/dist/core/termii-channel.d.ts.map +1 -0
  10. package/dist/core/termii-channel.js +94 -0
  11. package/dist/core/termii-channel.js.map +1 -0
  12. package/dist/core/termii-channel.spec.d.ts +2 -0
  13. package/dist/core/termii-channel.spec.d.ts.map +1 -0
  14. package/dist/core/termii-channel.spec.js +65 -0
  15. package/dist/core/termii-channel.spec.js.map +1 -0
  16. package/dist/core/termii.mapper.d.ts +9 -0
  17. package/dist/core/termii.mapper.d.ts.map +1 -0
  18. package/dist/core/termii.mapper.js +62 -0
  19. package/dist/core/termii.mapper.js.map +1 -0
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +20 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/interfaces/index.d.ts +19 -0
  25. package/dist/interfaces/index.d.ts.map +1 -0
  26. package/dist/interfaces/index.js +3 -0
  27. package/dist/interfaces/index.js.map +1 -0
  28. package/dist/types/index.d.ts +8 -0
  29. package/dist/types/index.d.ts.map +1 -0
  30. package/dist/types/index.js +3 -0
  31. package/dist/types/index.js.map +1 -0
  32. package/jest.config.js +8 -0
  33. package/package.json +39 -0
  34. package/src/core/index.ts +1 -0
  35. package/src/core/termii-channel.spec.ts +80 -0
  36. package/src/core/termii-channel.ts +146 -0
  37. package/src/core/termii.mapper.ts +77 -0
  38. package/src/index.ts +4 -0
  39. package/src/interfaces/index.ts +29 -0
  40. package/src/types/index.ts +26 -0
  41. package/tsconfig.json +12 -0
  42. package/tsconfig.spec.json +9 -0
  43. package/tsconfig.tsbuildinfo +1 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # 1.0.0-alpha.5 (2026-02-02)
7
+
8
+ ### Bug Fixes
9
+
10
+ - Update publish workflow to enforce frozen lockfile and adjust publish commands ([ae73a10](https://github.com/jeremiah-olisa/townkrier/commit/ae73a10c7629f54b0e2de4b1f648e6e721125dbc))
11
+
12
+ ### Features
13
+
14
+ - add bulk SMS support to Termii channel by enabling multiple recipients and dynamic endpoint selection, with improved error handling. ([d45121a](https://github.com/jeremiah-olisa/townkrier/commit/d45121a3857f93170c1566910eb3ab30e70f2583))
15
+ - Add Firebase-backed push notification handling to the Expo example and improve Termii channel error message parsing. ([6959e52](https://github.com/jeremiah-olisa/townkrier/commit/6959e5258109c6812532b3ee1f0aa979583f000e))
16
+ - Implement a new logger system and refactor the Resend email channel with a dedicated mapper and enhanced response handling. ([4517e6e](https://github.com/jeremiah-olisa/townkrier/commit/4517e6e76bd07cf381ad1c1b35ada4b0ab829a9e))
17
+ - Introduce FCM and Termii channels, refactor FCM with mappers, and update playground usage. ([cb5b556](https://github.com/jeremiah-olisa/townkrier/commit/cb5b556ee8d7e70f9c4587eaac24f6f67e3fcad4))
18
+
19
+ # 1.0.0-alpha.4 (2026-01-28)
20
+
21
+ ### Features
22
+
23
+ - add bulk SMS support to Termii channel by enabling multiple recipients and dynamic endpoint selection, with improved error handling. ([d45121a](https://github.com/jeremiah-olisa/townkrier/commit/d45121a3857f93170c1566910eb3ab30e70f2583))
24
+ - Add Firebase-backed push notification handling to the Expo example and improve Termii channel error message parsing. ([6959e52](https://github.com/jeremiah-olisa/townkrier/commit/6959e5258109c6812532b3ee1f0aa979583f000e))
25
+ - Implement a new logger system and refactor the Resend email channel with a dedicated mapper and enhanced response handling. ([4517e6e](https://github.com/jeremiah-olisa/townkrier/commit/4517e6e76bd07cf381ad1c1b35ada4b0ab829a9e))
26
+ - Introduce FCM and Termii channels, refactor FCM with mappers, and update playground usage. ([cb5b556](https://github.com/jeremiah-olisa/townkrier/commit/cb5b556ee8d7e70f9c4587eaac24f6f67e3fcad4))
27
+
28
+ # 1.0.0-alpha.3 (2026-01-28)
29
+
30
+ ### Features
31
+
32
+ - add bulk SMS support to Termii channel by enabling multiple recipients and dynamic endpoint selection, with improved error handling. ([d45121a](https://github.com/jeremiah-olisa/townkrier/commit/d45121a3857f93170c1566910eb3ab30e70f2583))
33
+ - Add Firebase-backed push notification handling to the Expo example and improve Termii channel error message parsing. ([6959e52](https://github.com/jeremiah-olisa/townkrier/commit/6959e5258109c6812532b3ee1f0aa979583f000e))
34
+ - Implement a new logger system and refactor the Resend email channel with a dedicated mapper and enhanced response handling. ([4517e6e](https://github.com/jeremiah-olisa/townkrier/commit/4517e6e76bd07cf381ad1c1b35ada4b0ab829a9e))
35
+ - Introduce FCM and Termii channels, refactor FCM with mappers, and update playground usage. ([cb5b556](https://github.com/jeremiah-olisa/townkrier/commit/cb5b556ee8d7e70f9c4587eaac24f6f67e3fcad4))
36
+
37
+ # 1.0.0-alpha.2 (2026-01-28)
38
+
39
+ ### Features
40
+
41
+ - add bulk SMS support to Termii channel by enabling multiple recipients and dynamic endpoint selection, with improved error handling. ([d45121a](https://github.com/jeremiah-olisa/townkrier/commit/d45121a3857f93170c1566910eb3ab30e70f2583))
42
+ - Add Firebase-backed push notification handling to the Expo example and improve Termii channel error message parsing. ([6959e52](https://github.com/jeremiah-olisa/townkrier/commit/6959e5258109c6812532b3ee1f0aa979583f000e))
43
+ - Implement a new logger system and refactor the Resend email channel with a dedicated mapper and enhanced response handling. ([4517e6e](https://github.com/jeremiah-olisa/townkrier/commit/4517e6e76bd07cf381ad1c1b35ada4b0ab829a9e))
44
+ - Introduce FCM and Termii channels, refactor FCM with mappers, and update playground usage. ([cb5b556](https://github.com/jeremiah-olisa/townkrier/commit/cb5b556ee8d7e70f9c4587eaac24f6f67e3fcad4))
package/README.md ADDED
@@ -0,0 +1,296 @@
1
+ # townkrier-termii
2
+
3
+ Termii SMS adapter for the TownKrier notification system.
4
+
5
+ ## Features
6
+
7
+ - 📱 SMS notifications via Termii API
8
+ - 🌍 Support for multiple countries and regions
9
+ - 🔄 Automatic retry with exponential backoff
10
+ - 📝 Message templates and personalization
11
+ - 🔍 Delivery status tracking
12
+ - 🛡️ Error handling and validation
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install townkrier-termii townkrier-core
18
+ # or
19
+ pnpm add townkrier-termii townkrier-core
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import { NotificationManager, NotificationChannel } from 'townkrier-core';
26
+ import { createTermiiChannel } from 'townkrier-termii';
27
+
28
+ // Configure the manager with Termii channel
29
+ const manager = new NotificationManager({
30
+ defaultChannel: 'sms-termii',
31
+ channels: [
32
+ {
33
+ name: 'sms-termii',
34
+ enabled: true,
35
+ config: {
36
+ apiKey: process.env.TERMII_API_KEY,
37
+ senderId: 'YourApp', // Your registered sender ID
38
+ channel: 'generic', // or 'dnd', 'whatsapp'
39
+ },
40
+ },
41
+ ],
42
+ });
43
+
44
+ // Register the Termii channel factory
45
+ manager.registerFactory('sms-termii', createTermiiChannel);
46
+
47
+ // Create a notification
48
+ class WelcomeSmsNotification extends Notification {
49
+ constructor(private userName: string) {
50
+ super();
51
+ }
52
+
53
+ via() {
54
+ return [NotificationChannel.SMS];
55
+ }
56
+
57
+ toSms() {
58
+ return {
59
+ to: '+1234567890',
60
+ message: `Welcome ${this.userName}! Thank you for joining our service.`,
61
+ };
62
+ }
63
+ }
64
+
65
+ // Send the notification
66
+ const notification = new WelcomeSmsNotification('John');
67
+ const recipient = {
68
+ [NotificationChannel.SMS]: {
69
+ phone: '+1234567890',
70
+ },
71
+ };
72
+
73
+ await manager.send(notification, recipient);
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ ### TermiiConfig
79
+
80
+ ```typescript
81
+ interface TermiiConfig {
82
+ apiKey: string; // Required: Your Termii API key
83
+ senderId: string; // Required: Your registered sender ID (max 11 chars)
84
+ channel?: string; // Optional: Message channel (default: 'generic')
85
+ timeout?: number; // Optional: Request timeout in ms (default: 30000)
86
+ debug?: boolean; // Optional: Enable debug logging (default: false)
87
+ }
88
+ ```
89
+
90
+ ### Channel Types
91
+
92
+ Termii supports different message channels:
93
+
94
+ - `generic` - Standard SMS (default, most reliable)
95
+ - `dnd` - For numbers on DND (Do Not Disturb)
96
+ - `whatsapp` - For WhatsApp Business messages
97
+
98
+ ```typescript
99
+ {
100
+ channel: 'generic', // Change based on your needs
101
+ }
102
+ ```
103
+
104
+ ## Advanced Usage
105
+
106
+ ### With Personalization
107
+
108
+ ```typescript
109
+ class OrderConfirmationNotification extends Notification {
110
+ constructor(
111
+ private orderNumber: string,
112
+ private totalAmount: string,
113
+ private estimatedDelivery: string,
114
+ ) {
115
+ super();
116
+ }
117
+
118
+ via() {
119
+ return [NotificationChannel.SMS];
120
+ }
121
+
122
+ toSms() {
123
+ return {
124
+ to: '+1234567890',
125
+ message: `Order #${this.orderNumber} confirmed! Total: ${this.totalAmount}. Estimated delivery: ${this.estimatedDelivery}`,
126
+ };
127
+ }
128
+ }
129
+ ```
130
+
131
+ ### Multi-Channel with Fallback
132
+
133
+ ```typescript
134
+ class CriticalAlertNotification extends Notification {
135
+ via() {
136
+ // Try SMS first, fallback to email if SMS fails
137
+ return [NotificationChannel.SMS, NotificationChannel.EMAIL];
138
+ }
139
+
140
+ toSms() {
141
+ return {
142
+ to: '+1234567890',
143
+ message: 'Critical Alert: Your account requires immediate attention.',
144
+ };
145
+ }
146
+
147
+ toEmail() {
148
+ return {
149
+ subject: 'Critical Alert',
150
+ html: '<h1>Critical Alert</h1><p>Your account requires immediate attention.</p>',
151
+ };
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### International Numbers
157
+
158
+ ```typescript
159
+ // Always use E.164 format for international numbers
160
+ {
161
+ to: '+44712345678', // UK
162
+ to: '+12125551234', // USA
163
+ to: '+234812345678', // Nigeria
164
+ message: 'Your message here',
165
+ }
166
+ ```
167
+
168
+ ## Getting Your API Key
169
+
170
+ 1. Sign up at [termii.com](https://termii.com)
171
+ 2. Navigate to your Dashboard
172
+ 3. Go to API Settings
173
+ 4. Copy your API Key
174
+ 5. Register a Sender ID (this may require verification)
175
+
176
+ ### Sender ID Requirements
177
+
178
+ - Maximum 11 characters
179
+ - Alphanumeric only
180
+ - Must be approved by Termii
181
+ - Some countries require pre-registration
182
+
183
+ ## Error Handling
184
+
185
+ The adapter includes comprehensive error handling:
186
+
187
+ ```typescript
188
+ import { NotificationFailed } from 'townkrier-core';
189
+
190
+ // Listen for failures
191
+ eventDispatcher.on(NotificationFailed, async (event) => {
192
+ console.error('SMS failed:', event.error.message);
193
+ console.error('Failed channel:', event.failedChannel);
194
+
195
+ // Implement custom retry logic or alerts
196
+ if (event.error.message.includes('Invalid number')) {
197
+ // Handle invalid phone numbers
198
+ }
199
+ });
200
+ ```
201
+
202
+ ## Message Length
203
+
204
+ - Standard SMS: 160 characters
205
+ - Unicode messages (emojis, etc.): 70 characters
206
+ - Messages longer than these limits are split into multiple parts
207
+
208
+ ## Testing
209
+
210
+ ### Development Mode
211
+
212
+ ```typescript
213
+ {
214
+ apiKey: process.env.TERMII_API_KEY,
215
+ senderId: 'Test',
216
+ debug: true, // Enable detailed logging
217
+ }
218
+ ```
219
+
220
+ ### Test Numbers
221
+
222
+ Termii provides test numbers for development. Check their documentation for current test numbers.
223
+
224
+ ## Pricing
225
+
226
+ Termii charges per SMS sent. Pricing varies by:
227
+
228
+ - Destination country
229
+ - Message type (SMS vs WhatsApp)
230
+ - Volume discounts
231
+
232
+ Check [Termii Pricing](https://termii.com/pricing) for current rates.
233
+
234
+ ## Best Practices
235
+
236
+ 1. **Validate Phone Numbers**: Always validate numbers before sending
237
+ 2. **Keep Messages Concise**: Stay under 160 characters when possible
238
+ 3. **Use Sender ID Wisely**: Choose a recognizable sender ID
239
+ 4. **Handle Errors**: Implement proper error handling and logging
240
+ 5. **Respect Regulations**: Follow SMS marketing regulations (TCPA, etc.)
241
+ 6. **Rate Limiting**: Be mindful of rate limits
242
+ 7. **Store Preferences**: Track user SMS preferences and opt-outs
243
+
244
+ ## Troubleshooting
245
+
246
+ ### "Invalid API Key"
247
+
248
+ - Verify your API key is correct
249
+ - Check if the key has necessary permissions
250
+ - Ensure the key hasn't expired
251
+
252
+ ### "Invalid Sender ID"
253
+
254
+ - Verify sender ID is registered with Termii
255
+ - Check sender ID is maximum 11 characters
256
+ - Ensure sender ID is approved for your account
257
+
258
+ ### "Insufficient Balance"
259
+
260
+ - Check your Termii account balance
261
+ - Add credits to your account
262
+
263
+ ### "Invalid Phone Number"
264
+
265
+ - Use E.164 format (+countrycode + number)
266
+ - Remove spaces, dashes, or special characters
267
+ - Verify the number is valid for the destination country
268
+
269
+ ## Related Packages
270
+
271
+ - [townkrier-core](../../core) - Core notification system
272
+ - [townkrier-resend](../../resend) - Email provider
273
+ - [townkrier-fcm](../push/fcm) - Push notifications provider
274
+ - [townkrier-queue](../../queue) - Queue system for background processing
275
+ - [townkrier-dashboard](../../dashboard) - Monitoring dashboard
276
+
277
+ ## Examples
278
+
279
+ See the [examples directory](../../../../examples) for complete working examples:
280
+
281
+ - [Complete Example](../../../../examples/complete-example.ts) - Full multi-channel setup
282
+ - [SMS Specific Examples](../../../../examples/notifications) - SMS notification examples
283
+
284
+ ## License
285
+
286
+ MIT
287
+
288
+ ## Support
289
+
290
+ - [Termii Documentation](https://developers.termii.com)
291
+ - [TownKrier Documentation](../../../../README.md)
292
+ - [Report Issues](https://github.com/jeremiah-olisa/townkrier/issues)
293
+
294
+ ## Author
295
+
296
+ Jeremiah Olisa
@@ -0,0 +1,64 @@
1
+ import { TermiiChannel, createTermiiChannel } from '../src/core';
2
+ import { TermiiConfig } from '../src/types';
3
+ import axios from 'axios';
4
+
5
+ // Mock axios
6
+ jest.mock('axios');
7
+ const mockedAxios = axios as jest.Mocked<typeof axios>;
8
+
9
+ describe('TermiiChannel', () => {
10
+ const config: TermiiConfig = {
11
+ apiKey: 'test-api-key',
12
+ from: 'TestSender',
13
+ };
14
+
15
+ let channel: TermiiChannel;
16
+
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ // Mock axios.create to return the mocked axios instance so we can spy on post
20
+ (mockedAxios.create as jest.Mock).mockReturnValue(mockedAxios);
21
+ channel = new TermiiChannel(config);
22
+ });
23
+
24
+ it('should be defined', () => {
25
+ expect(channel).toBeDefined();
26
+ });
27
+
28
+ it('should send sms successfully', async () => {
29
+ mockedAxios.post.mockResolvedValue({
30
+ data: {
31
+ message_id: '12345',
32
+ message: 'Successfully Sent',
33
+ balance: 10,
34
+ user: 'test-user',
35
+ },
36
+ });
37
+
38
+ const result = await channel.sendSms({
39
+ to: { phone: '2348012345678' },
40
+ message: 'Hello World',
41
+ text: 'Hello World',
42
+ });
43
+
44
+ expect(mockedAxios.post).toHaveBeenCalledWith(
45
+ '/api/sms/send', // Method uses relative path on client
46
+ expect.objectContaining({
47
+ api_key: 'test-api-key',
48
+ to: '2348012345678',
49
+ from: 'TestSender',
50
+ sms: 'Hello World',
51
+ type: 'plain',
52
+ channel: 'generic',
53
+ }),
54
+ );
55
+
56
+ expect(result.status).toBe('sent');
57
+ expect(result.messageId).toBe('12345');
58
+ });
59
+
60
+ it('should create channel using factory', () => {
61
+ const channelFromFactory = createTermiiChannel(config);
62
+ expect(channelFromFactory).toBeInstanceOf(TermiiChannel);
63
+ });
64
+ });
@@ -0,0 +1,2 @@
1
+ export * from './termii-channel';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./termii-channel"), exports);
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,mDAAiC"}
@@ -0,0 +1,13 @@
1
+ import { SmsChannel, SendSmsRequest, SendSmsResponse } from 'townkrier-core';
2
+ import { TermiiConfig } from '../types';
3
+ export declare class TermiiChannel extends SmsChannel {
4
+ private readonly client;
5
+ private readonly termiiConfig;
6
+ private readonly baseUrl;
7
+ constructor(config: TermiiConfig);
8
+ sendSms(request: SendSmsRequest): Promise<SendSmsResponse>;
9
+ protected isValidNotificationRequest(request: any): request is SendSmsRequest;
10
+ private handleError;
11
+ }
12
+ export declare function createTermiiChannel(config: TermiiConfig): TermiiChannel;
13
+ //# sourceMappingURL=termii-channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"termii-channel.d.ts","sourceRoot":"","sources":["../../src/core/termii-channel.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,cAAc,EACd,eAAe,EAGhB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAQxC,qBAAa,aAAc,SAAQ,UAAU;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,MAAM,EAAE,YAAY;IAoD1B,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAoDhE,SAAS,CAAC,0BAA0B,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,IAAI,cAAc;IAU7E,OAAO,CAAC,WAAW;CAGpB;AAKD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,aAAa,CAEvE"}
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TermiiChannel = void 0;
7
+ exports.createTermiiChannel = createTermiiChannel;
8
+ const townkrier_core_1 = require("townkrier-core");
9
+ const termii_mapper_1 = require("./termii.mapper");
10
+ const axios_1 = __importDefault(require("axios"));
11
+ class TermiiChannel extends townkrier_core_1.SmsChannel {
12
+ constructor(config) {
13
+ if (!config.apiKey) {
14
+ throw new townkrier_core_1.NotificationConfigurationException('API key is required for Termii', {
15
+ channel: 'Termii',
16
+ });
17
+ }
18
+ super(config, 'Termii');
19
+ this.termiiConfig = config;
20
+ this.baseUrl = config.baseUrl || 'https://api.ng.termii.com';
21
+ this.client = axios_1.default.create({
22
+ baseURL: this.baseUrl,
23
+ timeout: config.timeout || 30000,
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ },
27
+ });
28
+ if (config.debug) {
29
+ this.client.interceptors.request.use((request) => {
30
+ townkrier_core_1.Logger.debug('[Termii Request]', {
31
+ url: request.url,
32
+ method: request.method,
33
+ data: request.data,
34
+ });
35
+ return request;
36
+ });
37
+ this.client.interceptors.response.use((response) => {
38
+ townkrier_core_1.Logger.debug('[Termii Response]', {
39
+ status: response.status,
40
+ data: response.data,
41
+ });
42
+ return response;
43
+ }, (error) => {
44
+ townkrier_core_1.Logger.error('[Termii Error]', {
45
+ message: error.message,
46
+ response: error.response?.data,
47
+ });
48
+ return Promise.reject(error);
49
+ });
50
+ }
51
+ }
52
+ async sendSms(request) {
53
+ try {
54
+ const recipients = Array.isArray(request.to) ? request.to : [request.to];
55
+ if (recipients.length === 0) {
56
+ throw new townkrier_core_1.NotificationConfigurationException('No recipients provided', {
57
+ recipients,
58
+ });
59
+ }
60
+ const data = termii_mapper_1.TermiiMapper.toTermiiData(request, this.termiiConfig);
61
+ const endpoint = Array.isArray(data.to) ? '/api/sms/send/bulk' : '/api/sms/send';
62
+ const response = await this.client.post(endpoint, data);
63
+ if (!response.data || !response.data.message_id) {
64
+ if (response.data && response.data.message && !response.data.message_id) {
65
+ throw new Error(response.data.message);
66
+ }
67
+ }
68
+ return termii_mapper_1.TermiiMapper.toSuccessResponse(response.data, request);
69
+ }
70
+ catch (error) {
71
+ let finalError = error;
72
+ if (axios_1.default.isAxiosError(error) && error.response) {
73
+ const errorData = error.response.data;
74
+ let message = errorData?.message || errorData?.error || error.message;
75
+ if (typeof message === 'object') {
76
+ message = JSON.stringify(message);
77
+ }
78
+ finalError = new Error(message);
79
+ }
80
+ return termii_mapper_1.TermiiMapper.toErrorResponse(finalError, 'Failed to send SMS');
81
+ }
82
+ }
83
+ isValidNotificationRequest(request) {
84
+ return request && request.message && (request.to || Array.isArray(request.to));
85
+ }
86
+ handleError(error, defaultMessage) {
87
+ return termii_mapper_1.TermiiMapper.toErrorResponse(error, defaultMessage);
88
+ }
89
+ }
90
+ exports.TermiiChannel = TermiiChannel;
91
+ function createTermiiChannel(config) {
92
+ return new TermiiChannel(config);
93
+ }
94
+ //# sourceMappingURL=termii-channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"termii-channel.js","sourceRoot":"","sources":["../../src/core/termii-channel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=termii-channel.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"termii-channel.spec.d.ts","sourceRoot":"","sources":["../../src/core/termii-channel.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const termii_channel_1 = require("./termii-channel");
7
+ const axios_1 = __importDefault(require("axios"));
8
+ jest.mock('axios');
9
+ const mockedAxios = axios_1.default;
10
+ describe('TermiiChannel', () => {
11
+ let channel;
12
+ const config = {
13
+ apiKey: 'test-api-key',
14
+ from: 'Townkrier',
15
+ };
16
+ beforeEach(() => {
17
+ jest.clearAllMocks();
18
+ mockedAxios.create.mockReturnThis();
19
+ mockedAxios.post.mockResolvedValue({
20
+ data: { message_id: '12345', message: 'Successfully Sent' },
21
+ });
22
+ channel = new termii_channel_1.TermiiChannel(config);
23
+ });
24
+ it('should send single SMS via standard endpoint', async () => {
25
+ const request = {
26
+ to: { phone: '2341234567890' },
27
+ message: 'Hello World',
28
+ text: 'Hello World',
29
+ };
30
+ await channel.sendSms(request);
31
+ expect(mockedAxios.post).toHaveBeenCalledWith('/api/sms/send', expect.objectContaining({
32
+ to: '2341234567890',
33
+ }));
34
+ });
35
+ it('should send bulk SMS via bulk endpoint', async () => {
36
+ const request = {
37
+ to: [{ phone: '2341234567890' }, { phone: '2341234567891' }],
38
+ message: 'Hello World Bulk',
39
+ text: 'Hello World Bulk',
40
+ };
41
+ await channel.sendSms(request);
42
+ expect(mockedAxios.post).toHaveBeenCalledWith('/api/sms/send/bulk', expect.objectContaining({
43
+ to: ['2341234567890', '2341234567891'],
44
+ }));
45
+ });
46
+ it('should throw error if API returns error message with 200 OK', async () => {
47
+ mockedAxios.post.mockResolvedValue({
48
+ data: {
49
+ message: 'Invalid API Key',
50
+ },
51
+ });
52
+ const request = {
53
+ to: { phone: '2341234567890' },
54
+ message: 'Hello World',
55
+ text: 'Hello World',
56
+ };
57
+ await expect(channel.sendSms(request)).resolves.toEqual(expect.objectContaining({
58
+ status: 'failed',
59
+ error: expect.objectContaining({
60
+ message: 'Invalid API Key',
61
+ }),
62
+ }));
63
+ });
64
+ });
65
+ //# sourceMappingURL=termii-channel.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"termii-channel.spec.js","sourceRoot":"","sources":["../../src/core/termii-channel.spec.ts"],"names":[],"mappings":";;;;;AAAA,qDAAiD;AACjD,kDAA0B;AAG1B,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACnB,MAAM,WAAW,GAAG,eAAkC,CAAC;AAEvD,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,IAAI,OAAsB,CAAC;IAC3B,MAAM,MAAM,GAAG;QACb,MAAM,EAAE,cAAc;QACtB,IAAI,EAAE,WAAW;KAClB,CAAC;IAEF,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,WAAW,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QACpC,WAAW,CAAC,IAAI,CAAC,iBAAiB,CAAC;YACjC,IAAI,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE;SAC5D,CAAC,CAAC;QACH,OAAO,GAAG,IAAI,8BAAa,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,MAAM,OAAO,GAAmB;YAC9B,EAAE,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE;YAC9B,OAAO,EAAE,aAAa;YACtB,IAAI,EAAE,aAAa;SACpB,CAAC;QAEF,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAE/B,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAC3C,eAAe,EACf,MAAM,CAAC,gBAAgB,CAAC;YACtB,EAAE,EAAE,eAAe;SACpB,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,OAAO,GAAmB;YAC9B,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;YAC5D,OAAO,EAAE,kBAAkB;YAC3B,IAAI,EAAE,kBAAkB;SACzB,CAAC;QAEF,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAE/B,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAC3C,oBAAoB,EACpB,MAAM,CAAC,gBAAgB,CAAC;YACtB,EAAE,EAAE,CAAC,eAAe,EAAE,eAAe,CAAC;SACvC,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,WAAW,CAAC,IAAI,CAAC,iBAAiB,CAAC;YACjC,IAAI,EAAE;gBACJ,OAAO,EAAE,iBAAiB;aAC3B;SACF,CAAC,CAAC;QAEH,MAAM,OAAO,GAAmB;YAC9B,EAAE,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE;YAC9B,OAAO,EAAE,aAAa;YACtB,IAAI,EAAE,aAAa;SACpB,CAAC;QAEF,MAAM,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CACrD,MAAM,CAAC,gBAAgB,CAAC;YACtB,MAAM,EAAE,QAAQ;YAChB,KAAK,EAAE,MAAM,CAAC,gBAAgB,CAAC;gBAC7B,OAAO,EAAE,iBAAiB;aAC3B,CAAC;SACH,CAAC,CACH,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { SendSmsRequest, SendSmsResponse } from 'townkrier-core';
2
+ import { TermiiSmsData, TermiiApiResponse } from '../interfaces';
3
+ import { TermiiConfig } from '../types';
4
+ export declare class TermiiMapper {
5
+ static toTermiiData(request: SendSmsRequest, config: TermiiConfig): TermiiSmsData;
6
+ static toSuccessResponse(data: TermiiApiResponse, request: SendSmsRequest): SendSmsResponse;
7
+ static toErrorResponse(error: unknown, defaultMessage: string): SendSmsResponse;
8
+ }
9
+ //# sourceMappingURL=termii.mapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"termii.mapper.d.ts","sourceRoot":"","sources":["../../src/core/termii.mapper.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,eAAe,EAIhB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,qBAAa,YAAY;IACvB,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,GAAG,aAAa;IA0BjF,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe;IAgB3F,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,eAAe;CAuBhF"}