softycomp-node 1.0.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/LICENSE +21 -0
- package/README.md +418 -0
- package/dist/index.d.ts +228 -0
- package/dist/index.js +339 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kobie Wentzel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# softycomp-node
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/softycomp-node)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
**Official Node.js SDK for SoftyComp** — South African bill presentment and debit order platform.
|
|
8
|
+
|
|
9
|
+
Accept once-off and recurring payments via card, EFT, and debit order with a simple, type-safe API.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Bill Presentment** — Create payment links for once-off or recurring bills
|
|
14
|
+
- **Debit Orders** — Monthly and yearly recurring collections
|
|
15
|
+
- **Refunds** — Process full or partial refunds via credit transactions
|
|
16
|
+
- **Webhooks** — Real-time payment notifications with signature validation
|
|
17
|
+
- **TypeScript** — Fully typed for autocomplete and type safety
|
|
18
|
+
- **Zero Dependencies** — Uses native `fetch()` (Node.js 18+)
|
|
19
|
+
- **Sandbox Support** — Test with sandbox environment before going live
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install softycomp-node
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Requirements:** Node.js 18+ (for native `fetch()` support)
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { SoftyComp } from 'softycomp-node';
|
|
33
|
+
|
|
34
|
+
// Initialize client
|
|
35
|
+
const client = new SoftyComp({
|
|
36
|
+
apiKey: 'your-api-key',
|
|
37
|
+
secretKey: 'your-secret-key',
|
|
38
|
+
sandbox: true, // Use test environment
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Create a once-off bill
|
|
42
|
+
const bill = await client.createBill({
|
|
43
|
+
amount: 299.00, // In Rands (not cents!)
|
|
44
|
+
customerName: 'John Doe',
|
|
45
|
+
customerEmail: 'john@example.com',
|
|
46
|
+
customerPhone: '0825551234',
|
|
47
|
+
reference: 'INV-001',
|
|
48
|
+
description: 'Product purchase',
|
|
49
|
+
frequency: 'once-off',
|
|
50
|
+
returnUrl: 'https://myapp.com/payment/success',
|
|
51
|
+
cancelUrl: 'https://myapp.com/payment/cancel',
|
|
52
|
+
notifyUrl: 'https://myapp.com/payment/webhook',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Redirect customer to payment page
|
|
56
|
+
console.log(bill.paymentUrl);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Reference
|
|
60
|
+
|
|
61
|
+
### Initialize Client
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const client = new SoftyComp({
|
|
65
|
+
apiKey: string; // Your SoftyComp API key
|
|
66
|
+
secretKey: string; // Your SoftyComp API secret
|
|
67
|
+
sandbox?: boolean; // Use sandbox environment (default: true)
|
|
68
|
+
webhookSecret?: string; // Optional secret for webhook signature validation
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Environments:**
|
|
73
|
+
- **Sandbox:** `sandbox.softycomp.co.za`
|
|
74
|
+
- **Production:** `api.softycomp.co.za`
|
|
75
|
+
|
|
76
|
+
### Create Bill
|
|
77
|
+
|
|
78
|
+
Create a payment bill (once-off or recurring).
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const bill = await client.createBill({
|
|
82
|
+
amount: number; // Amount in Rands (not cents!) e.g. 299.00
|
|
83
|
+
customerName: string; // Customer full name
|
|
84
|
+
customerEmail: string; // Customer email
|
|
85
|
+
customerPhone: string; // Customer mobile (e.g. "0825551234")
|
|
86
|
+
reference: string; // Your internal reference/invoice number
|
|
87
|
+
description?: string; // Bill description
|
|
88
|
+
frequency: 'once-off' | 'monthly' | 'yearly';
|
|
89
|
+
|
|
90
|
+
// Recurring bill fields (ignored for once-off):
|
|
91
|
+
commencementDate?: string; // Start date (YYYY-MM-DD). Must be future (min tomorrow)
|
|
92
|
+
recurringDay?: number; // Day of month to charge (1-28). Defaults to tomorrow
|
|
93
|
+
recurringMonth?: number; // Month for yearly bills (1-12). Defaults to tomorrow
|
|
94
|
+
|
|
95
|
+
// URLs:
|
|
96
|
+
returnUrl: string; // Success redirect URL
|
|
97
|
+
cancelUrl: string; // Cancel redirect URL
|
|
98
|
+
notifyUrl: string; // Webhook notification URL
|
|
99
|
+
|
|
100
|
+
// Optional branding:
|
|
101
|
+
companyName?: string; // Company name to display
|
|
102
|
+
companyContact?: string; // Company contact number
|
|
103
|
+
companyEmail?: string; // Company email
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Returns:
|
|
107
|
+
{
|
|
108
|
+
reference: string; // SoftyComp bill reference
|
|
109
|
+
paymentUrl: string; // URL to redirect customer to
|
|
110
|
+
expiresAt: string; // ISO 8601 expiry timestamp (typically 30 mins)
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Examples
|
|
115
|
+
|
|
116
|
+
**Once-off payment:**
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const bill = await client.createBill({
|
|
120
|
+
amount: 150.00,
|
|
121
|
+
customerName: 'Jane Smith',
|
|
122
|
+
customerEmail: 'jane@example.com',
|
|
123
|
+
customerPhone: '0821234567',
|
|
124
|
+
reference: 'ORDER-789',
|
|
125
|
+
frequency: 'once-off',
|
|
126
|
+
returnUrl: 'https://myapp.com/success',
|
|
127
|
+
cancelUrl: 'https://myapp.com/cancel',
|
|
128
|
+
notifyUrl: 'https://myapp.com/webhook',
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Monthly subscription:**
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const bill = await client.createBill({
|
|
136
|
+
amount: 99.00,
|
|
137
|
+
customerName: 'John Doe',
|
|
138
|
+
customerEmail: 'john@example.com',
|
|
139
|
+
customerPhone: '0825551234',
|
|
140
|
+
reference: 'SUB-001',
|
|
141
|
+
description: 'Premium Monthly Subscription',
|
|
142
|
+
frequency: 'monthly',
|
|
143
|
+
commencementDate: '2026-04-01', // First charge date
|
|
144
|
+
recurringDay: 1, // Charge on 1st of each month
|
|
145
|
+
returnUrl: 'https://myapp.com/success',
|
|
146
|
+
cancelUrl: 'https://myapp.com/cancel',
|
|
147
|
+
notifyUrl: 'https://myapp.com/webhook',
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Yearly subscription:**
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const bill = await client.createBill({
|
|
155
|
+
amount: 999.00,
|
|
156
|
+
customerName: 'Alice Johnson',
|
|
157
|
+
customerEmail: 'alice@example.com',
|
|
158
|
+
customerPhone: '0827778888',
|
|
159
|
+
reference: 'SUB-ANNUAL-042',
|
|
160
|
+
description: 'Annual Premium Plan',
|
|
161
|
+
frequency: 'yearly',
|
|
162
|
+
commencementDate: '2027-01-15',
|
|
163
|
+
recurringDay: 15,
|
|
164
|
+
recurringMonth: 1, // Charge on January 15th each year
|
|
165
|
+
returnUrl: 'https://myapp.com/success',
|
|
166
|
+
cancelUrl: 'https://myapp.com/cancel',
|
|
167
|
+
notifyUrl: 'https://myapp.com/webhook',
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Get Bill Status
|
|
172
|
+
|
|
173
|
+
Check payment status of a bill.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
const status = await client.getBillStatus('BILL-REF-123');
|
|
177
|
+
|
|
178
|
+
// Returns:
|
|
179
|
+
{
|
|
180
|
+
reference: string; // Bill reference
|
|
181
|
+
status: 'pending' | 'completed' | 'failed' | 'cancelled';
|
|
182
|
+
amount: number; // Amount in Rands
|
|
183
|
+
paidAt?: string; // ISO 8601 payment timestamp (if paid)
|
|
184
|
+
data: any; // Raw response from SoftyComp
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Refund Payment
|
|
189
|
+
|
|
190
|
+
Process a full or partial refund (credit transaction).
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// Full refund
|
|
194
|
+
const refund = await client.refund({
|
|
195
|
+
transactionId: 'TXN-123',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Partial refund
|
|
199
|
+
const refund = await client.refund({
|
|
200
|
+
transactionId: 'TXN-123',
|
|
201
|
+
amount: 50.00, // Amount in Rands
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Returns:
|
|
205
|
+
{
|
|
206
|
+
refundId: string; // Refund reference
|
|
207
|
+
status: 'completed' | 'pending' | 'failed';
|
|
208
|
+
amount: number; // Amount refunded in Rands
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Webhook Handling
|
|
213
|
+
|
|
214
|
+
SoftyComp sends real-time payment notifications to your `notifyUrl`.
|
|
215
|
+
|
|
216
|
+
#### Verify Webhook Signature
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import express from 'express';
|
|
220
|
+
|
|
221
|
+
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
222
|
+
const signature = req.headers['x-signature'] as string;
|
|
223
|
+
|
|
224
|
+
if (!client.verifyWebhook(req.body, { signature })) {
|
|
225
|
+
return res.status(400).send('Invalid signature');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Signature valid, process webhook...
|
|
229
|
+
res.status(200).send('OK');
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Parse Webhook Event
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const event = client.parseWebhook(req.body);
|
|
237
|
+
|
|
238
|
+
// Returns:
|
|
239
|
+
{
|
|
240
|
+
type: 'pending' | 'successful' | 'failed' | 'cancelled';
|
|
241
|
+
reference: string; // Transaction reference
|
|
242
|
+
status: 'pending' | 'completed' | 'failed' | 'cancelled';
|
|
243
|
+
amount: number; // Amount in Rands
|
|
244
|
+
transactionDate: string; // ISO 8601 timestamp
|
|
245
|
+
paymentMethodId: number; // Payment method ID (1=Card, 2=EFT, etc.)
|
|
246
|
+
paymentMethod: string; // Payment method description
|
|
247
|
+
userReference: string; // Your original reference
|
|
248
|
+
information: string; // Additional info
|
|
249
|
+
raw: any; // Raw webhook payload
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Full Webhook Example
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
257
|
+
// 1. Verify signature (optional but recommended)
|
|
258
|
+
const signature = req.headers['x-signature'] as string;
|
|
259
|
+
if (!client.verifyWebhook(req.body, { signature })) {
|
|
260
|
+
return res.status(400).send('Invalid signature');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 2. Parse webhook event
|
|
264
|
+
const event = client.parseWebhook(req.body);
|
|
265
|
+
|
|
266
|
+
// 3. Handle event types
|
|
267
|
+
switch (event.type) {
|
|
268
|
+
case 'successful':
|
|
269
|
+
console.log('Payment successful!');
|
|
270
|
+
console.log(`Reference: ${event.reference}`);
|
|
271
|
+
console.log(`Amount: R${event.amount}`);
|
|
272
|
+
console.log(`Method: ${event.paymentMethod}`);
|
|
273
|
+
// Update your database, send confirmation email, etc.
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case 'failed':
|
|
277
|
+
console.log('Payment failed:', event.reference);
|
|
278
|
+
// Notify customer, retry payment, etc.
|
|
279
|
+
break;
|
|
280
|
+
|
|
281
|
+
case 'cancelled':
|
|
282
|
+
console.log('Payment cancelled:', event.reference);
|
|
283
|
+
// Handle cancellation
|
|
284
|
+
break;
|
|
285
|
+
|
|
286
|
+
case 'pending':
|
|
287
|
+
console.log('Payment pending:', event.reference);
|
|
288
|
+
// Wait for final status
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 4. Respond with 200 OK
|
|
293
|
+
res.status(200).send('OK');
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Test Cards
|
|
298
|
+
|
|
299
|
+
Use these test cards in the **sandbox environment** only:
|
|
300
|
+
|
|
301
|
+
| Card Number | 3DS | MOTO | Description |
|
|
302
|
+
|-------------|-----|------|-------------|
|
|
303
|
+
| `4790 4444 4444 4444` | ✅ Success | ✅ Success | Both 3DS and MOTO succeed |
|
|
304
|
+
| `4790 3333 3333 3333` | ✅ Success | ❌ Fail | 3DS succeeds, MOTO fails |
|
|
305
|
+
|
|
306
|
+
**CVV:** Any 3 digits
|
|
307
|
+
**Expiry:** Any future date
|
|
308
|
+
|
|
309
|
+
## Important Notes
|
|
310
|
+
|
|
311
|
+
### Amounts in Rands (Not Cents!)
|
|
312
|
+
|
|
313
|
+
Unlike most payment SDKs, SoftyComp uses **Rands**, not cents:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// ✅ Correct
|
|
317
|
+
amount: 299.00 // R299.00
|
|
318
|
+
|
|
319
|
+
// ❌ Wrong
|
|
320
|
+
amount: 29900 // Would be R29,900.00!
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Recurring Bill Requirements
|
|
324
|
+
|
|
325
|
+
For `frequency: 'monthly'` or `'yearly'`:
|
|
326
|
+
|
|
327
|
+
1. **Commencement Date** must be a future date (minimum tomorrow)
|
|
328
|
+
2. **Recurring Day** should be 1-28 (avoids month-end issues)
|
|
329
|
+
3. **Recurring Month** only for yearly bills (1=Jan, 12=Dec)
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// ✅ Correct
|
|
333
|
+
commencementDate: '2026-04-01', // Future date
|
|
334
|
+
recurringDay: 15, // 15th of each month
|
|
335
|
+
|
|
336
|
+
// ❌ Wrong
|
|
337
|
+
commencementDate: '2026-03-20', // Past date
|
|
338
|
+
recurringDay: 31, // Doesn't exist in all months
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Frequency Codes (Internal)
|
|
342
|
+
|
|
343
|
+
The SDK handles this automatically, but for reference:
|
|
344
|
+
|
|
345
|
+
- `1` = Once-off
|
|
346
|
+
- `2` = Monthly
|
|
347
|
+
- `7` = Yearly
|
|
348
|
+
|
|
349
|
+
### Webhook Activity Types (Internal)
|
|
350
|
+
|
|
351
|
+
The SDK maps these to friendly event types:
|
|
352
|
+
|
|
353
|
+
- `1` = Pending → `type: 'pending'`
|
|
354
|
+
- `2` = Successful → `type: 'successful'`
|
|
355
|
+
- `3` = Failed → `type: 'failed'`
|
|
356
|
+
- `4` = Cancelled → `type: 'cancelled'`
|
|
357
|
+
|
|
358
|
+
## TypeScript Support
|
|
359
|
+
|
|
360
|
+
The SDK is written in TypeScript and includes full type definitions:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import {
|
|
364
|
+
SoftyComp,
|
|
365
|
+
BillFrequency,
|
|
366
|
+
PaymentStatus,
|
|
367
|
+
WebhookEvent,
|
|
368
|
+
CreateBillParams,
|
|
369
|
+
RefundParams
|
|
370
|
+
} from 'softycomp-node';
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Error Handling
|
|
374
|
+
|
|
375
|
+
All methods throw descriptive errors:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
try {
|
|
379
|
+
const bill = await client.createBill({
|
|
380
|
+
// ... params
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error('Failed to create bill:', error.message);
|
|
384
|
+
// "Failed to create bill: SoftyComp API error (POST /api/paygatecontroller/requestbillpresentment): 400 - Invalid email address"
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Common errors:
|
|
389
|
+
- Authentication: `SoftyComp authentication failed: 401 - Unauthorized`
|
|
390
|
+
- Invalid date: `commencementDate must be a future date (minimum tomorrow)`
|
|
391
|
+
- API errors: `SoftyComp API error (POST /path): 400 - Error message`
|
|
392
|
+
|
|
393
|
+
## Production Checklist
|
|
394
|
+
|
|
395
|
+
Before going live:
|
|
396
|
+
|
|
397
|
+
1. **Get production credentials** from SoftyComp
|
|
398
|
+
2. **Set `sandbox: false`** in config
|
|
399
|
+
3. **Configure webhook secret** for signature validation
|
|
400
|
+
4. **Test webhook endpoint** with production credentials
|
|
401
|
+
5. **Handle all webhook event types** (pending, successful, failed, cancelled)
|
|
402
|
+
6. **Implement idempotency** to avoid duplicate processing
|
|
403
|
+
7. **Log all transactions** for debugging and reconciliation
|
|
404
|
+
8. **Set up monitoring** for failed webhooks
|
|
405
|
+
|
|
406
|
+
## License
|
|
407
|
+
|
|
408
|
+
MIT © Kobie Wentzel
|
|
409
|
+
|
|
410
|
+
## Links
|
|
411
|
+
|
|
412
|
+
- **GitHub:** [kobie3717/softycomp-node](https://github.com/kobie3717/softycomp-node)
|
|
413
|
+
- **SoftyComp:** [softycompdistribution.co.za](https://softycompdistribution.co.za)
|
|
414
|
+
- **API Docs:** [webapps.softycomp.co.za](https://webapps.softycomp.co.za)
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
**Built by [Kobie Wentzel](https://github.com/kobie3717)** for the South African developer community.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SoftyComp Node.js SDK
|
|
3
|
+
*
|
|
4
|
+
* Official Node.js SDK for SoftyComp — South African bill presentment and debit order platform.
|
|
5
|
+
*
|
|
6
|
+
* @see https://softycompdistribution.co.za
|
|
7
|
+
* @see https://webapps.softycomp.co.za (API documentation)
|
|
8
|
+
*/
|
|
9
|
+
export type BillFrequency = 'once-off' | 'monthly' | 'yearly';
|
|
10
|
+
export type PaymentStatus = 'pending' | 'completed' | 'failed' | 'cancelled';
|
|
11
|
+
export type WebhookType = 'pending' | 'successful' | 'failed' | 'cancelled';
|
|
12
|
+
export interface SoftyCompConfig {
|
|
13
|
+
/** Your SoftyComp API key */
|
|
14
|
+
apiKey: string;
|
|
15
|
+
/** Your SoftyComp API secret */
|
|
16
|
+
secretKey: string;
|
|
17
|
+
/** Use sandbox environment (testapi.softycompdistribution.co.za) */
|
|
18
|
+
sandbox?: boolean;
|
|
19
|
+
/** Optional webhook secret for signature validation */
|
|
20
|
+
webhookSecret?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CreateBillParams {
|
|
23
|
+
/** Amount in Rands (not cents!) e.g. 299.00 */
|
|
24
|
+
amount: number;
|
|
25
|
+
/** Customer full name */
|
|
26
|
+
customerName: string;
|
|
27
|
+
/** Customer email address */
|
|
28
|
+
customerEmail: string;
|
|
29
|
+
/** Customer mobile number (e.g. "0825551234") */
|
|
30
|
+
customerPhone: string;
|
|
31
|
+
/** Your internal reference/invoice number */
|
|
32
|
+
reference: string;
|
|
33
|
+
/** Bill description */
|
|
34
|
+
description?: string;
|
|
35
|
+
/** Payment frequency: 'once-off', 'monthly', or 'yearly' */
|
|
36
|
+
frequency: BillFrequency;
|
|
37
|
+
/** Commencement date for recurring bills (YYYY-MM-DD). Must be future date (min tomorrow). Ignored for once-off. */
|
|
38
|
+
commencementDate?: string;
|
|
39
|
+
/** Day of month to charge for recurring bills (1-28). Ignored for once-off. Defaults to tomorrow's day. */
|
|
40
|
+
recurringDay?: number;
|
|
41
|
+
/** Month to charge for yearly bills (1-12). Ignored for monthly/once-off. Defaults to tomorrow's month. */
|
|
42
|
+
recurringMonth?: number;
|
|
43
|
+
/** URL to redirect customer after successful payment */
|
|
44
|
+
returnUrl: string;
|
|
45
|
+
/** URL to redirect customer after cancelled payment */
|
|
46
|
+
cancelUrl: string;
|
|
47
|
+
/** URL to receive webhook notifications */
|
|
48
|
+
notifyUrl: string;
|
|
49
|
+
/** Company name to display on bill */
|
|
50
|
+
companyName?: string;
|
|
51
|
+
/** Company contact number to display */
|
|
52
|
+
companyContact?: string;
|
|
53
|
+
/** Company email to display */
|
|
54
|
+
companyEmail?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface CreateBillResult {
|
|
57
|
+
/** Unique bill reference from SoftyComp */
|
|
58
|
+
reference: string;
|
|
59
|
+
/** Payment URL to redirect customer to */
|
|
60
|
+
paymentUrl: string;
|
|
61
|
+
/** ISO 8601 timestamp when the payment link expires (typically 30 minutes) */
|
|
62
|
+
expiresAt: string;
|
|
63
|
+
}
|
|
64
|
+
export interface RefundParams {
|
|
65
|
+
/** Original transaction ID/reference to refund */
|
|
66
|
+
transactionId: string;
|
|
67
|
+
/** Amount to refund in Rands (not cents!). Omit for full refund. */
|
|
68
|
+
amount?: number;
|
|
69
|
+
}
|
|
70
|
+
export interface RefundResult {
|
|
71
|
+
/** Refund reference ID */
|
|
72
|
+
refundId: string;
|
|
73
|
+
/** Refund status */
|
|
74
|
+
status: 'completed' | 'pending' | 'failed';
|
|
75
|
+
/** Amount refunded in Rands */
|
|
76
|
+
amount: number;
|
|
77
|
+
}
|
|
78
|
+
export interface WebhookEvent {
|
|
79
|
+
/** Event type: 'pending', 'successful', 'failed', 'cancelled' */
|
|
80
|
+
type: WebhookType;
|
|
81
|
+
/** Transaction reference */
|
|
82
|
+
reference: string;
|
|
83
|
+
/** Payment status */
|
|
84
|
+
status: PaymentStatus;
|
|
85
|
+
/** Amount in Rands */
|
|
86
|
+
amount: number;
|
|
87
|
+
/** Transaction date (ISO 8601) */
|
|
88
|
+
transactionDate: string;
|
|
89
|
+
/** Payment method ID (1=Card, 2=EFT, etc.) */
|
|
90
|
+
paymentMethodId: number;
|
|
91
|
+
/** Payment method description */
|
|
92
|
+
paymentMethod: string;
|
|
93
|
+
/** Your original reference */
|
|
94
|
+
userReference: string;
|
|
95
|
+
/** Additional information */
|
|
96
|
+
information: string;
|
|
97
|
+
/** Raw webhook payload */
|
|
98
|
+
raw: any;
|
|
99
|
+
}
|
|
100
|
+
export interface BillStatusResult {
|
|
101
|
+
/** Bill reference */
|
|
102
|
+
reference: string;
|
|
103
|
+
/** Current payment status */
|
|
104
|
+
status: PaymentStatus;
|
|
105
|
+
/** Amount in Rands */
|
|
106
|
+
amount: number;
|
|
107
|
+
/** Transaction date if paid */
|
|
108
|
+
paidAt?: string;
|
|
109
|
+
/** Raw response data */
|
|
110
|
+
data: any;
|
|
111
|
+
}
|
|
112
|
+
export declare class SoftyComp {
|
|
113
|
+
private apiKey;
|
|
114
|
+
private secretKey;
|
|
115
|
+
private sandbox;
|
|
116
|
+
private baseUrl;
|
|
117
|
+
private webhookSecret?;
|
|
118
|
+
private token;
|
|
119
|
+
private tokenExpiry;
|
|
120
|
+
constructor(config: SoftyCompConfig);
|
|
121
|
+
/**
|
|
122
|
+
* Authenticate and get Bearer token (cached for ~30 minutes)
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
private authenticate;
|
|
126
|
+
/**
|
|
127
|
+
* Make authenticated API request
|
|
128
|
+
* @internal
|
|
129
|
+
*/
|
|
130
|
+
private apiRequest;
|
|
131
|
+
/**
|
|
132
|
+
* Create a payment bill (once-off or recurring)
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const bill = await client.createBill({
|
|
137
|
+
* amount: 299.00,
|
|
138
|
+
* customerName: 'John Doe',
|
|
139
|
+
* customerEmail: 'john@example.com',
|
|
140
|
+
* customerPhone: '0825551234',
|
|
141
|
+
* reference: 'INV-001',
|
|
142
|
+
* description: 'Monthly subscription',
|
|
143
|
+
* frequency: 'monthly',
|
|
144
|
+
* commencementDate: '2026-04-01',
|
|
145
|
+
* recurringDay: 1,
|
|
146
|
+
* returnUrl: 'https://myapp.com/success',
|
|
147
|
+
* cancelUrl: 'https://myapp.com/cancel',
|
|
148
|
+
* notifyUrl: 'https://myapp.com/webhook'
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* // Redirect customer to payment page
|
|
152
|
+
* console.log(bill.paymentUrl);
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
createBill(params: CreateBillParams): Promise<CreateBillResult>;
|
|
156
|
+
/**
|
|
157
|
+
* Get bill payment status
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* const status = await client.getBillStatus('BILL-REF-123');
|
|
162
|
+
* if (status.status === 'completed') {
|
|
163
|
+
* console.log('Payment received on:', status.paidAt);
|
|
164
|
+
* }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
getBillStatus(reference: string): Promise<BillStatusResult>;
|
|
168
|
+
/**
|
|
169
|
+
* Process a refund (credit transaction)
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* // Full refund
|
|
174
|
+
* const refund = await client.refund({ transactionId: 'TXN-123' });
|
|
175
|
+
*
|
|
176
|
+
* // Partial refund
|
|
177
|
+
* const refund = await client.refund({
|
|
178
|
+
* transactionId: 'TXN-123',
|
|
179
|
+
* amount: 100.00
|
|
180
|
+
* });
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
refund(params: RefundParams): Promise<RefundResult>;
|
|
184
|
+
/**
|
|
185
|
+
* Verify webhook signature (HMAC-SHA256)
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
190
|
+
* const signature = req.headers['x-signature'] as string;
|
|
191
|
+
* if (!client.verifyWebhook(req.body, { signature })) {
|
|
192
|
+
* return res.status(400).send('Invalid signature');
|
|
193
|
+
* }
|
|
194
|
+
* // Process webhook...
|
|
195
|
+
* });
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
verifyWebhook(body: string | Buffer, headers: {
|
|
199
|
+
signature?: string;
|
|
200
|
+
} | Record<string, string>): boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Parse webhook event payload
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* const event = client.parseWebhook(req.body);
|
|
207
|
+
*
|
|
208
|
+
* switch (event.type) {
|
|
209
|
+
* case 'successful':
|
|
210
|
+
* console.log('Payment successful:', event.reference, event.amount);
|
|
211
|
+
* break;
|
|
212
|
+
* case 'failed':
|
|
213
|
+
* console.log('Payment failed:', event.reference);
|
|
214
|
+
* break;
|
|
215
|
+
* case 'cancelled':
|
|
216
|
+
* console.log('Payment cancelled:', event.reference);
|
|
217
|
+
* break;
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
parseWebhook(body: any): WebhookEvent;
|
|
222
|
+
/**
|
|
223
|
+
* Map SoftyComp status type ID to payment status
|
|
224
|
+
* @internal
|
|
225
|
+
*/
|
|
226
|
+
private mapBillStatus;
|
|
227
|
+
}
|
|
228
|
+
export default SoftyComp;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SoftyComp Node.js SDK
|
|
4
|
+
*
|
|
5
|
+
* Official Node.js SDK for SoftyComp — South African bill presentment and debit order platform.
|
|
6
|
+
*
|
|
7
|
+
* @see https://softycompdistribution.co.za
|
|
8
|
+
* @see https://webapps.softycomp.co.za (API documentation)
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.SoftyComp = void 0;
|
|
15
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
16
|
+
// ==================== Main SDK Class ====================
|
|
17
|
+
class SoftyComp {
|
|
18
|
+
constructor(config) {
|
|
19
|
+
// Token cache
|
|
20
|
+
this.token = null;
|
|
21
|
+
this.tokenExpiry = 0;
|
|
22
|
+
this.apiKey = config.apiKey;
|
|
23
|
+
this.secretKey = config.secretKey;
|
|
24
|
+
this.sandbox = config.sandbox ?? true;
|
|
25
|
+
this.webhookSecret = config.webhookSecret;
|
|
26
|
+
// Base URL mapping
|
|
27
|
+
if (this.sandbox) {
|
|
28
|
+
this.baseUrl = 'https://sandbox.softycomp.co.za/SoftyCompBureauAPI';
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.baseUrl = 'https://api.softycomp.co.za/SoftyCompBureauAPI';
|
|
32
|
+
}
|
|
33
|
+
if (!this.apiKey || !this.secretKey) {
|
|
34
|
+
throw new Error('SoftyComp requires apiKey and secretKey');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ==================== Authentication ====================
|
|
38
|
+
/**
|
|
39
|
+
* Authenticate and get Bearer token (cached for ~30 minutes)
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
async authenticate() {
|
|
43
|
+
// Return cached token if still valid (with 60s buffer)
|
|
44
|
+
if (this.token && Date.now() < this.tokenExpiry - 60000) {
|
|
45
|
+
return this.token;
|
|
46
|
+
}
|
|
47
|
+
const response = await fetch(`${this.baseUrl}/api/auth/generatetoken`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
apiKey: this.apiKey,
|
|
52
|
+
apiSecret: this.secretKey,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const errorText = await response.text();
|
|
57
|
+
throw new Error(`SoftyComp authentication failed: ${response.status} - ${errorText}`);
|
|
58
|
+
}
|
|
59
|
+
const data = (await response.json());
|
|
60
|
+
this.token = data.token;
|
|
61
|
+
this.tokenExpiry = new Date(data.expiration).getTime();
|
|
62
|
+
return this.token;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Make authenticated API request
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
async apiRequest(method, path, data) {
|
|
69
|
+
const token = await this.authenticate();
|
|
70
|
+
const url = `${this.baseUrl}${path}`;
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
method,
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${token}`,
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
},
|
|
77
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const errorText = await response.text();
|
|
81
|
+
throw new Error(`SoftyComp API error (${method} ${path}): ${response.status} - ${errorText}`);
|
|
82
|
+
}
|
|
83
|
+
const contentType = response.headers.get('content-type');
|
|
84
|
+
if (contentType && contentType.includes('application/json')) {
|
|
85
|
+
return (await response.json());
|
|
86
|
+
}
|
|
87
|
+
return (await response.text());
|
|
88
|
+
}
|
|
89
|
+
// ==================== Bill Presentment ====================
|
|
90
|
+
/**
|
|
91
|
+
* Create a payment bill (once-off or recurring)
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const bill = await client.createBill({
|
|
96
|
+
* amount: 299.00,
|
|
97
|
+
* customerName: 'John Doe',
|
|
98
|
+
* customerEmail: 'john@example.com',
|
|
99
|
+
* customerPhone: '0825551234',
|
|
100
|
+
* reference: 'INV-001',
|
|
101
|
+
* description: 'Monthly subscription',
|
|
102
|
+
* frequency: 'monthly',
|
|
103
|
+
* commencementDate: '2026-04-01',
|
|
104
|
+
* recurringDay: 1,
|
|
105
|
+
* returnUrl: 'https://myapp.com/success',
|
|
106
|
+
* cancelUrl: 'https://myapp.com/cancel',
|
|
107
|
+
* notifyUrl: 'https://myapp.com/webhook'
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* // Redirect customer to payment page
|
|
111
|
+
* console.log(bill.paymentUrl);
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
async createBill(params) {
|
|
115
|
+
const isRecurring = params.frequency !== 'once-off';
|
|
116
|
+
// Frequency type mapping: 1=once-off, 2=monthly, 7=yearly
|
|
117
|
+
const frequencyTypeID = params.frequency === 'yearly' ? 7 : params.frequency === 'monthly' ? 2 : 1;
|
|
118
|
+
// Build the bill item
|
|
119
|
+
const item = {
|
|
120
|
+
Description: params.description || 'Payment',
|
|
121
|
+
Amount: parseFloat(params.amount.toFixed(2)),
|
|
122
|
+
FrequencyTypeID: frequencyTypeID,
|
|
123
|
+
DisplayCompanyName: params.companyName || 'Your Company',
|
|
124
|
+
DisplayCompanyContactNo: params.companyContact || '',
|
|
125
|
+
DisplayCompanyEmailAddress: params.companyEmail || params.customerEmail,
|
|
126
|
+
};
|
|
127
|
+
// Add recurring-specific fields
|
|
128
|
+
if (isRecurring) {
|
|
129
|
+
// Validate commencement date is in the future
|
|
130
|
+
let commencementDate;
|
|
131
|
+
if (params.commencementDate) {
|
|
132
|
+
commencementDate = new Date(params.commencementDate);
|
|
133
|
+
const now = new Date();
|
|
134
|
+
now.setHours(0, 0, 0, 0);
|
|
135
|
+
if (commencementDate <= now) {
|
|
136
|
+
throw new Error('commencementDate must be a future date (minimum tomorrow)');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// Default to tomorrow
|
|
141
|
+
commencementDate = new Date();
|
|
142
|
+
commencementDate.setDate(commencementDate.getDate() + 1);
|
|
143
|
+
}
|
|
144
|
+
item.CommencementDate = commencementDate.toISOString().split('T')[0];
|
|
145
|
+
item.RecurringDay = params.recurringDay || commencementDate.getDate();
|
|
146
|
+
if (params.frequency === 'yearly') {
|
|
147
|
+
item.RecurringMonth = params.recurringMonth || (commencementDate.getMonth() + 1);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
item.RecurringMonth = null;
|
|
151
|
+
}
|
|
152
|
+
item.DayOfWeek = null;
|
|
153
|
+
item.ExpiryDate = null;
|
|
154
|
+
item.InitialAmount = null;
|
|
155
|
+
item.ToCollectAmount = null;
|
|
156
|
+
}
|
|
157
|
+
// Build the bill request
|
|
158
|
+
const billData = {
|
|
159
|
+
Name: params.customerName,
|
|
160
|
+
ModeTypeID: 4, // Plugin mode (returns payment URL)
|
|
161
|
+
Emailaddress: params.customerEmail,
|
|
162
|
+
Cellno: params.customerPhone,
|
|
163
|
+
UserReference: params.reference,
|
|
164
|
+
Items: [item],
|
|
165
|
+
ScheduledDateTime: null,
|
|
166
|
+
CallbackUrl: params.notifyUrl,
|
|
167
|
+
SuccessURL: params.returnUrl,
|
|
168
|
+
FailURL: params.cancelUrl,
|
|
169
|
+
NotifyURL: params.notifyUrl,
|
|
170
|
+
CancelURL: params.cancelUrl,
|
|
171
|
+
};
|
|
172
|
+
const result = await this.apiRequest('POST', '/api/paygatecontroller/requestbillpresentment', billData);
|
|
173
|
+
if (!result.success) {
|
|
174
|
+
throw new Error(`Failed to create bill: ${result.message}`);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
reference: result.reference,
|
|
178
|
+
paymentUrl: result.paymentURL,
|
|
179
|
+
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// ==================== Bill Status ====================
|
|
183
|
+
/**
|
|
184
|
+
* Get bill payment status
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* const status = await client.getBillStatus('BILL-REF-123');
|
|
189
|
+
* if (status.status === 'completed') {
|
|
190
|
+
* console.log('Payment received on:', status.paidAt);
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
async getBillStatus(reference) {
|
|
195
|
+
const result = await this.apiRequest('GET', `/api/paygatecontroller/listBillPresentmentDetails/${reference}/${reference}`);
|
|
196
|
+
// Map status: 1=pending, 2=completed, 3=failed, 4/5=cancelled
|
|
197
|
+
const statusTypeID = result?.statusTypeID || result?.status || 1;
|
|
198
|
+
const status = this.mapBillStatus(statusTypeID);
|
|
199
|
+
return {
|
|
200
|
+
reference: result?.reference || reference,
|
|
201
|
+
status,
|
|
202
|
+
amount: parseFloat(result?.amount || '0'),
|
|
203
|
+
paidAt: result?.transactionDate || undefined,
|
|
204
|
+
data: result,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// ==================== Refunds ====================
|
|
208
|
+
/**
|
|
209
|
+
* Process a refund (credit transaction)
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* // Full refund
|
|
214
|
+
* const refund = await client.refund({ transactionId: 'TXN-123' });
|
|
215
|
+
*
|
|
216
|
+
* // Partial refund
|
|
217
|
+
* const refund = await client.refund({
|
|
218
|
+
* transactionId: 'TXN-123',
|
|
219
|
+
* amount: 100.00
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
async refund(params) {
|
|
224
|
+
const refundData = {
|
|
225
|
+
Reference: params.transactionId,
|
|
226
|
+
UserReference: params.transactionId,
|
|
227
|
+
};
|
|
228
|
+
if (params.amount !== undefined) {
|
|
229
|
+
// IMPORTANT: SoftyComp uses capital "A" in Amount field for refunds
|
|
230
|
+
refundData.Amount = parseFloat(params.amount.toFixed(2));
|
|
231
|
+
}
|
|
232
|
+
const result = await this.apiRequest('POST', '/api/paygatecontroller/requestCreditTransaction', refundData);
|
|
233
|
+
return {
|
|
234
|
+
refundId: result?.reference || `refund_${params.transactionId}_${Date.now()}`,
|
|
235
|
+
status: result?.success ? 'completed' : 'pending',
|
|
236
|
+
amount: params.amount || 0,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// ==================== Webhooks ====================
|
|
240
|
+
/**
|
|
241
|
+
* Verify webhook signature (HMAC-SHA256)
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
|
|
246
|
+
* const signature = req.headers['x-signature'] as string;
|
|
247
|
+
* if (!client.verifyWebhook(req.body, { signature })) {
|
|
248
|
+
* return res.status(400).send('Invalid signature');
|
|
249
|
+
* }
|
|
250
|
+
* // Process webhook...
|
|
251
|
+
* });
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
verifyWebhook(body, headers) {
|
|
255
|
+
const signature = 'signature' in headers ? headers.signature : headers['x-signature'];
|
|
256
|
+
if (!signature || !this.webhookSecret) {
|
|
257
|
+
// No signature validation configured
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
const expectedSignature = crypto_1.default
|
|
261
|
+
.createHmac('sha256', this.webhookSecret)
|
|
262
|
+
.update(body)
|
|
263
|
+
.digest('hex');
|
|
264
|
+
return signature === expectedSignature || signature === `sha256=${expectedSignature}`;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Parse webhook event payload
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* const event = client.parseWebhook(req.body);
|
|
272
|
+
*
|
|
273
|
+
* switch (event.type) {
|
|
274
|
+
* case 'successful':
|
|
275
|
+
* console.log('Payment successful:', event.reference, event.amount);
|
|
276
|
+
* break;
|
|
277
|
+
* case 'failed':
|
|
278
|
+
* console.log('Payment failed:', event.reference);
|
|
279
|
+
* break;
|
|
280
|
+
* case 'cancelled':
|
|
281
|
+
* console.log('Payment cancelled:', event.reference);
|
|
282
|
+
* break;
|
|
283
|
+
* }
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
286
|
+
parseWebhook(body) {
|
|
287
|
+
const event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
288
|
+
// activityTypeID mapping: 1=Pending, 2=Successful, 3=Failed, 4=Cancelled
|
|
289
|
+
let type = 'pending';
|
|
290
|
+
let status = 'pending';
|
|
291
|
+
switch (event.activityTypeID) {
|
|
292
|
+
case 2:
|
|
293
|
+
type = 'successful';
|
|
294
|
+
status = 'completed';
|
|
295
|
+
break;
|
|
296
|
+
case 3:
|
|
297
|
+
type = 'failed';
|
|
298
|
+
status = 'failed';
|
|
299
|
+
break;
|
|
300
|
+
case 4:
|
|
301
|
+
type = 'cancelled';
|
|
302
|
+
status = 'cancelled';
|
|
303
|
+
break;
|
|
304
|
+
default:
|
|
305
|
+
type = 'pending';
|
|
306
|
+
status = 'pending';
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
type,
|
|
310
|
+
reference: event.reference,
|
|
311
|
+
status,
|
|
312
|
+
amount: event.amount,
|
|
313
|
+
transactionDate: event.transactionDate,
|
|
314
|
+
paymentMethodId: event.paymentMethodTypeID,
|
|
315
|
+
paymentMethod: event.paymentMethodTypeDescription,
|
|
316
|
+
userReference: event.userReference,
|
|
317
|
+
information: event.information,
|
|
318
|
+
raw: event,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// ==================== Helpers ====================
|
|
322
|
+
/**
|
|
323
|
+
* Map SoftyComp status type ID to payment status
|
|
324
|
+
* @internal
|
|
325
|
+
*/
|
|
326
|
+
mapBillStatus(statusTypeID) {
|
|
327
|
+
switch (Number(statusTypeID)) {
|
|
328
|
+
case 1: return 'pending'; // New
|
|
329
|
+
case 2: return 'completed'; // Paid
|
|
330
|
+
case 3: return 'failed'; // Failed
|
|
331
|
+
case 4: return 'cancelled'; // Expired
|
|
332
|
+
case 5: return 'cancelled'; // Cancelled
|
|
333
|
+
default: return 'pending';
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
exports.SoftyComp = SoftyComp;
|
|
338
|
+
// ==================== Default Export ====================
|
|
339
|
+
exports.default = SoftyComp;
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "softycomp-node",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node.js SDK for SoftyComp — South African bill presentment and debit order platform",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"prepublishOnly": "npm run build",
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"softycomp",
|
|
14
|
+
"south-africa",
|
|
15
|
+
"payments",
|
|
16
|
+
"debit-order",
|
|
17
|
+
"billing",
|
|
18
|
+
"nodejs",
|
|
19
|
+
"typescript",
|
|
20
|
+
"zar",
|
|
21
|
+
"bill-presentment",
|
|
22
|
+
"recurring-billing"
|
|
23
|
+
],
|
|
24
|
+
"author": "Kobie Wentzel",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/kobie3717/softycomp-node.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/kobie3717/softycomp-node/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/kobie3717/softycomp-node#readme",
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^18.19.130",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
}
|
|
46
|
+
}
|