@katorymnd/pawapay-node-sdk 2.6.1 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1267 -2
- package/package.json +1 -1
- package/scripts/install-sdk.js +1 -1
- package/src/api/ApiClient.js +1 -1
- package/src/utils/license/integrity.js +1 -1
- package/src/utils/license/protection.js +1 -1
- package/src/utils/license/server-check.js +1 -1
- package/src/utils/license/validator.js +1 -1
- package/src/utils/vm/bytecode-encoder.js +1 -1
- package/src/utils/vm/degradation-manager.js +1 -1
- package/src/utils/vm/interpreter.js +1 -1
package/README.md
CHANGED
|
@@ -123,8 +123,13 @@ Easily switch between sandbox and production environments using environment vari
|
|
|
123
123
|
* [Configuration (.env)](#configuration-env)
|
|
124
124
|
* [Usage](#usage)
|
|
125
125
|
|
|
126
|
-
* [Initializing the SDK](#initializing-the-sdk)
|
|
127
|
-
* [
|
|
126
|
+
* [Initializing the SDK(The brain)](#initializing-the-sdk)
|
|
127
|
+
* [The SDK heart](#the-sdk-heart)
|
|
128
|
+
* [Deposit Senario(MNO)](#deposit-senariomno)
|
|
129
|
+
* [Deposit Senario (Hosted Page)](#deposit-senario-hosted-page)
|
|
130
|
+
* [Payout Senario](#payout-senario)
|
|
131
|
+
* [Refund Senario](#refund-senario)
|
|
132
|
+
* [MNO Configuration & Version Switching](#mno-configuration--version-switching)
|
|
128
133
|
|
|
129
134
|
* [Support](#support)
|
|
130
135
|
|
|
@@ -207,6 +212,1144 @@ On first initialization, the SDK securely binds:
|
|
|
207
212
|
|
|
208
213
|
This binding is permanent for that license and prevents unauthorized reuse across domains.
|
|
209
214
|
|
|
215
|
+
---
|
|
216
|
+
### The SDK heart
|
|
217
|
+
|
|
218
|
+
At this stage i assume that the user has arleady purchased the premium packge and also installed the SDK to there work space.
|
|
219
|
+
|
|
220
|
+
You need to call the sdk (`@katorymnd/pawapay-node-sdk`) to your project so that you can use it for `deposit`,`hosted deposit`,`refund`,`payout`,`MNO Config` and also to confirm transactions.
|
|
221
|
+
|
|
222
|
+
Create a page `pawapayService.js` for example and add this code
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
//pawapayService.js
|
|
226
|
+
|
|
227
|
+
const path = require('path');
|
|
228
|
+
const winston = require('winston');
|
|
229
|
+
require('dotenv').config();
|
|
230
|
+
|
|
231
|
+
// Load the SDK and destructure the public exports directly
|
|
232
|
+
// We use 'ApiClient' because that's the class name export
|
|
233
|
+
const { ApiClient, Helpers, FailureCodeHelper } = require('@katorymnd/pawapay-node-sdk');
|
|
234
|
+
|
|
235
|
+
// ========== LOGGING SETUP ==========
|
|
236
|
+
const logsDir = path.resolve(__dirname, '../logs');
|
|
237
|
+
|
|
238
|
+
const logger = winston.createLogger({
|
|
239
|
+
format: winston.format.combine(
|
|
240
|
+
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
241
|
+
winston.format.errors({ stack: true }),
|
|
242
|
+
winston.format.json()
|
|
243
|
+
),
|
|
244
|
+
transports: [
|
|
245
|
+
new winston.transports.File({
|
|
246
|
+
filename: path.join(logsDir, 'payment_success.log'),
|
|
247
|
+
level: 'info',
|
|
248
|
+
format: winston.format.combine(
|
|
249
|
+
winston.format.timestamp(),
|
|
250
|
+
winston.format.json()
|
|
251
|
+
)
|
|
252
|
+
}),
|
|
253
|
+
new winston.transports.File({
|
|
254
|
+
filename: path.join(logsDir, 'payment_failed.log'),
|
|
255
|
+
level: 'error',
|
|
256
|
+
format: winston.format.combine(
|
|
257
|
+
winston.format.timestamp(),
|
|
258
|
+
winston.format.json()
|
|
259
|
+
)
|
|
260
|
+
})
|
|
261
|
+
]
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
265
|
+
logger.add(new winston.transports.Console({
|
|
266
|
+
format: winston.format.combine(
|
|
267
|
+
winston.format.colorize(),
|
|
268
|
+
winston.format.simple()
|
|
269
|
+
)
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
// ========== END LOGGING SETUP ==========
|
|
273
|
+
|
|
274
|
+
class PawaPayService {
|
|
275
|
+
/**
|
|
276
|
+
* @param {Object} config - Optional configuration
|
|
277
|
+
* @param {string} config.token - Custom API Token to override .env
|
|
278
|
+
*/
|
|
279
|
+
constructor(config = {}) {
|
|
280
|
+
// Prioritize ENV
|
|
281
|
+
const activeToken = process.env.PAWAPAY_SANDBOX_API_TOKEN;
|
|
282
|
+
|
|
283
|
+
// Debug log to confirm token is being used (masked)
|
|
284
|
+
const maskedToken = activeToken ? `${activeToken.substring(0, 5)}...` : 'NONE';
|
|
285
|
+
console.log(`[PawaPayService] Initializing with token: ${maskedToken}`);
|
|
286
|
+
|
|
287
|
+
this.pawapay = new ApiClient({
|
|
288
|
+
apiToken: activeToken,
|
|
289
|
+
environment: 'sandbox', //production/sandbox
|
|
290
|
+
licenseKey: process.env.KATORYMND_PAWAPAY_SDK_LICENSE_KEY, //required
|
|
291
|
+
sslVerify: false // true -> production
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Deposit money to a mobile money account
|
|
296
|
+
* @param {Object} depositData - Deposit details
|
|
297
|
+
* @param {string} apiVersion - 'v1' or 'v2'
|
|
298
|
+
*/
|
|
299
|
+
async deposit(depositData, apiVersion = 'v1') {
|
|
300
|
+
// Use the Helper directly from the SDK
|
|
301
|
+
const depositId = Helpers.generateUniqueId();
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const {
|
|
305
|
+
amount,
|
|
306
|
+
currency,
|
|
307
|
+
mno,
|
|
308
|
+
payerMsisdn,
|
|
309
|
+
description,
|
|
310
|
+
metadata = []
|
|
311
|
+
} = depositData;
|
|
312
|
+
|
|
313
|
+
// 1. STRICT VALIDATION
|
|
314
|
+
if (!amount || !mno || !payerMsisdn || !description || !currency) {
|
|
315
|
+
const missingMsg = 'Validation failed - Missing required fields';
|
|
316
|
+
logger.error(missingMsg, depositData);
|
|
317
|
+
return { success: false, error: missingMsg };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Validate Amount
|
|
321
|
+
const amountRegex = /^\d+(\.\d{1,2})?$/;
|
|
322
|
+
if (!amountRegex.test(amount) || parseFloat(amount) <= 0) {
|
|
323
|
+
const msg = 'Invalid amount. Must be positive with max 2 decimals.';
|
|
324
|
+
logger.error(msg, { amount });
|
|
325
|
+
return { success: false, error: msg };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Validate Description
|
|
329
|
+
const descriptionRegex = /^[A-Za-z0-9 ]{1,22}$/;
|
|
330
|
+
if (!descriptionRegex.test(description)) {
|
|
331
|
+
const msg = 'Invalid description. Max 22 chars, alphanumeric only.';
|
|
332
|
+
logger.error(msg, { description });
|
|
333
|
+
return { success: false, error: msg };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
logger.info('Initiating deposit', {
|
|
337
|
+
depositId,
|
|
338
|
+
amount,
|
|
339
|
+
currency,
|
|
340
|
+
mno,
|
|
341
|
+
apiVersion
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 2. PROCESS DEPOSIT
|
|
345
|
+
let response;
|
|
346
|
+
|
|
347
|
+
if (apiVersion === 'v2') {
|
|
348
|
+
response = await this.pawapay.initiateDepositV2(
|
|
349
|
+
depositId,
|
|
350
|
+
amount,
|
|
351
|
+
currency,
|
|
352
|
+
payerMsisdn,
|
|
353
|
+
mno, // provider
|
|
354
|
+
description, // customerMessage
|
|
355
|
+
null, // clientReferenceId
|
|
356
|
+
null, // preAuthorisationCode
|
|
357
|
+
metadata
|
|
358
|
+
);
|
|
359
|
+
} else {
|
|
360
|
+
response = await this.pawapay.initiateDeposit(
|
|
361
|
+
depositId,
|
|
362
|
+
amount,
|
|
363
|
+
currency,
|
|
364
|
+
mno, // correspondent
|
|
365
|
+
payerMsisdn,
|
|
366
|
+
description, // statementDescription
|
|
367
|
+
metadata
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 3. HANDLE RESPONSE
|
|
372
|
+
if (response.status === 200 || response.status === 201) {
|
|
373
|
+
logger.info('Deposit initiated successfully', { depositId, status: response.status });
|
|
374
|
+
|
|
375
|
+
const statusCheck = await this.checkTransactionStatus(depositId, apiVersion);
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
success: true,
|
|
379
|
+
depositId,
|
|
380
|
+
transactionId: depositId,
|
|
381
|
+
reference: depositId,
|
|
382
|
+
status: statusCheck.status || 'SUBMITTED',
|
|
383
|
+
message: 'Deposit initiated successfully',
|
|
384
|
+
rawResponse: response,
|
|
385
|
+
statusCheck: statusCheck
|
|
386
|
+
};
|
|
387
|
+
} else {
|
|
388
|
+
// 4. HANDLE ERRORS
|
|
389
|
+
let errorMessage = 'Deposit initiation failed';
|
|
390
|
+
let failureCode = 'UNKNOWN';
|
|
391
|
+
|
|
392
|
+
if (response.response?.rejectionReason?.rejectionMessage) {
|
|
393
|
+
errorMessage = response.response.rejectionReason.rejectionMessage;
|
|
394
|
+
} else if (response.response?.failureReason?.failureCode) {
|
|
395
|
+
failureCode = response.response.failureReason.failureCode;
|
|
396
|
+
// Use the SDK's built-in error helper
|
|
397
|
+
errorMessage = FailureCodeHelper.getFailureMessage(failureCode);
|
|
398
|
+
} else if (response.response?.message) {
|
|
399
|
+
errorMessage = response.response.message;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
logger.error('Deposit initiation failed', {
|
|
403
|
+
depositId,
|
|
404
|
+
error: errorMessage,
|
|
405
|
+
failureCode,
|
|
406
|
+
response: response.response
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
error: errorMessage,
|
|
412
|
+
depositId,
|
|
413
|
+
statusCode: response.status,
|
|
414
|
+
rawResponse: response
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
} catch (error) {
|
|
419
|
+
logger.error('System Error during deposit', {
|
|
420
|
+
depositId,
|
|
421
|
+
error: error.message,
|
|
422
|
+
stack: error.stack
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
success: false,
|
|
427
|
+
error: error.message || 'Internal processing error',
|
|
428
|
+
depositId
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Check Status of ANY transaction (Deposit, Payout, Refund)
|
|
435
|
+
* @param {string} transactionId - The ID to check
|
|
436
|
+
* @param {string} apiVersion - 'v1' or 'v2'
|
|
437
|
+
* @param {string} type - 'deposit', 'payout', 'refund', 'remittance'
|
|
438
|
+
*/
|
|
439
|
+
async checkTransactionStatus(transactionId, apiVersion = 'v1', type = 'deposit') {
|
|
440
|
+
try {
|
|
441
|
+
let response;
|
|
442
|
+
|
|
443
|
+
// Pass the 'type' to the SDK so it hits the correct endpoint (e.g., /payouts vs /deposits)
|
|
444
|
+
if (apiVersion === 'v2') {
|
|
445
|
+
response = await this.pawapay.checkTransactionStatusV2(transactionId, type);
|
|
446
|
+
} else {
|
|
447
|
+
response = await this.pawapay.checkTransactionStatus(transactionId, type);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
logger.info(`Checking ${type} status`, { transactionId, status: response.status });
|
|
451
|
+
|
|
452
|
+
if (response.status === 200) {
|
|
453
|
+
let data;
|
|
454
|
+
let status;
|
|
455
|
+
|
|
456
|
+
// Normalize V1 (Array/Object) vs V2 (Object wrapper)
|
|
457
|
+
if (apiVersion === 'v2') {
|
|
458
|
+
if (response.response?.status !== 'FOUND') {
|
|
459
|
+
return {
|
|
460
|
+
success: true,
|
|
461
|
+
status: 'PROCESSING',
|
|
462
|
+
transactionId,
|
|
463
|
+
message: 'Transaction processing'
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
data = response.response.data;
|
|
467
|
+
status = data?.status || 'UNKNOWN';
|
|
468
|
+
} else {
|
|
469
|
+
// V1 legacy can be array [ { ... } ] or object
|
|
470
|
+
const raw = response.response;
|
|
471
|
+
data = Array.isArray(raw) ? raw[0] : raw;
|
|
472
|
+
status = data?.status || 'UNKNOWN';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
success: true,
|
|
477
|
+
status: status,
|
|
478
|
+
transactionId: transactionId,
|
|
479
|
+
data: data,
|
|
480
|
+
rawResponse: response
|
|
481
|
+
};
|
|
482
|
+
} else {
|
|
483
|
+
return {
|
|
484
|
+
success: false,
|
|
485
|
+
error: `Status check failed with code ${response.status}`,
|
|
486
|
+
statusCode: response.status
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
} catch (error) {
|
|
490
|
+
logger.error('Status check error', { error: error.message });
|
|
491
|
+
return {
|
|
492
|
+
success: false,
|
|
493
|
+
error: error.message || 'Status check failed'
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
validateToken(token) {
|
|
499
|
+
if (!token || token.trim() === '') return { isValid: false, error: 'Token is required' };
|
|
500
|
+
return { isValid: true, type: 'JWT', message: 'Valid token format' };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Create a Payment Page Session
|
|
505
|
+
* @param {Object} pageData - Payment details
|
|
506
|
+
* @param {string} apiVersion - 'v1' or 'v2'
|
|
507
|
+
*/
|
|
508
|
+
async initiatePaymentPage(pageData, apiVersion = 'v1') {
|
|
509
|
+
const depositId = Helpers.generateUniqueId();
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const {
|
|
513
|
+
amount,
|
|
514
|
+
currency,
|
|
515
|
+
payerMsisdn,
|
|
516
|
+
description,
|
|
517
|
+
returnUrl,
|
|
518
|
+
metadata = [],
|
|
519
|
+
country = 'UGA', // Default to Uganda for testing
|
|
520
|
+
reason = 'Payment'
|
|
521
|
+
} = pageData;
|
|
522
|
+
|
|
523
|
+
// 1. STRICT VALIDATION
|
|
524
|
+
if (!amount || !description || !currency || !returnUrl) {
|
|
525
|
+
const missingMsg = 'Validation failed - Missing required fields (returnUrl is mandatory)';
|
|
526
|
+
logger.error(missingMsg, pageData);
|
|
527
|
+
return { success: false, error: missingMsg };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
logger.info('Initiating Payment Page', {
|
|
531
|
+
depositId,
|
|
532
|
+
amount,
|
|
533
|
+
apiVersion
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// 2. PREPARE PAYLOAD & CALL SDK
|
|
537
|
+
let response;
|
|
538
|
+
|
|
539
|
+
// Normalize phone (remove +)
|
|
540
|
+
const cleanMsisdn = payerMsisdn ? payerMsisdn.replace(/\D/g, '') : null;
|
|
541
|
+
|
|
542
|
+
if (apiVersion === 'v2') {
|
|
543
|
+
// V2 Payload Construction
|
|
544
|
+
const v2Params = {
|
|
545
|
+
depositId,
|
|
546
|
+
returnUrl,
|
|
547
|
+
customerMessage: description,
|
|
548
|
+
amountDetails: {
|
|
549
|
+
amount: String(amount),
|
|
550
|
+
currency: currency
|
|
551
|
+
},
|
|
552
|
+
phoneNumber: cleanMsisdn,
|
|
553
|
+
country,
|
|
554
|
+
reason,
|
|
555
|
+
metadata
|
|
556
|
+
};
|
|
557
|
+
response = await this.pawapay.createPaymentPageSessionV2(v2Params);
|
|
558
|
+
} else {
|
|
559
|
+
// V1 Payload Construction
|
|
560
|
+
const v1Params = {
|
|
561
|
+
depositId,
|
|
562
|
+
returnUrl,
|
|
563
|
+
amount: String(amount),
|
|
564
|
+
currency,
|
|
565
|
+
msisdn: cleanMsisdn,
|
|
566
|
+
statementDescription: description,
|
|
567
|
+
country,
|
|
568
|
+
reason,
|
|
569
|
+
metadata
|
|
570
|
+
};
|
|
571
|
+
response = await this.pawapay.createPaymentPageSession(v1Params);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// 3. HANDLE RESPONSE
|
|
575
|
+
// Note: API returns 200/201 for success
|
|
576
|
+
if (response.status >= 200 && response.status < 300) {
|
|
577
|
+
const redirectUrl = response.response?.redirectUrl || response.response?.url;
|
|
578
|
+
|
|
579
|
+
logger.info('Payment Page created', { depositId, redirectUrl });
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
success: true,
|
|
583
|
+
depositId,
|
|
584
|
+
redirectUrl,
|
|
585
|
+
message: 'Session created successfully',
|
|
586
|
+
rawResponse: response
|
|
587
|
+
};
|
|
588
|
+
} else {
|
|
589
|
+
// 4. HANDLE ERRORS
|
|
590
|
+
const errorMsg = response.response?.message || 'Failed to create payment session';
|
|
591
|
+
|
|
592
|
+
logger.error('Payment Page creation failed', {
|
|
593
|
+
depositId,
|
|
594
|
+
error: errorMsg,
|
|
595
|
+
response: response.response
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
success: false,
|
|
600
|
+
error: errorMsg,
|
|
601
|
+
depositId,
|
|
602
|
+
statusCode: response.status
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
} catch (error) {
|
|
607
|
+
logger.error('System Error during payment page creation', {
|
|
608
|
+
depositId,
|
|
609
|
+
error: error.message,
|
|
610
|
+
stack: error.stack
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
success: false,
|
|
615
|
+
error: error.message || 'Internal processing error',
|
|
616
|
+
depositId
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Payout money to a mobile money account (Disbursement)
|
|
624
|
+
* @param {Object} payoutData - Payout details
|
|
625
|
+
* @param {string} apiVersion - 'v1' or 'v2'
|
|
626
|
+
*/
|
|
627
|
+
async payout(payoutData, apiVersion = 'v1') {
|
|
628
|
+
const payoutId = Helpers.generateUniqueId();
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
// 1. EXTRACT DATA WITH FALLBACKS
|
|
632
|
+
let {
|
|
633
|
+
amount,
|
|
634
|
+
currency,
|
|
635
|
+
mno, // Logic might send this
|
|
636
|
+
provider, // V2 logic might send this
|
|
637
|
+
correspondent, // V1 logic might send this
|
|
638
|
+
recipientMsisdn,
|
|
639
|
+
description, // Direct description
|
|
640
|
+
statementDescription, // V1 alternative
|
|
641
|
+
customerMessage, // V2 alternative
|
|
642
|
+
reason, // Another possible field
|
|
643
|
+
metadata = []
|
|
644
|
+
} = payoutData;
|
|
645
|
+
|
|
646
|
+
// 🛠️ FIX: Normalize the operator code
|
|
647
|
+
// If 'mno' is undefined, use 'provider' (V2) or 'correspondent' (V1)
|
|
648
|
+
const resolvedMno = mno || provider || correspondent;
|
|
649
|
+
|
|
650
|
+
// ============================================================
|
|
651
|
+
// Ensure 'description' is never missing
|
|
652
|
+
// PawaPay API requires this field for both V1 and V2.
|
|
653
|
+
// ============================================================
|
|
654
|
+
const resolvedDescription = description
|
|
655
|
+
|| statementDescription
|
|
656
|
+
|| customerMessage
|
|
657
|
+
|| reason
|
|
658
|
+
|| 'Transaction Processing'; // Ultimate fallback
|
|
659
|
+
|
|
660
|
+
// 2. STRICT VALIDATION & DEBUG LOGGING
|
|
661
|
+
// We check specific fields to give a precise error message
|
|
662
|
+
const missingFields = [];
|
|
663
|
+
if (!amount) missingFields.push('amount');
|
|
664
|
+
if (!resolvedMno) missingFields.push(`mno (looked for: mno, provider, correspondent)`);
|
|
665
|
+
if (!recipientMsisdn) missingFields.push('recipientMsisdn');
|
|
666
|
+
if (!resolvedDescription) missingFields.push('description');
|
|
667
|
+
if (!currency) missingFields.push('currency');
|
|
668
|
+
|
|
669
|
+
if (missingFields.length > 0) {
|
|
670
|
+
const missingMsg = `Validation failed - Missing fields: [${missingFields.join(', ')}]`;
|
|
671
|
+
|
|
672
|
+
// 🔍 DEBUG: Construct a detailed log entry for the error.log
|
|
673
|
+
const debugPayload = {
|
|
674
|
+
ERROR_TYPE: 'PAYOUT_VALIDATION_ERROR',
|
|
675
|
+
PAYOUT_ID: payoutId,
|
|
676
|
+
API_VERSION: apiVersion,
|
|
677
|
+
MISSING: missingFields,
|
|
678
|
+
RESOLVED_MNO: resolvedMno || 'UNDEFINED (This is likely the issue)',
|
|
679
|
+
RESOLVED_DESCRIPTION: resolvedDescription || 'UNDEFINED',
|
|
680
|
+
RAW_RECEIVED: JSON.stringify(payoutData, null, 2) // Pretty print the full object
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// Log to your system logger
|
|
684
|
+
logger.error(missingMsg, debugPayload);
|
|
685
|
+
|
|
686
|
+
// Return failure with details
|
|
687
|
+
return {
|
|
688
|
+
success: false,
|
|
689
|
+
error: missingMsg,
|
|
690
|
+
debug: debugPayload // Return this so the frontend/controller can see it too
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Assign the resolved values back to variables used in logic
|
|
695
|
+
mno = resolvedMno;
|
|
696
|
+
description = resolvedDescription; // CRITICAL: Update the description variable
|
|
697
|
+
|
|
698
|
+
// Validate Amount Format
|
|
699
|
+
const amountRegex = /^\d+(\.\d{1,2})?$/;
|
|
700
|
+
if (!amountRegex.test(amount) || parseFloat(amount) <= 0) {
|
|
701
|
+
const msg = 'Invalid amount. Must be positive with max 2 decimals.';
|
|
702
|
+
logger.error(msg, { amount, payoutId });
|
|
703
|
+
return { success: false, error: msg };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
logger.info('Initiating payout', {
|
|
707
|
+
payoutId,
|
|
708
|
+
amount,
|
|
709
|
+
currency,
|
|
710
|
+
mno,
|
|
711
|
+
description, // Log the resolved description
|
|
712
|
+
apiVersion
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// 3. PROCESS PAYOUT
|
|
716
|
+
let response;
|
|
717
|
+
|
|
718
|
+
if (apiVersion === 'v2') {
|
|
719
|
+
// V2 Payout
|
|
720
|
+
response = await this.pawapay.initiatePayoutV2(
|
|
721
|
+
payoutId,
|
|
722
|
+
amount,
|
|
723
|
+
currency,
|
|
724
|
+
recipientMsisdn,
|
|
725
|
+
mno, // provider
|
|
726
|
+
description, // customerMessage - using the resolved description
|
|
727
|
+
metadata
|
|
728
|
+
);
|
|
729
|
+
} else {
|
|
730
|
+
// V1 Payout
|
|
731
|
+
response = await this.pawapay.initiatePayout(
|
|
732
|
+
payoutId,
|
|
733
|
+
amount,
|
|
734
|
+
currency,
|
|
735
|
+
mno, // correspondent
|
|
736
|
+
recipientMsisdn, // recipient address
|
|
737
|
+
description, // statementDescription - using the resolved description
|
|
738
|
+
metadata
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// 4. HANDLE RESPONSE
|
|
743
|
+
if (response.status === 200 || response.status === 201 || response.status === 202) {
|
|
744
|
+
logger.info('Payout initiated successfully', {
|
|
745
|
+
payoutId,
|
|
746
|
+
status: response.status,
|
|
747
|
+
description // Log successful description
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
const statusCheck = await this.checkTransactionStatus(payoutId, apiVersion);
|
|
751
|
+
|
|
752
|
+
return {
|
|
753
|
+
success: true,
|
|
754
|
+
payoutId,
|
|
755
|
+
transactionId: payoutId,
|
|
756
|
+
status: statusCheck.status || 'SUBMITTED',
|
|
757
|
+
message: 'Payout initiated successfully',
|
|
758
|
+
rawResponse: response,
|
|
759
|
+
statusCheck: statusCheck
|
|
760
|
+
};
|
|
761
|
+
} else {
|
|
762
|
+
// 5. HANDLE ERRORS
|
|
763
|
+
let errorMessage = 'Payout initiation failed';
|
|
764
|
+
let failureCode = 'UNKNOWN';
|
|
765
|
+
|
|
766
|
+
if (response.response?.rejectionReason?.rejectionMessage) {
|
|
767
|
+
errorMessage = response.response.rejectionReason.rejectionMessage;
|
|
768
|
+
} else if (response.response?.failureReason?.failureCode) {
|
|
769
|
+
failureCode = response.response.failureReason.failureCode;
|
|
770
|
+
errorMessage = FailureCodeHelper.getFailureMessage(failureCode);
|
|
771
|
+
} else if (response.response?.message) {
|
|
772
|
+
errorMessage = response.response.message;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
logger.error('Payout initiation failed', {
|
|
776
|
+
payoutId,
|
|
777
|
+
error: errorMessage,
|
|
778
|
+
failureCode,
|
|
779
|
+
response: response.response,
|
|
780
|
+
description // Log description even on failure
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
success: false,
|
|
785
|
+
error: errorMessage,
|
|
786
|
+
payoutId,
|
|
787
|
+
statusCode: response.status,
|
|
788
|
+
rawResponse: response
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
} catch (error) {
|
|
793
|
+
logger.error('System Error during payout', {
|
|
794
|
+
payoutId,
|
|
795
|
+
error: error.message,
|
|
796
|
+
stack: error.stack,
|
|
797
|
+
inputData: JSON.stringify(payoutData) // Log input on crash too
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
return {
|
|
801
|
+
success: false,
|
|
802
|
+
error: error.message || 'Internal processing error',
|
|
803
|
+
payoutId
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Initiate a Refund (Partial or Full)
|
|
811
|
+
* @param {Object} refundData - Refund details
|
|
812
|
+
* @param {string} apiVersion - 'v1' or 'v2'
|
|
813
|
+
*/
|
|
814
|
+
async refund(refundData, apiVersion = 'v1') {
|
|
815
|
+
const refundId = Helpers.generateUniqueId();
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
const {
|
|
819
|
+
depositId,
|
|
820
|
+
amount,
|
|
821
|
+
currency, // Required for V2
|
|
822
|
+
reason,
|
|
823
|
+
metadata = []
|
|
824
|
+
} = refundData;
|
|
825
|
+
|
|
826
|
+
// 1. STRICT VALIDATION
|
|
827
|
+
if (!depositId || !amount) {
|
|
828
|
+
const missingMsg = 'Validation failed - Missing required fields (depositId, amount)';
|
|
829
|
+
logger.error(missingMsg, refundData);
|
|
830
|
+
return { success: false, error: missingMsg };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// V2 Specific Validation
|
|
834
|
+
if (apiVersion === 'v2' && !currency) {
|
|
835
|
+
const msg = 'Validation failed - V2 Refunds require a currency code';
|
|
836
|
+
logger.error(msg, refundData);
|
|
837
|
+
return { success: false, error: msg };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Validate Amount
|
|
841
|
+
const amountRegex = /^\d+(\.\d{1,2})?$/;
|
|
842
|
+
if (!amountRegex.test(amount) || parseFloat(amount) <= 0) {
|
|
843
|
+
const msg = 'Invalid amount. Must be positive with max 2 decimals.';
|
|
844
|
+
logger.error(msg, { amount });
|
|
845
|
+
return { success: false, error: msg };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
logger.info('Initiating refund', {
|
|
849
|
+
refundId,
|
|
850
|
+
depositId,
|
|
851
|
+
amount,
|
|
852
|
+
currency,
|
|
853
|
+
apiVersion
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// 2. PROCESS REFUND
|
|
857
|
+
let response;
|
|
858
|
+
|
|
859
|
+
if (apiVersion === 'v2') {
|
|
860
|
+
// V2 Refund
|
|
861
|
+
response = await this.pawapay.initiateRefundV2(
|
|
862
|
+
refundId,
|
|
863
|
+
depositId,
|
|
864
|
+
amount,
|
|
865
|
+
currency,
|
|
866
|
+
metadata
|
|
867
|
+
);
|
|
868
|
+
} else {
|
|
869
|
+
// V1 Refund
|
|
870
|
+
response = await this.pawapay.initiateRefund(
|
|
871
|
+
refundId,
|
|
872
|
+
depositId,
|
|
873
|
+
amount,
|
|
874
|
+
metadata
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// 3. HANDLE RESPONSE
|
|
879
|
+
// Refunds typically return 200/201/202
|
|
880
|
+
if (response.status >= 200 && response.status < 300) {
|
|
881
|
+
logger.info('Refund initiated successfully', { refundId, status: response.status });
|
|
882
|
+
|
|
883
|
+
// Check status immediately (passing 'refund' as type is critical)
|
|
884
|
+
const statusCheck = await this.checkTransactionStatus(refundId, apiVersion, 'refund');
|
|
885
|
+
|
|
886
|
+
return {
|
|
887
|
+
success: true,
|
|
888
|
+
refundId,
|
|
889
|
+
transactionId: refundId,
|
|
890
|
+
depositId: depositId,
|
|
891
|
+
status: statusCheck.status || 'SUBMITTED',
|
|
892
|
+
message: 'Refund initiated successfully',
|
|
893
|
+
rawResponse: response,
|
|
894
|
+
statusCheck: statusCheck
|
|
895
|
+
};
|
|
896
|
+
} else {
|
|
897
|
+
// 4. HANDLE ERRORS
|
|
898
|
+
let errorMessage = 'Refund initiation failed';
|
|
899
|
+
let failureCode = 'UNKNOWN';
|
|
900
|
+
|
|
901
|
+
if (response.response?.rejectionReason?.rejectionMessage) {
|
|
902
|
+
errorMessage = response.response.rejectionReason.rejectionMessage;
|
|
903
|
+
} else if (response.response?.failureReason?.failureCode) {
|
|
904
|
+
failureCode = response.response.failureReason.failureCode;
|
|
905
|
+
errorMessage = FailureCodeHelper.getFailureMessage(failureCode);
|
|
906
|
+
} else if (response.response?.message) {
|
|
907
|
+
errorMessage = response.response.message;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
logger.error('Refund initiation failed', {
|
|
911
|
+
refundId,
|
|
912
|
+
depositId,
|
|
913
|
+
error: errorMessage,
|
|
914
|
+
failureCode,
|
|
915
|
+
response: response.response
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
success: false,
|
|
920
|
+
error: errorMessage,
|
|
921
|
+
refundId,
|
|
922
|
+
statusCode: response.status,
|
|
923
|
+
rawResponse: response
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
} catch (error) {
|
|
928
|
+
logger.error('System Error during refund', {
|
|
929
|
+
refundId,
|
|
930
|
+
depositId: refundData.depositId,
|
|
931
|
+
error: error.message,
|
|
932
|
+
stack: error.stack
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
return {
|
|
936
|
+
success: false,
|
|
937
|
+
error: error.message || 'Internal processing error',
|
|
938
|
+
refundId
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
module.exports = PawaPayService;
|
|
946
|
+
```
|
|
947
|
+
what our `pawapayService.js` has is all what the SDK needs to make any process possible.
|
|
948
|
+
|
|
949
|
+
**Lets demostrate how to use the heart with examples**
|
|
950
|
+
|
|
951
|
+
---
|
|
952
|
+
### Deposit Senario(MNO)
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
//test-deposit.js
|
|
956
|
+
const path = require('path');
|
|
957
|
+
// Ensure strict loading of the root .env file
|
|
958
|
+
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
|
959
|
+
|
|
960
|
+
const PawaPayService = require('./pawapayService');
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Run a full test of the PawaPay SDK integration (Deposits)
|
|
964
|
+
*/
|
|
965
|
+
async function testSDKConnection() {
|
|
966
|
+
console.log('\n========================================');
|
|
967
|
+
console.log('🧪 PAWAPAY SDK DEPOSIT TEST (With Status Check)');
|
|
968
|
+
console.log('========================================\n');
|
|
969
|
+
|
|
970
|
+
const service = new PawaPayService();
|
|
971
|
+
|
|
972
|
+
// 1. Validate Token Format
|
|
973
|
+
console.log('🔹 Step 1: Validating API Token...');
|
|
974
|
+
const testToken = process.env.PAWAPAY_SANDBOX_API_TOKEN;
|
|
975
|
+
const validation = service.validateToken(testToken);
|
|
976
|
+
|
|
977
|
+
if (!validation.isValid) {
|
|
978
|
+
console.error('❌ Token Validation Failed:', validation);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
console.log('✅ Token looks valid:', validation.type);
|
|
982
|
+
|
|
983
|
+
// Common Test Data (Uganda MTN Sandbox)
|
|
984
|
+
const commonData = {
|
|
985
|
+
amount: '1000',
|
|
986
|
+
currency: 'UGX',
|
|
987
|
+
mno: 'MTN_MOMO_UGA',
|
|
988
|
+
payerMsisdn: '256783456789', // Valid Sandbox Payer
|
|
989
|
+
description: 'SDK Integration Test'
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
// --- 2. Test V1 Deposit ---
|
|
993
|
+
console.log('\n🔹 Step 2: Testing V1 Deposit...');
|
|
994
|
+
try {
|
|
995
|
+
const v1Result = await service.deposit({
|
|
996
|
+
...commonData,
|
|
997
|
+
description: 'V1 Test Payment'
|
|
998
|
+
}, 'v1');
|
|
999
|
+
|
|
1000
|
+
if (v1Result.success) {
|
|
1001
|
+
console.log('✅ V1 Initiation Success:', {
|
|
1002
|
+
depositId: v1Result.depositId,
|
|
1003
|
+
status: v1Result.status
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
// WAIT AND CHECK
|
|
1007
|
+
console.log('⏳ Waiting 5 seconds for V1 propagation...');
|
|
1008
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
1009
|
+
|
|
1010
|
+
console.log('🔍 Checking V1 Status...');
|
|
1011
|
+
const statusCheck = await service.checkTransactionStatus(
|
|
1012
|
+
v1Result.depositId,
|
|
1013
|
+
'v1',
|
|
1014
|
+
'deposit'
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
console.log(`📊 Final V1 Status: [ ${statusCheck.status} ]`);
|
|
1018
|
+
if (statusCheck.status === 'FAILED') {
|
|
1019
|
+
console.warn(` Reason: ${statusCheck.data?.failureReason?.failureMessage || 'Unknown'}`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
} else {
|
|
1023
|
+
console.error('❌ V1 Failed:', v1Result.error);
|
|
1024
|
+
}
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
console.error('❌ V1 Exception:', error.message);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// --- 3. Test V2 Deposit ---
|
|
1030
|
+
console.log('\n🔹 Step 3: Testing V2 Deposit...');
|
|
1031
|
+
try {
|
|
1032
|
+
const v2Result = await service.deposit({
|
|
1033
|
+
...commonData,
|
|
1034
|
+
description: 'V2 Test Payment',
|
|
1035
|
+
metadata: [
|
|
1036
|
+
{ orderId: "ORD-SDK-TEST" },
|
|
1037
|
+
{ customerId: "test-user@example.com", isPII: true }
|
|
1038
|
+
]
|
|
1039
|
+
}, 'v2');
|
|
1040
|
+
|
|
1041
|
+
if (v2Result.success) {
|
|
1042
|
+
console.log('✅ V2 Initiation Success:', {
|
|
1043
|
+
depositId: v2Result.depositId,
|
|
1044
|
+
status: v2Result.status
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
// WAIT AND CHECK
|
|
1048
|
+
console.log('⏳ Waiting 5 seconds for V2 propagation...');
|
|
1049
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
1050
|
+
|
|
1051
|
+
console.log('🔍 Checking V2 Status...');
|
|
1052
|
+
const statusCheck = await service.checkTransactionStatus(
|
|
1053
|
+
v2Result.depositId,
|
|
1054
|
+
'v2',
|
|
1055
|
+
'deposit'
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
console.log(`📊 Final V2 Status: [ ${statusCheck.status} ]`);
|
|
1059
|
+
if (statusCheck.status === 'FAILED') {
|
|
1060
|
+
// V2 failure messages are nested in 'data' usually
|
|
1061
|
+
const msg = statusCheck.data?.failureReason?.failureMessage || 'Unknown';
|
|
1062
|
+
console.warn(` Reason: ${msg}`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
} else {
|
|
1066
|
+
console.error('❌ V2 Failed:', v2Result.error);
|
|
1067
|
+
}
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
console.error('❌ V2 Exception:', error.message);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
console.log('\n🏁 SDK Testing Complete');
|
|
1073
|
+
process.exit(0);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Run test if called directly
|
|
1077
|
+
if (require.main === module) {
|
|
1078
|
+
testSDKConnection().catch(console.error);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
module.exports = { testSDKConnection };
|
|
1082
|
+
```
|
|
1083
|
+
---
|
|
1084
|
+
|
|
1085
|
+
### Deposit Senario (Hosted Page)
|
|
1086
|
+
|
|
1087
|
+
```typescript
|
|
1088
|
+
|
|
1089
|
+
// test-payment-page.js
|
|
1090
|
+
require('dotenv').config();
|
|
1091
|
+
const PawaPayService = require('./pawapayService');
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Run a full test of the PawaPay Payment Page Integration
|
|
1095
|
+
*/
|
|
1096
|
+
async function testPaymentPage() {
|
|
1097
|
+
console.log('🔗 Starting PawaPay Payment Page Test...\n');
|
|
1098
|
+
|
|
1099
|
+
const service = new PawaPayService();
|
|
1100
|
+
|
|
1101
|
+
// 1. Validate Token Format
|
|
1102
|
+
console.log('🔹 Step 1: Validating API Token...');
|
|
1103
|
+
const testToken = process.env.PAWAPAY_SANDBOX_API_TOKEN;
|
|
1104
|
+
const validation = service.validateToken(testToken);
|
|
1105
|
+
|
|
1106
|
+
if (!validation.isValid) {
|
|
1107
|
+
console.error('❌ Token Validation Failed:', validation);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
console.log('✅ Token looks valid:', validation.type);
|
|
1111
|
+
|
|
1112
|
+
// Common Test Data
|
|
1113
|
+
const commonData = {
|
|
1114
|
+
amount: '500',
|
|
1115
|
+
currency: '51345789',
|
|
1116
|
+
payerMsisdn: '22951345789', // Optional for V2, but good for V1
|
|
1117
|
+
description: 'Page Test',
|
|
1118
|
+
// IMPORTANT: You need a return URL
|
|
1119
|
+
returnUrl: 'https://example.com/payment-success',
|
|
1120
|
+
country: 'UGA'
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
// 2. Test V1 Payment Page
|
|
1124
|
+
console.log('\n🔹 Step 2: Testing V1 Payment Page Generation...');
|
|
1125
|
+
try {
|
|
1126
|
+
const v1Result = await service.initiatePaymentPage({
|
|
1127
|
+
...commonData,
|
|
1128
|
+
description: 'V1 Page Test'
|
|
1129
|
+
}, 'v1');
|
|
1130
|
+
|
|
1131
|
+
if (v1Result.success) {
|
|
1132
|
+
console.log('✅ V1 Success!');
|
|
1133
|
+
console.log(' Deposit ID:', v1Result.depositId);
|
|
1134
|
+
console.log(' 👉 CLICK TO PAY:', v1Result.redirectUrl);
|
|
1135
|
+
} else {
|
|
1136
|
+
console.error('❌ V1 Failed:', v1Result.error);
|
|
1137
|
+
}
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
console.error('❌ V1 Exception:', error.message);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// 3. Test V2 Payment Page
|
|
1143
|
+
console.log('\n🔹 Step 3: Testing V2 Payment Page Generation...');
|
|
1144
|
+
try {
|
|
1145
|
+
const v2Result = await service.initiatePaymentPage({
|
|
1146
|
+
...commonData,
|
|
1147
|
+
description: 'V2 Page Test',
|
|
1148
|
+
// V2 specific metadata
|
|
1149
|
+
metadata: [
|
|
1150
|
+
{ fieldName: "product_id", fieldValue: "PROD-999" },
|
|
1151
|
+
{ fieldName: "email", fieldValue: "user@test.com", isPII: true }
|
|
1152
|
+
]
|
|
1153
|
+
}, 'v2');
|
|
1154
|
+
|
|
1155
|
+
if (v2Result.success) {
|
|
1156
|
+
console.log('✅ V2 Success!');
|
|
1157
|
+
console.log(' Deposit ID:', v2Result.depositId);
|
|
1158
|
+
console.log(' 👉 CLICK TO PAY:', v2Result.redirectUrl);
|
|
1159
|
+
} else {
|
|
1160
|
+
console.error('❌ V2 Failed:', v2Result.error);
|
|
1161
|
+
}
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
console.error('❌ V2 Exception:', error.message);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
console.log('\n🏁 Payment Page Testing Complete');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Run test if called directly
|
|
1170
|
+
if (require.main === module) {
|
|
1171
|
+
testPaymentPage().catch(console.error);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
module.exports = { testPaymentPage };
|
|
1175
|
+
```
|
|
1176
|
+
---
|
|
1177
|
+
|
|
1178
|
+
### Payout Senario
|
|
1179
|
+
|
|
1180
|
+
```typescript
|
|
1181
|
+
|
|
1182
|
+
// test-payout.js
|
|
1183
|
+
const path = require('path');
|
|
1184
|
+
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
|
1185
|
+
|
|
1186
|
+
const PawaPayService = require('./pawapayService');
|
|
1187
|
+
|
|
1188
|
+
async function runPayoutTest() {
|
|
1189
|
+
console.log('\n========================================');
|
|
1190
|
+
console.log('🚀 PAWAPAY SDK PAYOUT TEST (With Status Check)');
|
|
1191
|
+
console.log('========================================\n');
|
|
1192
|
+
|
|
1193
|
+
const service = new PawaPayService({});
|
|
1194
|
+
|
|
1195
|
+
const testData = {
|
|
1196
|
+
amount: "1000",
|
|
1197
|
+
currency: "UGX",
|
|
1198
|
+
recipientMsisdn: "256783456789",
|
|
1199
|
+
mno: "MTN_MOMO_UGA",
|
|
1200
|
+
description: "SDK Payout Test",
|
|
1201
|
+
metadata: [
|
|
1202
|
+
{ fieldName: "test_run", fieldValue: "true" }
|
|
1203
|
+
]
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
const API_VERSION = 'v1';
|
|
1207
|
+
|
|
1208
|
+
console.log(`Initiating Payout [${API_VERSION}]...`);
|
|
1209
|
+
|
|
1210
|
+
try {
|
|
1211
|
+
// --- STEP 1: INITIATE ---
|
|
1212
|
+
const result = await service.payout(testData, API_VERSION);
|
|
1213
|
+
|
|
1214
|
+
if (result.success) {
|
|
1215
|
+
console.log('\n Payout Submitted Successfully!');
|
|
1216
|
+
console.log(` ID: ${result.payoutId}`);
|
|
1217
|
+
|
|
1218
|
+
// --- STEP 2: THE WAIT (Crucial for logical testing) ---
|
|
1219
|
+
console.log('\n⏳ Waiting 5 seconds for Sandbox propagation...');
|
|
1220
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
1221
|
+
|
|
1222
|
+
// --- STEP 3: CHECK STATUS ---
|
|
1223
|
+
console.log('🔍 Checking Payout Status...');
|
|
1224
|
+
|
|
1225
|
+
// We specifically pass 'payout' as the 3rd argument here
|
|
1226
|
+
const statusCheck = await service.checkTransactionStatus(
|
|
1227
|
+
result.payoutId,
|
|
1228
|
+
API_VERSION,
|
|
1229
|
+
'payout'
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
if (statusCheck.success) {
|
|
1233
|
+
const status = statusCheck.status;
|
|
1234
|
+
const failureMsg = statusCheck.data?.failureReason?.failureMessage;
|
|
1235
|
+
|
|
1236
|
+
console.log(`\n FINAL STATUS: [ ${status} ]`);
|
|
1237
|
+
|
|
1238
|
+
if (status === 'COMPLETED') {
|
|
1239
|
+
console.log(' SUCCESS: Money sent.');
|
|
1240
|
+
} else if (status === 'FAILED') {
|
|
1241
|
+
console.log(` FAILED: ${failureMsg || 'Unknown reason'}`);
|
|
1242
|
+
} else {
|
|
1243
|
+
console.log(' PENDING: Still processing.');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// console.log('Debug Data:', JSON.stringify(statusCheck.data, null, 2));
|
|
1247
|
+
} else {
|
|
1248
|
+
console.error(' Could not fetch status:', statusCheck.error);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
} else {
|
|
1252
|
+
console.error('\n Payout Initiation Failed');
|
|
1253
|
+
console.error(`Error: ${result.error}`);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
} catch (e) {
|
|
1257
|
+
console.error(' Script Error:', e);
|
|
1258
|
+
} finally {
|
|
1259
|
+
process.exit(0);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
runPayoutTest();
|
|
1264
|
+
|
|
1265
|
+
```
|
|
1266
|
+
---
|
|
1267
|
+
### Refund Senario
|
|
1268
|
+
|
|
1269
|
+
```typescript
|
|
1270
|
+
|
|
1271
|
+
// test-refund.js
|
|
1272
|
+
const path = require('path');
|
|
1273
|
+
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
|
1274
|
+
|
|
1275
|
+
const PawaPayService = require('./pawapayService');
|
|
1276
|
+
|
|
1277
|
+
async function runRefundTest() {
|
|
1278
|
+
console.log('\n========================================');
|
|
1279
|
+
console.log('💸 PAWAPAY SDK REFUND TEST');
|
|
1280
|
+
console.log('========================================\n');
|
|
1281
|
+
|
|
1282
|
+
const service = new PawaPayService({});
|
|
1283
|
+
|
|
1284
|
+
// DATA FROM YOUR LOGS
|
|
1285
|
+
const EXISTING_DEPOSIT_ID = "e6abef2a-7b54-4d5b-9afb-8ccc831a228b";//test deposit id - example
|
|
1286
|
+
|
|
1287
|
+
// We refund a partial amount to be safe/realistic
|
|
1288
|
+
const testData = {
|
|
1289
|
+
depositId: EXISTING_DEPOSIT_ID,
|
|
1290
|
+
amount: "500", // Refund half of the original 1000
|
|
1291
|
+
currency: "UGX",
|
|
1292
|
+
reason: "Customer requested partial refund",
|
|
1293
|
+
metadata: [
|
|
1294
|
+
{ fieldName: "reason", fieldValue: "sdk_test_script" },
|
|
1295
|
+
{ fieldName: "original_order", fieldValue: "ORD-SDK-TEST" }
|
|
1296
|
+
]
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
// Toggle this to test V1 vs V2
|
|
1300
|
+
const API_VERSION = 'v1';
|
|
1301
|
+
|
|
1302
|
+
console.log(` Initiating Refund [${API_VERSION}] for Deposit: ${EXISTING_DEPOSIT_ID}...`);
|
|
1303
|
+
|
|
1304
|
+
try {
|
|
1305
|
+
// --- STEP 1: INITIATE REFUND ---
|
|
1306
|
+
const result = await service.refund(testData, API_VERSION);
|
|
1307
|
+
|
|
1308
|
+
if (result.success) {
|
|
1309
|
+
console.log('\n Refund Submitted Successfully!');
|
|
1310
|
+
console.log(` Refund ID: ${result.refundId}`);
|
|
1311
|
+
console.log(` Status: ${result.status}`);
|
|
1312
|
+
|
|
1313
|
+
// --- STEP 2: WAIT ---
|
|
1314
|
+
console.log('\n Waiting 5 seconds for processing...');
|
|
1315
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
1316
|
+
|
|
1317
|
+
// --- STEP 3: CHECK STATUS ---
|
|
1318
|
+
console.log(' Checking Final Refund Status...');
|
|
1319
|
+
|
|
1320
|
+
// Critical: Pass 'refund' as the 3rd argument
|
|
1321
|
+
const statusCheck = await service.checkTransactionStatus(
|
|
1322
|
+
result.refundId,
|
|
1323
|
+
API_VERSION,
|
|
1324
|
+
'refund'
|
|
1325
|
+
);
|
|
1326
|
+
|
|
1327
|
+
if (statusCheck.success) {
|
|
1328
|
+
console.log(`\n FINAL STATUS: [ ${statusCheck.status} ]`);
|
|
1329
|
+
console.log('Data:', JSON.stringify(statusCheck.data, null, 2));
|
|
1330
|
+
} else {
|
|
1331
|
+
console.error(' Could not fetch status:', statusCheck.error);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
} else {
|
|
1335
|
+
console.error('\n Refund Initiation Failed');
|
|
1336
|
+
console.error(`Error: ${result.error}`);
|
|
1337
|
+
if (result.rawResponse) {
|
|
1338
|
+
console.error('Raw:', JSON.stringify(result.rawResponse.response || result.rawResponse, null, 2));
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
} catch (e) {
|
|
1343
|
+
console.error(' Script Error:', e);
|
|
1344
|
+
} finally {
|
|
1345
|
+
process.exit(0);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
runRefundTest();
|
|
1350
|
+
|
|
1351
|
+
```
|
|
1352
|
+
|
|
210
1353
|
---
|
|
211
1354
|
|
|
212
1355
|
### MNO Configuration & Version Switching
|
|
@@ -230,6 +1373,128 @@ You may toggle **V1 or V2** behavior through these configuration files.
|
|
|
230
1373
|
> **Important**
|
|
231
1374
|
> You should configure a cron job to periodically refresh these files to ensure MNO availability and configuration data remain current.
|
|
232
1375
|
|
|
1376
|
+
Here is a sample code too using the installed SDK
|
|
1377
|
+
|
|
1378
|
+
```typescript
|
|
1379
|
+
|
|
1380
|
+
/**
|
|
1381
|
+
* pawapayFetchConfig.js
|
|
1382
|
+
* * AUTOMATED CONFIGURATION UPDATER
|
|
1383
|
+
* Uses the PawaPayService to fetch the latest MNO definitions and Active Configurations
|
|
1384
|
+
* from the Sandbox environment and updates the static JSON files.
|
|
1385
|
+
*
|
|
1386
|
+
*/
|
|
1387
|
+
|
|
1388
|
+
const fs = require('fs');
|
|
1389
|
+
const path = require('path');
|
|
1390
|
+
|
|
1391
|
+
// 🔒 HARD LOCK THE WORKING DIRECTORY (CRON-PROOF)
|
|
1392
|
+
process.chdir('path/to/nodejs/dir');
|
|
1393
|
+
|
|
1394
|
+
// Load environment variables explicitly
|
|
1395
|
+
require('dotenv').config({
|
|
1396
|
+
path: 'path/to/nodejs/dir/.env'
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
// (Optional debug – safe to remove once satisfied)
|
|
1400
|
+
// console.log({
|
|
1401
|
+
// cwd: process.cwd(),
|
|
1402
|
+
// home: process.env.HOME
|
|
1403
|
+
// });
|
|
1404
|
+
|
|
1405
|
+
// Load PawaPay service AFTER cwd + env are stable
|
|
1406
|
+
const PawaPayService = require('../pawapayService');
|
|
1407
|
+
|
|
1408
|
+
// CONFIGURATION
|
|
1409
|
+
// Use __dirname to ensure data directory is script-relative
|
|
1410
|
+
const OUTPUT_DIR = path.join(__dirname, '../data');
|
|
1411
|
+
|
|
1412
|
+
// Ensure directory exists
|
|
1413
|
+
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
1414
|
+
console.log(`[Config] Directory not found, creating: ${OUTPUT_DIR}`);
|
|
1415
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Initialize Service (Automatically loads ENV tokens and Sandbox mode)
|
|
1419
|
+
const service = new PawaPayService();
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Helper to write JSON files surgically
|
|
1423
|
+
*/
|
|
1424
|
+
const saveJson = (filename, data) => {
|
|
1425
|
+
const filePath = path.join(OUTPUT_DIR, filename);
|
|
1426
|
+
try {
|
|
1427
|
+
const content = JSON.stringify(data, null, 2);
|
|
1428
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
1429
|
+
console.log(` [SUCCESS] Updated: ${filename} (${content.length} bytes)`);
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
console.error(` [ERROR] Failed to write ${filename}:`, err.message);
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Main Execution Function
|
|
1437
|
+
*/
|
|
1438
|
+
const updateConfigurations = async () => {
|
|
1439
|
+
console.log(' [Config] Starting PawaPay Configuration Update (Sandbox)...');
|
|
1440
|
+
console.log(`TB: ${OUTPUT_DIR}`);
|
|
1441
|
+
|
|
1442
|
+
try {
|
|
1443
|
+
// ==========================================
|
|
1444
|
+
// 1. FETCH V1 CONFIGURATIONS
|
|
1445
|
+
// ==========================================
|
|
1446
|
+
|
|
1447
|
+
console.log(' [V1] Fetching Active Conf...');
|
|
1448
|
+
const activeConfV1 = await service.pawapay.checkActiveConf();
|
|
1449
|
+
if (activeConfV1.status === 200) {
|
|
1450
|
+
saveJson('active_conf_v1.json', activeConfV1.response);
|
|
1451
|
+
} else {
|
|
1452
|
+
console.error(` [V1] Active Conf Failed: ${activeConfV1.status}`);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
console.log(' [V1] Fetching MNO Availability...');
|
|
1456
|
+
const availabilityV1 = await service.pawapay.checkMNOAvailability();
|
|
1457
|
+
if (availabilityV1.status === 200) {
|
|
1458
|
+
saveJson('mno_availability_v1.json', availabilityV1.response);
|
|
1459
|
+
} else {
|
|
1460
|
+
console.error(` [V1] Availability Failed: ${availabilityV1.status}`);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// ==========================================
|
|
1464
|
+
// 2. FETCH V2 CONFIGURATIONS
|
|
1465
|
+
// ==========================================
|
|
1466
|
+
|
|
1467
|
+
console.log(' [V2] Fetching Active Conf...');
|
|
1468
|
+
const activeConfV2 = await service.pawapay.checkActiveConfV2();
|
|
1469
|
+
if (activeConfV2.status === 200) {
|
|
1470
|
+
saveJson('active_conf_v2.json', activeConfV2.response);
|
|
1471
|
+
} else {
|
|
1472
|
+
console.error(` [V2] Active Conf Failed: ${activeConfV2.status}`);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
console.log(' [V2] Fetching MNO Availability...');
|
|
1476
|
+
const availabilityV2 = await service.pawapay.checkMNOAvailabilityV2();
|
|
1477
|
+
if (availabilityV2.status === 200) {
|
|
1478
|
+
saveJson('mno_availability_v2.json', availabilityV2.response);
|
|
1479
|
+
} else {
|
|
1480
|
+
console.error(` [V2] Availability Failed: ${availabilityV2.status}`);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
console.log(' [DONE] All configurations updated successfully.');
|
|
1484
|
+
process.exit(0);
|
|
1485
|
+
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
console.error(' [CRITICAL] Script failed:', error.message);
|
|
1488
|
+
process.exit(1);
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1492
|
+
// Run
|
|
1493
|
+
updateConfigurations();
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
```
|
|
1497
|
+
|
|
233
1498
|
This approach keeps runtime fast, predictable, and independent of unnecessary API calls.
|
|
234
1499
|
|
|
235
1500
|
## Support
|