paybridge 0.1.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 +408 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +227 -0
- package/dist/providers/base.d.ts +48 -0
- package/dist/providers/base.js +29 -0
- package/dist/providers/ozow.d.ts +33 -0
- package/dist/providers/ozow.js +203 -0
- package/dist/providers/softycomp.d.ts +34 -0
- package/dist/providers/softycomp.js +292 -0
- package/dist/providers/yoco.d.ts +30 -0
- package/dist/providers/yoco.js +158 -0
- package/dist/types.d.ts +167 -0
- package/dist/types.js +5 -0
- package/dist/utils/currency.d.ts +28 -0
- package/dist/utils/currency.js +67 -0
- package/package.json +50 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SoftyComp payment provider
|
|
4
|
+
* South African bill presentment and debit order platform
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.SoftyCompProvider = void 0;
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
const base_1 = require("./base");
|
|
13
|
+
class SoftyCompProvider extends base_1.PaymentProvider {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
super();
|
|
16
|
+
this.name = 'softycomp';
|
|
17
|
+
this.supportedCurrencies = ['ZAR'];
|
|
18
|
+
// Token cache
|
|
19
|
+
this.token = null;
|
|
20
|
+
this.tokenExpiry = 0;
|
|
21
|
+
this.apiKey = config.apiKey;
|
|
22
|
+
this.secretKey = config.secretKey;
|
|
23
|
+
this.sandbox = config.sandbox;
|
|
24
|
+
this.webhookSecret = config.webhookSecret;
|
|
25
|
+
// Base URL mapping
|
|
26
|
+
if (this.sandbox) {
|
|
27
|
+
this.baseUrl = 'https://sandbox.softycomp.co.za/SoftyCompBureauAPI';
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
this.baseUrl = 'https://api.softycomp.co.za/SoftyCompBureauAPI';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ==================== Authentication ====================
|
|
34
|
+
async authenticate() {
|
|
35
|
+
// Return cached token if still valid (with 60s buffer)
|
|
36
|
+
if (this.token && Date.now() < this.tokenExpiry - 60000) {
|
|
37
|
+
return this.token;
|
|
38
|
+
}
|
|
39
|
+
const response = await fetch(`${this.baseUrl}/api/auth/generatetoken`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
apiKey: this.apiKey,
|
|
44
|
+
apiSecret: this.secretKey,
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const errorText = await response.text();
|
|
49
|
+
throw new Error(`SoftyComp authentication failed: ${response.status} - ${errorText}`);
|
|
50
|
+
}
|
|
51
|
+
const data = (await response.json());
|
|
52
|
+
this.token = data.token;
|
|
53
|
+
this.tokenExpiry = new Date(data.expiration).getTime();
|
|
54
|
+
return this.token;
|
|
55
|
+
}
|
|
56
|
+
async apiRequest(method, path, data) {
|
|
57
|
+
const token = await this.authenticate();
|
|
58
|
+
const url = `${this.baseUrl}${path}`;
|
|
59
|
+
const response = await fetch(url, {
|
|
60
|
+
method,
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${token}`,
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
},
|
|
65
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const errorText = await response.text();
|
|
69
|
+
throw new Error(`SoftyComp API error (${method} ${path}): ${response.status} - ${errorText}`);
|
|
70
|
+
}
|
|
71
|
+
const contentType = response.headers.get('content-type');
|
|
72
|
+
if (contentType && contentType.includes('application/json')) {
|
|
73
|
+
return (await response.json());
|
|
74
|
+
}
|
|
75
|
+
return (await response.text());
|
|
76
|
+
}
|
|
77
|
+
// ==================== Payment Methods ====================
|
|
78
|
+
async createPayment(params) {
|
|
79
|
+
this.validateCurrency(params.currency);
|
|
80
|
+
// Build the bill item (once-off payment)
|
|
81
|
+
const item = {
|
|
82
|
+
Description: params.description || 'Payment',
|
|
83
|
+
Amount: parseFloat(params.amount.toFixed(2)),
|
|
84
|
+
FrequencyTypeID: 1, // Once-off
|
|
85
|
+
DisplayCompanyName: 'Your Company',
|
|
86
|
+
DisplayCompanyContactNo: '',
|
|
87
|
+
DisplayCompanyEmailAddress: params.customer.email,
|
|
88
|
+
};
|
|
89
|
+
// Build the bill request
|
|
90
|
+
const billData = {
|
|
91
|
+
Name: params.customer.name,
|
|
92
|
+
ModeTypeID: 4, // Plugin mode (returns payment URL)
|
|
93
|
+
Emailaddress: params.customer.email,
|
|
94
|
+
Cellno: params.customer.phone || '',
|
|
95
|
+
UserReference: params.reference,
|
|
96
|
+
Items: [item],
|
|
97
|
+
ScheduledDateTime: null,
|
|
98
|
+
CallbackUrl: params.urls.webhook,
|
|
99
|
+
SuccessURL: params.urls.success,
|
|
100
|
+
FailURL: params.urls.cancel,
|
|
101
|
+
NotifyURL: params.urls.webhook,
|
|
102
|
+
CancelURL: params.urls.cancel,
|
|
103
|
+
};
|
|
104
|
+
const result = await this.apiRequest('POST', '/api/paygatecontroller/requestbillpresentment', billData);
|
|
105
|
+
if (!result.success) {
|
|
106
|
+
throw new Error(`Failed to create payment: ${result.message}`);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
id: result.reference,
|
|
110
|
+
checkoutUrl: result.paymentURL,
|
|
111
|
+
status: 'pending',
|
|
112
|
+
amount: params.amount,
|
|
113
|
+
currency: params.currency,
|
|
114
|
+
reference: params.reference,
|
|
115
|
+
provider: 'softycomp',
|
|
116
|
+
createdAt: new Date().toISOString(),
|
|
117
|
+
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
|
|
118
|
+
raw: result,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async createSubscription(params) {
|
|
122
|
+
this.validateCurrency(params.currency);
|
|
123
|
+
// Map interval to SoftyComp frequency: 2=monthly, 7=yearly
|
|
124
|
+
let frequencyTypeID;
|
|
125
|
+
if (params.interval === 'monthly') {
|
|
126
|
+
frequencyTypeID = 2;
|
|
127
|
+
}
|
|
128
|
+
else if (params.interval === 'yearly') {
|
|
129
|
+
frequencyTypeID = 7;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
throw new Error(`SoftyComp does not support ${params.interval} subscriptions. Use monthly or yearly.`);
|
|
133
|
+
}
|
|
134
|
+
// Parse and validate start date
|
|
135
|
+
let startDate;
|
|
136
|
+
if (params.startDate) {
|
|
137
|
+
this.validateFutureDate(params.startDate, 'startDate');
|
|
138
|
+
startDate = new Date(params.startDate);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Default to tomorrow
|
|
142
|
+
startDate = new Date();
|
|
143
|
+
startDate.setDate(startDate.getDate() + 1);
|
|
144
|
+
}
|
|
145
|
+
// Build the bill item (recurring)
|
|
146
|
+
const item = {
|
|
147
|
+
Description: params.description || 'Subscription',
|
|
148
|
+
Amount: parseFloat(params.amount.toFixed(2)),
|
|
149
|
+
FrequencyTypeID: frequencyTypeID,
|
|
150
|
+
DisplayCompanyName: 'Your Company',
|
|
151
|
+
DisplayCompanyContactNo: '',
|
|
152
|
+
DisplayCompanyEmailAddress: params.customer.email,
|
|
153
|
+
CommencementDate: startDate.toISOString().split('T')[0],
|
|
154
|
+
RecurringDay: params.billingDay || startDate.getDate(),
|
|
155
|
+
RecurringMonth: params.interval === 'yearly' ? startDate.getMonth() + 1 : null,
|
|
156
|
+
DayOfWeek: null,
|
|
157
|
+
ExpiryDate: null,
|
|
158
|
+
InitialAmount: null,
|
|
159
|
+
ToCollectAmount: null,
|
|
160
|
+
};
|
|
161
|
+
// Build the bill request
|
|
162
|
+
const billData = {
|
|
163
|
+
Name: params.customer.name,
|
|
164
|
+
ModeTypeID: 4,
|
|
165
|
+
Emailaddress: params.customer.email,
|
|
166
|
+
Cellno: params.customer.phone || '',
|
|
167
|
+
UserReference: params.reference,
|
|
168
|
+
Items: [item],
|
|
169
|
+
ScheduledDateTime: null,
|
|
170
|
+
CallbackUrl: params.urls.webhook,
|
|
171
|
+
SuccessURL: params.urls.success,
|
|
172
|
+
FailURL: params.urls.cancel,
|
|
173
|
+
NotifyURL: params.urls.webhook,
|
|
174
|
+
CancelURL: params.urls.cancel,
|
|
175
|
+
};
|
|
176
|
+
const result = await this.apiRequest('POST', '/api/paygatecontroller/requestbillpresentment', billData);
|
|
177
|
+
if (!result.success) {
|
|
178
|
+
throw new Error(`Failed to create subscription: ${result.message}`);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
id: result.reference,
|
|
182
|
+
checkoutUrl: result.paymentURL,
|
|
183
|
+
status: 'pending',
|
|
184
|
+
amount: params.amount,
|
|
185
|
+
currency: params.currency,
|
|
186
|
+
interval: params.interval,
|
|
187
|
+
reference: params.reference,
|
|
188
|
+
provider: 'softycomp',
|
|
189
|
+
startsAt: startDate.toISOString(),
|
|
190
|
+
createdAt: new Date().toISOString(),
|
|
191
|
+
raw: result,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async getPayment(id) {
|
|
195
|
+
const result = await this.apiRequest('GET', `/api/paygatecontroller/listBillPresentmentDetails/${id}/${id}`);
|
|
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
|
+
id: result?.reference || id,
|
|
201
|
+
checkoutUrl: '', // Not available from status endpoint
|
|
202
|
+
status,
|
|
203
|
+
amount: parseFloat(result?.amount || '0'),
|
|
204
|
+
currency: 'ZAR',
|
|
205
|
+
reference: result?.userReference || id,
|
|
206
|
+
provider: 'softycomp',
|
|
207
|
+
createdAt: result?.createdDate || new Date().toISOString(),
|
|
208
|
+
raw: result,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async refund(params) {
|
|
212
|
+
const refundData = {
|
|
213
|
+
Reference: params.paymentId,
|
|
214
|
+
UserReference: params.paymentId,
|
|
215
|
+
};
|
|
216
|
+
if (params.amount !== undefined) {
|
|
217
|
+
refundData.Amount = parseFloat(params.amount.toFixed(2));
|
|
218
|
+
}
|
|
219
|
+
const result = await this.apiRequest('POST', '/api/paygatecontroller/requestCreditTransaction', refundData);
|
|
220
|
+
return {
|
|
221
|
+
id: result?.reference || `refund_${params.paymentId}_${Date.now()}`,
|
|
222
|
+
status: result?.success ? 'completed' : 'pending',
|
|
223
|
+
amount: params.amount || 0,
|
|
224
|
+
currency: 'ZAR',
|
|
225
|
+
paymentId: params.paymentId,
|
|
226
|
+
createdAt: new Date().toISOString(),
|
|
227
|
+
raw: result,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// ==================== Webhooks ====================
|
|
231
|
+
parseWebhook(body, _headers) {
|
|
232
|
+
const event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
233
|
+
// activityTypeID mapping: 1=Pending, 2=Successful, 3=Failed, 4=Cancelled
|
|
234
|
+
let eventType = 'payment.pending';
|
|
235
|
+
let status = 'pending';
|
|
236
|
+
switch (event.activityTypeID) {
|
|
237
|
+
case 2:
|
|
238
|
+
eventType = 'payment.completed';
|
|
239
|
+
status = 'completed';
|
|
240
|
+
break;
|
|
241
|
+
case 3:
|
|
242
|
+
eventType = 'payment.failed';
|
|
243
|
+
status = 'failed';
|
|
244
|
+
break;
|
|
245
|
+
case 4:
|
|
246
|
+
eventType = 'payment.cancelled';
|
|
247
|
+
status = 'cancelled';
|
|
248
|
+
break;
|
|
249
|
+
default:
|
|
250
|
+
eventType = 'payment.pending';
|
|
251
|
+
status = 'pending';
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
type: eventType,
|
|
255
|
+
payment: {
|
|
256
|
+
id: event.reference,
|
|
257
|
+
checkoutUrl: '',
|
|
258
|
+
status,
|
|
259
|
+
amount: event.amount,
|
|
260
|
+
currency: 'ZAR',
|
|
261
|
+
reference: event.userReference,
|
|
262
|
+
provider: 'softycomp',
|
|
263
|
+
createdAt: event.transactionDate,
|
|
264
|
+
},
|
|
265
|
+
raw: event,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
verifyWebhook(body, headers) {
|
|
269
|
+
const signature = headers?.signature || headers?.['x-signature'];
|
|
270
|
+
if (!signature || !this.webhookSecret) {
|
|
271
|
+
// No signature validation configured
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
const expectedSignature = crypto_1.default
|
|
275
|
+
.createHmac('sha256', this.webhookSecret)
|
|
276
|
+
.update(body)
|
|
277
|
+
.digest('hex');
|
|
278
|
+
return signature === expectedSignature || signature === `sha256=${expectedSignature}`;
|
|
279
|
+
}
|
|
280
|
+
// ==================== Helpers ====================
|
|
281
|
+
mapBillStatus(statusTypeID) {
|
|
282
|
+
switch (Number(statusTypeID)) {
|
|
283
|
+
case 1: return 'pending'; // New
|
|
284
|
+
case 2: return 'completed'; // Paid
|
|
285
|
+
case 3: return 'failed'; // Failed
|
|
286
|
+
case 4: return 'cancelled'; // Expired
|
|
287
|
+
case 5: return 'cancelled'; // Cancelled
|
|
288
|
+
default: return 'pending';
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
exports.SoftyCompProvider = SoftyCompProvider;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yoco payment provider
|
|
3
|
+
* South African online payment gateway
|
|
4
|
+
* @see https://developer.yoco.com
|
|
5
|
+
*/
|
|
6
|
+
import { PaymentProvider } from './base';
|
|
7
|
+
import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent } from '../types';
|
|
8
|
+
interface YocoConfig {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
sandbox: boolean;
|
|
11
|
+
webhookSecret?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class YocoProvider extends PaymentProvider {
|
|
14
|
+
readonly name = "yoco";
|
|
15
|
+
readonly supportedCurrencies: string[];
|
|
16
|
+
private apiKey;
|
|
17
|
+
private sandbox;
|
|
18
|
+
private baseUrl;
|
|
19
|
+
private webhookSecret?;
|
|
20
|
+
constructor(config: YocoConfig);
|
|
21
|
+
createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
|
|
22
|
+
createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
23
|
+
getPayment(id: string): Promise<PaymentResult>;
|
|
24
|
+
refund(params: RefundParams): Promise<RefundResult>;
|
|
25
|
+
parseWebhook(body: any, _headers?: any): WebhookEvent;
|
|
26
|
+
verifyWebhook(body: string | Buffer, headers?: any): boolean;
|
|
27
|
+
private mapYocoStatus;
|
|
28
|
+
private mapYocoEventType;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Yoco payment provider
|
|
4
|
+
* South African online payment gateway
|
|
5
|
+
* @see https://developer.yoco.com
|
|
6
|
+
*/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.YocoProvider = void 0;
|
|
12
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
13
|
+
const base_1 = require("./base");
|
|
14
|
+
const currency_1 = require("../utils/currency");
|
|
15
|
+
class YocoProvider extends base_1.PaymentProvider {
|
|
16
|
+
constructor(config) {
|
|
17
|
+
super();
|
|
18
|
+
this.name = 'yoco';
|
|
19
|
+
this.supportedCurrencies = ['ZAR'];
|
|
20
|
+
this.apiKey = config.apiKey;
|
|
21
|
+
this.sandbox = config.sandbox;
|
|
22
|
+
this.webhookSecret = config.webhookSecret;
|
|
23
|
+
// Yoco uses same API for sandbox and production, differentiated by API keys
|
|
24
|
+
this.baseUrl = 'https://payments.yoco.com/api/v1';
|
|
25
|
+
}
|
|
26
|
+
// ==================== Payment Methods ====================
|
|
27
|
+
async createPayment(params) {
|
|
28
|
+
this.validateCurrency(params.currency);
|
|
29
|
+
// TODO: Implement Yoco checkout creation
|
|
30
|
+
// POST /v1/checkouts
|
|
31
|
+
// Amount must be in CENTS (minor unit)
|
|
32
|
+
// Auth: Bearer {secret_key}
|
|
33
|
+
// Body: {
|
|
34
|
+
// amount: 29900, // cents
|
|
35
|
+
// currency: "ZAR",
|
|
36
|
+
// cancelUrl: "...",
|
|
37
|
+
// successUrl: "...",
|
|
38
|
+
// failureUrl: "...",
|
|
39
|
+
// metadata: { ... }
|
|
40
|
+
// }
|
|
41
|
+
const amountInCents = (0, currency_1.toMinorUnit)(params.amount, params.currency);
|
|
42
|
+
const requestBody = {
|
|
43
|
+
amount: amountInCents,
|
|
44
|
+
currency: params.currency,
|
|
45
|
+
cancelUrl: params.urls.cancel,
|
|
46
|
+
successUrl: params.urls.success,
|
|
47
|
+
failureUrl: params.urls.cancel,
|
|
48
|
+
metadata: {
|
|
49
|
+
reference: params.reference,
|
|
50
|
+
description: params.description,
|
|
51
|
+
customerName: params.customer.name,
|
|
52
|
+
customerEmail: params.customer.email,
|
|
53
|
+
...params.metadata,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
// TODO: Make actual API request
|
|
57
|
+
console.warn('[PayBridge:Yoco] createPayment not yet implemented:', requestBody);
|
|
58
|
+
throw new Error('Yoco provider not yet fully implemented. Coming soon!');
|
|
59
|
+
}
|
|
60
|
+
async createSubscription(params) {
|
|
61
|
+
this.validateCurrency(params.currency);
|
|
62
|
+
// TODO: Implement Yoco subscription creation
|
|
63
|
+
// Yoco may not support subscriptions directly - need to check their API
|
|
64
|
+
// May need to implement via recurring charges or use Yoco recurring billing
|
|
65
|
+
console.warn('[PayBridge:Yoco] createSubscription not yet implemented');
|
|
66
|
+
throw new Error('Yoco subscriptions not yet implemented. Coming soon!');
|
|
67
|
+
}
|
|
68
|
+
async getPayment(id) {
|
|
69
|
+
// TODO: Implement Yoco payment status check
|
|
70
|
+
// GET /v1/checkouts/{id}
|
|
71
|
+
// Auth: Bearer {secret_key}
|
|
72
|
+
console.warn('[PayBridge:Yoco] getPayment not yet implemented:', id);
|
|
73
|
+
throw new Error('Yoco getPayment not yet implemented. Coming soon!');
|
|
74
|
+
}
|
|
75
|
+
async refund(params) {
|
|
76
|
+
// TODO: Implement Yoco refund
|
|
77
|
+
// POST /v1/refunds
|
|
78
|
+
// Amount in CENTS
|
|
79
|
+
// Auth: Bearer {secret_key}
|
|
80
|
+
// Body: {
|
|
81
|
+
// paymentId: "...",
|
|
82
|
+
// amount: 29900 // cents, optional for partial refund
|
|
83
|
+
// }
|
|
84
|
+
const amountInCents = params.amount ? (0, currency_1.toMinorUnit)(params.amount, 'ZAR') : undefined;
|
|
85
|
+
console.warn('[PayBridge:Yoco] refund not yet implemented:', { ...params, amountInCents });
|
|
86
|
+
throw new Error('Yoco refunds not yet implemented. Coming soon!');
|
|
87
|
+
}
|
|
88
|
+
// ==================== Webhooks ====================
|
|
89
|
+
parseWebhook(body, _headers) {
|
|
90
|
+
const event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
91
|
+
// TODO: Map Yoco webhook structure to PayBridge WebhookEvent
|
|
92
|
+
// Yoco webhook payload structure:
|
|
93
|
+
// {
|
|
94
|
+
// type: "payment.succeeded" | "payment.failed" | "payment.refunded",
|
|
95
|
+
// payload: {
|
|
96
|
+
// id: "...",
|
|
97
|
+
// amount: 29900, // cents
|
|
98
|
+
// currency: "ZAR",
|
|
99
|
+
// status: "succeeded" | "failed" | "refunded",
|
|
100
|
+
// metadata: { ... }
|
|
101
|
+
// }
|
|
102
|
+
// }
|
|
103
|
+
const yocoStatus = event.payload?.status || event.status;
|
|
104
|
+
const status = this.mapYocoStatus(yocoStatus);
|
|
105
|
+
const eventType = this.mapYocoEventType(event.type);
|
|
106
|
+
return {
|
|
107
|
+
type: eventType,
|
|
108
|
+
payment: {
|
|
109
|
+
id: event.payload?.id || event.id,
|
|
110
|
+
checkoutUrl: '',
|
|
111
|
+
status,
|
|
112
|
+
amount: (0, currency_1.toMajorUnit)(event.payload?.amount || 0, 'ZAR'),
|
|
113
|
+
currency: 'ZAR',
|
|
114
|
+
reference: event.payload?.metadata?.reference || '',
|
|
115
|
+
provider: 'yoco',
|
|
116
|
+
createdAt: event.payload?.createdDate || new Date().toISOString(),
|
|
117
|
+
},
|
|
118
|
+
raw: event,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
verifyWebhook(body, headers) {
|
|
122
|
+
const signature = headers?.['x-yoco-signature'] || headers?.signature;
|
|
123
|
+
if (!signature || !this.webhookSecret) {
|
|
124
|
+
// No signature validation configured
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
// TODO: Implement Yoco signature validation
|
|
128
|
+
// Yoco uses HMAC-SHA256 with X-Yoco-Signature header
|
|
129
|
+
const expectedSignature = crypto_1.default
|
|
130
|
+
.createHmac('sha256', this.webhookSecret)
|
|
131
|
+
.update(body)
|
|
132
|
+
.digest('hex');
|
|
133
|
+
return signature === expectedSignature;
|
|
134
|
+
}
|
|
135
|
+
// ==================== Helpers ====================
|
|
136
|
+
mapYocoStatus(yocoStatus) {
|
|
137
|
+
const statusMap = {
|
|
138
|
+
succeeded: 'completed',
|
|
139
|
+
successful: 'completed',
|
|
140
|
+
failed: 'failed',
|
|
141
|
+
cancelled: 'cancelled',
|
|
142
|
+
refunded: 'refunded',
|
|
143
|
+
pending: 'pending',
|
|
144
|
+
};
|
|
145
|
+
return statusMap[yocoStatus?.toLowerCase()] || 'pending';
|
|
146
|
+
}
|
|
147
|
+
mapYocoEventType(yocoType) {
|
|
148
|
+
const typeMap = {
|
|
149
|
+
'payment.succeeded': 'payment.completed',
|
|
150
|
+
'payment.successful': 'payment.completed',
|
|
151
|
+
'payment.failed': 'payment.failed',
|
|
152
|
+
'payment.cancelled': 'payment.cancelled',
|
|
153
|
+
'payment.refunded': 'refund.completed',
|
|
154
|
+
};
|
|
155
|
+
return typeMap[yocoType] || 'payment.pending';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.YocoProvider = YocoProvider;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PayBridge — Unified payment SDK types
|
|
3
|
+
*/
|
|
4
|
+
export type Provider = 'softycomp' | 'yoco' | 'ozow' | 'payfast' | 'paystack' | 'stripe' | 'peach';
|
|
5
|
+
export type PaymentStatus = 'pending' | 'completed' | 'failed' | 'cancelled' | 'refunded';
|
|
6
|
+
export type SubscriptionInterval = 'weekly' | 'monthly' | 'yearly';
|
|
7
|
+
export type Currency = 'ZAR' | 'USD' | 'EUR' | 'GBP' | 'NGN';
|
|
8
|
+
export type WebhookEventType = 'payment.pending' | 'payment.completed' | 'payment.failed' | 'payment.cancelled' | 'subscription.created' | 'subscription.cancelled' | 'refund.completed';
|
|
9
|
+
export interface PayBridgeConfig {
|
|
10
|
+
/** Payment provider to use */
|
|
11
|
+
provider: Provider;
|
|
12
|
+
/** Provider-specific credentials */
|
|
13
|
+
credentials: {
|
|
14
|
+
/** API key or merchant ID */
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
/** Secret key or merchant key */
|
|
17
|
+
secretKey?: string;
|
|
18
|
+
/** Additional provider-specific credentials */
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
};
|
|
21
|
+
/** Use sandbox/test environment */
|
|
22
|
+
sandbox?: boolean;
|
|
23
|
+
/** Optional webhook secret for signature validation */
|
|
24
|
+
webhookSecret?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface Customer {
|
|
27
|
+
/** Customer full name */
|
|
28
|
+
name: string;
|
|
29
|
+
/** Customer email address */
|
|
30
|
+
email: string;
|
|
31
|
+
/** Customer phone number (e.g. "0825551234" or "+27825551234") */
|
|
32
|
+
phone?: string;
|
|
33
|
+
/** Customer ID in your system */
|
|
34
|
+
customerId?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface CreatePaymentParams {
|
|
37
|
+
/** Amount in major currency unit (e.g. 299.00 for R299) */
|
|
38
|
+
amount: number;
|
|
39
|
+
/** Currency code (ISO 4217) */
|
|
40
|
+
currency: Currency;
|
|
41
|
+
/** Your internal reference/invoice number */
|
|
42
|
+
reference: string;
|
|
43
|
+
/** Payment description */
|
|
44
|
+
description?: string;
|
|
45
|
+
/** Customer details */
|
|
46
|
+
customer: Customer;
|
|
47
|
+
/** Redirect URLs */
|
|
48
|
+
urls: {
|
|
49
|
+
/** URL to redirect customer after successful payment */
|
|
50
|
+
success: string;
|
|
51
|
+
/** URL to redirect customer after cancelled payment */
|
|
52
|
+
cancel: string;
|
|
53
|
+
/** URL to receive webhook notifications */
|
|
54
|
+
webhook: string;
|
|
55
|
+
};
|
|
56
|
+
/** Additional metadata (up to 10 key-value pairs) */
|
|
57
|
+
metadata?: Record<string, any>;
|
|
58
|
+
}
|
|
59
|
+
export interface PaymentResult {
|
|
60
|
+
/** Unique payment ID from provider */
|
|
61
|
+
id: string;
|
|
62
|
+
/** Payment checkout URL (redirect customer here) */
|
|
63
|
+
checkoutUrl: string;
|
|
64
|
+
/** Payment status */
|
|
65
|
+
status: PaymentStatus;
|
|
66
|
+
/** Amount in major currency unit */
|
|
67
|
+
amount: number;
|
|
68
|
+
/** Currency code */
|
|
69
|
+
currency: Currency;
|
|
70
|
+
/** Your reference */
|
|
71
|
+
reference: string;
|
|
72
|
+
/** Provider name */
|
|
73
|
+
provider: Provider;
|
|
74
|
+
/** ISO 8601 timestamp when payment was created */
|
|
75
|
+
createdAt: string;
|
|
76
|
+
/** ISO 8601 timestamp when checkout URL expires (if applicable) */
|
|
77
|
+
expiresAt?: string;
|
|
78
|
+
/** Raw provider response */
|
|
79
|
+
raw?: any;
|
|
80
|
+
}
|
|
81
|
+
export interface CreateSubscriptionParams {
|
|
82
|
+
/** Amount in major currency unit (e.g. 299.00 for R299) */
|
|
83
|
+
amount: number;
|
|
84
|
+
/** Currency code (ISO 4217) */
|
|
85
|
+
currency: Currency;
|
|
86
|
+
/** Billing interval */
|
|
87
|
+
interval: SubscriptionInterval;
|
|
88
|
+
/** Your internal subscription reference */
|
|
89
|
+
reference: string;
|
|
90
|
+
/** Subscription description */
|
|
91
|
+
description?: string;
|
|
92
|
+
/** Customer details */
|
|
93
|
+
customer: Customer;
|
|
94
|
+
/** Redirect URLs */
|
|
95
|
+
urls: {
|
|
96
|
+
/** URL to redirect customer after successful setup */
|
|
97
|
+
success: string;
|
|
98
|
+
/** URL to redirect customer after cancelled setup */
|
|
99
|
+
cancel: string;
|
|
100
|
+
/** URL to receive webhook notifications */
|
|
101
|
+
webhook: string;
|
|
102
|
+
};
|
|
103
|
+
/** Subscription start date (ISO 8601). Must be future date. */
|
|
104
|
+
startDate?: string;
|
|
105
|
+
/** Day of month to charge (1-28). Only for monthly subscriptions. */
|
|
106
|
+
billingDay?: number;
|
|
107
|
+
/** Additional metadata */
|
|
108
|
+
metadata?: Record<string, any>;
|
|
109
|
+
}
|
|
110
|
+
export interface SubscriptionResult {
|
|
111
|
+
/** Unique subscription ID from provider */
|
|
112
|
+
id: string;
|
|
113
|
+
/** Subscription setup URL (redirect customer here) */
|
|
114
|
+
checkoutUrl: string;
|
|
115
|
+
/** Subscription status */
|
|
116
|
+
status: 'pending' | 'active' | 'cancelled' | 'expired';
|
|
117
|
+
/** Amount in major currency unit */
|
|
118
|
+
amount: number;
|
|
119
|
+
/** Currency code */
|
|
120
|
+
currency: Currency;
|
|
121
|
+
/** Billing interval */
|
|
122
|
+
interval: SubscriptionInterval;
|
|
123
|
+
/** Your reference */
|
|
124
|
+
reference: string;
|
|
125
|
+
/** Provider name */
|
|
126
|
+
provider: Provider;
|
|
127
|
+
/** ISO 8601 timestamp when subscription starts */
|
|
128
|
+
startsAt?: string;
|
|
129
|
+
/** ISO 8601 timestamp when subscription was created */
|
|
130
|
+
createdAt: string;
|
|
131
|
+
/** Raw provider response */
|
|
132
|
+
raw?: any;
|
|
133
|
+
}
|
|
134
|
+
export interface RefundParams {
|
|
135
|
+
/** Original payment ID to refund */
|
|
136
|
+
paymentId: string;
|
|
137
|
+
/** Amount to refund in major currency unit. Omit for full refund. */
|
|
138
|
+
amount?: number;
|
|
139
|
+
/** Reason for refund */
|
|
140
|
+
reason?: string;
|
|
141
|
+
}
|
|
142
|
+
export interface RefundResult {
|
|
143
|
+
/** Refund ID */
|
|
144
|
+
id: string;
|
|
145
|
+
/** Refund status */
|
|
146
|
+
status: 'pending' | 'completed' | 'failed';
|
|
147
|
+
/** Amount refunded in major currency unit */
|
|
148
|
+
amount: number;
|
|
149
|
+
/** Currency code */
|
|
150
|
+
currency: Currency;
|
|
151
|
+
/** Original payment ID */
|
|
152
|
+
paymentId: string;
|
|
153
|
+
/** ISO 8601 timestamp when refund was created */
|
|
154
|
+
createdAt: string;
|
|
155
|
+
/** Raw provider response */
|
|
156
|
+
raw?: any;
|
|
157
|
+
}
|
|
158
|
+
export interface WebhookEvent {
|
|
159
|
+
/** Event type */
|
|
160
|
+
type: WebhookEventType;
|
|
161
|
+
/** Payment or subscription details */
|
|
162
|
+
payment?: PaymentResult;
|
|
163
|
+
subscription?: SubscriptionResult;
|
|
164
|
+
refund?: RefundResult;
|
|
165
|
+
/** Raw provider payload */
|
|
166
|
+
raw: any;
|
|
167
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Currency conversion utilities
|
|
3
|
+
*/
|
|
4
|
+
import { Currency } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* Convert major currency unit to minor unit (cents)
|
|
7
|
+
* @example toMinorUnit(299.00, 'ZAR') => 29900
|
|
8
|
+
*/
|
|
9
|
+
export declare function toMinorUnit(amount: number, currency: Currency): number;
|
|
10
|
+
/**
|
|
11
|
+
* Convert minor unit (cents) to major currency unit
|
|
12
|
+
* @example toMajorUnit(29900, 'ZAR') => 299.00
|
|
13
|
+
*/
|
|
14
|
+
export declare function toMajorUnit(amount: number, currency: Currency): number;
|
|
15
|
+
/**
|
|
16
|
+
* Get decimal places for currency
|
|
17
|
+
* Most currencies use 2 decimal places, some use 0
|
|
18
|
+
*/
|
|
19
|
+
export declare function getDecimalPlaces(currency: Currency): number;
|
|
20
|
+
/**
|
|
21
|
+
* Format amount with currency symbol
|
|
22
|
+
* @example formatCurrency(299.00, 'ZAR') => "R299.00"
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatCurrency(amount: number, currency: Currency): string;
|
|
25
|
+
/**
|
|
26
|
+
* Validate amount is positive and has correct decimal places
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateAmount(amount: number, currency: Currency): void;
|