@paykit-sdk/monnify 1.2.0 → 1.3.1
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 +149 -0
- package/dist/index.js +106 -72
- package/dist/index.mjs +107 -73
- package/dist/monnify-provider.d.mts +20 -1
- package/dist/monnify-provider.d.ts +20 -1
- package/dist/monnify-provider.js +106 -72
- package/dist/monnify-provider.mjs +107 -73
- package/package.json +10 -8
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @paykit-sdk/monnify
|
|
2
|
+
|
|
3
|
+
Monnify provider for PayKit.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @paykit-sdk/monnify
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @paykit-sdk/monnify
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createEndpointHandlers, PayKit } from '@paykit-sdk/core';
|
|
17
|
+
import { monnify } from '@paykit-sdk/monnify';
|
|
18
|
+
|
|
19
|
+
export const paykit = new PayKit(monnify());
|
|
20
|
+
export const endpoints = createEndpointHandlers(paykit);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or with direct config:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { PayKit } from '@paykit-sdk/core';
|
|
27
|
+
import { createMonnify } from '@paykit-sdk/monnify';
|
|
28
|
+
|
|
29
|
+
export const paykit = new PayKit(
|
|
30
|
+
createMonnify({
|
|
31
|
+
apiKey: 'MK_TEST_...',
|
|
32
|
+
secretKey: 'your-secret-key',
|
|
33
|
+
isSandbox: true,
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Environment Variables
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
MONNIFY_API_KEY=MK_TEST_...
|
|
42
|
+
MONNIFY_SECRET_KEY=your-secret-key
|
|
43
|
+
MONNIFY_SANDBOX=true
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Next.js API Route
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// app/api/paykit/[...endpoint]/route.ts
|
|
50
|
+
import { endpoints } from '@/lib/paykit';
|
|
51
|
+
import { EndpointPath } from '@paykit-sdk/core';
|
|
52
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
53
|
+
|
|
54
|
+
export async function POST(
|
|
55
|
+
request: NextRequest,
|
|
56
|
+
{ params }: { params: Promise<{ endpoint: string[] }> },
|
|
57
|
+
) {
|
|
58
|
+
const { endpoint: endpointArray } = await params;
|
|
59
|
+
const endpoint = ('/' + endpointArray.join('/')) as EndpointPath;
|
|
60
|
+
const handler = endpoints[endpoint];
|
|
61
|
+
|
|
62
|
+
if (!handler) {
|
|
63
|
+
return NextResponse.json(
|
|
64
|
+
{ message: 'Not found' },
|
|
65
|
+
{ status: 404 },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { args } = await request.json();
|
|
70
|
+
const result = await handler(...args);
|
|
71
|
+
return NextResponse.json({ result });
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Webhooks
|
|
76
|
+
|
|
77
|
+
Monnify sends signed POST requests with an HMAC-SHA512 signature.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// app/api/paykit/webhooks/route.ts
|
|
81
|
+
import { paykit } from '@/lib/paykit';
|
|
82
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
83
|
+
|
|
84
|
+
export async function POST(request: NextRequest) {
|
|
85
|
+
const body = await request.text();
|
|
86
|
+
|
|
87
|
+
const webhook = paykit.webhooks
|
|
88
|
+
.setup({ webhookSecret: process.env.MONNIFY_SECRET_KEY! })
|
|
89
|
+
.on('payment.created', async event => {
|
|
90
|
+
/* SUCCESSFUL_TRANSACTION or SUCCESSFUL_TRANSACTION_OFFLINE */
|
|
91
|
+
})
|
|
92
|
+
.on('payment.updated', async event => {
|
|
93
|
+
/* SETTLEMENT */
|
|
94
|
+
})
|
|
95
|
+
.on('payment.failed', async event => {
|
|
96
|
+
/* REJECTED_PAYMENT */
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await webhook.handle({
|
|
100
|
+
body,
|
|
101
|
+
headersAsObject: Object.fromEntries(request.headers),
|
|
102
|
+
fullUrl: request.url,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return NextResponse.json({ received: true });
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Monnify event mappings:
|
|
110
|
+
|
|
111
|
+
| Monnify event | PayKit event emitted |
|
|
112
|
+
| -------------------------------- | -------------------- |
|
|
113
|
+
| `SUCCESSFUL_TRANSACTION` | `payment.created` |
|
|
114
|
+
| `SUCCESSFUL_TRANSACTION_OFFLINE` | `payment.created` |
|
|
115
|
+
| `REJECTED_PAYMENT` | `payment.failed` |
|
|
116
|
+
| `SETTLEMENT` | `payment.updated` |
|
|
117
|
+
|
|
118
|
+
## Checkout
|
|
119
|
+
|
|
120
|
+
Monnify requires an email customer and amount/currency in `provider_metadata`:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const checkout = await paykit.checkouts.create({
|
|
124
|
+
customer: { email: 'user@example.com' },
|
|
125
|
+
item_id: 'prod_123',
|
|
126
|
+
session_type: 'one_time',
|
|
127
|
+
quantity: 1,
|
|
128
|
+
success_url: 'https://example.com/success',
|
|
129
|
+
provider_metadata: {
|
|
130
|
+
amount: 5000,
|
|
131
|
+
currency: 'NGN',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Redirect user to checkout.payment_url
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Documentation
|
|
139
|
+
|
|
140
|
+
Full docs at [docs.usepaykit.com/providers/monnify](https://docs.usepaykit.com/providers/monnify).
|
|
141
|
+
|
|
142
|
+
## Support
|
|
143
|
+
|
|
144
|
+
- [Monnify Documentation](https://developers.monnify.com)
|
|
145
|
+
- [PayKit Issues](https://github.com/usepaykit/paykit-sdk/issues)
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
ISC
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var core = require('@paykit-sdk/core');
|
|
4
|
+
var crypto$1 = require('crypto');
|
|
4
5
|
var jsSha512 = require('js-sha512');
|
|
5
6
|
|
|
6
7
|
// src/index.ts
|
|
@@ -210,6 +211,48 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
210
211
|
}
|
|
211
212
|
return altResponse.value.responseBody;
|
|
212
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Monnify only has one way to move money: a hosted redirect
|
|
216
|
+
* transaction. createCheckout and createPayment both boil down to
|
|
217
|
+
* this same call - only the input validation and the response
|
|
218
|
+
* mapper differ between the two.
|
|
219
|
+
*/
|
|
220
|
+
async initializeTransaction(params) {
|
|
221
|
+
const paymentReference = crypto.randomUUID();
|
|
222
|
+
const body = {
|
|
223
|
+
amount: params.amount,
|
|
224
|
+
paymentReference,
|
|
225
|
+
paymentDescription: params.description,
|
|
226
|
+
currencyCode: params.currency,
|
|
227
|
+
redirectUrl: params.redirectUrl,
|
|
228
|
+
paymentMethods: ["CARD", "ACCOUNT_TRANSFER"],
|
|
229
|
+
metadata: params.metadata,
|
|
230
|
+
customerEmail: params.email
|
|
231
|
+
};
|
|
232
|
+
const response = await this._client.post(
|
|
233
|
+
"/v1/merchant/transactions/init-transaction",
|
|
234
|
+
{
|
|
235
|
+
body: JSON.stringify(body),
|
|
236
|
+
headers: await this.tokenManager.getAuthHeaders()
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
const responseBody = this.ensureResponse(
|
|
240
|
+
response,
|
|
241
|
+
"initializeTransaction"
|
|
242
|
+
);
|
|
243
|
+
const transactionReference = responseBody.transactionReference;
|
|
244
|
+
const checkoutUrl = responseBody.checkoutUrl;
|
|
245
|
+
const transactionResponse = await this._client.get(
|
|
246
|
+
`/v2/merchant/transactions/query?paymentReference=${paymentReference}`,
|
|
247
|
+
{ headers: await this.tokenManager.getAuthHeaders() }
|
|
248
|
+
);
|
|
249
|
+
const transactionData = this.ensureResponse(
|
|
250
|
+
transactionResponse,
|
|
251
|
+
"initializeTransaction",
|
|
252
|
+
"Failed to retrieve transaction details"
|
|
253
|
+
);
|
|
254
|
+
return { ...transactionData, checkoutUrl, transactionReference };
|
|
255
|
+
}
|
|
213
256
|
createCheckout = async (params) => {
|
|
214
257
|
const data = this.validateSchema(
|
|
215
258
|
core.createCheckoutSchema,
|
|
@@ -232,50 +275,21 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
232
275
|
data.provider_metadata ?? { currency: "NGN" },
|
|
233
276
|
"The following fields must be present in the provider_metadata of createCheckout: {keys}"
|
|
234
277
|
);
|
|
235
|
-
const
|
|
236
|
-
|
|
278
|
+
const transactionData = await this.initializeTransaction({
|
|
279
|
+
email: data.customer.email,
|
|
237
280
|
amount,
|
|
238
|
-
|
|
239
|
-
paymentDescription: `Checkout for ${data.item_id} x ${data.quantity} item${data.quantity > 1 ? "s" : ""}`,
|
|
240
|
-
currencyCode: currency,
|
|
281
|
+
currency,
|
|
241
282
|
redirectUrl: data.success_url,
|
|
242
|
-
|
|
283
|
+
description: `Checkout for ${data.item_id} x ${data.quantity} item${data.quantity > 1 ? "s" : ""}`,
|
|
243
284
|
metadata: {
|
|
244
285
|
...params.metadata,
|
|
245
286
|
[core.PAYKIT_METADATA_KEY]: JSON.stringify({
|
|
246
287
|
item: data.item_id,
|
|
247
288
|
qty: data.quantity
|
|
248
289
|
})
|
|
249
|
-
},
|
|
250
|
-
customerEmail: data.customer.email
|
|
251
|
-
};
|
|
252
|
-
const response = await this._client.post(
|
|
253
|
-
"/v1/merchant/transactions/init-transaction",
|
|
254
|
-
{
|
|
255
|
-
body: JSON.stringify(body),
|
|
256
|
-
headers: await this.tokenManager.getAuthHeaders()
|
|
257
290
|
}
|
|
258
|
-
);
|
|
259
|
-
const responseBody = this.ensureResponse(
|
|
260
|
-
response,
|
|
261
|
-
"createCheckout"
|
|
262
|
-
);
|
|
263
|
-
const transactionReference = responseBody.transactionReference;
|
|
264
|
-
const checkoutUrl = responseBody.checkoutUrl;
|
|
265
|
-
const checkoutResponse = await this._client.get(
|
|
266
|
-
`/v2/merchant/transactions/query?paymentReference=${paymentReference}`,
|
|
267
|
-
{ headers: await this.tokenManager.getAuthHeaders() }
|
|
268
|
-
);
|
|
269
|
-
const checkoutData = this.ensureResponse(
|
|
270
|
-
checkoutResponse,
|
|
271
|
-
"createCheckout",
|
|
272
|
-
"Failed to retrieve checkout details"
|
|
273
|
-
);
|
|
274
|
-
return Checkout$inboundSchema({
|
|
275
|
-
...checkoutData,
|
|
276
|
-
checkoutUrl,
|
|
277
|
-
transactionReference
|
|
278
291
|
});
|
|
292
|
+
return Checkout$inboundSchema(transactionData);
|
|
279
293
|
};
|
|
280
294
|
retrieveCheckout = async (id) => {
|
|
281
295
|
const transactionData = await this.queryTransaction(
|
|
@@ -394,14 +408,39 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
394
408
|
);
|
|
395
409
|
};
|
|
396
410
|
createPayment = async (params) => {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
reason: "Moniepoint doesn't support creating payments",
|
|
402
|
-
alternative: "Use the createPayment method instead"
|
|
403
|
-
}
|
|
411
|
+
const data = this.validateSchema(
|
|
412
|
+
core.createPaymentSchema,
|
|
413
|
+
params,
|
|
414
|
+
"createPayment"
|
|
404
415
|
);
|
|
416
|
+
if (!core.isEmailCustomer(data.customer)) {
|
|
417
|
+
throw new core.InvalidTypeError(
|
|
418
|
+
"customer",
|
|
419
|
+
"object (customer) with email",
|
|
420
|
+
"string (customer ID)",
|
|
421
|
+
{
|
|
422
|
+
provider: this.providerName,
|
|
423
|
+
method: "createPayment"
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
const { success_url } = core.validateRequiredKeys(
|
|
428
|
+
["success_url"],
|
|
429
|
+
data.provider_metadata ?? {},
|
|
430
|
+
"The following fields must be present in the provider_metadata of createPayment: {keys}"
|
|
431
|
+
);
|
|
432
|
+
const transactionData = await this.initializeTransaction({
|
|
433
|
+
email: data.customer.email,
|
|
434
|
+
amount: data.amount.toString(),
|
|
435
|
+
currency: data.currency,
|
|
436
|
+
redirectUrl: success_url,
|
|
437
|
+
description: `Payment for ${data.item_id}`,
|
|
438
|
+
metadata: {
|
|
439
|
+
...data.metadata,
|
|
440
|
+
[core.PAYKIT_METADATA_KEY]: JSON.stringify({ item: data.item_id })
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
return Payment$inboundSchema(transactionData);
|
|
405
444
|
};
|
|
406
445
|
retrievePayment = async (id) => {
|
|
407
446
|
try {
|
|
@@ -503,11 +542,10 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
503
542
|
provider: this.providerName
|
|
504
543
|
});
|
|
505
544
|
}
|
|
506
|
-
const computedHash = jsSha512.sha512.hmac(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
)
|
|
510
|
-
if (computedHash !== receivedHash)
|
|
545
|
+
const computedHash = jsSha512.sha512.hmac(webhookSecret, body);
|
|
546
|
+
const computedBuf = Buffer.from(computedHash, "hex");
|
|
547
|
+
const receivedBuf = Buffer.from(receivedHash, "hex");
|
|
548
|
+
if (computedBuf.length !== receivedBuf.length || !crypto$1.timingSafeEqual(computedBuf, receivedBuf))
|
|
511
549
|
throw new core.WebhookError("Invalid Monnify signature", {
|
|
512
550
|
provider: this.providerName
|
|
513
551
|
});
|
|
@@ -527,35 +565,31 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
527
565
|
);
|
|
528
566
|
}
|
|
529
567
|
const { eventType, eventData } = parsedBody;
|
|
568
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
569
|
+
const results = [];
|
|
570
|
+
results.push({
|
|
571
|
+
id: `monnify:${eventType}:${crypto.randomUUID()}`,
|
|
572
|
+
type: `monnify.${eventType}`,
|
|
573
|
+
created,
|
|
574
|
+
data: eventData,
|
|
575
|
+
is_raw: true
|
|
576
|
+
});
|
|
530
577
|
const eventMapper = monnifyToPaykitEventMap[eventType];
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
578
|
+
const standardType = typeof eventMapper === "function" ? eventMapper(eventData) : eventMapper;
|
|
579
|
+
if (standardType) {
|
|
580
|
+
results.push({
|
|
581
|
+
id: `paykit:${eventType}:${crypto.randomUUID()}`,
|
|
582
|
+
type: standardType,
|
|
583
|
+
created,
|
|
584
|
+
data: eventData
|
|
585
|
+
// todo: add mapper for event data
|
|
534
586
|
});
|
|
587
|
+
} else if (this.opts.debug) {
|
|
588
|
+
console.info(
|
|
589
|
+
`[Monnify] No standard mapping for event: ${eventType}. Available as raw event.`
|
|
590
|
+
);
|
|
535
591
|
}
|
|
536
|
-
|
|
537
|
-
const event = eventMapper(eventData);
|
|
538
|
-
return [
|
|
539
|
-
{
|
|
540
|
-
type: event,
|
|
541
|
-
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
542
|
-
id: `paykit:webhook:${Math.random().toString(36).substring(2, 15)}`,
|
|
543
|
-
data: eventData
|
|
544
|
-
// todo: add mapper for event data
|
|
545
|
-
}
|
|
546
|
-
];
|
|
547
|
-
} else {
|
|
548
|
-
const event = eventMapper;
|
|
549
|
-
return [
|
|
550
|
-
{
|
|
551
|
-
type: event,
|
|
552
|
-
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
553
|
-
id: `paykit:webhook:${Math.random().toString(36).substring(2, 15)}`,
|
|
554
|
-
data: eventData
|
|
555
|
-
// todo: add mapper for event data
|
|
556
|
-
}
|
|
557
|
-
];
|
|
558
|
-
}
|
|
592
|
+
return results;
|
|
559
593
|
};
|
|
560
594
|
};
|
|
561
595
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { schema, Schema, validateRequiredKeys, AbstractPayKitProvider, HTTPClient, OAuth2TokenManager, ValidationError, OperationFailedError, createCheckoutSchema, isEmailCustomer, InvalidTypeError, PAYKIT_METADATA_KEY, ProviderNotSupportedError, createRefundSchema, WebhookError, parseJSON, omitInternalMetadata } from '@paykit-sdk/core';
|
|
1
|
+
import { schema, Schema, validateRequiredKeys, AbstractPayKitProvider, HTTPClient, OAuth2TokenManager, ValidationError, OperationFailedError, createCheckoutSchema, isEmailCustomer, InvalidTypeError, PAYKIT_METADATA_KEY, ProviderNotSupportedError, createPaymentSchema, createRefundSchema, WebhookError, parseJSON, omitInternalMetadata } from '@paykit-sdk/core';
|
|
2
|
+
import { timingSafeEqual } from 'crypto';
|
|
2
3
|
import { sha512 } from 'js-sha512';
|
|
3
4
|
|
|
4
5
|
// src/index.ts
|
|
@@ -208,6 +209,48 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
208
209
|
}
|
|
209
210
|
return altResponse.value.responseBody;
|
|
210
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Monnify only has one way to move money: a hosted redirect
|
|
214
|
+
* transaction. createCheckout and createPayment both boil down to
|
|
215
|
+
* this same call - only the input validation and the response
|
|
216
|
+
* mapper differ between the two.
|
|
217
|
+
*/
|
|
218
|
+
async initializeTransaction(params) {
|
|
219
|
+
const paymentReference = crypto.randomUUID();
|
|
220
|
+
const body = {
|
|
221
|
+
amount: params.amount,
|
|
222
|
+
paymentReference,
|
|
223
|
+
paymentDescription: params.description,
|
|
224
|
+
currencyCode: params.currency,
|
|
225
|
+
redirectUrl: params.redirectUrl,
|
|
226
|
+
paymentMethods: ["CARD", "ACCOUNT_TRANSFER"],
|
|
227
|
+
metadata: params.metadata,
|
|
228
|
+
customerEmail: params.email
|
|
229
|
+
};
|
|
230
|
+
const response = await this._client.post(
|
|
231
|
+
"/v1/merchant/transactions/init-transaction",
|
|
232
|
+
{
|
|
233
|
+
body: JSON.stringify(body),
|
|
234
|
+
headers: await this.tokenManager.getAuthHeaders()
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
const responseBody = this.ensureResponse(
|
|
238
|
+
response,
|
|
239
|
+
"initializeTransaction"
|
|
240
|
+
);
|
|
241
|
+
const transactionReference = responseBody.transactionReference;
|
|
242
|
+
const checkoutUrl = responseBody.checkoutUrl;
|
|
243
|
+
const transactionResponse = await this._client.get(
|
|
244
|
+
`/v2/merchant/transactions/query?paymentReference=${paymentReference}`,
|
|
245
|
+
{ headers: await this.tokenManager.getAuthHeaders() }
|
|
246
|
+
);
|
|
247
|
+
const transactionData = this.ensureResponse(
|
|
248
|
+
transactionResponse,
|
|
249
|
+
"initializeTransaction",
|
|
250
|
+
"Failed to retrieve transaction details"
|
|
251
|
+
);
|
|
252
|
+
return { ...transactionData, checkoutUrl, transactionReference };
|
|
253
|
+
}
|
|
211
254
|
createCheckout = async (params) => {
|
|
212
255
|
const data = this.validateSchema(
|
|
213
256
|
createCheckoutSchema,
|
|
@@ -230,50 +273,21 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
230
273
|
data.provider_metadata ?? { currency: "NGN" },
|
|
231
274
|
"The following fields must be present in the provider_metadata of createCheckout: {keys}"
|
|
232
275
|
);
|
|
233
|
-
const
|
|
234
|
-
|
|
276
|
+
const transactionData = await this.initializeTransaction({
|
|
277
|
+
email: data.customer.email,
|
|
235
278
|
amount,
|
|
236
|
-
|
|
237
|
-
paymentDescription: `Checkout for ${data.item_id} x ${data.quantity} item${data.quantity > 1 ? "s" : ""}`,
|
|
238
|
-
currencyCode: currency,
|
|
279
|
+
currency,
|
|
239
280
|
redirectUrl: data.success_url,
|
|
240
|
-
|
|
281
|
+
description: `Checkout for ${data.item_id} x ${data.quantity} item${data.quantity > 1 ? "s" : ""}`,
|
|
241
282
|
metadata: {
|
|
242
283
|
...params.metadata,
|
|
243
284
|
[PAYKIT_METADATA_KEY]: JSON.stringify({
|
|
244
285
|
item: data.item_id,
|
|
245
286
|
qty: data.quantity
|
|
246
287
|
})
|
|
247
|
-
},
|
|
248
|
-
customerEmail: data.customer.email
|
|
249
|
-
};
|
|
250
|
-
const response = await this._client.post(
|
|
251
|
-
"/v1/merchant/transactions/init-transaction",
|
|
252
|
-
{
|
|
253
|
-
body: JSON.stringify(body),
|
|
254
|
-
headers: await this.tokenManager.getAuthHeaders()
|
|
255
288
|
}
|
|
256
|
-
);
|
|
257
|
-
const responseBody = this.ensureResponse(
|
|
258
|
-
response,
|
|
259
|
-
"createCheckout"
|
|
260
|
-
);
|
|
261
|
-
const transactionReference = responseBody.transactionReference;
|
|
262
|
-
const checkoutUrl = responseBody.checkoutUrl;
|
|
263
|
-
const checkoutResponse = await this._client.get(
|
|
264
|
-
`/v2/merchant/transactions/query?paymentReference=${paymentReference}`,
|
|
265
|
-
{ headers: await this.tokenManager.getAuthHeaders() }
|
|
266
|
-
);
|
|
267
|
-
const checkoutData = this.ensureResponse(
|
|
268
|
-
checkoutResponse,
|
|
269
|
-
"createCheckout",
|
|
270
|
-
"Failed to retrieve checkout details"
|
|
271
|
-
);
|
|
272
|
-
return Checkout$inboundSchema({
|
|
273
|
-
...checkoutData,
|
|
274
|
-
checkoutUrl,
|
|
275
|
-
transactionReference
|
|
276
289
|
});
|
|
290
|
+
return Checkout$inboundSchema(transactionData);
|
|
277
291
|
};
|
|
278
292
|
retrieveCheckout = async (id) => {
|
|
279
293
|
const transactionData = await this.queryTransaction(
|
|
@@ -392,14 +406,39 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
392
406
|
);
|
|
393
407
|
};
|
|
394
408
|
createPayment = async (params) => {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
reason: "Moniepoint doesn't support creating payments",
|
|
400
|
-
alternative: "Use the createPayment method instead"
|
|
401
|
-
}
|
|
409
|
+
const data = this.validateSchema(
|
|
410
|
+
createPaymentSchema,
|
|
411
|
+
params,
|
|
412
|
+
"createPayment"
|
|
402
413
|
);
|
|
414
|
+
if (!isEmailCustomer(data.customer)) {
|
|
415
|
+
throw new InvalidTypeError(
|
|
416
|
+
"customer",
|
|
417
|
+
"object (customer) with email",
|
|
418
|
+
"string (customer ID)",
|
|
419
|
+
{
|
|
420
|
+
provider: this.providerName,
|
|
421
|
+
method: "createPayment"
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
const { success_url } = validateRequiredKeys(
|
|
426
|
+
["success_url"],
|
|
427
|
+
data.provider_metadata ?? {},
|
|
428
|
+
"The following fields must be present in the provider_metadata of createPayment: {keys}"
|
|
429
|
+
);
|
|
430
|
+
const transactionData = await this.initializeTransaction({
|
|
431
|
+
email: data.customer.email,
|
|
432
|
+
amount: data.amount.toString(),
|
|
433
|
+
currency: data.currency,
|
|
434
|
+
redirectUrl: success_url,
|
|
435
|
+
description: `Payment for ${data.item_id}`,
|
|
436
|
+
metadata: {
|
|
437
|
+
...data.metadata,
|
|
438
|
+
[PAYKIT_METADATA_KEY]: JSON.stringify({ item: data.item_id })
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
return Payment$inboundSchema(transactionData);
|
|
403
442
|
};
|
|
404
443
|
retrievePayment = async (id) => {
|
|
405
444
|
try {
|
|
@@ -501,11 +540,10 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
501
540
|
provider: this.providerName
|
|
502
541
|
});
|
|
503
542
|
}
|
|
504
|
-
const computedHash = sha512.hmac(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
)
|
|
508
|
-
if (computedHash !== receivedHash)
|
|
543
|
+
const computedHash = sha512.hmac(webhookSecret, body);
|
|
544
|
+
const computedBuf = Buffer.from(computedHash, "hex");
|
|
545
|
+
const receivedBuf = Buffer.from(receivedHash, "hex");
|
|
546
|
+
if (computedBuf.length !== receivedBuf.length || !timingSafeEqual(computedBuf, receivedBuf))
|
|
509
547
|
throw new WebhookError("Invalid Monnify signature", {
|
|
510
548
|
provider: this.providerName
|
|
511
549
|
});
|
|
@@ -525,35 +563,31 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
525
563
|
);
|
|
526
564
|
}
|
|
527
565
|
const { eventType, eventData } = parsedBody;
|
|
566
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
567
|
+
const results = [];
|
|
568
|
+
results.push({
|
|
569
|
+
id: `monnify:${eventType}:${crypto.randomUUID()}`,
|
|
570
|
+
type: `monnify.${eventType}`,
|
|
571
|
+
created,
|
|
572
|
+
data: eventData,
|
|
573
|
+
is_raw: true
|
|
574
|
+
});
|
|
528
575
|
const eventMapper = monnifyToPaykitEventMap[eventType];
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
576
|
+
const standardType = typeof eventMapper === "function" ? eventMapper(eventData) : eventMapper;
|
|
577
|
+
if (standardType) {
|
|
578
|
+
results.push({
|
|
579
|
+
id: `paykit:${eventType}:${crypto.randomUUID()}`,
|
|
580
|
+
type: standardType,
|
|
581
|
+
created,
|
|
582
|
+
data: eventData
|
|
583
|
+
// todo: add mapper for event data
|
|
532
584
|
});
|
|
585
|
+
} else if (this.opts.debug) {
|
|
586
|
+
console.info(
|
|
587
|
+
`[Monnify] No standard mapping for event: ${eventType}. Available as raw event.`
|
|
588
|
+
);
|
|
533
589
|
}
|
|
534
|
-
|
|
535
|
-
const event = eventMapper(eventData);
|
|
536
|
-
return [
|
|
537
|
-
{
|
|
538
|
-
type: event,
|
|
539
|
-
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
540
|
-
id: `paykit:webhook:${Math.random().toString(36).substring(2, 15)}`,
|
|
541
|
-
data: eventData
|
|
542
|
-
// todo: add mapper for event data
|
|
543
|
-
}
|
|
544
|
-
];
|
|
545
|
-
} else {
|
|
546
|
-
const event = eventMapper;
|
|
547
|
-
return [
|
|
548
|
-
{
|
|
549
|
-
type: event,
|
|
550
|
-
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
551
|
-
id: `paykit:webhook:${Math.random().toString(36).substring(2, 15)}`,
|
|
552
|
-
data: eventData
|
|
553
|
-
// todo: add mapper for event data
|
|
554
|
-
}
|
|
555
|
-
];
|
|
556
|
-
}
|
|
590
|
+
return results;
|
|
557
591
|
};
|
|
558
592
|
};
|
|
559
593
|
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { PaykitProviderOptions, AbstractPayKitProvider, PayKitProvider, ProviderMetadataRegistry, CreateCheckoutSchema, Checkout, UpdateCheckoutSchema, CreateCustomerParams, Customer, UpdateCustomerParams, CreateSubscriptionSchema, Subscription, UpdateSubscriptionSchema, CreatePaymentSchema, Payment, UpdatePaymentSchema, CapturePaymentSchema, CreateRefundSchema, Refund, WebhookHandlerConfig, WebhookEventPayload } from '@paykit-sdk/core';
|
|
2
2
|
|
|
3
3
|
interface MonnifyMetadata extends ProviderMetadataRegistry {
|
|
4
|
+
checkout?: {
|
|
5
|
+
amount?: string;
|
|
6
|
+
currency?: string;
|
|
7
|
+
};
|
|
8
|
+
payment?: {
|
|
9
|
+
/**
|
|
10
|
+
* Monnify only supports hosted/redirect transactions - a
|
|
11
|
+
* direct createPayment call still needs somewhere to send the
|
|
12
|
+
* customer, same as createCheckout's success_url.
|
|
13
|
+
*/
|
|
14
|
+
success_url?: string;
|
|
15
|
+
};
|
|
4
16
|
}
|
|
5
17
|
interface MonnifyRawEvents extends Record<string, any> {
|
|
6
18
|
}
|
|
@@ -39,6 +51,13 @@ declare class MonnifyProvider extends AbstractPayKitProvider implements PayKitPr
|
|
|
39
51
|
* Queries transaction by transactionReference or paymentReference (with fallback)
|
|
40
52
|
*/
|
|
41
53
|
private queryTransaction;
|
|
54
|
+
/**
|
|
55
|
+
* Monnify only has one way to move money: a hosted redirect
|
|
56
|
+
* transaction. createCheckout and createPayment both boil down to
|
|
57
|
+
* this same call - only the input validation and the response
|
|
58
|
+
* mapper differ between the two.
|
|
59
|
+
*/
|
|
60
|
+
private initializeTransaction;
|
|
42
61
|
createCheckout: (params: CreateCheckoutSchema) => Promise<Checkout>;
|
|
43
62
|
retrieveCheckout: (id: string) => Promise<Checkout>;
|
|
44
63
|
updateCheckout: (id: string, params: UpdateCheckoutSchema) => Promise<Checkout>;
|
|
@@ -52,7 +71,7 @@ declare class MonnifyProvider extends AbstractPayKitProvider implements PayKitPr
|
|
|
52
71
|
cancelSubscription: (id: string) => Promise<Subscription>;
|
|
53
72
|
deleteSubscription: (id: string) => Promise<null>;
|
|
54
73
|
retrieveSubscription: (id: string) => Promise<Subscription | null>;
|
|
55
|
-
createPayment: (params: CreatePaymentSchema) => Promise<Payment>;
|
|
74
|
+
createPayment: (params: CreatePaymentSchema<MonnifyMetadata["payment"]>) => Promise<Payment>;
|
|
56
75
|
retrievePayment: (id: string) => Promise<Payment | null>;
|
|
57
76
|
updatePayment: (id: string, params: UpdatePaymentSchema) => Promise<Payment>;
|
|
58
77
|
deletePayment: (id: string) => Promise<null>;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { PaykitProviderOptions, AbstractPayKitProvider, PayKitProvider, ProviderMetadataRegistry, CreateCheckoutSchema, Checkout, UpdateCheckoutSchema, CreateCustomerParams, Customer, UpdateCustomerParams, CreateSubscriptionSchema, Subscription, UpdateSubscriptionSchema, CreatePaymentSchema, Payment, UpdatePaymentSchema, CapturePaymentSchema, CreateRefundSchema, Refund, WebhookHandlerConfig, WebhookEventPayload } from '@paykit-sdk/core';
|
|
2
2
|
|
|
3
3
|
interface MonnifyMetadata extends ProviderMetadataRegistry {
|
|
4
|
+
checkout?: {
|
|
5
|
+
amount?: string;
|
|
6
|
+
currency?: string;
|
|
7
|
+
};
|
|
8
|
+
payment?: {
|
|
9
|
+
/**
|
|
10
|
+
* Monnify only supports hosted/redirect transactions - a
|
|
11
|
+
* direct createPayment call still needs somewhere to send the
|
|
12
|
+
* customer, same as createCheckout's success_url.
|
|
13
|
+
*/
|
|
14
|
+
success_url?: string;
|
|
15
|
+
};
|
|
4
16
|
}
|
|
5
17
|
interface MonnifyRawEvents extends Record<string, any> {
|
|
6
18
|
}
|
|
@@ -39,6 +51,13 @@ declare class MonnifyProvider extends AbstractPayKitProvider implements PayKitPr
|
|
|
39
51
|
* Queries transaction by transactionReference or paymentReference (with fallback)
|
|
40
52
|
*/
|
|
41
53
|
private queryTransaction;
|
|
54
|
+
/**
|
|
55
|
+
* Monnify only has one way to move money: a hosted redirect
|
|
56
|
+
* transaction. createCheckout and createPayment both boil down to
|
|
57
|
+
* this same call - only the input validation and the response
|
|
58
|
+
* mapper differ between the two.
|
|
59
|
+
*/
|
|
60
|
+
private initializeTransaction;
|
|
42
61
|
createCheckout: (params: CreateCheckoutSchema) => Promise<Checkout>;
|
|
43
62
|
retrieveCheckout: (id: string) => Promise<Checkout>;
|
|
44
63
|
updateCheckout: (id: string, params: UpdateCheckoutSchema) => Promise<Checkout>;
|
|
@@ -52,7 +71,7 @@ declare class MonnifyProvider extends AbstractPayKitProvider implements PayKitPr
|
|
|
52
71
|
cancelSubscription: (id: string) => Promise<Subscription>;
|
|
53
72
|
deleteSubscription: (id: string) => Promise<null>;
|
|
54
73
|
retrieveSubscription: (id: string) => Promise<Subscription | null>;
|
|
55
|
-
createPayment: (params: CreatePaymentSchema) => Promise<Payment>;
|
|
74
|
+
createPayment: (params: CreatePaymentSchema<MonnifyMetadata["payment"]>) => Promise<Payment>;
|
|
56
75
|
retrievePayment: (id: string) => Promise<Payment | null>;
|
|
57
76
|
updatePayment: (id: string, params: UpdatePaymentSchema) => Promise<Payment>;
|
|
58
77
|
deletePayment: (id: string) => Promise<null>;
|
package/dist/monnify-provider.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var core = require('@paykit-sdk/core');
|
|
4
|
+
var crypto$1 = require('crypto');
|
|
4
5
|
var jsSha512 = require('js-sha512');
|
|
5
6
|
|
|
6
7
|
// src/monnify-provider.ts
|
|
@@ -210,6 +211,48 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
210
211
|
}
|
|
211
212
|
return altResponse.value.responseBody;
|
|
212
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Monnify only has one way to move money: a hosted redirect
|
|
216
|
+
* transaction. createCheckout and createPayment both boil down to
|
|
217
|
+
* this same call - only the input validation and the response
|
|
218
|
+
* mapper differ between the two.
|
|
219
|
+
*/
|
|
220
|
+
async initializeTransaction(params) {
|
|
221
|
+
const paymentReference = crypto.randomUUID();
|
|
222
|
+
const body = {
|
|
223
|
+
amount: params.amount,
|
|
224
|
+
paymentReference,
|
|
225
|
+
paymentDescription: params.description,
|
|
226
|
+
currencyCode: params.currency,
|
|
227
|
+
redirectUrl: params.redirectUrl,
|
|
228
|
+
paymentMethods: ["CARD", "ACCOUNT_TRANSFER"],
|
|
229
|
+
metadata: params.metadata,
|
|
230
|
+
customerEmail: params.email
|
|
231
|
+
};
|
|
232
|
+
const response = await this._client.post(
|
|
233
|
+
"/v1/merchant/transactions/init-transaction",
|
|
234
|
+
{
|
|
235
|
+
body: JSON.stringify(body),
|
|
236
|
+
headers: await this.tokenManager.getAuthHeaders()
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
const responseBody = this.ensureResponse(
|
|
240
|
+
response,
|
|
241
|
+
"initializeTransaction"
|
|
242
|
+
);
|
|
243
|
+
const transactionReference = responseBody.transactionReference;
|
|
244
|
+
const checkoutUrl = responseBody.checkoutUrl;
|
|
245
|
+
const transactionResponse = await this._client.get(
|
|
246
|
+
`/v2/merchant/transactions/query?paymentReference=${paymentReference}`,
|
|
247
|
+
{ headers: await this.tokenManager.getAuthHeaders() }
|
|
248
|
+
);
|
|
249
|
+
const transactionData = this.ensureResponse(
|
|
250
|
+
transactionResponse,
|
|
251
|
+
"initializeTransaction",
|
|
252
|
+
"Failed to retrieve transaction details"
|
|
253
|
+
);
|
|
254
|
+
return { ...transactionData, checkoutUrl, transactionReference };
|
|
255
|
+
}
|
|
213
256
|
createCheckout = async (params) => {
|
|
214
257
|
const data = this.validateSchema(
|
|
215
258
|
core.createCheckoutSchema,
|
|
@@ -232,50 +275,21 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
232
275
|
data.provider_metadata ?? { currency: "NGN" },
|
|
233
276
|
"The following fields must be present in the provider_metadata of createCheckout: {keys}"
|
|
234
277
|
);
|
|
235
|
-
const
|
|
236
|
-
|
|
278
|
+
const transactionData = await this.initializeTransaction({
|
|
279
|
+
email: data.customer.email,
|
|
237
280
|
amount,
|
|
238
|
-
|
|
239
|
-
paymentDescription: `Checkout for ${data.item_id} x ${data.quantity} item${data.quantity > 1 ? "s" : ""}`,
|
|
240
|
-
currencyCode: currency,
|
|
281
|
+
currency,
|
|
241
282
|
redirectUrl: data.success_url,
|
|
242
|
-
|
|
283
|
+
description: `Checkout for ${data.item_id} x ${data.quantity} item${data.quantity > 1 ? "s" : ""}`,
|
|
243
284
|
metadata: {
|
|
244
285
|
...params.metadata,
|
|
245
286
|
[core.PAYKIT_METADATA_KEY]: JSON.stringify({
|
|
246
287
|
item: data.item_id,
|
|
247
288
|
qty: data.quantity
|
|
248
289
|
})
|
|
249
|
-
},
|
|
250
|
-
customerEmail: data.customer.email
|
|
251
|
-
};
|
|
252
|
-
const response = await this._client.post(
|
|
253
|
-
"/v1/merchant/transactions/init-transaction",
|
|
254
|
-
{
|
|
255
|
-
body: JSON.stringify(body),
|
|
256
|
-
headers: await this.tokenManager.getAuthHeaders()
|
|
257
290
|
}
|
|
258
|
-
);
|
|
259
|
-
const responseBody = this.ensureResponse(
|
|
260
|
-
response,
|
|
261
|
-
"createCheckout"
|
|
262
|
-
);
|
|
263
|
-
const transactionReference = responseBody.transactionReference;
|
|
264
|
-
const checkoutUrl = responseBody.checkoutUrl;
|
|
265
|
-
const checkoutResponse = await this._client.get(
|
|
266
|
-
`/v2/merchant/transactions/query?paymentReference=${paymentReference}`,
|
|
267
|
-
{ headers: await this.tokenManager.getAuthHeaders() }
|
|
268
|
-
);
|
|
269
|
-
const checkoutData = this.ensureResponse(
|
|
270
|
-
checkoutResponse,
|
|
271
|
-
"createCheckout",
|
|
272
|
-
"Failed to retrieve checkout details"
|
|
273
|
-
);
|
|
274
|
-
return Checkout$inboundSchema({
|
|
275
|
-
...checkoutData,
|
|
276
|
-
checkoutUrl,
|
|
277
|
-
transactionReference
|
|
278
291
|
});
|
|
292
|
+
return Checkout$inboundSchema(transactionData);
|
|
279
293
|
};
|
|
280
294
|
retrieveCheckout = async (id) => {
|
|
281
295
|
const transactionData = await this.queryTransaction(
|
|
@@ -394,14 +408,39 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
394
408
|
);
|
|
395
409
|
};
|
|
396
410
|
createPayment = async (params) => {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
reason: "Moniepoint doesn't support creating payments",
|
|
402
|
-
alternative: "Use the createPayment method instead"
|
|
403
|
-
}
|
|
411
|
+
const data = this.validateSchema(
|
|
412
|
+
core.createPaymentSchema,
|
|
413
|
+
params,
|
|
414
|
+
"createPayment"
|
|
404
415
|
);
|
|
416
|
+
if (!core.isEmailCustomer(data.customer)) {
|
|
417
|
+
throw new core.InvalidTypeError(
|
|
418
|
+
"customer",
|
|
419
|
+
"object (customer) with email",
|
|
420
|
+
"string (customer ID)",
|
|
421
|
+
{
|
|
422
|
+
provider: this.providerName,
|
|
423
|
+
method: "createPayment"
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
const { success_url } = core.validateRequiredKeys(
|
|
428
|
+
["success_url"],
|
|
429
|
+
data.provider_metadata ?? {},
|
|
430
|
+
"The following fields must be present in the provider_metadata of createPayment: {keys}"
|
|
431
|
+
);
|
|
432
|
+
const transactionData = await this.initializeTransaction({
|
|
433
|
+
email: data.customer.email,
|
|
434
|
+
amount: data.amount.toString(),
|
|
435
|
+
currency: data.currency,
|
|
436
|
+
redirectUrl: success_url,
|
|
437
|
+
description: `Payment for ${data.item_id}`,
|
|
438
|
+
metadata: {
|
|
439
|
+
...data.metadata,
|
|
440
|
+
[core.PAYKIT_METADATA_KEY]: JSON.stringify({ item: data.item_id })
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
return Payment$inboundSchema(transactionData);
|
|
405
444
|
};
|
|
406
445
|
retrievePayment = async (id) => {
|
|
407
446
|
try {
|
|
@@ -503,11 +542,10 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
503
542
|
provider: this.providerName
|
|
504
543
|
});
|
|
505
544
|
}
|
|
506
|
-
const computedHash = jsSha512.sha512.hmac(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
)
|
|
510
|
-
if (computedHash !== receivedHash)
|
|
545
|
+
const computedHash = jsSha512.sha512.hmac(webhookSecret, body);
|
|
546
|
+
const computedBuf = Buffer.from(computedHash, "hex");
|
|
547
|
+
const receivedBuf = Buffer.from(receivedHash, "hex");
|
|
548
|
+
if (computedBuf.length !== receivedBuf.length || !crypto$1.timingSafeEqual(computedBuf, receivedBuf))
|
|
511
549
|
throw new core.WebhookError("Invalid Monnify signature", {
|
|
512
550
|
provider: this.providerName
|
|
513
551
|
});
|
|
@@ -527,35 +565,31 @@ var MonnifyProvider = class extends core.AbstractPayKitProvider {
|
|
|
527
565
|
);
|
|
528
566
|
}
|
|
529
567
|
const { eventType, eventData } = parsedBody;
|
|
568
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
569
|
+
const results = [];
|
|
570
|
+
results.push({
|
|
571
|
+
id: `monnify:${eventType}:${crypto.randomUUID()}`,
|
|
572
|
+
type: `monnify.${eventType}`,
|
|
573
|
+
created,
|
|
574
|
+
data: eventData,
|
|
575
|
+
is_raw: true
|
|
576
|
+
});
|
|
530
577
|
const eventMapper = monnifyToPaykitEventMap[eventType];
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
578
|
+
const standardType = typeof eventMapper === "function" ? eventMapper(eventData) : eventMapper;
|
|
579
|
+
if (standardType) {
|
|
580
|
+
results.push({
|
|
581
|
+
id: `paykit:${eventType}:${crypto.randomUUID()}`,
|
|
582
|
+
type: standardType,
|
|
583
|
+
created,
|
|
584
|
+
data: eventData
|
|
585
|
+
// todo: add mapper for event data
|
|
534
586
|
});
|
|
587
|
+
} else if (this.opts.debug) {
|
|
588
|
+
console.info(
|
|
589
|
+
`[Monnify] No standard mapping for event: ${eventType}. Available as raw event.`
|
|
590
|
+
);
|
|
535
591
|
}
|
|
536
|
-
|
|
537
|
-
const event = eventMapper(eventData);
|
|
538
|
-
return [
|
|
539
|
-
{
|
|
540
|
-
type: event,
|
|
541
|
-
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
542
|
-
id: `paykit:webhook:${Math.random().toString(36).substring(2, 15)}`,
|
|
543
|
-
data: eventData
|
|
544
|
-
// todo: add mapper for event data
|
|
545
|
-
}
|
|
546
|
-
];
|
|
547
|
-
} else {
|
|
548
|
-
const event = eventMapper;
|
|
549
|
-
return [
|
|
550
|
-
{
|
|
551
|
-
type: event,
|
|
552
|
-
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
553
|
-
id: `paykit:webhook:${Math.random().toString(36).substring(2, 15)}`,
|
|
554
|
-
data: eventData
|
|
555
|
-
// todo: add mapper for event data
|
|
556
|
-
}
|
|
557
|
-
];
|
|
558
|
-
}
|
|
592
|
+
return results;
|
|
559
593
|
};
|
|
560
594
|
};
|
|
561
595
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { schema, Schema, AbstractPayKitProvider, HTTPClient, OAuth2TokenManager, ValidationError, OperationFailedError, createCheckoutSchema, isEmailCustomer, InvalidTypeError, validateRequiredKeys, PAYKIT_METADATA_KEY, ProviderNotSupportedError, createRefundSchema, WebhookError, parseJSON, omitInternalMetadata } from '@paykit-sdk/core';
|
|
1
|
+
import { schema, Schema, AbstractPayKitProvider, HTTPClient, OAuth2TokenManager, ValidationError, OperationFailedError, createCheckoutSchema, isEmailCustomer, InvalidTypeError, validateRequiredKeys, PAYKIT_METADATA_KEY, ProviderNotSupportedError, createPaymentSchema, createRefundSchema, WebhookError, parseJSON, omitInternalMetadata } from '@paykit-sdk/core';
|
|
2
|
+
import { timingSafeEqual } from 'crypto';
|
|
2
3
|
import { sha512 } from 'js-sha512';
|
|
3
4
|
|
|
4
5
|
// src/monnify-provider.ts
|
|
@@ -208,6 +209,48 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
208
209
|
}
|
|
209
210
|
return altResponse.value.responseBody;
|
|
210
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Monnify only has one way to move money: a hosted redirect
|
|
214
|
+
* transaction. createCheckout and createPayment both boil down to
|
|
215
|
+
* this same call - only the input validation and the response
|
|
216
|
+
* mapper differ between the two.
|
|
217
|
+
*/
|
|
218
|
+
async initializeTransaction(params) {
|
|
219
|
+
const paymentReference = crypto.randomUUID();
|
|
220
|
+
const body = {
|
|
221
|
+
amount: params.amount,
|
|
222
|
+
paymentReference,
|
|
223
|
+
paymentDescription: params.description,
|
|
224
|
+
currencyCode: params.currency,
|
|
225
|
+
redirectUrl: params.redirectUrl,
|
|
226
|
+
paymentMethods: ["CARD", "ACCOUNT_TRANSFER"],
|
|
227
|
+
metadata: params.metadata,
|
|
228
|
+
customerEmail: params.email
|
|
229
|
+
};
|
|
230
|
+
const response = await this._client.post(
|
|
231
|
+
"/v1/merchant/transactions/init-transaction",
|
|
232
|
+
{
|
|
233
|
+
body: JSON.stringify(body),
|
|
234
|
+
headers: await this.tokenManager.getAuthHeaders()
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
const responseBody = this.ensureResponse(
|
|
238
|
+
response,
|
|
239
|
+
"initializeTransaction"
|
|
240
|
+
);
|
|
241
|
+
const transactionReference = responseBody.transactionReference;
|
|
242
|
+
const checkoutUrl = responseBody.checkoutUrl;
|
|
243
|
+
const transactionResponse = await this._client.get(
|
|
244
|
+
`/v2/merchant/transactions/query?paymentReference=${paymentReference}`,
|
|
245
|
+
{ headers: await this.tokenManager.getAuthHeaders() }
|
|
246
|
+
);
|
|
247
|
+
const transactionData = this.ensureResponse(
|
|
248
|
+
transactionResponse,
|
|
249
|
+
"initializeTransaction",
|
|
250
|
+
"Failed to retrieve transaction details"
|
|
251
|
+
);
|
|
252
|
+
return { ...transactionData, checkoutUrl, transactionReference };
|
|
253
|
+
}
|
|
211
254
|
createCheckout = async (params) => {
|
|
212
255
|
const data = this.validateSchema(
|
|
213
256
|
createCheckoutSchema,
|
|
@@ -230,50 +273,21 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
230
273
|
data.provider_metadata ?? { currency: "NGN" },
|
|
231
274
|
"The following fields must be present in the provider_metadata of createCheckout: {keys}"
|
|
232
275
|
);
|
|
233
|
-
const
|
|
234
|
-
|
|
276
|
+
const transactionData = await this.initializeTransaction({
|
|
277
|
+
email: data.customer.email,
|
|
235
278
|
amount,
|
|
236
|
-
|
|
237
|
-
paymentDescription: `Checkout for ${data.item_id} x ${data.quantity} item${data.quantity > 1 ? "s" : ""}`,
|
|
238
|
-
currencyCode: currency,
|
|
279
|
+
currency,
|
|
239
280
|
redirectUrl: data.success_url,
|
|
240
|
-
|
|
281
|
+
description: `Checkout for ${data.item_id} x ${data.quantity} item${data.quantity > 1 ? "s" : ""}`,
|
|
241
282
|
metadata: {
|
|
242
283
|
...params.metadata,
|
|
243
284
|
[PAYKIT_METADATA_KEY]: JSON.stringify({
|
|
244
285
|
item: data.item_id,
|
|
245
286
|
qty: data.quantity
|
|
246
287
|
})
|
|
247
|
-
},
|
|
248
|
-
customerEmail: data.customer.email
|
|
249
|
-
};
|
|
250
|
-
const response = await this._client.post(
|
|
251
|
-
"/v1/merchant/transactions/init-transaction",
|
|
252
|
-
{
|
|
253
|
-
body: JSON.stringify(body),
|
|
254
|
-
headers: await this.tokenManager.getAuthHeaders()
|
|
255
288
|
}
|
|
256
|
-
);
|
|
257
|
-
const responseBody = this.ensureResponse(
|
|
258
|
-
response,
|
|
259
|
-
"createCheckout"
|
|
260
|
-
);
|
|
261
|
-
const transactionReference = responseBody.transactionReference;
|
|
262
|
-
const checkoutUrl = responseBody.checkoutUrl;
|
|
263
|
-
const checkoutResponse = await this._client.get(
|
|
264
|
-
`/v2/merchant/transactions/query?paymentReference=${paymentReference}`,
|
|
265
|
-
{ headers: await this.tokenManager.getAuthHeaders() }
|
|
266
|
-
);
|
|
267
|
-
const checkoutData = this.ensureResponse(
|
|
268
|
-
checkoutResponse,
|
|
269
|
-
"createCheckout",
|
|
270
|
-
"Failed to retrieve checkout details"
|
|
271
|
-
);
|
|
272
|
-
return Checkout$inboundSchema({
|
|
273
|
-
...checkoutData,
|
|
274
|
-
checkoutUrl,
|
|
275
|
-
transactionReference
|
|
276
289
|
});
|
|
290
|
+
return Checkout$inboundSchema(transactionData);
|
|
277
291
|
};
|
|
278
292
|
retrieveCheckout = async (id) => {
|
|
279
293
|
const transactionData = await this.queryTransaction(
|
|
@@ -392,14 +406,39 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
392
406
|
);
|
|
393
407
|
};
|
|
394
408
|
createPayment = async (params) => {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
reason: "Moniepoint doesn't support creating payments",
|
|
400
|
-
alternative: "Use the createPayment method instead"
|
|
401
|
-
}
|
|
409
|
+
const data = this.validateSchema(
|
|
410
|
+
createPaymentSchema,
|
|
411
|
+
params,
|
|
412
|
+
"createPayment"
|
|
402
413
|
);
|
|
414
|
+
if (!isEmailCustomer(data.customer)) {
|
|
415
|
+
throw new InvalidTypeError(
|
|
416
|
+
"customer",
|
|
417
|
+
"object (customer) with email",
|
|
418
|
+
"string (customer ID)",
|
|
419
|
+
{
|
|
420
|
+
provider: this.providerName,
|
|
421
|
+
method: "createPayment"
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
const { success_url } = validateRequiredKeys(
|
|
426
|
+
["success_url"],
|
|
427
|
+
data.provider_metadata ?? {},
|
|
428
|
+
"The following fields must be present in the provider_metadata of createPayment: {keys}"
|
|
429
|
+
);
|
|
430
|
+
const transactionData = await this.initializeTransaction({
|
|
431
|
+
email: data.customer.email,
|
|
432
|
+
amount: data.amount.toString(),
|
|
433
|
+
currency: data.currency,
|
|
434
|
+
redirectUrl: success_url,
|
|
435
|
+
description: `Payment for ${data.item_id}`,
|
|
436
|
+
metadata: {
|
|
437
|
+
...data.metadata,
|
|
438
|
+
[PAYKIT_METADATA_KEY]: JSON.stringify({ item: data.item_id })
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
return Payment$inboundSchema(transactionData);
|
|
403
442
|
};
|
|
404
443
|
retrievePayment = async (id) => {
|
|
405
444
|
try {
|
|
@@ -501,11 +540,10 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
501
540
|
provider: this.providerName
|
|
502
541
|
});
|
|
503
542
|
}
|
|
504
|
-
const computedHash = sha512.hmac(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
)
|
|
508
|
-
if (computedHash !== receivedHash)
|
|
543
|
+
const computedHash = sha512.hmac(webhookSecret, body);
|
|
544
|
+
const computedBuf = Buffer.from(computedHash, "hex");
|
|
545
|
+
const receivedBuf = Buffer.from(receivedHash, "hex");
|
|
546
|
+
if (computedBuf.length !== receivedBuf.length || !timingSafeEqual(computedBuf, receivedBuf))
|
|
509
547
|
throw new WebhookError("Invalid Monnify signature", {
|
|
510
548
|
provider: this.providerName
|
|
511
549
|
});
|
|
@@ -525,35 +563,31 @@ var MonnifyProvider = class extends AbstractPayKitProvider {
|
|
|
525
563
|
);
|
|
526
564
|
}
|
|
527
565
|
const { eventType, eventData } = parsedBody;
|
|
566
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
567
|
+
const results = [];
|
|
568
|
+
results.push({
|
|
569
|
+
id: `monnify:${eventType}:${crypto.randomUUID()}`,
|
|
570
|
+
type: `monnify.${eventType}`,
|
|
571
|
+
created,
|
|
572
|
+
data: eventData,
|
|
573
|
+
is_raw: true
|
|
574
|
+
});
|
|
528
575
|
const eventMapper = monnifyToPaykitEventMap[eventType];
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
576
|
+
const standardType = typeof eventMapper === "function" ? eventMapper(eventData) : eventMapper;
|
|
577
|
+
if (standardType) {
|
|
578
|
+
results.push({
|
|
579
|
+
id: `paykit:${eventType}:${crypto.randomUUID()}`,
|
|
580
|
+
type: standardType,
|
|
581
|
+
created,
|
|
582
|
+
data: eventData
|
|
583
|
+
// todo: add mapper for event data
|
|
532
584
|
});
|
|
585
|
+
} else if (this.opts.debug) {
|
|
586
|
+
console.info(
|
|
587
|
+
`[Monnify] No standard mapping for event: ${eventType}. Available as raw event.`
|
|
588
|
+
);
|
|
533
589
|
}
|
|
534
|
-
|
|
535
|
-
const event = eventMapper(eventData);
|
|
536
|
-
return [
|
|
537
|
-
{
|
|
538
|
-
type: event,
|
|
539
|
-
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
540
|
-
id: `paykit:webhook:${Math.random().toString(36).substring(2, 15)}`,
|
|
541
|
-
data: eventData
|
|
542
|
-
// todo: add mapper for event data
|
|
543
|
-
}
|
|
544
|
-
];
|
|
545
|
-
} else {
|
|
546
|
-
const event = eventMapper;
|
|
547
|
-
return [
|
|
548
|
-
{
|
|
549
|
-
type: event,
|
|
550
|
-
created: (/* @__PURE__ */ new Date()).getTime(),
|
|
551
|
-
id: `paykit:webhook:${Math.random().toString(36).substring(2, 15)}`,
|
|
552
|
-
data: eventData
|
|
553
|
-
// todo: add mapper for event data
|
|
554
|
-
}
|
|
555
|
-
];
|
|
556
|
-
}
|
|
590
|
+
return results;
|
|
557
591
|
};
|
|
558
592
|
};
|
|
559
593
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paykit-sdk/monnify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Moniepoint provider for PayKit",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -18,19 +18,16 @@
|
|
|
18
18
|
"paykit": {
|
|
19
19
|
"type": "provider"
|
|
20
20
|
},
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "tsup"
|
|
23
|
-
},
|
|
24
21
|
"keywords": [],
|
|
25
22
|
"license": "ISC",
|
|
26
23
|
"author": "Emmanuel Odii",
|
|
27
24
|
"peerDependencies": {
|
|
28
|
-
"@paykit-sdk/core": "
|
|
25
|
+
"@paykit-sdk/core": "^1.2.3"
|
|
29
26
|
},
|
|
30
27
|
"devDependencies": {
|
|
31
|
-
"@paykit-sdk/core": "workspace:*",
|
|
32
28
|
"tsup": "^8.5.0",
|
|
33
|
-
"typescript": "^5.9.2"
|
|
29
|
+
"typescript": "^5.9.2",
|
|
30
|
+
"@paykit-sdk/core": "1.3.1"
|
|
34
31
|
},
|
|
35
32
|
"dependencies": {
|
|
36
33
|
"js-sha512": "^0.9.0"
|
|
@@ -44,5 +41,10 @@
|
|
|
44
41
|
},
|
|
45
42
|
"bugs": {
|
|
46
43
|
"url": "https://github.com/usepaykit/paykit-sdk/issues"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"test": "vitest run --config ../../vitest.config.ts packages/monnify/src",
|
|
48
|
+
"typecheck": "tsc --noEmit"
|
|
47
49
|
}
|
|
48
|
-
}
|
|
50
|
+
}
|