@patalink/node-api-client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.d.mts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +244 -0
- package/dist/index.mjs +209 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# patalink_node_api_client
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface PaymentRequest {
|
|
2
|
+
amount: number;
|
|
3
|
+
currency?: string;
|
|
4
|
+
paymentMethod: 'MTN' | 'Airtel' | 'PesaPal';
|
|
5
|
+
phoneNumber?: string;
|
|
6
|
+
customerName?: string;
|
|
7
|
+
customerEmail?: string;
|
|
8
|
+
}
|
|
9
|
+
interface PaymentResponse {
|
|
10
|
+
transactionId: string;
|
|
11
|
+
status: 'pending' | 'success' | 'failed';
|
|
12
|
+
providerRef?: string;
|
|
13
|
+
redirectUrl?: string;
|
|
14
|
+
message?: string;
|
|
15
|
+
}
|
|
16
|
+
interface TransactionStatus {
|
|
17
|
+
id: string;
|
|
18
|
+
status: 'pending' | 'success' | 'failed';
|
|
19
|
+
amount: number;
|
|
20
|
+
currency: string;
|
|
21
|
+
paymentMethod: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
completedAt?: string;
|
|
24
|
+
providerRef?: string;
|
|
25
|
+
}
|
|
26
|
+
interface ClientConfig {
|
|
27
|
+
apiKey: string;
|
|
28
|
+
encryptionKey: string;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
}
|
|
31
|
+
declare class PataLinkClient {
|
|
32
|
+
private apiKey;
|
|
33
|
+
private encryptionKey;
|
|
34
|
+
private timeout;
|
|
35
|
+
constructor(config: ClientConfig);
|
|
36
|
+
/**
|
|
37
|
+
* Encrypt payment data before sending to API
|
|
38
|
+
*/
|
|
39
|
+
private encrypt;
|
|
40
|
+
/**
|
|
41
|
+
* Make authenticated request to API
|
|
42
|
+
*/
|
|
43
|
+
private request;
|
|
44
|
+
/**
|
|
45
|
+
* Create a payment
|
|
46
|
+
*/
|
|
47
|
+
createPayment(request: PaymentRequest): Promise<PaymentResponse>;
|
|
48
|
+
/**
|
|
49
|
+
* Get transaction status
|
|
50
|
+
*/
|
|
51
|
+
getTransactionStatus(transactionId: string): Promise<TransactionStatus>;
|
|
52
|
+
}
|
|
53
|
+
declare function createClient(config: ClientConfig): PataLinkClient;
|
|
54
|
+
|
|
55
|
+
export { type ClientConfig, type PaymentRequest, type PaymentResponse, type TransactionStatus, createClient, PataLinkClient as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface PaymentRequest {
|
|
2
|
+
amount: number;
|
|
3
|
+
currency?: string;
|
|
4
|
+
paymentMethod: 'MTN' | 'Airtel' | 'PesaPal';
|
|
5
|
+
phoneNumber?: string;
|
|
6
|
+
customerName?: string;
|
|
7
|
+
customerEmail?: string;
|
|
8
|
+
}
|
|
9
|
+
interface PaymentResponse {
|
|
10
|
+
transactionId: string;
|
|
11
|
+
status: 'pending' | 'success' | 'failed';
|
|
12
|
+
providerRef?: string;
|
|
13
|
+
redirectUrl?: string;
|
|
14
|
+
message?: string;
|
|
15
|
+
}
|
|
16
|
+
interface TransactionStatus {
|
|
17
|
+
id: string;
|
|
18
|
+
status: 'pending' | 'success' | 'failed';
|
|
19
|
+
amount: number;
|
|
20
|
+
currency: string;
|
|
21
|
+
paymentMethod: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
completedAt?: string;
|
|
24
|
+
providerRef?: string;
|
|
25
|
+
}
|
|
26
|
+
interface ClientConfig {
|
|
27
|
+
apiKey: string;
|
|
28
|
+
encryptionKey: string;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
}
|
|
31
|
+
declare class PataLinkClient {
|
|
32
|
+
private apiKey;
|
|
33
|
+
private encryptionKey;
|
|
34
|
+
private timeout;
|
|
35
|
+
constructor(config: ClientConfig);
|
|
36
|
+
/**
|
|
37
|
+
* Encrypt payment data before sending to API
|
|
38
|
+
*/
|
|
39
|
+
private encrypt;
|
|
40
|
+
/**
|
|
41
|
+
* Make authenticated request to API
|
|
42
|
+
*/
|
|
43
|
+
private request;
|
|
44
|
+
/**
|
|
45
|
+
* Create a payment
|
|
46
|
+
*/
|
|
47
|
+
createPayment(request: PaymentRequest): Promise<PaymentResponse>;
|
|
48
|
+
/**
|
|
49
|
+
* Get transaction status
|
|
50
|
+
*/
|
|
51
|
+
getTransactionStatus(transactionId: string): Promise<TransactionStatus>;
|
|
52
|
+
}
|
|
53
|
+
declare function createClient(config: ClientConfig): PataLinkClient;
|
|
54
|
+
|
|
55
|
+
export { type ClientConfig, type PaymentRequest, type PaymentResponse, type TransactionStatus, createClient, PataLinkClient as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
createClient: () => createClient,
|
|
34
|
+
default: () => index_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
var CryptoJS = __toESM(require("crypto-js"));
|
|
38
|
+
var BASE_URL = "http://localhost:3001";
|
|
39
|
+
var PataLinkClient = class {
|
|
40
|
+
constructor(config) {
|
|
41
|
+
if (!config.apiKey) throw new Error("API key is required");
|
|
42
|
+
if (!config.encryptionKey) throw new Error("Encryption key is required");
|
|
43
|
+
this.apiKey = config.apiKey;
|
|
44
|
+
this.encryptionKey = config.encryptionKey;
|
|
45
|
+
this.timeout = config.timeout || 3e4;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Encrypt payment data before sending to API
|
|
49
|
+
*/
|
|
50
|
+
encrypt(data) {
|
|
51
|
+
const dataString = JSON.stringify(data);
|
|
52
|
+
const keyWA = CryptoJS.enc.Base64.parse(this.encryptionKey);
|
|
53
|
+
const iv = CryptoJS.lib.WordArray.random(16);
|
|
54
|
+
const ivHex = iv.toString(CryptoJS.enc.Hex);
|
|
55
|
+
const ivWA = CryptoJS.enc.Hex.parse(ivHex);
|
|
56
|
+
const encrypted = CryptoJS.AES.encrypt(dataString, keyWA, {
|
|
57
|
+
iv: ivWA,
|
|
58
|
+
mode: CryptoJS.mode.CBC,
|
|
59
|
+
padding: CryptoJS.pad.Pkcs7
|
|
60
|
+
});
|
|
61
|
+
const encryptedBase64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64);
|
|
62
|
+
const hmac = CryptoJS.HmacSHA256(dataString, keyWA).toString();
|
|
63
|
+
return {
|
|
64
|
+
encrypted: encryptedBase64,
|
|
65
|
+
iv: ivHex,
|
|
66
|
+
hmac
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Make authenticated request to API
|
|
71
|
+
*/
|
|
72
|
+
async request(endpoint, options = {}) {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
|
77
|
+
...options,
|
|
78
|
+
headers: {
|
|
79
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
...options.headers
|
|
82
|
+
},
|
|
83
|
+
signal: controller.signal
|
|
84
|
+
});
|
|
85
|
+
clearTimeout(timeoutId);
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const error = await response.json();
|
|
88
|
+
throw new Error(error.message || `HTTP ${response.status}`);
|
|
89
|
+
}
|
|
90
|
+
return await response.json();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
clearTimeout(timeoutId);
|
|
93
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
94
|
+
throw new Error("Request timeout");
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Create a payment
|
|
101
|
+
*/
|
|
102
|
+
async createPayment(request) {
|
|
103
|
+
if (!request.amount) throw new Error("amount is required");
|
|
104
|
+
if (!request.paymentMethod) throw new Error("paymentMethod is required");
|
|
105
|
+
if (request.paymentMethod !== "PesaPal" && !request.phoneNumber) {
|
|
106
|
+
throw new Error("phoneNumber is required for mobile money payments");
|
|
107
|
+
}
|
|
108
|
+
if (request.amount < 100) {
|
|
109
|
+
throw new Error("Amount must be at least 100 RWF");
|
|
110
|
+
}
|
|
111
|
+
if (request.amount > 2e6 && request.paymentMethod !== "PesaPal") {
|
|
112
|
+
throw new Error("Mobile Money limit is 2,000,000 RWF");
|
|
113
|
+
}
|
|
114
|
+
const paymentData = {
|
|
115
|
+
amount: request.amount,
|
|
116
|
+
currency: request.currency || "RWF",
|
|
117
|
+
paymentMethod: request.paymentMethod,
|
|
118
|
+
supporterPhoneNumber: request.phoneNumber || "",
|
|
119
|
+
supporterName: request.customerName || "",
|
|
120
|
+
supporterEmail: request.customerEmail || ""
|
|
121
|
+
};
|
|
122
|
+
const encryptedPayload = this.encrypt(paymentData);
|
|
123
|
+
const response = await this.request("/api/pay", {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"x-encrypted": "true"
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify(encryptedPayload)
|
|
129
|
+
});
|
|
130
|
+
return response;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get transaction status
|
|
134
|
+
*/
|
|
135
|
+
async getTransactionStatus(transactionId) {
|
|
136
|
+
if (!transactionId) throw new Error("transactionId is required");
|
|
137
|
+
return await this.request(`/api/pay/${transactionId}`, {
|
|
138
|
+
method: "GET"
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Poll for transaction completion
|
|
143
|
+
*/
|
|
144
|
+
// async waitForCompletion(
|
|
145
|
+
// transactionId: string,
|
|
146
|
+
// options?: {
|
|
147
|
+
// maxAttempts?: number;
|
|
148
|
+
// interval?: number;
|
|
149
|
+
// onProgress?: (attempt: number, status: string) => void;
|
|
150
|
+
// }
|
|
151
|
+
// ): Promise<TransactionStatus> {
|
|
152
|
+
// const maxAttempts = options?.maxAttempts || 60;
|
|
153
|
+
// const interval = options?.interval || 2000;
|
|
154
|
+
// let attempts = 0;
|
|
155
|
+
// while (attempts < maxAttempts) {
|
|
156
|
+
// attempts++;
|
|
157
|
+
// options?.onProgress?.(attempts, 'checking');
|
|
158
|
+
// const status = await this.getTransactionStatus(transactionId);
|
|
159
|
+
// if (status.status === 'success' || status.status === 'failed') {
|
|
160
|
+
// return status;
|
|
161
|
+
// }
|
|
162
|
+
// // Wait before next attempt
|
|
163
|
+
// await new Promise(resolve => setTimeout(resolve, interval));
|
|
164
|
+
// }
|
|
165
|
+
// throw new Error('Transaction timeout - still pending after maximum attempts');
|
|
166
|
+
// }
|
|
167
|
+
/**
|
|
168
|
+
* List all transactions for this account
|
|
169
|
+
*/
|
|
170
|
+
// async listTransactions(options?: {
|
|
171
|
+
// limit?: number;
|
|
172
|
+
// offset?: number;
|
|
173
|
+
// status?: 'pending' | 'success' | 'failed';
|
|
174
|
+
// }): Promise<{ transactions: TransactionStatus[]; total: number }> {
|
|
175
|
+
// const params = new URLSearchParams();
|
|
176
|
+
// if (options?.limit) params.append('limit', options.limit.toString());
|
|
177
|
+
// if (options?.offset) params.append('offset', options.offset.toString());
|
|
178
|
+
// if (options?.status) params.append('status', options.status);
|
|
179
|
+
// const query = params.toString();
|
|
180
|
+
// const endpoint = `/v1/transactions${query ? `?${query}` : ''}`;
|
|
181
|
+
// return this.request<{ transactions: TransactionStatus[]; total: number }>(endpoint, {
|
|
182
|
+
// method: 'GET'
|
|
183
|
+
// });
|
|
184
|
+
// }
|
|
185
|
+
/**
|
|
186
|
+
* Rotate encryption key (generates new key, revokes old one after 24h)
|
|
187
|
+
*/
|
|
188
|
+
// async rotateEncryptionKey(): Promise<{ newEncryptionKey: string; message: string }> {
|
|
189
|
+
// return this.request<{ newEncryptionKey: string; message: string }>('/v1/keys/rotate', {
|
|
190
|
+
// method: 'POST'
|
|
191
|
+
// });
|
|
192
|
+
// }
|
|
193
|
+
/**
|
|
194
|
+
* Get account information
|
|
195
|
+
*/
|
|
196
|
+
// async getAccountInfo(): Promise<{
|
|
197
|
+
// id: string;
|
|
198
|
+
// email: string;
|
|
199
|
+
// walletBalance: number;
|
|
200
|
+
// pendingBalance: number;
|
|
201
|
+
// availableBalance: number;
|
|
202
|
+
// }> {
|
|
203
|
+
// return this.request('/v1/account', { method: 'GET' });
|
|
204
|
+
// }
|
|
205
|
+
/**
|
|
206
|
+
* Initiate withdrawal
|
|
207
|
+
*/
|
|
208
|
+
// async createWithdrawal(request: {
|
|
209
|
+
// amount: number;
|
|
210
|
+
// phoneNumber: string;
|
|
211
|
+
// network: 'MTN' | 'Airtel';
|
|
212
|
+
// }): Promise<{ withdrawalId: string; status: string; message: string }> {
|
|
213
|
+
// if (!request.amount) throw new Error('amount is required');
|
|
214
|
+
// if (!request.phoneNumber) throw new Error('phoneNumber is required');
|
|
215
|
+
// if (request.amount < 100) throw new Error('Minimum withdrawal is 100 RWF');
|
|
216
|
+
// if (request.amount > 2000000) throw new Error('Maximum withdrawal is 2,000,000 RWF');
|
|
217
|
+
// return this.request('/v1/withdrawals', {
|
|
218
|
+
// method: 'POST',
|
|
219
|
+
// body: JSON.stringify(request)
|
|
220
|
+
// });
|
|
221
|
+
// }
|
|
222
|
+
/**
|
|
223
|
+
* List withdrawals
|
|
224
|
+
*/
|
|
225
|
+
// async listWithdrawals(options?: {
|
|
226
|
+
// limit?: number;
|
|
227
|
+
// offset?: number;
|
|
228
|
+
// }): Promise<{ withdrawals: any[]; total: number }> {
|
|
229
|
+
// const params = new URLSearchParams();
|
|
230
|
+
// if (options?.limit) params.append('limit', options.limit.toString());
|
|
231
|
+
// if (options?.offset) params.append('offset', options.offset.toString());
|
|
232
|
+
// const query = params.toString();
|
|
233
|
+
// const endpoint = `/v1/withdrawals${query ? `?${query}` : ''}`;
|
|
234
|
+
// return this.request(endpoint, { method: 'GET' });
|
|
235
|
+
// }
|
|
236
|
+
};
|
|
237
|
+
function createClient(config) {
|
|
238
|
+
return new PataLinkClient(config);
|
|
239
|
+
}
|
|
240
|
+
var index_default = PataLinkClient;
|
|
241
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
242
|
+
0 && (module.exports = {
|
|
243
|
+
createClient
|
|
244
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import * as CryptoJS from "crypto-js";
|
|
3
|
+
var BASE_URL = "http://localhost:3001";
|
|
4
|
+
var PataLinkClient = class {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
if (!config.apiKey) throw new Error("API key is required");
|
|
7
|
+
if (!config.encryptionKey) throw new Error("Encryption key is required");
|
|
8
|
+
this.apiKey = config.apiKey;
|
|
9
|
+
this.encryptionKey = config.encryptionKey;
|
|
10
|
+
this.timeout = config.timeout || 3e4;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Encrypt payment data before sending to API
|
|
14
|
+
*/
|
|
15
|
+
encrypt(data) {
|
|
16
|
+
const dataString = JSON.stringify(data);
|
|
17
|
+
const keyWA = CryptoJS.enc.Base64.parse(this.encryptionKey);
|
|
18
|
+
const iv = CryptoJS.lib.WordArray.random(16);
|
|
19
|
+
const ivHex = iv.toString(CryptoJS.enc.Hex);
|
|
20
|
+
const ivWA = CryptoJS.enc.Hex.parse(ivHex);
|
|
21
|
+
const encrypted = CryptoJS.AES.encrypt(dataString, keyWA, {
|
|
22
|
+
iv: ivWA,
|
|
23
|
+
mode: CryptoJS.mode.CBC,
|
|
24
|
+
padding: CryptoJS.pad.Pkcs7
|
|
25
|
+
});
|
|
26
|
+
const encryptedBase64 = encrypted.ciphertext.toString(CryptoJS.enc.Base64);
|
|
27
|
+
const hmac = CryptoJS.HmacSHA256(dataString, keyWA).toString();
|
|
28
|
+
return {
|
|
29
|
+
encrypted: encryptedBase64,
|
|
30
|
+
iv: ivHex,
|
|
31
|
+
hmac
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Make authenticated request to API
|
|
36
|
+
*/
|
|
37
|
+
async request(endpoint, options = {}) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(`${BASE_URL}${endpoint}`, {
|
|
42
|
+
...options,
|
|
43
|
+
headers: {
|
|
44
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
...options.headers
|
|
47
|
+
},
|
|
48
|
+
signal: controller.signal
|
|
49
|
+
});
|
|
50
|
+
clearTimeout(timeoutId);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const error = await response.json();
|
|
53
|
+
throw new Error(error.message || `HTTP ${response.status}`);
|
|
54
|
+
}
|
|
55
|
+
return await response.json();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
59
|
+
throw new Error("Request timeout");
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Create a payment
|
|
66
|
+
*/
|
|
67
|
+
async createPayment(request) {
|
|
68
|
+
if (!request.amount) throw new Error("amount is required");
|
|
69
|
+
if (!request.paymentMethod) throw new Error("paymentMethod is required");
|
|
70
|
+
if (request.paymentMethod !== "PesaPal" && !request.phoneNumber) {
|
|
71
|
+
throw new Error("phoneNumber is required for mobile money payments");
|
|
72
|
+
}
|
|
73
|
+
if (request.amount < 100) {
|
|
74
|
+
throw new Error("Amount must be at least 100 RWF");
|
|
75
|
+
}
|
|
76
|
+
if (request.amount > 2e6 && request.paymentMethod !== "PesaPal") {
|
|
77
|
+
throw new Error("Mobile Money limit is 2,000,000 RWF");
|
|
78
|
+
}
|
|
79
|
+
const paymentData = {
|
|
80
|
+
amount: request.amount,
|
|
81
|
+
currency: request.currency || "RWF",
|
|
82
|
+
paymentMethod: request.paymentMethod,
|
|
83
|
+
supporterPhoneNumber: request.phoneNumber || "",
|
|
84
|
+
supporterName: request.customerName || "",
|
|
85
|
+
supporterEmail: request.customerEmail || ""
|
|
86
|
+
};
|
|
87
|
+
const encryptedPayload = this.encrypt(paymentData);
|
|
88
|
+
const response = await this.request("/api/pay", {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"x-encrypted": "true"
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify(encryptedPayload)
|
|
94
|
+
});
|
|
95
|
+
return response;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get transaction status
|
|
99
|
+
*/
|
|
100
|
+
async getTransactionStatus(transactionId) {
|
|
101
|
+
if (!transactionId) throw new Error("transactionId is required");
|
|
102
|
+
return await this.request(`/api/pay/${transactionId}`, {
|
|
103
|
+
method: "GET"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Poll for transaction completion
|
|
108
|
+
*/
|
|
109
|
+
// async waitForCompletion(
|
|
110
|
+
// transactionId: string,
|
|
111
|
+
// options?: {
|
|
112
|
+
// maxAttempts?: number;
|
|
113
|
+
// interval?: number;
|
|
114
|
+
// onProgress?: (attempt: number, status: string) => void;
|
|
115
|
+
// }
|
|
116
|
+
// ): Promise<TransactionStatus> {
|
|
117
|
+
// const maxAttempts = options?.maxAttempts || 60;
|
|
118
|
+
// const interval = options?.interval || 2000;
|
|
119
|
+
// let attempts = 0;
|
|
120
|
+
// while (attempts < maxAttempts) {
|
|
121
|
+
// attempts++;
|
|
122
|
+
// options?.onProgress?.(attempts, 'checking');
|
|
123
|
+
// const status = await this.getTransactionStatus(transactionId);
|
|
124
|
+
// if (status.status === 'success' || status.status === 'failed') {
|
|
125
|
+
// return status;
|
|
126
|
+
// }
|
|
127
|
+
// // Wait before next attempt
|
|
128
|
+
// await new Promise(resolve => setTimeout(resolve, interval));
|
|
129
|
+
// }
|
|
130
|
+
// throw new Error('Transaction timeout - still pending after maximum attempts');
|
|
131
|
+
// }
|
|
132
|
+
/**
|
|
133
|
+
* List all transactions for this account
|
|
134
|
+
*/
|
|
135
|
+
// async listTransactions(options?: {
|
|
136
|
+
// limit?: number;
|
|
137
|
+
// offset?: number;
|
|
138
|
+
// status?: 'pending' | 'success' | 'failed';
|
|
139
|
+
// }): Promise<{ transactions: TransactionStatus[]; total: number }> {
|
|
140
|
+
// const params = new URLSearchParams();
|
|
141
|
+
// if (options?.limit) params.append('limit', options.limit.toString());
|
|
142
|
+
// if (options?.offset) params.append('offset', options.offset.toString());
|
|
143
|
+
// if (options?.status) params.append('status', options.status);
|
|
144
|
+
// const query = params.toString();
|
|
145
|
+
// const endpoint = `/v1/transactions${query ? `?${query}` : ''}`;
|
|
146
|
+
// return this.request<{ transactions: TransactionStatus[]; total: number }>(endpoint, {
|
|
147
|
+
// method: 'GET'
|
|
148
|
+
// });
|
|
149
|
+
// }
|
|
150
|
+
/**
|
|
151
|
+
* Rotate encryption key (generates new key, revokes old one after 24h)
|
|
152
|
+
*/
|
|
153
|
+
// async rotateEncryptionKey(): Promise<{ newEncryptionKey: string; message: string }> {
|
|
154
|
+
// return this.request<{ newEncryptionKey: string; message: string }>('/v1/keys/rotate', {
|
|
155
|
+
// method: 'POST'
|
|
156
|
+
// });
|
|
157
|
+
// }
|
|
158
|
+
/**
|
|
159
|
+
* Get account information
|
|
160
|
+
*/
|
|
161
|
+
// async getAccountInfo(): Promise<{
|
|
162
|
+
// id: string;
|
|
163
|
+
// email: string;
|
|
164
|
+
// walletBalance: number;
|
|
165
|
+
// pendingBalance: number;
|
|
166
|
+
// availableBalance: number;
|
|
167
|
+
// }> {
|
|
168
|
+
// return this.request('/v1/account', { method: 'GET' });
|
|
169
|
+
// }
|
|
170
|
+
/**
|
|
171
|
+
* Initiate withdrawal
|
|
172
|
+
*/
|
|
173
|
+
// async createWithdrawal(request: {
|
|
174
|
+
// amount: number;
|
|
175
|
+
// phoneNumber: string;
|
|
176
|
+
// network: 'MTN' | 'Airtel';
|
|
177
|
+
// }): Promise<{ withdrawalId: string; status: string; message: string }> {
|
|
178
|
+
// if (!request.amount) throw new Error('amount is required');
|
|
179
|
+
// if (!request.phoneNumber) throw new Error('phoneNumber is required');
|
|
180
|
+
// if (request.amount < 100) throw new Error('Minimum withdrawal is 100 RWF');
|
|
181
|
+
// if (request.amount > 2000000) throw new Error('Maximum withdrawal is 2,000,000 RWF');
|
|
182
|
+
// return this.request('/v1/withdrawals', {
|
|
183
|
+
// method: 'POST',
|
|
184
|
+
// body: JSON.stringify(request)
|
|
185
|
+
// });
|
|
186
|
+
// }
|
|
187
|
+
/**
|
|
188
|
+
* List withdrawals
|
|
189
|
+
*/
|
|
190
|
+
// async listWithdrawals(options?: {
|
|
191
|
+
// limit?: number;
|
|
192
|
+
// offset?: number;
|
|
193
|
+
// }): Promise<{ withdrawals: any[]; total: number }> {
|
|
194
|
+
// const params = new URLSearchParams();
|
|
195
|
+
// if (options?.limit) params.append('limit', options.limit.toString());
|
|
196
|
+
// if (options?.offset) params.append('offset', options.offset.toString());
|
|
197
|
+
// const query = params.toString();
|
|
198
|
+
// const endpoint = `/v1/withdrawals${query ? `?${query}` : ''}`;
|
|
199
|
+
// return this.request(endpoint, { method: 'GET' });
|
|
200
|
+
// }
|
|
201
|
+
};
|
|
202
|
+
function createClient(config) {
|
|
203
|
+
return new PataLinkClient(config);
|
|
204
|
+
}
|
|
205
|
+
var index_default = PataLinkClient;
|
|
206
|
+
export {
|
|
207
|
+
createClient,
|
|
208
|
+
index_default as default
|
|
209
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@patalink/node-api-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official PataLink API client - Simple, secure payment integration",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
18
|
+
"test": "jest",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"patalink",
|
|
23
|
+
"payment",
|
|
24
|
+
"mtn",
|
|
25
|
+
"airtel",
|
|
26
|
+
"pesapal",
|
|
27
|
+
"rwanda",
|
|
28
|
+
"mobile-money"
|
|
29
|
+
],
|
|
30
|
+
"author": "PataLink",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"crypto-js": "^4.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/crypto-js": "^4.0.0",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"crypto-js": "^4.2.0"
|
|
42
|
+
}
|
|
43
|
+
}
|