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,367 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stripe payment provider
|
|
4
|
+
* Global payment processor supporting 135+ currencies
|
|
5
|
+
* @see https://stripe.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.StripeProvider = 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 StripeProvider extends base_1.PaymentProvider {
|
|
47
|
+
constructor(config) {
|
|
48
|
+
super();
|
|
49
|
+
this.name = 'stripe';
|
|
50
|
+
this.supportedCurrencies = ['USD', 'EUR', 'GBP', 'ZAR', 'NGN'];
|
|
51
|
+
this.baseUrl = 'https://api.stripe.com/v1';
|
|
52
|
+
this.apiKey = config.apiKey;
|
|
53
|
+
this.webhookSecret = config.webhookSecret;
|
|
54
|
+
this.sandbox = config.sandbox ?? this.apiKey.startsWith('sk_test_');
|
|
55
|
+
}
|
|
56
|
+
buildFormData(data, prefix = '') {
|
|
57
|
+
const parts = [];
|
|
58
|
+
for (const [key, value] of Object.entries(data)) {
|
|
59
|
+
if (value === undefined || value === null)
|
|
60
|
+
continue;
|
|
61
|
+
const fieldName = prefix ? `${prefix}[${key}]` : key;
|
|
62
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
63
|
+
parts.push(this.buildFormData(value, fieldName));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
parts.push(`${encodeURIComponent(fieldName)}=${encodeURIComponent(String(value))}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return parts.join('&');
|
|
70
|
+
}
|
|
71
|
+
async apiRequest(method, path, data) {
|
|
72
|
+
const url = `${this.baseUrl}${path}`;
|
|
73
|
+
const body = data ? this.buildFormData(data) : undefined;
|
|
74
|
+
const response = await (0, fetch_1.timedFetchOrThrow)(url, {
|
|
75
|
+
method,
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
78
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
79
|
+
},
|
|
80
|
+
body,
|
|
81
|
+
});
|
|
82
|
+
return (await response.json());
|
|
83
|
+
}
|
|
84
|
+
async createPayment(params) {
|
|
85
|
+
this.validateCurrency(params.currency);
|
|
86
|
+
const amountInMinorUnits = (0, currency_1.toMinorUnit)(params.amount, params.currency);
|
|
87
|
+
const currencyLower = params.currency.toLowerCase();
|
|
88
|
+
const metadata = {
|
|
89
|
+
reference: params.reference,
|
|
90
|
+
};
|
|
91
|
+
if (params.metadata) {
|
|
92
|
+
for (const [key, value] of Object.entries(params.metadata)) {
|
|
93
|
+
metadata[key] = String(value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const sessionData = {
|
|
97
|
+
mode: 'payment',
|
|
98
|
+
'line_items[0][price_data][currency]': currencyLower,
|
|
99
|
+
'line_items[0][price_data][unit_amount]': amountInMinorUnits,
|
|
100
|
+
'line_items[0][price_data][product_data][name]': params.description || params.reference,
|
|
101
|
+
'line_items[0][quantity]': 1,
|
|
102
|
+
success_url: params.urls.success,
|
|
103
|
+
cancel_url: params.urls.cancel,
|
|
104
|
+
client_reference_id: params.reference,
|
|
105
|
+
customer_email: params.customer.email,
|
|
106
|
+
};
|
|
107
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
108
|
+
sessionData[`metadata[${key}]`] = value;
|
|
109
|
+
}
|
|
110
|
+
const response = await this.apiRequest('POST', '/checkout/sessions', sessionData);
|
|
111
|
+
return {
|
|
112
|
+
id: response.id,
|
|
113
|
+
checkoutUrl: response.url,
|
|
114
|
+
status: 'pending',
|
|
115
|
+
amount: (0, currency_1.toMajorUnit)(response.amount_total || amountInMinorUnits, params.currency),
|
|
116
|
+
currency: (response.currency || currencyLower).toUpperCase(),
|
|
117
|
+
reference: response.client_reference_id || params.reference,
|
|
118
|
+
provider: 'stripe',
|
|
119
|
+
createdAt: new Date(response.created * 1000).toISOString(),
|
|
120
|
+
expiresAt: response.expires_at ? new Date(response.expires_at * 1000).toISOString() : undefined,
|
|
121
|
+
raw: response,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async createSubscription(params) {
|
|
125
|
+
this.validateCurrency(params.currency);
|
|
126
|
+
const amountInMinorUnits = (0, currency_1.toMinorUnit)(params.amount, params.currency);
|
|
127
|
+
const currencyLower = params.currency.toLowerCase();
|
|
128
|
+
const intervalMap = {
|
|
129
|
+
weekly: 'week',
|
|
130
|
+
monthly: 'month',
|
|
131
|
+
yearly: 'year',
|
|
132
|
+
};
|
|
133
|
+
const stripeInterval = intervalMap[params.interval];
|
|
134
|
+
const metadata = {
|
|
135
|
+
reference: params.reference,
|
|
136
|
+
};
|
|
137
|
+
if (params.metadata) {
|
|
138
|
+
for (const [key, value] of Object.entries(params.metadata)) {
|
|
139
|
+
metadata[key] = String(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const sessionData = {
|
|
143
|
+
mode: 'subscription',
|
|
144
|
+
'line_items[0][price_data][currency]': currencyLower,
|
|
145
|
+
'line_items[0][price_data][unit_amount]': amountInMinorUnits,
|
|
146
|
+
'line_items[0][price_data][recurring][interval]': stripeInterval,
|
|
147
|
+
'line_items[0][price_data][product_data][name]': params.description || params.reference,
|
|
148
|
+
'line_items[0][quantity]': 1,
|
|
149
|
+
success_url: params.urls.success,
|
|
150
|
+
cancel_url: params.urls.cancel,
|
|
151
|
+
client_reference_id: params.reference,
|
|
152
|
+
customer_email: params.customer.email,
|
|
153
|
+
};
|
|
154
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
155
|
+
sessionData[`metadata[${key}]`] = value;
|
|
156
|
+
}
|
|
157
|
+
const response = await this.apiRequest('POST', '/checkout/sessions', sessionData);
|
|
158
|
+
return {
|
|
159
|
+
id: response.id,
|
|
160
|
+
checkoutUrl: response.url,
|
|
161
|
+
status: 'pending',
|
|
162
|
+
amount: (0, currency_1.toMajorUnit)(response.amount_total || amountInMinorUnits, params.currency),
|
|
163
|
+
currency: (response.currency || currencyLower).toUpperCase(),
|
|
164
|
+
interval: params.interval,
|
|
165
|
+
reference: response.client_reference_id || params.reference,
|
|
166
|
+
provider: 'stripe',
|
|
167
|
+
startsAt: params.startDate,
|
|
168
|
+
createdAt: new Date(response.created * 1000).toISOString(),
|
|
169
|
+
raw: response,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async getPayment(id) {
|
|
173
|
+
const session = await this.apiRequest('GET', `/checkout/sessions/${id}`);
|
|
174
|
+
let status = 'pending';
|
|
175
|
+
if (session.payment_status === 'paid') {
|
|
176
|
+
status = 'completed';
|
|
177
|
+
}
|
|
178
|
+
else if (session.payment_status === 'unpaid' && session.status === 'open') {
|
|
179
|
+
status = 'pending';
|
|
180
|
+
}
|
|
181
|
+
else if (session.status === 'expired') {
|
|
182
|
+
status = 'failed';
|
|
183
|
+
}
|
|
184
|
+
const currency = (session.currency || 'USD').toUpperCase();
|
|
185
|
+
return {
|
|
186
|
+
id: session.id,
|
|
187
|
+
checkoutUrl: session.url || '',
|
|
188
|
+
status,
|
|
189
|
+
amount: (0, currency_1.toMajorUnit)(session.amount_total || 0, currency),
|
|
190
|
+
currency,
|
|
191
|
+
reference: session.client_reference_id || session.id,
|
|
192
|
+
provider: 'stripe',
|
|
193
|
+
createdAt: new Date(session.created * 1000).toISOString(),
|
|
194
|
+
expiresAt: session.expires_at ? new Date(session.expires_at * 1000).toISOString() : undefined,
|
|
195
|
+
raw: session,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async refund(params) {
|
|
199
|
+
let paymentIntentId;
|
|
200
|
+
if (params.paymentId.startsWith('cs_')) {
|
|
201
|
+
const session = await this.apiRequest('GET', `/checkout/sessions/${params.paymentId}`);
|
|
202
|
+
paymentIntentId = session.payment_intent;
|
|
203
|
+
if (!paymentIntentId) {
|
|
204
|
+
throw new Error('Session has no payment_intent - payment may not be completed yet');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
paymentIntentId = params.paymentId;
|
|
209
|
+
}
|
|
210
|
+
const refundData = {
|
|
211
|
+
payment_intent: paymentIntentId,
|
|
212
|
+
};
|
|
213
|
+
if (params.amount !== undefined) {
|
|
214
|
+
refundData.amount = (0, currency_1.toMinorUnit)(params.amount, 'USD');
|
|
215
|
+
}
|
|
216
|
+
if (params.reason) {
|
|
217
|
+
refundData.reason = 'requested_by_customer';
|
|
218
|
+
refundData['metadata[reason]'] = params.reason;
|
|
219
|
+
}
|
|
220
|
+
const response = await this.apiRequest('POST', '/refunds', refundData);
|
|
221
|
+
const currency = (response.currency || 'USD').toUpperCase();
|
|
222
|
+
return {
|
|
223
|
+
id: response.id,
|
|
224
|
+
status: response.status === 'succeeded' ? 'completed' : 'pending',
|
|
225
|
+
amount: (0, currency_1.toMajorUnit)(response.amount || 0, currency),
|
|
226
|
+
currency,
|
|
227
|
+
paymentId: params.paymentId,
|
|
228
|
+
createdAt: new Date(response.created * 1000).toISOString(),
|
|
229
|
+
raw: response,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
parseWebhook(body, _headers) {
|
|
233
|
+
const event = typeof body === 'string' ? JSON.parse(body) : body;
|
|
234
|
+
const typeMap = {
|
|
235
|
+
'checkout.session.completed': 'payment.completed',
|
|
236
|
+
'checkout.session.expired': 'payment.cancelled',
|
|
237
|
+
'checkout.session.async_payment_failed': 'payment.failed',
|
|
238
|
+
'payment_intent.succeeded': 'payment.completed',
|
|
239
|
+
'payment_intent.payment_failed': 'payment.failed',
|
|
240
|
+
'charge.refunded': 'refund.completed',
|
|
241
|
+
'customer.subscription.created': 'subscription.created',
|
|
242
|
+
'customer.subscription.deleted': 'subscription.cancelled',
|
|
243
|
+
};
|
|
244
|
+
const eventType = typeMap[event.type] || 'payment.pending';
|
|
245
|
+
const data = event.data?.object || {};
|
|
246
|
+
let payment;
|
|
247
|
+
let subscription;
|
|
248
|
+
let refund;
|
|
249
|
+
if (event.type.startsWith('checkout.session') || event.type.startsWith('payment_intent')) {
|
|
250
|
+
const currency = (data.currency || 'USD').toUpperCase();
|
|
251
|
+
let status = 'pending';
|
|
252
|
+
if (data.payment_status === 'paid' || data.status === 'succeeded') {
|
|
253
|
+
status = 'completed';
|
|
254
|
+
}
|
|
255
|
+
else if (data.status === 'expired') {
|
|
256
|
+
status = 'failed';
|
|
257
|
+
}
|
|
258
|
+
else if (event.type.includes('failed')) {
|
|
259
|
+
status = 'failed';
|
|
260
|
+
}
|
|
261
|
+
payment = {
|
|
262
|
+
id: data.id,
|
|
263
|
+
checkoutUrl: data.url || '',
|
|
264
|
+
status,
|
|
265
|
+
amount: (0, currency_1.toMajorUnit)(data.amount_total || data.amount || 0, currency),
|
|
266
|
+
currency,
|
|
267
|
+
reference: data.client_reference_id || data.id,
|
|
268
|
+
provider: 'stripe',
|
|
269
|
+
createdAt: new Date((data.created || Date.now() / 1000) * 1000).toISOString(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
else if (event.type.startsWith('customer.subscription')) {
|
|
273
|
+
const currency = (data.currency || 'USD').toUpperCase();
|
|
274
|
+
subscription = {
|
|
275
|
+
id: data.id,
|
|
276
|
+
checkoutUrl: '',
|
|
277
|
+
status: event.type.includes('deleted') ? 'cancelled' : 'active',
|
|
278
|
+
amount: (0, currency_1.toMajorUnit)(data.plan?.amount || 0, currency),
|
|
279
|
+
currency,
|
|
280
|
+
interval: 'monthly',
|
|
281
|
+
reference: data.metadata?.reference || data.id,
|
|
282
|
+
provider: 'stripe',
|
|
283
|
+
createdAt: new Date((data.created || Date.now() / 1000) * 1000).toISOString(),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
else if (event.type === 'charge.refunded') {
|
|
287
|
+
const currency = (data.currency || 'USD').toUpperCase();
|
|
288
|
+
refund = {
|
|
289
|
+
id: data.refunds?.data?.[0]?.id || data.id,
|
|
290
|
+
status: 'completed',
|
|
291
|
+
amount: (0, currency_1.toMajorUnit)(data.amount_refunded || 0, currency),
|
|
292
|
+
currency,
|
|
293
|
+
paymentId: data.payment_intent || data.id,
|
|
294
|
+
createdAt: new Date((data.created || Date.now() / 1000) * 1000).toISOString(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
type: eventType,
|
|
299
|
+
payment,
|
|
300
|
+
subscription,
|
|
301
|
+
refund,
|
|
302
|
+
raw: event,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Verify webhook signature using Stripe's scheme.
|
|
307
|
+
*
|
|
308
|
+
* CRITICAL: body must be the raw string or Buffer from the webhook request.
|
|
309
|
+
* Passing a parsed JSON object will cause signature verification to fail.
|
|
310
|
+
*/
|
|
311
|
+
verifyWebhook(body, headers) {
|
|
312
|
+
if (!this.webhookSecret) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const signature = headers?.['stripe-signature'] || headers?.['Stripe-Signature'];
|
|
316
|
+
if (!signature) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
const parts = signature.split(',').reduce((acc, part) => {
|
|
320
|
+
const [key, value] = part.split('=');
|
|
321
|
+
if (key && value) {
|
|
322
|
+
acc[key] = value;
|
|
323
|
+
}
|
|
324
|
+
return acc;
|
|
325
|
+
}, {});
|
|
326
|
+
const timestamp = parts.t;
|
|
327
|
+
const expectedSig = parts.v1;
|
|
328
|
+
if (!timestamp || !expectedSig) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const timestampNum = parseInt(timestamp, 10);
|
|
332
|
+
const now = Math.floor(Date.now() / 1000);
|
|
333
|
+
if (now - timestampNum > 300) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const rawBody = typeof body === 'string' ? body : body.toString('utf8');
|
|
337
|
+
const signedPayload = `${timestamp}.${rawBody}`;
|
|
338
|
+
const computedSig = crypto
|
|
339
|
+
.createHmac('sha256', this.webhookSecret)
|
|
340
|
+
.update(signedPayload)
|
|
341
|
+
.digest('hex');
|
|
342
|
+
try {
|
|
343
|
+
const computedBuffer = Buffer.from(computedSig, 'hex');
|
|
344
|
+
const expectedBuffer = Buffer.from(expectedSig, 'hex');
|
|
345
|
+
if (computedBuffer.length !== expectedBuffer.length) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
return crypto.timingSafeEqual(computedBuffer, expectedBuffer);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
getCapabilities() {
|
|
355
|
+
return {
|
|
356
|
+
fees: {
|
|
357
|
+
fixed: 0.30,
|
|
358
|
+
percent: 2.9,
|
|
359
|
+
currency: 'USD',
|
|
360
|
+
},
|
|
361
|
+
currencies: this.supportedCurrencies,
|
|
362
|
+
country: 'GLOBAL',
|
|
363
|
+
avgLatencyMs: 400,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
exports.StripeProvider = StripeProvider;
|
package/dist/providers/yoco.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { PaymentProvider } from './base';
|
|
7
7
|
import { CreatePaymentParams, PaymentResult, CreateSubscriptionParams, SubscriptionResult, RefundParams, RefundResult, WebhookEvent } from '../types';
|
|
8
|
+
import { ProviderCapabilities } from '../routing-types';
|
|
8
9
|
interface YocoConfig {
|
|
9
10
|
apiKey: string;
|
|
10
11
|
sandbox: boolean;
|
|
@@ -19,11 +20,22 @@ export declare class YocoProvider extends PaymentProvider {
|
|
|
19
20
|
private webhookSecret?;
|
|
20
21
|
constructor(config: YocoConfig);
|
|
21
22
|
createPayment(params: CreatePaymentParams): Promise<PaymentResult>;
|
|
23
|
+
/**
|
|
24
|
+
* Yoco does not support subscriptions in the standard Online Payments API.
|
|
25
|
+
* Use the Yoco Recurring Billing API directly or choose another provider.
|
|
26
|
+
*/
|
|
22
27
|
createSubscription(params: CreateSubscriptionParams): Promise<SubscriptionResult>;
|
|
23
28
|
getPayment(id: string): Promise<PaymentResult>;
|
|
24
29
|
refund(params: RefundParams): Promise<RefundResult>;
|
|
25
30
|
parseWebhook(body: any, _headers?: any): WebhookEvent;
|
|
31
|
+
/**
|
|
32
|
+
* Verify webhook signature using Yoco's Svix-based signing scheme.
|
|
33
|
+
*
|
|
34
|
+
* CRITICAL: body must be the raw string or Buffer from the webhook request.
|
|
35
|
+
* Passing a parsed JSON object will cause signature verification to fail.
|
|
36
|
+
*/
|
|
26
37
|
verifyWebhook(body: string | Buffer, headers?: any): boolean;
|
|
38
|
+
getCapabilities(): ProviderCapabilities;
|
|
27
39
|
private mapYocoStatus;
|
|
28
40
|
private mapYocoEventType;
|
|
29
41
|
}
|
package/dist/providers/yoco.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.YocoProvider = void 0;
|
|
|
12
12
|
const crypto_1 = __importDefault(require("crypto"));
|
|
13
13
|
const base_1 = require("./base");
|
|
14
14
|
const currency_1 = require("../utils/currency");
|
|
15
|
+
const fetch_1 = require("../utils/fetch");
|
|
15
16
|
class YocoProvider extends base_1.PaymentProvider {
|
|
16
17
|
constructor(config) {
|
|
17
18
|
super();
|
|
@@ -26,18 +27,6 @@ class YocoProvider extends base_1.PaymentProvider {
|
|
|
26
27
|
// ==================== Payment Methods ====================
|
|
27
28
|
async createPayment(params) {
|
|
28
29
|
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
30
|
const amountInCents = (0, currency_1.toMinorUnit)(params.amount, params.currency);
|
|
42
31
|
const requestBody = {
|
|
43
32
|
amount: amountInCents,
|
|
@@ -47,59 +36,99 @@ class YocoProvider extends base_1.PaymentProvider {
|
|
|
47
36
|
failureUrl: params.urls.cancel,
|
|
48
37
|
metadata: {
|
|
49
38
|
reference: params.reference,
|
|
50
|
-
description: params.description,
|
|
39
|
+
description: params.description || '',
|
|
51
40
|
customerName: params.customer.name,
|
|
52
41
|
customerEmail: params.customer.email,
|
|
53
42
|
...params.metadata,
|
|
54
43
|
},
|
|
55
44
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
const response = await (0, fetch_1.timedFetchOrThrow)(`${this.baseUrl}/checkouts`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'Idempotency-Key': crypto_1.default.randomUUID(),
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(requestBody),
|
|
53
|
+
});
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
return {
|
|
56
|
+
id: data.id,
|
|
57
|
+
checkoutUrl: data.redirectUrl,
|
|
58
|
+
status: this.mapYocoStatus(data.status),
|
|
59
|
+
amount: (0, currency_1.toMajorUnit)(data.amount, params.currency),
|
|
60
|
+
currency: data.currency,
|
|
61
|
+
reference: data.metadata?.reference || params.reference,
|
|
62
|
+
provider: 'yoco',
|
|
63
|
+
createdAt: new Date().toISOString(),
|
|
64
|
+
raw: data,
|
|
65
|
+
};
|
|
59
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Yoco does not support subscriptions in the standard Online Payments API.
|
|
69
|
+
* Use the Yoco Recurring Billing API directly or choose another provider.
|
|
70
|
+
*/
|
|
60
71
|
async createSubscription(params) {
|
|
61
72
|
this.validateCurrency(params.currency);
|
|
62
|
-
|
|
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!');
|
|
73
|
+
throw new Error('Yoco does not support subscriptions. Use the Yoco Recurring Billing API directly or choose another provider.');
|
|
67
74
|
}
|
|
68
75
|
async getPayment(id) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
const response = await (0, fetch_1.timedFetchOrThrow)(`${this.baseUrl}/checkouts/${id}`, {
|
|
77
|
+
method: 'GET',
|
|
78
|
+
headers: {
|
|
79
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
return {
|
|
85
|
+
id: data.id,
|
|
86
|
+
checkoutUrl: data.redirectUrl || '',
|
|
87
|
+
status: this.mapYocoStatus(data.status),
|
|
88
|
+
amount: (0, currency_1.toMajorUnit)(data.amount, 'ZAR'),
|
|
89
|
+
currency: data.currency,
|
|
90
|
+
reference: data.metadata?.reference || id,
|
|
91
|
+
provider: 'yoco',
|
|
92
|
+
createdAt: new Date().toISOString(),
|
|
93
|
+
raw: data,
|
|
94
|
+
};
|
|
74
95
|
}
|
|
75
96
|
async refund(params) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
const requestBody = {
|
|
98
|
+
checkoutId: params.paymentId,
|
|
99
|
+
};
|
|
100
|
+
if (params.amount !== undefined) {
|
|
101
|
+
requestBody.amount = (0, currency_1.toMinorUnit)(params.amount, 'ZAR');
|
|
102
|
+
}
|
|
103
|
+
if (params.reason) {
|
|
104
|
+
requestBody.metadata = { reason: params.reason };
|
|
105
|
+
}
|
|
106
|
+
const response = await (0, fetch_1.timedFetchOrThrow)(`${this.baseUrl}/refunds`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
'Idempotency-Key': crypto_1.default.randomUUID(),
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(requestBody),
|
|
114
|
+
});
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
const status = this.mapYocoStatus(data.status);
|
|
117
|
+
const refundStatus = status === 'completed' ? 'completed' :
|
|
118
|
+
status === 'failed' ? 'failed' : 'pending';
|
|
119
|
+
return {
|
|
120
|
+
id: data.id,
|
|
121
|
+
status: refundStatus,
|
|
122
|
+
amount: (0, currency_1.toMajorUnit)(data.amount, 'ZAR'),
|
|
123
|
+
currency: data.currency,
|
|
124
|
+
paymentId: params.paymentId,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
raw: data,
|
|
127
|
+
};
|
|
87
128
|
}
|
|
88
129
|
// ==================== Webhooks ====================
|
|
89
130
|
parseWebhook(body, _headers) {
|
|
90
131
|
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
132
|
const yocoStatus = event.payload?.status || event.status;
|
|
104
133
|
const status = this.mapYocoStatus(yocoStatus);
|
|
105
134
|
const eventType = this.mapYocoEventType(event.type);
|
|
@@ -118,29 +147,85 @@ class YocoProvider extends base_1.PaymentProvider {
|
|
|
118
147
|
raw: event,
|
|
119
148
|
};
|
|
120
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Verify webhook signature using Yoco's Svix-based signing scheme.
|
|
152
|
+
*
|
|
153
|
+
* CRITICAL: body must be the raw string or Buffer from the webhook request.
|
|
154
|
+
* Passing a parsed JSON object will cause signature verification to fail.
|
|
155
|
+
*/
|
|
121
156
|
verifyWebhook(body, headers) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// No signature validation configured
|
|
125
|
-
return true;
|
|
157
|
+
if (!this.webhookSecret) {
|
|
158
|
+
return false;
|
|
126
159
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
const webhookId = headers?.['webhook-id'];
|
|
161
|
+
const webhookTimestamp = headers?.['webhook-timestamp'];
|
|
162
|
+
const webhookSignature = headers?.['webhook-signature'];
|
|
163
|
+
if (!webhookId || !webhookTimestamp || !webhookSignature) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const timestamp = parseInt(webhookTimestamp, 10);
|
|
167
|
+
const now = Math.floor(Date.now() / 1000);
|
|
168
|
+
if (now - timestamp > 300) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const rawBody = typeof body === 'string' ? body : body.toString('utf8');
|
|
172
|
+
const signedPayload = `${webhookId}.${webhookTimestamp}.${rawBody}`;
|
|
173
|
+
let secretBytes;
|
|
174
|
+
if (this.webhookSecret.startsWith('whsec_')) {
|
|
175
|
+
secretBytes = Buffer.from(this.webhookSecret.slice(6), 'base64');
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
secretBytes = Buffer.from(this.webhookSecret, 'utf8');
|
|
179
|
+
}
|
|
180
|
+
const computedSig = crypto_1.default
|
|
181
|
+
.createHmac('sha256', secretBytes)
|
|
182
|
+
.update(signedPayload, 'utf8')
|
|
183
|
+
.digest('base64');
|
|
184
|
+
const signatures = webhookSignature.split(' ');
|
|
185
|
+
for (const sig of signatures) {
|
|
186
|
+
const [version, expectedSig] = sig.split(',');
|
|
187
|
+
if (version === 'v1') {
|
|
188
|
+
try {
|
|
189
|
+
const computedBuffer = Buffer.from(computedSig, 'base64');
|
|
190
|
+
const expectedBuffer = Buffer.from(expectedSig, 'base64');
|
|
191
|
+
if (computedBuffer.length === expectedBuffer.length) {
|
|
192
|
+
if (crypto_1.default.timingSafeEqual(computedBuffer, expectedBuffer)) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
// ==================== Capabilities ====================
|
|
205
|
+
getCapabilities() {
|
|
206
|
+
return {
|
|
207
|
+
fees: {
|
|
208
|
+
fixed: 0,
|
|
209
|
+
percent: 2.95,
|
|
210
|
+
currency: 'ZAR',
|
|
211
|
+
},
|
|
212
|
+
currencies: this.supportedCurrencies,
|
|
213
|
+
country: 'ZA',
|
|
214
|
+
avgLatencyMs: 500,
|
|
215
|
+
};
|
|
134
216
|
}
|
|
135
217
|
// ==================== Helpers ====================
|
|
136
218
|
mapYocoStatus(yocoStatus) {
|
|
137
219
|
const statusMap = {
|
|
220
|
+
created: 'pending',
|
|
221
|
+
pending: 'pending',
|
|
222
|
+
processing: 'pending',
|
|
223
|
+
completed: 'completed',
|
|
138
224
|
succeeded: 'completed',
|
|
139
225
|
successful: 'completed',
|
|
140
226
|
failed: 'failed',
|
|
141
227
|
cancelled: 'cancelled',
|
|
142
228
|
refunded: 'refunded',
|
|
143
|
-
pending: 'pending',
|
|
144
229
|
};
|
|
145
230
|
return statusMap[yocoStatus?.toLowerCase()] || 'pending';
|
|
146
231
|
}
|
|
@@ -151,6 +236,8 @@ class YocoProvider extends base_1.PaymentProvider {
|
|
|
151
236
|
'payment.failed': 'payment.failed',
|
|
152
237
|
'payment.cancelled': 'payment.cancelled',
|
|
153
238
|
'payment.refunded': 'refund.completed',
|
|
239
|
+
'refund.succeeded': 'refund.completed',
|
|
240
|
+
'refund.failed': 'payment.failed',
|
|
154
241
|
};
|
|
155
242
|
return typeMap[yocoType] || 'payment.pending';
|
|
156
243
|
}
|