paybridge 0.1.3 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -7
- package/dist/circuit-breaker-store.d.ts +27 -0
- package/dist/circuit-breaker-store.js +25 -0
- package/dist/circuit-breaker.d.ts +30 -0
- package/dist/circuit-breaker.js +86 -0
- package/dist/crypto/base.d.ts +15 -0
- package/dist/crypto/base.js +24 -0
- package/dist/crypto/index.d.ts +35 -0
- package/dist/crypto/index.js +95 -0
- package/dist/crypto/mock.d.ts +15 -0
- package/dist/crypto/mock.js +112 -0
- package/dist/crypto/moonpay.d.ts +33 -0
- package/dist/crypto/moonpay.js +261 -0
- package/dist/crypto/router.d.ts +36 -0
- package/dist/crypto/router.js +287 -0
- package/dist/crypto/types.d.ts +89 -0
- package/dist/crypto/types.js +5 -0
- package/dist/crypto/yellowcard.d.ts +56 -0
- package/dist/crypto/yellowcard.js +311 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +60 -3
- package/dist/providers/base.d.ts +5 -0
- package/dist/providers/flutterwave.d.ts +36 -0
- package/dist/providers/flutterwave.js +339 -0
- package/dist/providers/ozow.d.ts +20 -2
- package/dist/providers/ozow.js +158 -114
- package/dist/providers/payfast.d.ts +40 -0
- package/dist/providers/payfast.js +352 -0
- package/dist/providers/paystack.d.ts +37 -0
- package/dist/providers/paystack.js +336 -0
- package/dist/providers/peach.d.ts +50 -0
- package/dist/providers/peach.js +302 -0
- package/dist/providers/softycomp.d.ts +106 -0
- package/dist/providers/softycomp.js +229 -10
- package/dist/providers/stripe.d.ts +38 -0
- package/dist/providers/stripe.js +367 -0
- package/dist/providers/yoco.d.ts +12 -0
- package/dist/providers/yoco.js +148 -61
- package/dist/router.d.ts +33 -0
- package/dist/router.js +282 -0
- package/dist/routing-types.d.ts +39 -0
- package/dist/routing-types.js +14 -0
- package/dist/stores/redis.d.ts +30 -0
- package/dist/stores/redis.js +42 -0
- package/dist/strategies.d.ts +18 -0
- package/dist/strategies.js +44 -0
- package/dist/types.d.ts +4 -2
- package/dist/utils/fetch.d.ts +24 -0
- package/dist/utils/fetch.js +74 -0
- package/package.json +7 -4
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PayStack payment provider
|
|
4
|
+
* Leading payment gateway for Africa (Nigeria, Ghana, South Africa, Kenya)
|
|
5
|
+
* @see https://paystack.com/docs/api
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.PayStackProvider = void 0;
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
const base_1 = require("./base");
|
|
44
|
+
const currency_1 = require("../utils/currency");
|
|
45
|
+
const fetch_1 = require("../utils/fetch");
|
|
46
|
+
class PayStackProvider extends base_1.PaymentProvider {
|
|
47
|
+
constructor(config) {
|
|
48
|
+
super();
|
|
49
|
+
this.name = 'paystack';
|
|
50
|
+
this.supportedCurrencies = ['NGN', 'GHS', 'ZAR', 'USD', 'KES'];
|
|
51
|
+
this.baseUrl = 'https://api.paystack.co';
|
|
52
|
+
this.apiKey = config.apiKey;
|
|
53
|
+
this.webhookSecret = config.webhookSecret;
|
|
54
|
+
this.sandbox = config.sandbox ?? this.apiKey.startsWith('sk_test_');
|
|
55
|
+
}
|
|
56
|
+
async apiRequest(method, path, data) {
|
|
57
|
+
const url = `${this.baseUrl}${path}`;
|
|
58
|
+
const response = await (0, fetch_1.timedFetch)(url, {
|
|
59
|
+
method,
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
},
|
|
64
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
65
|
+
});
|
|
66
|
+
const json = await response.json();
|
|
67
|
+
if (!response.ok || json.status === false) {
|
|
68
|
+
throw new Error(json.message || `PayStack API error (${method} ${path}): ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
return json;
|
|
71
|
+
}
|
|
72
|
+
async createPayment(params) {
|
|
73
|
+
this.validateCurrency(params.currency);
|
|
74
|
+
const amountInMinorUnits = (0, currency_1.toMinorUnit)(params.amount, params.currency);
|
|
75
|
+
const metadata = {
|
|
76
|
+
cancel_action: params.urls.cancel,
|
|
77
|
+
reference: params.reference,
|
|
78
|
+
custom_fields: [
|
|
79
|
+
{
|
|
80
|
+
display_name: 'Customer',
|
|
81
|
+
variable_name: 'customer_name',
|
|
82
|
+
value: params.customer.name,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
if (params.metadata) {
|
|
87
|
+
for (const [key, value] of Object.entries(params.metadata)) {
|
|
88
|
+
metadata[key] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const requestBody = {
|
|
92
|
+
email: params.customer.email,
|
|
93
|
+
amount: amountInMinorUnits,
|
|
94
|
+
currency: params.currency,
|
|
95
|
+
reference: params.reference,
|
|
96
|
+
callback_url: params.urls.success,
|
|
97
|
+
metadata,
|
|
98
|
+
};
|
|
99
|
+
const response = await this.apiRequest('POST', '/transaction/initialize', requestBody);
|
|
100
|
+
return {
|
|
101
|
+
id: response.data.reference,
|
|
102
|
+
checkoutUrl: response.data.authorization_url,
|
|
103
|
+
status: 'pending',
|
|
104
|
+
amount: params.amount,
|
|
105
|
+
currency: params.currency.toUpperCase(),
|
|
106
|
+
reference: response.data.reference,
|
|
107
|
+
provider: 'paystack',
|
|
108
|
+
createdAt: new Date().toISOString(),
|
|
109
|
+
raw: response,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async createSubscription(params) {
|
|
113
|
+
this.validateCurrency(params.currency);
|
|
114
|
+
const amountInMinorUnits = (0, currency_1.toMinorUnit)(params.amount, params.currency);
|
|
115
|
+
const intervalMap = {
|
|
116
|
+
weekly: 'weekly',
|
|
117
|
+
monthly: 'monthly',
|
|
118
|
+
yearly: 'annually',
|
|
119
|
+
};
|
|
120
|
+
const paystackInterval = intervalMap[params.interval];
|
|
121
|
+
const planData = {
|
|
122
|
+
name: params.description || params.reference,
|
|
123
|
+
amount: amountInMinorUnits,
|
|
124
|
+
interval: paystackInterval,
|
|
125
|
+
currency: params.currency,
|
|
126
|
+
};
|
|
127
|
+
const planResponse = await this.apiRequest('POST', '/plan', planData);
|
|
128
|
+
const planCode = planResponse.data.plan_code;
|
|
129
|
+
const metadata = {
|
|
130
|
+
cancel_action: params.urls.cancel,
|
|
131
|
+
reference: params.reference,
|
|
132
|
+
custom_fields: [
|
|
133
|
+
{
|
|
134
|
+
display_name: 'Customer',
|
|
135
|
+
variable_name: 'customer_name',
|
|
136
|
+
value: params.customer.name,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
if (params.metadata) {
|
|
141
|
+
for (const [key, value] of Object.entries(params.metadata)) {
|
|
142
|
+
metadata[key] = value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const transactionData = {
|
|
146
|
+
email: params.customer.email,
|
|
147
|
+
plan: planCode,
|
|
148
|
+
callback_url: params.urls.success,
|
|
149
|
+
metadata,
|
|
150
|
+
};
|
|
151
|
+
const transactionResponse = await this.apiRequest('POST', '/transaction/initialize', transactionData);
|
|
152
|
+
return {
|
|
153
|
+
id: transactionResponse.data.reference,
|
|
154
|
+
checkoutUrl: transactionResponse.data.authorization_url,
|
|
155
|
+
status: 'pending',
|
|
156
|
+
amount: params.amount,
|
|
157
|
+
currency: params.currency.toUpperCase(),
|
|
158
|
+
interval: params.interval,
|
|
159
|
+
reference: params.reference,
|
|
160
|
+
provider: 'paystack',
|
|
161
|
+
startsAt: params.startDate,
|
|
162
|
+
createdAt: new Date().toISOString(),
|
|
163
|
+
raw: transactionResponse,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async getPayment(id) {
|
|
167
|
+
const response = await this.apiRequest('GET', `/transaction/verify/${id}`);
|
|
168
|
+
const data = response.data;
|
|
169
|
+
const currency = (data.currency || 'NGN').toUpperCase();
|
|
170
|
+
let status = 'pending';
|
|
171
|
+
if (data.status === 'success') {
|
|
172
|
+
status = 'completed';
|
|
173
|
+
}
|
|
174
|
+
else if (data.status === 'failed') {
|
|
175
|
+
status = 'failed';
|
|
176
|
+
}
|
|
177
|
+
else if (data.status === 'abandoned') {
|
|
178
|
+
status = 'cancelled';
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
id: data.reference,
|
|
182
|
+
checkoutUrl: '',
|
|
183
|
+
status,
|
|
184
|
+
amount: (0, currency_1.toMajorUnit)(data.amount, currency),
|
|
185
|
+
currency,
|
|
186
|
+
reference: data.reference,
|
|
187
|
+
provider: 'paystack',
|
|
188
|
+
createdAt: new Date(data.created_at || data.createdAt || Date.now()).toISOString(),
|
|
189
|
+
raw: response,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async refund(params) {
|
|
193
|
+
const refundData = {
|
|
194
|
+
transaction: params.paymentId,
|
|
195
|
+
};
|
|
196
|
+
if (params.amount !== undefined) {
|
|
197
|
+
const currency = 'NGN';
|
|
198
|
+
refundData.amount = (0, currency_1.toMinorUnit)(params.amount, currency);
|
|
199
|
+
}
|
|
200
|
+
if (params.reason) {
|
|
201
|
+
refundData.merchant_note = params.reason;
|
|
202
|
+
}
|
|
203
|
+
const response = await this.apiRequest('POST', '/refund', refundData);
|
|
204
|
+
const data = response.data;
|
|
205
|
+
const currency = (data.currency || 'NGN').toUpperCase();
|
|
206
|
+
let status = 'pending';
|
|
207
|
+
if (data.status === 'processed') {
|
|
208
|
+
status = 'completed';
|
|
209
|
+
}
|
|
210
|
+
else if (data.status === 'failed') {
|
|
211
|
+
status = 'failed';
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
id: data.id?.toString() || data.reference,
|
|
215
|
+
status,
|
|
216
|
+
amount: (0, currency_1.toMajorUnit)(data.amount || 0, currency),
|
|
217
|
+
currency,
|
|
218
|
+
paymentId: params.paymentId,
|
|
219
|
+
createdAt: new Date(data.created_at || data.createdAt || Date.now()).toISOString(),
|
|
220
|
+
raw: response,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
parseWebhook(body, _headers) {
|
|
224
|
+
const event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
225
|
+
const typeMap = {
|
|
226
|
+
'charge.success': 'payment.completed',
|
|
227
|
+
'charge.failed': 'payment.failed',
|
|
228
|
+
'subscription.create': 'subscription.created',
|
|
229
|
+
'subscription.disable': 'subscription.cancelled',
|
|
230
|
+
'subscription.expiring_cards': 'payment.pending',
|
|
231
|
+
'refund.processed': 'refund.completed',
|
|
232
|
+
'refund.failed': 'payment.failed',
|
|
233
|
+
};
|
|
234
|
+
const eventType = typeMap[event.event] || 'payment.pending';
|
|
235
|
+
const data = event.data || {};
|
|
236
|
+
let payment;
|
|
237
|
+
let subscription;
|
|
238
|
+
let refund;
|
|
239
|
+
if (event.event.startsWith('charge.')) {
|
|
240
|
+
const currency = (data.currency || 'NGN').toUpperCase();
|
|
241
|
+
let status = 'pending';
|
|
242
|
+
if (event.event === 'charge.success') {
|
|
243
|
+
status = 'completed';
|
|
244
|
+
}
|
|
245
|
+
else if (event.event === 'charge.failed') {
|
|
246
|
+
status = 'failed';
|
|
247
|
+
}
|
|
248
|
+
payment = {
|
|
249
|
+
id: data.reference,
|
|
250
|
+
checkoutUrl: '',
|
|
251
|
+
status,
|
|
252
|
+
amount: (0, currency_1.toMajorUnit)(data.amount || 0, currency),
|
|
253
|
+
currency,
|
|
254
|
+
reference: data.reference,
|
|
255
|
+
provider: 'paystack',
|
|
256
|
+
createdAt: new Date(data.created_at || data.paid_at || Date.now()).toISOString(),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
else if (event.event.startsWith('subscription.')) {
|
|
260
|
+
const currency = (data.currency || 'NGN').toUpperCase();
|
|
261
|
+
subscription = {
|
|
262
|
+
id: data.subscription_code || data.id?.toString() || '',
|
|
263
|
+
checkoutUrl: '',
|
|
264
|
+
status: event.event === 'subscription.disable' ? 'cancelled' : 'active',
|
|
265
|
+
amount: (0, currency_1.toMajorUnit)(data.amount || 0, currency),
|
|
266
|
+
currency,
|
|
267
|
+
interval: 'monthly',
|
|
268
|
+
reference: data.subscription_code || '',
|
|
269
|
+
provider: 'paystack',
|
|
270
|
+
createdAt: new Date(data.created_at || Date.now()).toISOString(),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
else if (event.event.startsWith('refund.')) {
|
|
274
|
+
const currency = (data.currency || 'NGN').toUpperCase();
|
|
275
|
+
refund = {
|
|
276
|
+
id: data.id?.toString() || data.refund_id?.toString() || '',
|
|
277
|
+
status: event.event === 'refund.processed' ? 'completed' : 'failed',
|
|
278
|
+
amount: (0, currency_1.toMajorUnit)(data.amount || 0, currency),
|
|
279
|
+
currency,
|
|
280
|
+
paymentId: data.transaction_reference || data.transaction || '',
|
|
281
|
+
createdAt: new Date(data.created_at || Date.now()).toISOString(),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
type: eventType,
|
|
286
|
+
payment,
|
|
287
|
+
subscription,
|
|
288
|
+
refund,
|
|
289
|
+
raw: event,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Verify webhook signature using PayStack's HMAC-SHA512 scheme.
|
|
294
|
+
*
|
|
295
|
+
* CRITICAL: body must be the raw string or Buffer from the webhook request.
|
|
296
|
+
* Passing a parsed JSON object will cause signature verification to fail.
|
|
297
|
+
*/
|
|
298
|
+
verifyWebhook(body, headers) {
|
|
299
|
+
if (!this.apiKey) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const signature = headers?.['x-paystack-signature'] || headers?.['X-Paystack-Signature'];
|
|
303
|
+
if (!signature) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const rawBody = typeof body === 'string' ? body : body.toString('utf8');
|
|
307
|
+
const computedSig = crypto
|
|
308
|
+
.createHmac('sha512', this.apiKey)
|
|
309
|
+
.update(rawBody)
|
|
310
|
+
.digest('hex');
|
|
311
|
+
try {
|
|
312
|
+
const computedBuffer = Buffer.from(computedSig, 'hex');
|
|
313
|
+
const expectedBuffer = Buffer.from(signature, 'hex');
|
|
314
|
+
if (computedBuffer.length !== expectedBuffer.length) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
return crypto.timingSafeEqual(computedBuffer, expectedBuffer);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
getCapabilities() {
|
|
324
|
+
return {
|
|
325
|
+
fees: {
|
|
326
|
+
fixed: 100,
|
|
327
|
+
percent: 1.5,
|
|
328
|
+
currency: 'NGN',
|
|
329
|
+
},
|
|
330
|
+
currencies: this.supportedCurrencies,
|
|
331
|
+
country: 'NG',
|
|
332
|
+
avgLatencyMs: 600,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
exports.PayStackProvider = PayStackProvider;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Peach Payments provider
|
|
3
|
+
* South African payment gateway using Open Payment Platform (OPP)
|
|
4
|
+
* @see https://docs.peachpayments.com
|
|
5
|
+
*/
|
|
6
|
+
import { PaymentProvider } from './base';
|
|
7
|
+
import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent } from '../types';
|
|
8
|
+
import { ProviderCapabilities } from '../routing-types';
|
|
9
|
+
interface PeachConfig {
|
|
10
|
+
accessToken: string;
|
|
11
|
+
entityId: string;
|
|
12
|
+
sandbox: boolean;
|
|
13
|
+
webhookSecret?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class PeachProvider extends PaymentProvider {
|
|
16
|
+
readonly name = "peach";
|
|
17
|
+
readonly supportedCurrencies: string[];
|
|
18
|
+
private accessToken;
|
|
19
|
+
private entityId;
|
|
20
|
+
private sandbox;
|
|
21
|
+
private webhookSecret?;
|
|
22
|
+
private baseUrl;
|
|
23
|
+
constructor(config: PeachConfig);
|
|
24
|
+
private buildFormData;
|
|
25
|
+
private apiRequest;
|
|
26
|
+
private mapPeachStatus;
|
|
27
|
+
/**
|
|
28
|
+
* Create a one-time payment checkout.
|
|
29
|
+
*
|
|
30
|
+
* IMPORTANT: The returned checkoutUrl is a JavaScript widget URL, not a direct redirect.
|
|
31
|
+
* You must embed it in an HTML page with:
|
|
32
|
+
*
|
|
33
|
+
* <script src="{checkoutUrl}"></script>
|
|
34
|
+
* <form action="{successUrl}" class="paymentWidgets" data-brands="VISA MASTER"></form>
|
|
35
|
+
*
|
|
36
|
+
* The widget will render the payment form inside the <form> element.
|
|
37
|
+
*/
|
|
38
|
+
createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Peach Payments subscriptions require Registration + scheduled charges flow.
|
|
41
|
+
* This is not yet supported by paybridge. Use Stripe or PayFast for subscriptions.
|
|
42
|
+
*/
|
|
43
|
+
createSubscription(_params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
44
|
+
getPayment(id: string): Promise<PaymentResult>;
|
|
45
|
+
refund(params: RefundParams): Promise<RefundResult>;
|
|
46
|
+
parseWebhook(body: any, headers?: any): WebhookEvent;
|
|
47
|
+
verifyWebhook(body: string | Buffer, headers?: any): boolean;
|
|
48
|
+
getCapabilities(): ProviderCapabilities;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Peach Payments provider
|
|
4
|
+
* South African payment gateway using Open Payment Platform (OPP)
|
|
5
|
+
* @see https://docs.peachpayments.com
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.PeachProvider = void 0;
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
43
|
+
const base_1 = require("./base");
|
|
44
|
+
const fetch_1 = require("../utils/fetch");
|
|
45
|
+
class PeachProvider extends base_1.PaymentProvider {
|
|
46
|
+
constructor(config) {
|
|
47
|
+
super();
|
|
48
|
+
this.name = 'peach';
|
|
49
|
+
this.supportedCurrencies = ['ZAR', 'USD', 'EUR', 'GBP'];
|
|
50
|
+
this.accessToken = config.accessToken;
|
|
51
|
+
this.entityId = config.entityId;
|
|
52
|
+
this.sandbox = config.sandbox;
|
|
53
|
+
this.webhookSecret = config.webhookSecret;
|
|
54
|
+
this.baseUrl = this.sandbox
|
|
55
|
+
? 'https://eu-test.oppwa.com'
|
|
56
|
+
: 'https://eu-prod.oppwa.com';
|
|
57
|
+
}
|
|
58
|
+
buildFormData(data) {
|
|
59
|
+
const parts = [];
|
|
60
|
+
for (const [key, value] of Object.entries(data)) {
|
|
61
|
+
if (value === undefined || value === null)
|
|
62
|
+
continue;
|
|
63
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
64
|
+
}
|
|
65
|
+
return parts.join('&');
|
|
66
|
+
}
|
|
67
|
+
async apiRequest(method, path, data) {
|
|
68
|
+
const url = `${this.baseUrl}${path}`;
|
|
69
|
+
let finalUrl = url;
|
|
70
|
+
let body;
|
|
71
|
+
if (method === 'GET' && data) {
|
|
72
|
+
const queryData = { entityId: this.entityId, ...data };
|
|
73
|
+
const queryString = this.buildFormData(queryData);
|
|
74
|
+
finalUrl = `${url}?${queryString}`;
|
|
75
|
+
}
|
|
76
|
+
else if (data) {
|
|
77
|
+
const bodyData = { entityId: this.entityId, ...data };
|
|
78
|
+
body = this.buildFormData(bodyData);
|
|
79
|
+
}
|
|
80
|
+
const response = await (0, fetch_1.timedFetchOrThrow)(finalUrl, {
|
|
81
|
+
method,
|
|
82
|
+
headers: {
|
|
83
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
84
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
85
|
+
},
|
|
86
|
+
body,
|
|
87
|
+
});
|
|
88
|
+
return (await response.json());
|
|
89
|
+
}
|
|
90
|
+
mapPeachStatus(code) {
|
|
91
|
+
if (code.startsWith('000.000.') || code.startsWith('000.100.')) {
|
|
92
|
+
return 'completed';
|
|
93
|
+
}
|
|
94
|
+
if (code.startsWith('000.200.')) {
|
|
95
|
+
return 'pending';
|
|
96
|
+
}
|
|
97
|
+
if (code.startsWith('000.400.')) {
|
|
98
|
+
return 'cancelled';
|
|
99
|
+
}
|
|
100
|
+
if (code.startsWith('100.') ||
|
|
101
|
+
code.startsWith('200.') ||
|
|
102
|
+
code.startsWith('300.') ||
|
|
103
|
+
code.startsWith('400.') ||
|
|
104
|
+
code.startsWith('500.') ||
|
|
105
|
+
code.startsWith('600.') ||
|
|
106
|
+
code.startsWith('700.') ||
|
|
107
|
+
code.startsWith('800.') ||
|
|
108
|
+
code.startsWith('900.')) {
|
|
109
|
+
return 'failed';
|
|
110
|
+
}
|
|
111
|
+
return 'pending';
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Create a one-time payment checkout.
|
|
115
|
+
*
|
|
116
|
+
* IMPORTANT: The returned checkoutUrl is a JavaScript widget URL, not a direct redirect.
|
|
117
|
+
* You must embed it in an HTML page with:
|
|
118
|
+
*
|
|
119
|
+
* <script src="{checkoutUrl}"></script>
|
|
120
|
+
* <form action="{successUrl}" class="paymentWidgets" data-brands="VISA MASTER"></form>
|
|
121
|
+
*
|
|
122
|
+
* The widget will render the payment form inside the <form> element.
|
|
123
|
+
*/
|
|
124
|
+
async createPayment(params) {
|
|
125
|
+
this.validateCurrency(params.currency);
|
|
126
|
+
const nameParts = params.customer.name.split(' ');
|
|
127
|
+
const givenName = nameParts[0] || '';
|
|
128
|
+
const surname = nameParts.slice(1).join(' ') || givenName;
|
|
129
|
+
const formData = {
|
|
130
|
+
amount: params.amount.toFixed(2),
|
|
131
|
+
currency: params.currency,
|
|
132
|
+
paymentType: 'DB',
|
|
133
|
+
merchantTransactionId: params.reference,
|
|
134
|
+
'customer.email': params.customer.email,
|
|
135
|
+
'customer.givenName': givenName,
|
|
136
|
+
'customer.surname': surname,
|
|
137
|
+
'billing.country': 'ZA',
|
|
138
|
+
shopperResultUrl: params.urls.success,
|
|
139
|
+
notificationUrl: params.urls.webhook,
|
|
140
|
+
'customParameters[reference]': params.reference,
|
|
141
|
+
};
|
|
142
|
+
if (params.metadata) {
|
|
143
|
+
for (const [key, value] of Object.entries(params.metadata)) {
|
|
144
|
+
formData[`customParameters[${key}]`] = String(value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const response = await this.apiRequest('POST', '/v1/checkouts', formData);
|
|
148
|
+
if (!response.result?.code.startsWith('000.')) {
|
|
149
|
+
throw new Error(`${response.result?.code}: ${response.result?.description}`);
|
|
150
|
+
}
|
|
151
|
+
const checkoutUrl = `${this.baseUrl}/v1/paymentWidgets.js?checkoutId=${response.id}`;
|
|
152
|
+
return {
|
|
153
|
+
id: response.id,
|
|
154
|
+
checkoutUrl,
|
|
155
|
+
status: 'pending',
|
|
156
|
+
amount: params.amount,
|
|
157
|
+
currency: params.currency,
|
|
158
|
+
reference: params.reference,
|
|
159
|
+
provider: 'peach',
|
|
160
|
+
createdAt: response.timestamp || new Date().toISOString(),
|
|
161
|
+
raw: response,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Peach Payments subscriptions require Registration + scheduled charges flow.
|
|
166
|
+
* This is not yet supported by paybridge. Use Stripe or PayFast for subscriptions.
|
|
167
|
+
*/
|
|
168
|
+
async createSubscription(_params) {
|
|
169
|
+
throw new Error('Peach Payments subscriptions require Registration + scheduled charges; not yet supported by paybridge. Use Stripe or PayFast for subscriptions.');
|
|
170
|
+
}
|
|
171
|
+
async getPayment(id) {
|
|
172
|
+
const response = await this.apiRequest('GET', `/v1/checkouts/${id}/payment`, {});
|
|
173
|
+
const status = this.mapPeachStatus(response.result?.code || '');
|
|
174
|
+
return {
|
|
175
|
+
id: response.id,
|
|
176
|
+
checkoutUrl: '',
|
|
177
|
+
status,
|
|
178
|
+
amount: parseFloat(response.amount || '0'),
|
|
179
|
+
currency: response.currency || 'ZAR',
|
|
180
|
+
reference: response.merchantTransactionId || id,
|
|
181
|
+
provider: 'peach',
|
|
182
|
+
createdAt: response.timestamp || new Date().toISOString(),
|
|
183
|
+
raw: response,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async refund(params) {
|
|
187
|
+
const formData = {
|
|
188
|
+
paymentType: 'RF',
|
|
189
|
+
};
|
|
190
|
+
if (params.amount !== undefined) {
|
|
191
|
+
formData.amount = params.amount.toFixed(2);
|
|
192
|
+
formData.currency = 'ZAR';
|
|
193
|
+
}
|
|
194
|
+
const response = await this.apiRequest('POST', `/v1/payments/${params.paymentId}`, formData);
|
|
195
|
+
const code = response.result?.code || '';
|
|
196
|
+
let status = 'pending';
|
|
197
|
+
if (code.startsWith('000.') && !code.startsWith('000.200.')) {
|
|
198
|
+
status = 'completed';
|
|
199
|
+
}
|
|
200
|
+
else if (code.startsWith('000.200.')) {
|
|
201
|
+
status = 'pending';
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
status = 'failed';
|
|
205
|
+
}
|
|
206
|
+
const currency = response.currency || 'ZAR';
|
|
207
|
+
return {
|
|
208
|
+
id: response.id || `refund_${params.paymentId}_${Date.now()}`,
|
|
209
|
+
status,
|
|
210
|
+
amount: parseFloat(response.amount || String(params.amount || 0)),
|
|
211
|
+
currency,
|
|
212
|
+
paymentId: params.paymentId,
|
|
213
|
+
createdAt: response.timestamp || new Date().toISOString(),
|
|
214
|
+
raw: response,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
parseWebhook(body, headers) {
|
|
218
|
+
let event;
|
|
219
|
+
if (this.webhookSecret && headers) {
|
|
220
|
+
try {
|
|
221
|
+
const rawBody = typeof body === 'string' ? body : JSON.stringify(body);
|
|
222
|
+
const key = Buffer.from(this.webhookSecret, 'hex');
|
|
223
|
+
const iv = Buffer.from(headers['x-initialization-vector'], 'hex');
|
|
224
|
+
const authTag = Buffer.from(headers['x-authentication-tag'], 'hex');
|
|
225
|
+
const ciphertext = Buffer.from(rawBody, 'hex');
|
|
226
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
227
|
+
decipher.setAuthTag(authTag);
|
|
228
|
+
const plaintext = Buffer.concat([
|
|
229
|
+
decipher.update(ciphertext),
|
|
230
|
+
decipher.final(),
|
|
231
|
+
]).toString('utf8');
|
|
232
|
+
event = JSON.parse(plaintext);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
240
|
+
}
|
|
241
|
+
const payload = event.payload || event;
|
|
242
|
+
const resultCode = payload.result?.code || '';
|
|
243
|
+
const status = this.mapPeachStatus(resultCode);
|
|
244
|
+
let eventType = 'payment.pending';
|
|
245
|
+
if (status === 'completed') {
|
|
246
|
+
eventType = 'payment.completed';
|
|
247
|
+
}
|
|
248
|
+
else if (status === 'failed') {
|
|
249
|
+
eventType = 'payment.failed';
|
|
250
|
+
}
|
|
251
|
+
else if (status === 'cancelled') {
|
|
252
|
+
eventType = 'payment.cancelled';
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
type: eventType,
|
|
256
|
+
payment: {
|
|
257
|
+
id: payload.id || '',
|
|
258
|
+
checkoutUrl: '',
|
|
259
|
+
status,
|
|
260
|
+
amount: parseFloat(payload.amount || '0'),
|
|
261
|
+
currency: payload.currency || 'ZAR',
|
|
262
|
+
reference: payload.merchantTransactionId || '',
|
|
263
|
+
provider: 'peach',
|
|
264
|
+
createdAt: new Date().toISOString(),
|
|
265
|
+
},
|
|
266
|
+
raw: event,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
verifyWebhook(body, headers) {
|
|
270
|
+
if (!this.webhookSecret) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const rawBody = typeof body === 'string' ? body : body.toString('utf8');
|
|
275
|
+
const key = Buffer.from(this.webhookSecret, 'hex');
|
|
276
|
+
const iv = Buffer.from(headers['x-initialization-vector'], 'hex');
|
|
277
|
+
const authTag = Buffer.from(headers['x-authentication-tag'], 'hex');
|
|
278
|
+
const ciphertext = Buffer.from(rawBody, 'hex');
|
|
279
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
280
|
+
decipher.setAuthTag(authTag);
|
|
281
|
+
decipher.update(ciphertext);
|
|
282
|
+
decipher.final();
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
getCapabilities() {
|
|
290
|
+
return {
|
|
291
|
+
fees: {
|
|
292
|
+
fixed: 1.0,
|
|
293
|
+
percent: 2.85,
|
|
294
|
+
currency: 'ZAR',
|
|
295
|
+
},
|
|
296
|
+
currencies: this.supportedCurrencies,
|
|
297
|
+
country: 'ZA',
|
|
298
|
+
avgLatencyMs: 600,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
exports.PeachProvider = PeachProvider;
|