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 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
+ [![npm version](https://img.shields.io/npm/v/softycomp-node.svg)](https://www.npmjs.com/package/softycomp-node)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](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.
@@ -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
+ }