paybridge 0.9.0 → 0.11.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 +80 -0
- package/dist/cli/commands/drift-watch.d.ts +1 -0
- package/dist/cli/commands/drift-watch.js +118 -0
- package/dist/cli/commands/drift.d.ts +20 -0
- package/dist/cli/commands/drift.js +212 -0
- package/dist/cli/commands/reconcile.d.ts +1 -0
- package/dist/cli/commands/reconcile.js +341 -0
- package/dist/cli/drift-store.d.ts +14 -0
- package/dist/cli/drift-store.js +80 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/reconcile-types.d.ts +22 -0
- package/dist/cli/reconcile-types.js +2 -0
- package/dist/cli/reconcile.d.ts +12 -0
- package/dist/cli/reconcile.js +102 -0
- package/dist/cli/runners.js +49 -11
- package/dist/cli/utils.js +17 -0
- package/dist/drift-detector.d.ts +40 -0
- package/dist/drift-detector.js +103 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/providers/square.js +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runReconcileCommand = runReconcileCommand;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const readline = __importStar(require("node:readline"));
|
|
39
|
+
const index_1 = require("../../index");
|
|
40
|
+
const runners_1 = require("../runners");
|
|
41
|
+
const reconcile_1 = require("../reconcile");
|
|
42
|
+
const utils_1 = require("../utils");
|
|
43
|
+
function parseOptions(args) {
|
|
44
|
+
const opts = {};
|
|
45
|
+
for (let i = 0; i < args.length; i++) {
|
|
46
|
+
if (args[i] === '--input' && args[i + 1]) {
|
|
47
|
+
opts.input = args[i + 1];
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
else if (args[i] === '--json') {
|
|
51
|
+
opts.json = true;
|
|
52
|
+
}
|
|
53
|
+
else if (args[i] === '--webhook-url' && args[i + 1]) {
|
|
54
|
+
opts.webhookUrl = args[i + 1];
|
|
55
|
+
i++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return opts;
|
|
59
|
+
}
|
|
60
|
+
async function parseInput(input) {
|
|
61
|
+
const lines = [];
|
|
62
|
+
if (input) {
|
|
63
|
+
const content = fs.readFileSync(input, 'utf8');
|
|
64
|
+
lines.push(...content.split('\n').filter((l) => l.trim()));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const rl = readline.createInterface({
|
|
68
|
+
input: process.stdin,
|
|
69
|
+
output: process.stdout,
|
|
70
|
+
terminal: false,
|
|
71
|
+
});
|
|
72
|
+
for await (const line of rl) {
|
|
73
|
+
if (line.trim()) {
|
|
74
|
+
lines.push(line.trim());
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (lines.length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const firstLine = lines[0];
|
|
82
|
+
const isCsv = /^provider\s*,\s*reference\s*,\s*expectedStatus/i.test(firstLine);
|
|
83
|
+
const records = [];
|
|
84
|
+
if (isCsv) {
|
|
85
|
+
for (let i = 1; i < lines.length; i++) {
|
|
86
|
+
const parts = lines[i].split(',').map((s) => s.trim());
|
|
87
|
+
if (parts.length >= 3) {
|
|
88
|
+
records.push({
|
|
89
|
+
provider: parts[0],
|
|
90
|
+
reference: parts[1],
|
|
91
|
+
expectedStatus: parts[2],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
try {
|
|
99
|
+
const obj = JSON.parse(line);
|
|
100
|
+
if (obj.provider && obj.reference && obj.expectedStatus) {
|
|
101
|
+
records.push({
|
|
102
|
+
provider: obj.provider,
|
|
103
|
+
reference: obj.reference,
|
|
104
|
+
expectedStatus: obj.expectedStatus,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
console.error(`${(0, utils_1.colorize)('[!]', 'yellow')} Skipping invalid JSON line: ${line}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return records;
|
|
114
|
+
}
|
|
115
|
+
function buildProvider(providerName) {
|
|
116
|
+
const runner = runners_1.runners.find((r) => r.name === providerName);
|
|
117
|
+
if (!runner) {
|
|
118
|
+
throw new Error(`Unknown provider: ${providerName}`);
|
|
119
|
+
}
|
|
120
|
+
const missing = runner.envRequired.filter((key) => !process.env[key]);
|
|
121
|
+
if (missing.length > 0) {
|
|
122
|
+
throw new Error(`Missing: ${missing.join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
switch (providerName) {
|
|
125
|
+
case 'stripe':
|
|
126
|
+
return new index_1.PayBridge({
|
|
127
|
+
provider: 'stripe',
|
|
128
|
+
credentials: { apiKey: process.env.STRIPE_API_KEY },
|
|
129
|
+
sandbox: true,
|
|
130
|
+
});
|
|
131
|
+
case 'paystack':
|
|
132
|
+
return new index_1.PayBridge({
|
|
133
|
+
provider: 'paystack',
|
|
134
|
+
credentials: { apiKey: process.env.PAYSTACK_API_KEY },
|
|
135
|
+
sandbox: true,
|
|
136
|
+
});
|
|
137
|
+
case 'flutterwave':
|
|
138
|
+
return new index_1.PayBridge({
|
|
139
|
+
provider: 'flutterwave',
|
|
140
|
+
credentials: { apiKey: process.env.FLUTTERWAVE_API_KEY },
|
|
141
|
+
sandbox: true,
|
|
142
|
+
});
|
|
143
|
+
case 'adyen':
|
|
144
|
+
return new index_1.PayBridge({
|
|
145
|
+
provider: 'adyen',
|
|
146
|
+
credentials: {
|
|
147
|
+
apiKey: process.env.ADYEN_API_KEY,
|
|
148
|
+
merchantAccount: process.env.ADYEN_MERCHANT_ACCOUNT,
|
|
149
|
+
},
|
|
150
|
+
sandbox: true,
|
|
151
|
+
});
|
|
152
|
+
case 'softycomp':
|
|
153
|
+
return new index_1.PayBridge({
|
|
154
|
+
provider: 'softycomp',
|
|
155
|
+
credentials: {
|
|
156
|
+
apiKey: process.env.SOFTYCOMP_API_KEY,
|
|
157
|
+
secretKey: process.env.SOFTYCOMP_SECRET_KEY,
|
|
158
|
+
},
|
|
159
|
+
sandbox: true,
|
|
160
|
+
});
|
|
161
|
+
case 'yoco':
|
|
162
|
+
return new index_1.PayBridge({
|
|
163
|
+
provider: 'yoco',
|
|
164
|
+
credentials: { apiKey: process.env.YOCO_API_KEY },
|
|
165
|
+
sandbox: true,
|
|
166
|
+
});
|
|
167
|
+
case 'ozow':
|
|
168
|
+
return new index_1.PayBridge({
|
|
169
|
+
provider: 'ozow',
|
|
170
|
+
credentials: {
|
|
171
|
+
apiKey: process.env.OZOW_API_KEY,
|
|
172
|
+
siteCode: process.env.OZOW_SITE_CODE,
|
|
173
|
+
privateKey: process.env.OZOW_PRIVATE_KEY,
|
|
174
|
+
},
|
|
175
|
+
sandbox: true,
|
|
176
|
+
});
|
|
177
|
+
case 'payfast':
|
|
178
|
+
return new index_1.PayBridge({
|
|
179
|
+
provider: 'payfast',
|
|
180
|
+
credentials: {
|
|
181
|
+
merchantId: process.env.PAYFAST_MERCHANT_ID,
|
|
182
|
+
merchantKey: process.env.PAYFAST_MERCHANT_KEY,
|
|
183
|
+
passphrase: process.env.PAYFAST_PASSPHRASE,
|
|
184
|
+
},
|
|
185
|
+
sandbox: true,
|
|
186
|
+
});
|
|
187
|
+
case 'peach':
|
|
188
|
+
return new index_1.PayBridge({
|
|
189
|
+
provider: 'peach',
|
|
190
|
+
credentials: {
|
|
191
|
+
apiKey: process.env.PEACH_ACCESS_TOKEN,
|
|
192
|
+
secretKey: process.env.PEACH_ENTITY_ID,
|
|
193
|
+
},
|
|
194
|
+
sandbox: true,
|
|
195
|
+
});
|
|
196
|
+
case 'mercadopago':
|
|
197
|
+
return new index_1.PayBridge({
|
|
198
|
+
provider: 'mercadopago',
|
|
199
|
+
credentials: { apiKey: process.env.MERCADOPAGO_ACCESS_TOKEN },
|
|
200
|
+
sandbox: true,
|
|
201
|
+
});
|
|
202
|
+
case 'razorpay':
|
|
203
|
+
return new index_1.PayBridge({
|
|
204
|
+
provider: 'razorpay',
|
|
205
|
+
credentials: {
|
|
206
|
+
apiKey: process.env.RAZORPAY_KEY_ID,
|
|
207
|
+
secretKey: process.env.RAZORPAY_KEY_SECRET,
|
|
208
|
+
},
|
|
209
|
+
sandbox: true,
|
|
210
|
+
});
|
|
211
|
+
case 'mollie':
|
|
212
|
+
return new index_1.PayBridge({
|
|
213
|
+
provider: 'mollie',
|
|
214
|
+
credentials: { apiKey: process.env.MOLLIE_API_KEY },
|
|
215
|
+
sandbox: true,
|
|
216
|
+
});
|
|
217
|
+
case 'square':
|
|
218
|
+
return new index_1.PayBridge({
|
|
219
|
+
provider: 'square',
|
|
220
|
+
credentials: {
|
|
221
|
+
apiKey: process.env.SQUARE_ACCESS_TOKEN,
|
|
222
|
+
locationId: process.env.SQUARE_LOCATION_ID,
|
|
223
|
+
},
|
|
224
|
+
sandbox: true,
|
|
225
|
+
});
|
|
226
|
+
case 'pesapal':
|
|
227
|
+
return new index_1.PayBridge({
|
|
228
|
+
provider: 'pesapal',
|
|
229
|
+
credentials: {
|
|
230
|
+
apiKey: process.env.PESAPAL_CONSUMER_KEY,
|
|
231
|
+
secretKey: process.env.PESAPAL_CONSUMER_SECRET,
|
|
232
|
+
notificationId: process.env.PESAPAL_NOTIFICATION_ID,
|
|
233
|
+
},
|
|
234
|
+
sandbox: true,
|
|
235
|
+
});
|
|
236
|
+
default:
|
|
237
|
+
throw new Error(`Provider ${providerName} not implemented in reconcile`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function hasCredsFor(providerName) {
|
|
241
|
+
const runner = runners_1.runners.find((r) => r.name === providerName);
|
|
242
|
+
if (!runner) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
return runner.envRequired.every((key) => !!process.env[key]);
|
|
246
|
+
}
|
|
247
|
+
function printResult(result) {
|
|
248
|
+
const { provider, reference, classification, expectedStatus, actualStatus, errorMessage } = result;
|
|
249
|
+
let icon;
|
|
250
|
+
let message;
|
|
251
|
+
switch (classification) {
|
|
252
|
+
case 'match':
|
|
253
|
+
icon = (0, utils_1.colorize)('[✓]', 'green');
|
|
254
|
+
message = `${provider}:${reference} — ${actualStatus} (match)`;
|
|
255
|
+
break;
|
|
256
|
+
case 'mismatch':
|
|
257
|
+
icon = (0, utils_1.colorize)('[!]', 'yellow');
|
|
258
|
+
message = `${provider}:${reference} — expected ${expectedStatus}, actual ${actualStatus} (MISSED WEBHOOK)`;
|
|
259
|
+
break;
|
|
260
|
+
case 'not-found':
|
|
261
|
+
icon = (0, utils_1.colorize)('[?]', 'yellow');
|
|
262
|
+
message = `${provider}:${reference} — not-found (no provider record)`;
|
|
263
|
+
break;
|
|
264
|
+
case 'error':
|
|
265
|
+
icon = (0, utils_1.colorize)('[✗]', 'red');
|
|
266
|
+
message = `${provider}:${reference} — error (${errorMessage})`;
|
|
267
|
+
break;
|
|
268
|
+
case 'skipped':
|
|
269
|
+
icon = (0, utils_1.colorize)('[ ]', 'dim');
|
|
270
|
+
message = `${provider}:${reference} — skipped (${errorMessage})`;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
console.log(`${icon} ${message}`);
|
|
274
|
+
}
|
|
275
|
+
async function postWebhook(url, payload) {
|
|
276
|
+
const res = await fetch(url, {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
headers: { 'Content-Type': 'application/json' },
|
|
279
|
+
body: JSON.stringify(payload),
|
|
280
|
+
});
|
|
281
|
+
if (!res.ok) {
|
|
282
|
+
throw new Error(`Webhook POST failed: ${res.status} ${res.statusText}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function runReconcileCommand(args) {
|
|
286
|
+
const opts = parseOptions(args);
|
|
287
|
+
const records = await parseInput(opts.input);
|
|
288
|
+
if (records.length === 0) {
|
|
289
|
+
console.error('No records to reconcile. Provide input via stdin or --input <file>.');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const { results, summary } = await (0, reconcile_1.runReconcile)(records, { buildProvider, hasCredsFor }, {
|
|
293
|
+
onResult: opts.json ? undefined : printResult,
|
|
294
|
+
});
|
|
295
|
+
if (opts.json) {
|
|
296
|
+
for (const result of results) {
|
|
297
|
+
console.log(JSON.stringify(result));
|
|
298
|
+
}
|
|
299
|
+
console.log(JSON.stringify({ summary }));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.log(`\nReconciled: ${summary.total}`);
|
|
303
|
+
console.log(` Match: ${summary.match}`);
|
|
304
|
+
console.log(` Mismatch (missed webhook): ${summary.mismatch}`);
|
|
305
|
+
console.log(` Not found: ${summary.notFound}`);
|
|
306
|
+
console.log(` Error: ${summary.error}`);
|
|
307
|
+
console.log(` Skipped: ${summary.skipped}`);
|
|
308
|
+
}
|
|
309
|
+
if (opts.webhookUrl && summary.mismatch > 0) {
|
|
310
|
+
const mismatches = results.filter((r) => r.classification === 'mismatch');
|
|
311
|
+
const payload = {
|
|
312
|
+
totalReconciled: summary.total,
|
|
313
|
+
missed: summary.mismatch,
|
|
314
|
+
mismatches: mismatches.map((r) => ({
|
|
315
|
+
provider: r.provider,
|
|
316
|
+
reference: r.reference,
|
|
317
|
+
expected: r.expectedStatus,
|
|
318
|
+
actual: r.actualStatus,
|
|
319
|
+
})),
|
|
320
|
+
libVersion: '0.11.0',
|
|
321
|
+
};
|
|
322
|
+
try {
|
|
323
|
+
await postWebhook(opts.webhookUrl, payload);
|
|
324
|
+
if (!opts.json) {
|
|
325
|
+
console.log(`\n${(0, utils_1.colorize)('[webhook]', 'cyan')} Posted mismatch report to ${opts.webhookUrl}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
console.error(`${(0, utils_1.colorize)('[!]', 'yellow')} Webhook POST failed: ${err.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (summary.mismatch > 0) {
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
else if (summary.error > 0 && summary.match === 0) {
|
|
336
|
+
process.exit(2);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
process.exit(0);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ProviderBaseline } from '../drift-detector';
|
|
2
|
+
export interface DriftStore {
|
|
3
|
+
load(providerName: string): Promise<ProviderBaseline | null>;
|
|
4
|
+
save(baseline: ProviderBaseline): Promise<void>;
|
|
5
|
+
listProviders(): Promise<string[]>;
|
|
6
|
+
}
|
|
7
|
+
export declare class FileDriftStore implements DriftStore {
|
|
8
|
+
private dir;
|
|
9
|
+
constructor(dir: string);
|
|
10
|
+
private getFilePath;
|
|
11
|
+
load(providerName: string): Promise<ProviderBaseline | null>;
|
|
12
|
+
save(baseline: ProviderBaseline): Promise<void>;
|
|
13
|
+
listProviders(): Promise<string[]>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FileDriftStore = void 0;
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
class FileDriftStore {
|
|
40
|
+
constructor(dir) {
|
|
41
|
+
this.dir = dir;
|
|
42
|
+
}
|
|
43
|
+
getFilePath(providerName) {
|
|
44
|
+
return path.join(this.dir, `${providerName}.json`);
|
|
45
|
+
}
|
|
46
|
+
async load(providerName) {
|
|
47
|
+
const filePath = this.getFilePath(providerName);
|
|
48
|
+
try {
|
|
49
|
+
const content = await node_fs_1.promises.readFile(filePath, 'utf-8');
|
|
50
|
+
return JSON.parse(content);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err.code === 'ENOENT') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async save(baseline) {
|
|
60
|
+
await node_fs_1.promises.mkdir(this.dir, { recursive: true });
|
|
61
|
+
const filePath = this.getFilePath(baseline.providerName);
|
|
62
|
+
const content = JSON.stringify(baseline, null, 2);
|
|
63
|
+
await node_fs_1.promises.writeFile(filePath, content, 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
async listProviders() {
|
|
66
|
+
try {
|
|
67
|
+
const files = await node_fs_1.promises.readdir(this.dir);
|
|
68
|
+
return files
|
|
69
|
+
.filter((f) => f.endsWith('.json'))
|
|
70
|
+
.map((f) => f.slice(0, -5));
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
if (err.code === 'ENOENT') {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.FileDriftStore = FileDriftStore;
|
package/dist/cli/index.js
CHANGED
|
@@ -5,6 +5,9 @@ const test_1 = require("./commands/test");
|
|
|
5
5
|
const providers_1 = require("./commands/providers");
|
|
6
6
|
const webhook_1 = require("./commands/webhook");
|
|
7
7
|
const quote_1 = require("./commands/quote");
|
|
8
|
+
const drift_1 = require("./commands/drift");
|
|
9
|
+
const drift_watch_1 = require("./commands/drift-watch");
|
|
10
|
+
const reconcile_1 = require("./commands/reconcile");
|
|
8
11
|
const utils_1 = require("./utils");
|
|
9
12
|
async function main() {
|
|
10
13
|
const [, , command, ...args] = process.argv;
|
|
@@ -21,6 +24,15 @@ async function main() {
|
|
|
21
24
|
case 'quote':
|
|
22
25
|
await (0, quote_1.runQuote)(args);
|
|
23
26
|
break;
|
|
27
|
+
case 'drift-check':
|
|
28
|
+
await (0, drift_1.runDrift)(args);
|
|
29
|
+
break;
|
|
30
|
+
case 'drift-watch':
|
|
31
|
+
await (0, drift_watch_1.runDriftWatch)(args);
|
|
32
|
+
break;
|
|
33
|
+
case 'reconcile':
|
|
34
|
+
await (0, reconcile_1.runReconcileCommand)(args);
|
|
35
|
+
break;
|
|
24
36
|
case '-h':
|
|
25
37
|
case '--help':
|
|
26
38
|
case 'help':
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type CanonicalStatus = 'completed' | 'pending' | 'failed' | 'cancelled' | 'refunded' | 'unknown';
|
|
2
|
+
export interface ReconcileRecord {
|
|
3
|
+
provider: string;
|
|
4
|
+
reference: string;
|
|
5
|
+
expectedStatus: CanonicalStatus | string;
|
|
6
|
+
}
|
|
7
|
+
export interface ReconcileResult {
|
|
8
|
+
provider: string;
|
|
9
|
+
reference: string;
|
|
10
|
+
expectedStatus: string;
|
|
11
|
+
actualStatus?: string;
|
|
12
|
+
classification: 'match' | 'mismatch' | 'not-found' | 'error' | 'skipped';
|
|
13
|
+
errorMessage?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ReconcileSummary {
|
|
16
|
+
total: number;
|
|
17
|
+
match: number;
|
|
18
|
+
mismatch: number;
|
|
19
|
+
notFound: number;
|
|
20
|
+
error: number;
|
|
21
|
+
skipped: number;
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PayBridge } from '../index';
|
|
2
|
+
import { ReconcileRecord, ReconcileResult, ReconcileSummary } from './reconcile-types';
|
|
3
|
+
export interface ReconcileDeps {
|
|
4
|
+
buildProvider: (providerName: string) => PayBridge;
|
|
5
|
+
hasCredsFor: (providerName: string) => boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function runReconcile(records: ReconcileRecord[], deps: ReconcileDeps, opts?: {
|
|
8
|
+
onResult?: (r: ReconcileResult) => void;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
results: ReconcileResult[];
|
|
11
|
+
summary: ReconcileSummary;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runReconcile = runReconcile;
|
|
4
|
+
async function runReconcile(records, deps, opts = {}) {
|
|
5
|
+
const providerCache = new Map();
|
|
6
|
+
const results = [];
|
|
7
|
+
const summary = {
|
|
8
|
+
total: records.length,
|
|
9
|
+
match: 0,
|
|
10
|
+
mismatch: 0,
|
|
11
|
+
notFound: 0,
|
|
12
|
+
error: 0,
|
|
13
|
+
skipped: 0,
|
|
14
|
+
};
|
|
15
|
+
for (const record of records) {
|
|
16
|
+
if (!deps.hasCredsFor(record.provider)) {
|
|
17
|
+
const result = {
|
|
18
|
+
provider: record.provider,
|
|
19
|
+
reference: record.reference,
|
|
20
|
+
expectedStatus: record.expectedStatus,
|
|
21
|
+
classification: 'skipped',
|
|
22
|
+
errorMessage: 'missing credentials',
|
|
23
|
+
};
|
|
24
|
+
results.push(result);
|
|
25
|
+
summary.skipped++;
|
|
26
|
+
opts.onResult?.(result);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let pay;
|
|
30
|
+
if (providerCache.has(record.provider)) {
|
|
31
|
+
pay = providerCache.get(record.provider);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
try {
|
|
35
|
+
pay = deps.buildProvider(record.provider);
|
|
36
|
+
providerCache.set(record.provider, pay);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const result = {
|
|
40
|
+
provider: record.provider,
|
|
41
|
+
reference: record.reference,
|
|
42
|
+
expectedStatus: record.expectedStatus,
|
|
43
|
+
classification: 'error',
|
|
44
|
+
errorMessage: err.message || String(err),
|
|
45
|
+
};
|
|
46
|
+
results.push(result);
|
|
47
|
+
summary.error++;
|
|
48
|
+
opts.onResult?.(result);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const payment = await pay.getPayment(record.reference);
|
|
54
|
+
const actualStatus = payment.status;
|
|
55
|
+
const normalized = normalizeStatus(record.expectedStatus);
|
|
56
|
+
const match = normalized === actualStatus;
|
|
57
|
+
const result = {
|
|
58
|
+
provider: record.provider,
|
|
59
|
+
reference: record.reference,
|
|
60
|
+
expectedStatus: record.expectedStatus,
|
|
61
|
+
actualStatus,
|
|
62
|
+
classification: match ? 'match' : 'mismatch',
|
|
63
|
+
};
|
|
64
|
+
results.push(result);
|
|
65
|
+
if (match) {
|
|
66
|
+
summary.match++;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
summary.mismatch++;
|
|
70
|
+
}
|
|
71
|
+
opts.onResult?.(result);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const isNotFound = err.message?.includes('not found') ||
|
|
75
|
+
err.message?.includes('404') ||
|
|
76
|
+
err.message?.includes('No payment found');
|
|
77
|
+
const result = {
|
|
78
|
+
provider: record.provider,
|
|
79
|
+
reference: record.reference,
|
|
80
|
+
expectedStatus: record.expectedStatus,
|
|
81
|
+
classification: isNotFound ? 'not-found' : 'error',
|
|
82
|
+
errorMessage: err.message || String(err),
|
|
83
|
+
};
|
|
84
|
+
results.push(result);
|
|
85
|
+
if (isNotFound) {
|
|
86
|
+
summary.notFound++;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
summary.error++;
|
|
90
|
+
}
|
|
91
|
+
opts.onResult?.(result);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { results, summary };
|
|
95
|
+
}
|
|
96
|
+
function normalizeStatus(status) {
|
|
97
|
+
const lower = status.toLowerCase();
|
|
98
|
+
if (['completed', 'pending', 'failed', 'cancelled', 'refunded'].includes(lower)) {
|
|
99
|
+
return lower;
|
|
100
|
+
}
|
|
101
|
+
return 'unknown';
|
|
102
|
+
}
|
package/dist/cli/runners.js
CHANGED
|
@@ -6,7 +6,7 @@ const crypto_1 = require("../crypto");
|
|
|
6
6
|
const timestamp = Date.now();
|
|
7
7
|
const testCustomer = {
|
|
8
8
|
name: 'Test User',
|
|
9
|
-
email: '
|
|
9
|
+
email: 'paybridge-sandbox@gmail.com',
|
|
10
10
|
phone: '0825551234',
|
|
11
11
|
};
|
|
12
12
|
const testUrls = {
|
|
@@ -111,14 +111,26 @@ exports.runners = [
|
|
|
111
111
|
credentials: { apiKey: process.env.PAYSTACK_API_KEY },
|
|
112
112
|
sandbox: true,
|
|
113
113
|
});
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
const currencies = (process.env.PAYSTACK_TEST_CURRENCY || 'NGN,ZAR,GHS,KES').split(',');
|
|
115
|
+
let lastErr;
|
|
116
|
+
for (const currency of currencies) {
|
|
117
|
+
try {
|
|
118
|
+
const payment = await pay.createPayment({
|
|
119
|
+
amount: 100.0,
|
|
120
|
+
currency: currency.trim(),
|
|
121
|
+
reference: `cli-test-${timestamp}-${currency.trim()}`,
|
|
122
|
+
customer: testCustomer,
|
|
123
|
+
urls: testUrls,
|
|
124
|
+
});
|
|
125
|
+
return { id: payment.id, checkoutUrl: payment.checkoutUrl, status: payment.status };
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
lastErr = e;
|
|
129
|
+
if (!/currency not supported/i.test(e.message))
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw lastErr ?? new Error('No currency accepted by PayStack merchant');
|
|
122
134
|
},
|
|
123
135
|
},
|
|
124
136
|
{
|
|
@@ -289,11 +301,37 @@ exports.runners = [
|
|
|
289
301
|
name: 'pesapal',
|
|
290
302
|
envRequired: ['PESAPAL_CONSUMER_KEY', 'PESAPAL_CONSUMER_SECRET'],
|
|
291
303
|
run: async () => {
|
|
304
|
+
const consumerKey = process.env.PESAPAL_CONSUMER_KEY;
|
|
305
|
+
const consumerSecret = process.env.PESAPAL_CONSUMER_SECRET;
|
|
306
|
+
let notificationId = process.env.PESAPAL_NOTIFICATION_ID;
|
|
307
|
+
if (!notificationId) {
|
|
308
|
+
const tokenRes = await fetch('https://cybqa.pesapal.com/pesapalv3/api/Auth/RequestToken', {
|
|
309
|
+
method: 'POST',
|
|
310
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
311
|
+
body: JSON.stringify({ consumer_key: consumerKey, consumer_secret: consumerSecret }),
|
|
312
|
+
});
|
|
313
|
+
const tokenJson = await tokenRes.json();
|
|
314
|
+
const ipnRes = await fetch('https://cybqa.pesapal.com/pesapalv3/api/URLSetup/RegisterIPN', {
|
|
315
|
+
method: 'POST',
|
|
316
|
+
headers: {
|
|
317
|
+
'Content-Type': 'application/json',
|
|
318
|
+
Accept: 'application/json',
|
|
319
|
+
Authorization: `Bearer ${tokenJson.token}`,
|
|
320
|
+
},
|
|
321
|
+
body: JSON.stringify({
|
|
322
|
+
url: 'https://example.com/pesapal-ipn',
|
|
323
|
+
ipn_notification_type: 'GET',
|
|
324
|
+
}),
|
|
325
|
+
});
|
|
326
|
+
const ipnJson = await ipnRes.json();
|
|
327
|
+
notificationId = ipnJson.ipn_id;
|
|
328
|
+
}
|
|
292
329
|
const pay = new index_1.PayBridge({
|
|
293
330
|
provider: 'pesapal',
|
|
294
331
|
credentials: {
|
|
295
|
-
apiKey:
|
|
296
|
-
secretKey:
|
|
332
|
+
apiKey: consumerKey,
|
|
333
|
+
secretKey: consumerSecret,
|
|
334
|
+
notificationId,
|
|
297
335
|
},
|
|
298
336
|
sandbox: true,
|
|
299
337
|
});
|