pesafy 0.5.3 → 0.5.4
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/CHANGELOG.md +6 -0
- package/README.md +0 -1
- package/dist/adapters/express.d.ts +120 -63
- package/dist/adapters/express.js +1 -1
- package/dist/index.d.ts +180 -89
- package/dist/index.js +2 -2
- package/dist/signature-verifier.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
[](https://www.typescriptlang.org/)
|
|
12
12
|
[](https://github.com/levos-snr/pesafy/actions/workflows/ci.yml)
|
|
13
13
|
[](https://codecov.io/github/levos-snr/pesafy)
|
|
14
|
-
[](https://www.npmjs.com/package/pesafy)
|
|
15
14
|
|
|
16
15
|
---
|
|
17
16
|
|
|
@@ -489,82 +489,49 @@ type B2BExpressCheckoutCallback = B2BExpressCheckoutCallbackSuccess | B2BExpress
|
|
|
489
489
|
//#endregion
|
|
490
490
|
//#region src/mpesa/b2c/types.d.ts
|
|
491
491
|
/**
|
|
492
|
-
*
|
|
492
|
+
* src/mpesa/b2c/types.ts
|
|
493
493
|
*
|
|
494
|
-
* API
|
|
494
|
+
* Strictly aligned with Safaricom Daraja B2C Account Top Up API documentation.
|
|
495
|
+
* Endpoint: POST /mpesa/b2b/v1/paymentrequest
|
|
495
496
|
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
* for bulk disbursement (salaries, commissions, winnings, etc.)
|
|
499
|
-
* 2. Standard B2C — pay directly to a customer's M-PESA wallet
|
|
500
|
-
*
|
|
501
|
-
* NOTE: This is an ASYNCHRONOUS API.
|
|
502
|
-
* The synchronous response only confirms Safaricom received the request.
|
|
503
|
-
* The actual result arrives later via POST to your ResultURL.
|
|
504
|
-
*
|
|
505
|
-
* Ref: B2C Account Top Up — Daraja Developer Portal
|
|
497
|
+
* Only CommandID = "BusinessPayToBulk" is documented and supported.
|
|
498
|
+
* SenderIdentifierType and RecieverIdentifierType are always "4" per docs.
|
|
506
499
|
*/
|
|
507
500
|
/**
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
* BusinessPayment — Unsecured business payment to a customer (e.g. winnings)
|
|
511
|
-
* SalaryPayment — Disbursement of salaries to customers
|
|
512
|
-
* PromotionPayment — Payment of promotions/bonuses
|
|
513
|
-
* BusinessPayToBulk — Load funds to a B2C shortcode for bulk disbursement
|
|
501
|
+
* The only CommandID supported by the B2C Account Top Up API.
|
|
502
|
+
* Docs: "Use BusinessPayToBulk only"
|
|
514
503
|
*/
|
|
515
|
-
type B2CCommandID = '
|
|
504
|
+
type B2CCommandID = 'BusinessPayToBulk';
|
|
516
505
|
interface B2CRequest {
|
|
517
506
|
/**
|
|
518
|
-
*
|
|
519
|
-
* For direct customer payments use: "BusinessPayment", "SalaryPayment",
|
|
520
|
-
* or "PromotionPayment".
|
|
507
|
+
* Transaction type. Must be "BusinessPayToBulk".
|
|
521
508
|
* Daraja field: CommandID
|
|
522
509
|
*/
|
|
523
510
|
commandId: B2CCommandID;
|
|
524
511
|
/**
|
|
525
|
-
*
|
|
526
|
-
* Daraja field: Amount
|
|
512
|
+
* Transaction amount in KES. Must be a whole number ≥ 1.
|
|
513
|
+
* Daraja field: Amount (sent as string per API spec)
|
|
527
514
|
*/
|
|
528
515
|
amount: number;
|
|
529
516
|
/**
|
|
530
|
-
*
|
|
531
|
-
* This is the PartyA (debit party).
|
|
517
|
+
* Sender shortcode — the originating business shortcode.
|
|
532
518
|
* Daraja field: PartyA
|
|
519
|
+
* SenderIdentifierType is always "4" (Organisation ShortCode) per docs.
|
|
533
520
|
*/
|
|
534
521
|
partyA: string;
|
|
535
522
|
/**
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
* For BusinessPayToBulk:
|
|
539
|
-
* - The B2C shortcode to which money is moved (e.g. "600000")
|
|
540
|
-
*
|
|
541
|
-
* For BusinessPayment / SalaryPayment / PromotionPayment:
|
|
542
|
-
* - The customer's M-PESA phone number (254XXXXXXXXX format)
|
|
543
|
-
*
|
|
523
|
+
* Receiver shortcode — the B2C shortcode that receives the funds.
|
|
544
524
|
* Daraja field: PartyB
|
|
525
|
+
* RecieverIdentifierType is always "4" (Organisation ShortCode) per docs.
|
|
545
526
|
*/
|
|
546
527
|
partyB: string;
|
|
547
528
|
/**
|
|
548
|
-
*
|
|
549
|
-
* For this API, only "4" (Organisation ShortCode) is allowed.
|
|
550
|
-
* Daraja field: SenderIdentifierType
|
|
551
|
-
* Default: "4"
|
|
552
|
-
*/
|
|
553
|
-
senderIdentifierType?: '4';
|
|
554
|
-
/**
|
|
555
|
-
* Type of the receiver (PartyB) identifier.
|
|
556
|
-
* For this API, only "4" (Organisation ShortCode) is allowed.
|
|
557
|
-
* Daraja field: RecieverIdentifierType
|
|
558
|
-
* Default: "4"
|
|
559
|
-
*/
|
|
560
|
-
receiverIdentifierType?: '4';
|
|
561
|
-
/**
|
|
562
|
-
* A reference for this transaction (e.g. invoice number, batch reference).
|
|
529
|
+
* Account reference for the transaction (e.g. invoice or batch number).
|
|
563
530
|
* Daraja field: AccountReference
|
|
564
531
|
*/
|
|
565
532
|
accountReference: string;
|
|
566
533
|
/**
|
|
567
|
-
* Optional.
|
|
534
|
+
* Optional. Customer phone number on whose behalf the transfer is made.
|
|
568
535
|
* Format: 254XXXXXXXXX
|
|
569
536
|
* Daraja field: Requester
|
|
570
537
|
*/
|
|
@@ -575,7 +542,7 @@ interface B2CRequest {
|
|
|
575
542
|
*/
|
|
576
543
|
remarks?: string;
|
|
577
544
|
/**
|
|
578
|
-
* URL
|
|
545
|
+
* URL Safaricom calls with the final async result after processing.
|
|
579
546
|
* Must be publicly accessible. HTTPS required in production.
|
|
580
547
|
* Daraja field: ResultURL
|
|
581
548
|
*/
|
|
@@ -588,32 +555,52 @@ interface B2CRequest {
|
|
|
588
555
|
queueTimeOutUrl: string;
|
|
589
556
|
}
|
|
590
557
|
interface B2CResponse {
|
|
591
|
-
/**
|
|
558
|
+
/**
|
|
559
|
+
* Unique request identifier assigned by Daraja upon successful submission.
|
|
560
|
+
* Daraja field: OriginatorConversationID
|
|
561
|
+
*/
|
|
592
562
|
OriginatorConversationID: string;
|
|
593
|
-
/**
|
|
563
|
+
/**
|
|
564
|
+
* Unique request identifier assigned by M-Pesa.
|
|
565
|
+
* Daraja field: ConversationID
|
|
566
|
+
*/
|
|
594
567
|
ConversationID: string;
|
|
595
|
-
/**
|
|
568
|
+
/**
|
|
569
|
+
* "0" indicates the request was accepted for processing.
|
|
570
|
+
* Daraja field: ResponseCode
|
|
571
|
+
*/
|
|
596
572
|
ResponseCode: string;
|
|
597
|
-
/**
|
|
573
|
+
/**
|
|
574
|
+
* Human-readable submission status description.
|
|
575
|
+
* Daraja field: ResponseDescription
|
|
576
|
+
*/
|
|
598
577
|
ResponseDescription: string;
|
|
599
578
|
}
|
|
600
579
|
/**
|
|
601
|
-
*
|
|
580
|
+
* Documented result parameter keys for B2C Account Top Up transactions.
|
|
581
|
+
* Source: Safaricom Daraja B2C Account Top Up — Successful Result Parameters.
|
|
602
582
|
*
|
|
603
|
-
* `(string & {})` is used as
|
|
604
|
-
* -
|
|
605
|
-
* -
|
|
606
|
-
* - The `no-redundant-type-constituents` ESLint rule is not triggered.
|
|
583
|
+
* `(string & {})` is used as a catch-all so:
|
|
584
|
+
* - Named literals appear in IntelliSense/autocomplete.
|
|
585
|
+
* - Future undocumented keys from Daraja are still accepted at runtime.
|
|
607
586
|
*/
|
|
608
|
-
type B2CResultParameterKey = 'DebitAccountBalance' | 'Amount' | '
|
|
587
|
+
type B2CResultParameterKey = 'DebitAccountBalance' | 'Amount' | 'Currency' | 'ReceiverPartyPublicName' | 'TransactionCompletedTime' | 'DebitPartyCharges' | (string & {});
|
|
609
588
|
interface B2CResultParameter {
|
|
610
589
|
Key: B2CResultParameterKey;
|
|
611
590
|
Value: string | number;
|
|
612
591
|
}
|
|
613
592
|
interface B2CResult {
|
|
614
593
|
Result: {
|
|
615
|
-
/**
|
|
616
|
-
|
|
594
|
+
/**
|
|
595
|
+
* Usually "0" for success. Docs show "0" (string) on success,
|
|
596
|
+
* numeric (e.g. 2001) on failure — typed as both for safety.
|
|
597
|
+
*/
|
|
598
|
+
ResultType: string | number;
|
|
599
|
+
/**
|
|
600
|
+
* "0" or 0 = success; non-zero = failure.
|
|
601
|
+
* Docs show string "0" on success, number 2001 on failure.
|
|
602
|
+
*/
|
|
603
|
+
ResultCode: string | number; /** Human-readable result description */
|
|
617
604
|
ResultDesc: string;
|
|
618
605
|
OriginatorConversationID: string;
|
|
619
606
|
ConversationID: string;
|
|
@@ -1242,6 +1229,75 @@ interface TaxRemittanceResult {
|
|
|
1242
1229
|
};
|
|
1243
1230
|
}
|
|
1244
1231
|
//#endregion
|
|
1232
|
+
//#region src/mpesa/b2c-disbursement/types.d.ts
|
|
1233
|
+
/**
|
|
1234
|
+
* src/mpesa/b2c-disbursement/types.ts
|
|
1235
|
+
|
|
1236
|
+
// ── Command IDs ───────────────────────────────────────────────────────────────
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Supported CommandIDs for B2C Disbursement.
|
|
1240
|
+
* Source: Safaricom Daraja B2C API documentation.
|
|
1241
|
+
*/
|
|
1242
|
+
type B2CDisbursementCommandID = 'BusinessPayment' | 'SalaryPayment' | 'PromotionPayment';
|
|
1243
|
+
interface B2CDisbursementRequest {
|
|
1244
|
+
/**
|
|
1245
|
+
* Unique request ID from the merchant.
|
|
1246
|
+
* Daraja field: OriginatorConversationID
|
|
1247
|
+
*/
|
|
1248
|
+
originatorConversationId: string;
|
|
1249
|
+
/**
|
|
1250
|
+
* Transaction type.
|
|
1251
|
+
* Daraja field: CommandID
|
|
1252
|
+
*/
|
|
1253
|
+
commandId: B2CDisbursementCommandID;
|
|
1254
|
+
/**
|
|
1255
|
+
* Transaction amount in KES.
|
|
1256
|
+
* Daraja field: Amount
|
|
1257
|
+
*/
|
|
1258
|
+
amount: number;
|
|
1259
|
+
/**
|
|
1260
|
+
* Sending organisation shortcode.
|
|
1261
|
+
* Daraja field: PartyA
|
|
1262
|
+
*/
|
|
1263
|
+
partyA: string;
|
|
1264
|
+
/**
|
|
1265
|
+
* Receiving customer MSISDN (2547XXXXXXXX).
|
|
1266
|
+
* Daraja field: PartyB
|
|
1267
|
+
*/
|
|
1268
|
+
partyB: string;
|
|
1269
|
+
/**
|
|
1270
|
+
* Additional transaction info (2–100 characters).
|
|
1271
|
+
* Daraja field: Remarks
|
|
1272
|
+
*/
|
|
1273
|
+
remarks: string;
|
|
1274
|
+
/**
|
|
1275
|
+
* URL Safaricom calls with the async result.
|
|
1276
|
+
* Daraja field: ResultURL
|
|
1277
|
+
*/
|
|
1278
|
+
resultUrl: string;
|
|
1279
|
+
/**
|
|
1280
|
+
* URL Safaricom calls on queue timeout.
|
|
1281
|
+
* Daraja field: QueueTimeOutURL
|
|
1282
|
+
*/
|
|
1283
|
+
queueTimeOutUrl: string;
|
|
1284
|
+
/**
|
|
1285
|
+
* Optional additional info.
|
|
1286
|
+
* Daraja field: Occassion (sic — preserved from Daraja docs)
|
|
1287
|
+
*/
|
|
1288
|
+
occasion?: string;
|
|
1289
|
+
}
|
|
1290
|
+
interface B2CDisbursementResponse {
|
|
1291
|
+
/** Unique request ID assigned by M-Pesa */
|
|
1292
|
+
ConversationID: string;
|
|
1293
|
+
/** Merchant-supplied request ID echoed back */
|
|
1294
|
+
OriginatorConversationID: string;
|
|
1295
|
+
/** "0" = accepted */
|
|
1296
|
+
ResponseCode: string;
|
|
1297
|
+
/** Human-readable submission status */
|
|
1298
|
+
ResponseDescription: string;
|
|
1299
|
+
}
|
|
1300
|
+
//#endregion
|
|
1245
1301
|
//#region src/mpesa/index.d.ts
|
|
1246
1302
|
declare class Mpesa {
|
|
1247
1303
|
private readonly config;
|
|
@@ -1296,6 +1352,7 @@ declare class Mpesa {
|
|
|
1296
1352
|
remitTax(request: TaxRemittanceRequest): Promise<TaxRemittanceResponse>;
|
|
1297
1353
|
b2bExpressCheckout(request: B2BExpressCheckoutRequest): Promise<B2BExpressCheckoutResponse>;
|
|
1298
1354
|
b2cPayment(request: B2CRequest): Promise<B2CResponse>;
|
|
1355
|
+
b2cDisbursement(request: B2CDisbursementRequest): Promise<B2CDisbursementResponse>;
|
|
1299
1356
|
/**
|
|
1300
1357
|
* Opts in a shortcode for Bill Manager.
|
|
1301
1358
|
*
|
package/dist/adapters/express.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i as e,n as t,r as n}from"../signature-verifier.js";import{a as r,i,n as a,r as o,t as s}from"../webhook-handler.js";const c={SUCCESS:`0`,CANCELLED:`4001`,KYC_FAIL:`4102`,NO_NOMINATED_NUMBER:`4104`,USSD_NETWORK_ERROR:`4201`,USSD_EXCEPTION_ERROR:`4203`};new Set(Object.values(c));function l(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.resultCode==`string`&&typeof t.requestId==`string`&&typeof t.amount==`string`}function u(e){return e.resultCode===c.SUCCESS}function d(e){return e.resultCode===c.CANCELLED}function f(e){return Number(e.amount)}function p(e){return u(e)?e.transactionId??null:null}function m(e){return u(e)?e.conversationID??null:null}function h(e){if(!e||typeof e!=`object`)return!1;let t=e;if(!t.Result||typeof t.Result!=`object`)return!1;let n=t.Result;return typeof n.ResultCode==`number`&&typeof n.ConversationID==`string`}function g(e){return e.Result.ResultCode===0}function _(e){return e.Result.TransactionID??null}function v(e){return e.Result.ConversationID}function y(e){return e.Result.OriginatorConversationID}function b(e){let t=x(e,`Amount`);return t===void 0?null:Number(t)}function x(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function S(e){return{ResultCode:`0`,ResultDesc:`Accepted`,...e?{ThirdPartyTransID:e}:{}}}function C(t){if(!t.consumerKey||!t.consumerSecret)throw new e({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required`});if(!t.lipaNaMpesaShortCode||!t.lipaNaMpesaPassKey)throw new e({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push`});if(!t.callbackUrl)throw new e({code:`VALIDATION_ERROR`,message:`callbackUrl is required for STK Push callbacks`});return{mpesa:new n(t)}}function w(t,n){if(n instanceof e){let e=n.statusCode??400;t.status(e).json({error:n.code,message:n.message,statusCode:e});return}t.status(500).json({error:`REQUEST_FAILED`,message:`An unexpected error occurred while processing the M-Pesa request`})}function T(e){return e.headers[`x-forwarded-for`]?.split(`,`)[0]?.trim()??e.ip??``}function E(n,c){let{mpesa:x}=C(c);return n.post(`/mpesa/express/stk-push`,async(t,n,r)=>{try{let r=t.body;if(!r||typeof r.amount!=`number`||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});let i=await x.stkPush({amount:r.amount,phoneNumber:r.phoneNumber,callbackUrl:c.callbackUrl,accountReference:r.accountReference??`PESAFY-${Date.now().toString(36).toUpperCase()}`,transactionDesc:r.transactionDesc??`Payment`,...r.transactionType===void 0?{}:{transactionType:r.transactionType},...r.partyB===void 0?{}:{partyB:r.partyB}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/express/stk-query`,async(t,n,r)=>{try{let r=t.body;if(!r?.checkoutRequestId)throw new e({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});let i=await x.stkQuery({checkoutRequestId:r.checkoutRequestId});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/express/callback`,(e,t)=>{let n=T(e),l=i(e.body,{requestIP:n,...c.skipIPCheck===void 0?{}:{skipIPCheck:c.skipIPCheck}});if(!l.success)return console.error(`[pesafy] STK Push webhook rejected:`,l.error),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let u=l.data;return r(u)?console.info(`[pesafy] STK Push success:`,{receiptNumber:o(u),amount:s(u),phone:a(u)}):console.warn(`[pesafy] STK Push failed:`,{resultCode:u.Body.stkCallback.ResultCode,resultDesc:u.Body.stkCallback.ResultDesc}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/transaction-status/query`,async(t,n,r)=>{try{if(!c.resultUrl||!c.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl must be set in config to use transaction status routes`});let r=t.body;if(!r?.transactionId)throw new e({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!r.partyA)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required`});if(!r.identifierType)throw new e({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" | "2" | "4"`});let i=await x.transactionStatus({transactionId:r.transactionId,partyA:r.partyA,identifierType:r.identifierType,resultUrl:c.resultUrl,queueTimeOutUrl:c.queueTimeOutUrl,...r.remarks===void 0?{}:{remarks:r.remarks},...r.occasion===void 0?{}:{occasion:r.occasion}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/transaction-status/result`,(e,t)=>{let n=e.body?.Result;n&&(n.ResultCode===0?console.info(`[pesafy] Transaction Status result (success):`,{transactionId:n.TransactionID,conversationId:n.ConversationID,resultDesc:n.ResultDesc}):console.warn(`[pesafy] Transaction Status result (failed):`,{resultCode:n.ResultCode,resultDesc:n.ResultDesc,transactionId:n.TransactionID})),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/c2b/register-url`,async(t,n,r)=>{try{let r=t.body,i=r.shortCode??c.c2bShortCode,a=r.confirmationUrl??c.c2bConfirmationUrl,o=r.validationUrl??c.c2bValidationUrl,s=r.responseType??c.c2bResponseType??`Completed`,l=r.apiVersion??c.c2bApiVersion??`v2`;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!a)throw new e({code:`VALIDATION_ERROR`,message:`confirmationUrl is required`});if(!o)throw new e({code:`VALIDATION_ERROR`,message:`validationUrl is required`});let u=await x.registerC2BUrls({shortCode:i,responseType:s,confirmationUrl:a,validationUrl:o,apiVersion:l});n.status(200).json(u)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/c2b/simulate`,async(t,n,r)=>{try{let r=t.body;if(!r?.commandId)throw new e({code:`VALIDATION_ERROR`,message:`commandId is required: "CustomerPayBillOnline" | "CustomerBuyGoodsOnline"`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.msisdn)throw new e({code:`VALIDATION_ERROR`,message:`msisdn is required`});let i=await x.simulateC2B({shortCode:r.shortCode??c.c2bShortCode??``,commandId:r.commandId,amount:r.amount,msisdn:r.msisdn,apiVersion:c.c2bApiVersion??`v2`,...r.billRefNumber===void 0?{}:{billRefNumber:r.billRefNumber}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/c2b/validation`,async(e,n)=>{if(!c.skipIPCheck){let r=T(e);if(!t(r))return console.error(`[pesafy] C2B validation rejected — IP not in Safaricom whitelist:`,r),n.status(200).json({ResultCode:`0`,ResultDesc:`Accepted`})}let r=e.body;try{let e;return e=c.onC2BValidation?await c.onC2BValidation(r):S(),console.info(`[pesafy] C2B validation response:`,{transactionId:r.TransID,amount:r.TransAmount,billRef:r.BillRefNumber,resultCode:e.ResultCode}),n.status(200).json(e)}catch(e){return console.error(`[pesafy] C2B validation hook threw an error:`,e),n.status(200).json({ResultCode:`0`,ResultDesc:`Accepted`})}}),n.post(`/mpesa/c2b/confirmation`,(e,t)=>{let n=e.body;console.info(`[pesafy] C2B confirmation received:`,{transactionId:n.TransID,amount:n.TransAmount,shortCode:n.BusinessShortCode,billRef:n.BillRefNumber,transactionType:n.TransactionType,transTime:n.TransTime,balance:n.OrgAccountBalance}),c.onC2BConfirmation&&Promise.resolve(c.onC2BConfirmation(n)).catch(e=>{console.error(`[pesafy] C2B confirmation hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Success`})}),n.post(`/mpesa/tax/remit`,async(t,n,r)=>{try{if(!c.taxResultUrl||!c.taxQueueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`taxResultUrl and taxQueueTimeOutUrl must be set in config to use tax remittance routes`});let r=t.body;if(!r||typeof r.amount!=`number`||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.accountReference)throw new e({code:`VALIDATION_ERROR`,message:`accountReference is required — the KRA PRN`});let i=r.partyA??c.taxPartyA??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required — set taxPartyA in config or provide in request body`});let a=await x.remitTax({amount:r.amount,partyA:i,accountReference:r.accountReference,resultUrl:c.taxResultUrl,queueTimeOutUrl:c.taxQueueTimeOutUrl,...r.remarks===void 0?{}:{remarks:r.remarks}});n.status(200).json(a)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/tax/result`,(e,t)=>{let n=e.body,r=n?.Result;r&&(r.ResultCode===0?console.info(`[pesafy] Tax Remittance result (success):`,{transactionId:r.TransactionID,conversationId:r.ConversationID,resultDesc:r.ResultDesc}):console.warn(`[pesafy] Tax Remittance result (failed):`,{resultCode:r.ResultCode,resultDesc:r.ResultDesc,transactionId:r.TransactionID})),c.onTaxRemittanceResult&&n&&Promise.resolve(c.onTaxRemittanceResult(n)).catch(e=>{console.error(`[pesafy] Tax Remittance result hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/b2b/checkout`,async(t,n,r)=>{try{let r=t.body;if(!r?.primaryShortCode)throw new e({code:`VALIDATION_ERROR`,message:`primaryShortCode is required`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.paymentRef)throw new e({code:`VALIDATION_ERROR`,message:`paymentRef is required`});if(!r.partnerName)throw new e({code:`VALIDATION_ERROR`,message:`partnerName is required`});let i=r.receiverShortCode??c.b2bReceiverShortCode??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — set b2bReceiverShortCode in config or provide in request body`});let a=r.callbackUrl??c.b2bCallbackUrl??``;if(!a)throw new e({code:`VALIDATION_ERROR`,message:`callbackUrl is required — set b2bCallbackUrl in config or provide in request body`});let o=await x.b2bExpressCheckout({primaryShortCode:r.primaryShortCode,receiverShortCode:i,amount:r.amount,paymentRef:r.paymentRef,callbackUrl:a,partnerName:r.partnerName,...r.requestRefId===void 0?{}:{requestRefId:r.requestRefId}});n.status(200).json(o)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/b2b/callback`,(e,t)=>{let n=e.body;if(!l(n))return console.error(`[pesafy] B2B callback received unrecognised payload:`,JSON.stringify(n).slice(0,200)),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let r=n;return u(r)?console.info(`[pesafy] B2B Express Checkout success:`,{transactionId:p(r),conversationId:m(r),amount:f(r),requestId:r.requestId,status:r.status}):d(r)?console.warn(`[pesafy] B2B Express Checkout cancelled by merchant:`,{resultCode:r.resultCode,resultDesc:r.resultDesc,requestId:r.requestId,amount:f(r)}):console.warn(`[pesafy] B2B Express Checkout unknown result:`,{resultCode:r.resultCode,resultDesc:r.resultDesc}),c.onB2BCheckoutCallback&&Promise.resolve(c.onB2BCheckoutCallback(r)).catch(e=>{console.error(`[pesafy] B2B callback hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/b2c/payment`,async(t,n,r)=>{try{if(!c.b2cResultUrl||!c.b2cQueueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`b2cResultUrl and b2cQueueTimeOutUrl must be set in config to use B2C routes`});let r=t.body;if(!r?.commandId)throw new e({code:`VALIDATION_ERROR`,message:`commandId is required: "BusinessPayToBulk" | "BusinessPayment" | "SalaryPayment" | "PromotionPayment"`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.partyB)throw new e({code:`VALIDATION_ERROR`,message:`partyB is required — the recipient shortcode (BusinessPayToBulk) or customer MSISDN`});if(!r.accountReference)throw new e({code:`VALIDATION_ERROR`,message:`accountReference is required`});let i=r.partyA??c.b2cPartyA??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required — set b2cPartyA in config or provide in request body`});let a=await x.b2cPayment({commandId:r.commandId,amount:r.amount,partyA:i,partyB:r.partyB,accountReference:r.accountReference,resultUrl:r.resultUrl??c.b2cResultUrl,queueTimeOutUrl:r.queueTimeOutUrl??c.b2cQueueTimeOutUrl,...r.requester===void 0?{}:{requester:r.requester},...r.remarks===void 0?{}:{remarks:r.remarks}});n.status(200).json(a)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/b2c/result`,(e,t)=>{let n=e.body;if(!h(n))return console.error(`[pesafy] B2C result received unrecognised payload:`,JSON.stringify(n).slice(0,200)),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let r=n;return g(r)?console.info(`[pesafy] B2C payment result (success):`,{transactionId:_(r),conversationId:v(r),originatorConversationId:y(r),amount:b(r),resultDesc:r.Result.ResultDesc}):console.warn(`[pesafy] B2C payment result (failed):`,{resultCode:r.Result.ResultCode,resultDesc:r.Result.ResultDesc,transactionId:r.Result.TransactionID,conversationId:v(r),originatorConversationId:y(r)}),c.onB2CResult&&Promise.resolve(c.onB2CResult(r)).catch(e=>{console.error(`[pesafy] B2C result hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n}export{C as createMpesaExpressClient,E as createMpesaExpressRouter};
|
|
1
|
+
import{i as e,n as t,r as n}from"../signature-verifier.js";import{a as r,i,n as a,r as o,t as s}from"../webhook-handler.js";const c={SUCCESS:`0`,CANCELLED:`4001`,KYC_FAIL:`4102`,NO_NOMINATED_NUMBER:`4104`,USSD_NETWORK_ERROR:`4201`,USSD_EXCEPTION_ERROR:`4203`};new Set(Object.values(c));function l(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.resultCode==`string`&&typeof t.requestId==`string`&&typeof t.amount==`string`}function u(e){return e.resultCode===c.SUCCESS}function d(e){return e.resultCode===c.CANCELLED}function f(e){return Number(e.amount)}function p(e){return u(e)?e.transactionId??null:null}function m(e){return u(e)?e.conversationID??null:null}function h(e){if(!e||typeof e!=`object`)return!1;let t=e;if(!t.Result||typeof t.Result!=`object`)return!1;let n=t.Result;return(typeof n.ResultCode==`number`||typeof n.ResultCode==`string`)&&typeof n.ConversationID==`string`&&typeof n.OriginatorConversationID==`string`}function g(e){let t=e.Result.ResultCode;return t===0||t===`0`}function _(e){return e.Result.TransactionID??null}function v(e){return e.Result.ConversationID}function y(e){return e.Result.OriginatorConversationID}function b(e){let t=x(e,`Amount`);if(t===void 0)return null;let n=Number(t);return Number.isFinite(n)?n:null}function x(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function S(e){return{ResultCode:`0`,ResultDesc:`Accepted`,...e?{ThirdPartyTransID:e}:{}}}function C(t){if(!t.consumerKey||!t.consumerSecret)throw new e({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required`});if(!t.lipaNaMpesaShortCode||!t.lipaNaMpesaPassKey)throw new e({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push`});if(!t.callbackUrl)throw new e({code:`VALIDATION_ERROR`,message:`callbackUrl is required for STK Push callbacks`});return{mpesa:new n(t)}}function w(t,n){if(n instanceof e){let e=n.statusCode??400;t.status(e).json({error:n.code,message:n.message,statusCode:e});return}t.status(500).json({error:`REQUEST_FAILED`,message:`An unexpected error occurred while processing the M-Pesa request`})}function T(e){return e.headers[`x-forwarded-for`]?.split(`,`)[0]?.trim()??e.ip??``}function E(n,c){let{mpesa:x}=C(c);return n.post(`/mpesa/express/stk-push`,async(t,n,r)=>{try{let r=t.body;if(!r||typeof r.amount!=`number`||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});let i=await x.stkPush({amount:r.amount,phoneNumber:r.phoneNumber,callbackUrl:c.callbackUrl,accountReference:r.accountReference??`PESAFY-${Date.now().toString(36).toUpperCase()}`,transactionDesc:r.transactionDesc??`Payment`,...r.transactionType===void 0?{}:{transactionType:r.transactionType},...r.partyB===void 0?{}:{partyB:r.partyB}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/express/stk-query`,async(t,n,r)=>{try{let r=t.body;if(!r?.checkoutRequestId)throw new e({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});let i=await x.stkQuery({checkoutRequestId:r.checkoutRequestId});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/express/callback`,(e,t)=>{let n=T(e),l=i(e.body,{requestIP:n,...c.skipIPCheck===void 0?{}:{skipIPCheck:c.skipIPCheck}});if(!l.success)return console.error(`[pesafy] STK Push webhook rejected:`,l.error),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let u=l.data;return r(u)?console.info(`[pesafy] STK Push success:`,{receiptNumber:o(u),amount:s(u),phone:a(u)}):console.warn(`[pesafy] STK Push failed:`,{resultCode:u.Body.stkCallback.ResultCode,resultDesc:u.Body.stkCallback.ResultDesc}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/transaction-status/query`,async(t,n,r)=>{try{if(!c.resultUrl||!c.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl must be set in config to use transaction status routes`});let r=t.body;if(!r?.transactionId)throw new e({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!r.partyA)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required`});if(!r.identifierType)throw new e({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" | "2" | "4"`});let i=await x.transactionStatus({transactionId:r.transactionId,partyA:r.partyA,identifierType:r.identifierType,resultUrl:c.resultUrl,queueTimeOutUrl:c.queueTimeOutUrl,...r.remarks===void 0?{}:{remarks:r.remarks},...r.occasion===void 0?{}:{occasion:r.occasion}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/transaction-status/result`,(e,t)=>{let n=e.body?.Result;n&&(n.ResultCode===0?console.info(`[pesafy] Transaction Status result (success):`,{transactionId:n.TransactionID,conversationId:n.ConversationID,resultDesc:n.ResultDesc}):console.warn(`[pesafy] Transaction Status result (failed):`,{resultCode:n.ResultCode,resultDesc:n.ResultDesc,transactionId:n.TransactionID})),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/c2b/register-url`,async(t,n,r)=>{try{let r=t.body,i=r.shortCode??c.c2bShortCode,a=r.confirmationUrl??c.c2bConfirmationUrl,o=r.validationUrl??c.c2bValidationUrl,s=r.responseType??c.c2bResponseType??`Completed`,l=r.apiVersion??c.c2bApiVersion??`v2`;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!a)throw new e({code:`VALIDATION_ERROR`,message:`confirmationUrl is required`});if(!o)throw new e({code:`VALIDATION_ERROR`,message:`validationUrl is required`});let u=await x.registerC2BUrls({shortCode:i,responseType:s,confirmationUrl:a,validationUrl:o,apiVersion:l});n.status(200).json(u)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/c2b/simulate`,async(t,n,r)=>{try{let r=t.body;if(!r?.commandId)throw new e({code:`VALIDATION_ERROR`,message:`commandId is required: "CustomerPayBillOnline" | "CustomerBuyGoodsOnline"`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.msisdn)throw new e({code:`VALIDATION_ERROR`,message:`msisdn is required`});let i=await x.simulateC2B({shortCode:r.shortCode??c.c2bShortCode??``,commandId:r.commandId,amount:r.amount,msisdn:r.msisdn,apiVersion:c.c2bApiVersion??`v2`,...r.billRefNumber===void 0?{}:{billRefNumber:r.billRefNumber}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/c2b/validation`,async(e,n)=>{if(!c.skipIPCheck){let r=T(e);if(!t(r))return console.error(`[pesafy] C2B validation rejected — IP not in Safaricom whitelist:`,r),n.status(200).json({ResultCode:`0`,ResultDesc:`Accepted`})}let r=e.body;try{let e;return e=c.onC2BValidation?await c.onC2BValidation(r):S(),console.info(`[pesafy] C2B validation response:`,{transactionId:r.TransID,amount:r.TransAmount,billRef:r.BillRefNumber,resultCode:e.ResultCode}),n.status(200).json(e)}catch(e){return console.error(`[pesafy] C2B validation hook threw an error:`,e),n.status(200).json({ResultCode:`0`,ResultDesc:`Accepted`})}}),n.post(`/mpesa/c2b/confirmation`,(e,t)=>{let n=e.body;console.info(`[pesafy] C2B confirmation received:`,{transactionId:n.TransID,amount:n.TransAmount,shortCode:n.BusinessShortCode,billRef:n.BillRefNumber,transactionType:n.TransactionType,transTime:n.TransTime,balance:n.OrgAccountBalance}),c.onC2BConfirmation&&Promise.resolve(c.onC2BConfirmation(n)).catch(e=>{console.error(`[pesafy] C2B confirmation hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Success`})}),n.post(`/mpesa/tax/remit`,async(t,n,r)=>{try{if(!c.taxResultUrl||!c.taxQueueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`taxResultUrl and taxQueueTimeOutUrl must be set in config to use tax remittance routes`});let r=t.body;if(!r||typeof r.amount!=`number`||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.accountReference)throw new e({code:`VALIDATION_ERROR`,message:`accountReference is required — the KRA PRN`});let i=r.partyA??c.taxPartyA??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required — set taxPartyA in config or provide in request body`});let a=await x.remitTax({amount:r.amount,partyA:i,accountReference:r.accountReference,resultUrl:c.taxResultUrl,queueTimeOutUrl:c.taxQueueTimeOutUrl,...r.remarks===void 0?{}:{remarks:r.remarks}});n.status(200).json(a)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/tax/result`,(e,t)=>{let n=e.body,r=n?.Result;r&&(r.ResultCode===0?console.info(`[pesafy] Tax Remittance result (success):`,{transactionId:r.TransactionID,conversationId:r.ConversationID,resultDesc:r.ResultDesc}):console.warn(`[pesafy] Tax Remittance result (failed):`,{resultCode:r.ResultCode,resultDesc:r.ResultDesc,transactionId:r.TransactionID})),c.onTaxRemittanceResult&&n&&Promise.resolve(c.onTaxRemittanceResult(n)).catch(e=>{console.error(`[pesafy] Tax Remittance result hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/b2b/checkout`,async(t,n,r)=>{try{let r=t.body;if(!r?.primaryShortCode)throw new e({code:`VALIDATION_ERROR`,message:`primaryShortCode is required`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.paymentRef)throw new e({code:`VALIDATION_ERROR`,message:`paymentRef is required`});if(!r.partnerName)throw new e({code:`VALIDATION_ERROR`,message:`partnerName is required`});let i=r.receiverShortCode??c.b2bReceiverShortCode??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — set b2bReceiverShortCode in config or provide in request body`});let a=r.callbackUrl??c.b2bCallbackUrl??``;if(!a)throw new e({code:`VALIDATION_ERROR`,message:`callbackUrl is required — set b2bCallbackUrl in config or provide in request body`});let o=await x.b2bExpressCheckout({primaryShortCode:r.primaryShortCode,receiverShortCode:i,amount:r.amount,paymentRef:r.paymentRef,callbackUrl:a,partnerName:r.partnerName,...r.requestRefId===void 0?{}:{requestRefId:r.requestRefId}});n.status(200).json(o)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/b2b/callback`,(e,t)=>{let n=e.body;if(!l(n))return console.error(`[pesafy] B2B callback received unrecognised payload:`,JSON.stringify(n).slice(0,200)),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let r=n;return u(r)?console.info(`[pesafy] B2B Express Checkout success:`,{transactionId:p(r),conversationId:m(r),amount:f(r),requestId:r.requestId,status:r.status}):d(r)?console.warn(`[pesafy] B2B Express Checkout cancelled by merchant:`,{resultCode:r.resultCode,resultDesc:r.resultDesc,requestId:r.requestId,amount:f(r)}):console.warn(`[pesafy] B2B Express Checkout unknown result:`,{resultCode:r.resultCode,resultDesc:r.resultDesc}),c.onB2BCheckoutCallback&&Promise.resolve(c.onB2BCheckoutCallback(r)).catch(e=>{console.error(`[pesafy] B2B callback hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/b2c/payment`,async(t,n,r)=>{try{if(!c.b2cResultUrl||!c.b2cQueueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`b2cResultUrl and b2cQueueTimeOutUrl must be set in config to use B2C routes`});let r=t.body;if(!r?.commandId)throw new e({code:`VALIDATION_ERROR`,message:`commandId is required: must be "BusinessPayToBulk"`});if(r.commandId!==`BusinessPayToBulk`)throw new e({code:`VALIDATION_ERROR`,message:`commandId must be "BusinessPayToBulk" — the only CommandID supported by the B2C Account Top Up API`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.partyB)throw new e({code:`VALIDATION_ERROR`,message:`partyB is required — the receiver B2C shortcode`});if(!r.accountReference)throw new e({code:`VALIDATION_ERROR`,message:`accountReference is required`});let i=r.partyA??c.b2cPartyA??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required — set b2cPartyA in config or provide in request body`});let a=await x.b2cPayment({commandId:r.commandId,amount:r.amount,partyA:i,partyB:r.partyB,accountReference:r.accountReference,resultUrl:r.resultUrl??c.b2cResultUrl,queueTimeOutUrl:r.queueTimeOutUrl??c.b2cQueueTimeOutUrl,...r.requester===void 0?{}:{requester:r.requester},...r.remarks===void 0?{}:{remarks:r.remarks}});n.status(200).json(a)}catch(e){if(n.headersSent)return r(e);w(n,e)}}),n.post(`/mpesa/b2c/result`,(e,t)=>{let n=e.body;if(!h(n))return console.error(`[pesafy] B2C result received unrecognised payload:`,JSON.stringify(n).slice(0,200)),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let r=n;return g(r)?console.info(`[pesafy] B2C payment result (success):`,{transactionId:_(r),conversationId:v(r),originatorConversationId:y(r),amount:b(r),resultDesc:r.Result.ResultDesc}):console.warn(`[pesafy] B2C payment result (failed):`,{resultCode:r.Result.ResultCode,resultDesc:r.Result.ResultDesc,transactionId:r.Result.TransactionID,conversationId:v(r),originatorConversationId:y(r)}),c.onB2CResult&&Promise.resolve(c.onB2CResult(r)).catch(e=>{console.error(`[pesafy] B2C result hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n}export{C as createMpesaExpressClient,E as createMpesaExpressRouter};
|
package/dist/index.d.ts
CHANGED
|
@@ -885,82 +885,49 @@ declare function getB2BConversationId(callback: B2BExpressCheckoutCallback): str
|
|
|
885
885
|
//#endregion
|
|
886
886
|
//#region src/mpesa/b2c/types.d.ts
|
|
887
887
|
/**
|
|
888
|
-
*
|
|
888
|
+
* src/mpesa/b2c/types.ts
|
|
889
889
|
*
|
|
890
|
-
* API
|
|
890
|
+
* Strictly aligned with Safaricom Daraja B2C Account Top Up API documentation.
|
|
891
|
+
* Endpoint: POST /mpesa/b2b/v1/paymentrequest
|
|
891
892
|
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
* for bulk disbursement (salaries, commissions, winnings, etc.)
|
|
895
|
-
* 2. Standard B2C — pay directly to a customer's M-PESA wallet
|
|
896
|
-
*
|
|
897
|
-
* NOTE: This is an ASYNCHRONOUS API.
|
|
898
|
-
* The synchronous response only confirms Safaricom received the request.
|
|
899
|
-
* The actual result arrives later via POST to your ResultURL.
|
|
900
|
-
*
|
|
901
|
-
* Ref: B2C Account Top Up — Daraja Developer Portal
|
|
893
|
+
* Only CommandID = "BusinessPayToBulk" is documented and supported.
|
|
894
|
+
* SenderIdentifierType and RecieverIdentifierType are always "4" per docs.
|
|
902
895
|
*/
|
|
903
896
|
/**
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
* BusinessPayment — Unsecured business payment to a customer (e.g. winnings)
|
|
907
|
-
* SalaryPayment — Disbursement of salaries to customers
|
|
908
|
-
* PromotionPayment — Payment of promotions/bonuses
|
|
909
|
-
* BusinessPayToBulk — Load funds to a B2C shortcode for bulk disbursement
|
|
897
|
+
* The only CommandID supported by the B2C Account Top Up API.
|
|
898
|
+
* Docs: "Use BusinessPayToBulk only"
|
|
910
899
|
*/
|
|
911
|
-
type B2CCommandID = '
|
|
900
|
+
type B2CCommandID = 'BusinessPayToBulk';
|
|
912
901
|
interface B2CRequest {
|
|
913
902
|
/**
|
|
914
|
-
*
|
|
915
|
-
* For direct customer payments use: "BusinessPayment", "SalaryPayment",
|
|
916
|
-
* or "PromotionPayment".
|
|
903
|
+
* Transaction type. Must be "BusinessPayToBulk".
|
|
917
904
|
* Daraja field: CommandID
|
|
918
905
|
*/
|
|
919
906
|
commandId: B2CCommandID;
|
|
920
907
|
/**
|
|
921
|
-
*
|
|
922
|
-
* Daraja field: Amount
|
|
908
|
+
* Transaction amount in KES. Must be a whole number ≥ 1.
|
|
909
|
+
* Daraja field: Amount (sent as string per API spec)
|
|
923
910
|
*/
|
|
924
911
|
amount: number;
|
|
925
912
|
/**
|
|
926
|
-
*
|
|
927
|
-
* This is the PartyA (debit party).
|
|
913
|
+
* Sender shortcode — the originating business shortcode.
|
|
928
914
|
* Daraja field: PartyA
|
|
915
|
+
* SenderIdentifierType is always "4" (Organisation ShortCode) per docs.
|
|
929
916
|
*/
|
|
930
917
|
partyA: string;
|
|
931
918
|
/**
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
* For BusinessPayToBulk:
|
|
935
|
-
* - The B2C shortcode to which money is moved (e.g. "600000")
|
|
936
|
-
*
|
|
937
|
-
* For BusinessPayment / SalaryPayment / PromotionPayment:
|
|
938
|
-
* - The customer's M-PESA phone number (254XXXXXXXXX format)
|
|
939
|
-
*
|
|
919
|
+
* Receiver shortcode — the B2C shortcode that receives the funds.
|
|
940
920
|
* Daraja field: PartyB
|
|
921
|
+
* RecieverIdentifierType is always "4" (Organisation ShortCode) per docs.
|
|
941
922
|
*/
|
|
942
923
|
partyB: string;
|
|
943
924
|
/**
|
|
944
|
-
*
|
|
945
|
-
* For this API, only "4" (Organisation ShortCode) is allowed.
|
|
946
|
-
* Daraja field: SenderIdentifierType
|
|
947
|
-
* Default: "4"
|
|
948
|
-
*/
|
|
949
|
-
senderIdentifierType?: '4';
|
|
950
|
-
/**
|
|
951
|
-
* Type of the receiver (PartyB) identifier.
|
|
952
|
-
* For this API, only "4" (Organisation ShortCode) is allowed.
|
|
953
|
-
* Daraja field: RecieverIdentifierType
|
|
954
|
-
* Default: "4"
|
|
955
|
-
*/
|
|
956
|
-
receiverIdentifierType?: '4';
|
|
957
|
-
/**
|
|
958
|
-
* A reference for this transaction (e.g. invoice number, batch reference).
|
|
925
|
+
* Account reference for the transaction (e.g. invoice or batch number).
|
|
959
926
|
* Daraja field: AccountReference
|
|
960
927
|
*/
|
|
961
928
|
accountReference: string;
|
|
962
929
|
/**
|
|
963
|
-
* Optional.
|
|
930
|
+
* Optional. Customer phone number on whose behalf the transfer is made.
|
|
964
931
|
* Format: 254XXXXXXXXX
|
|
965
932
|
* Daraja field: Requester
|
|
966
933
|
*/
|
|
@@ -971,7 +938,7 @@ interface B2CRequest {
|
|
|
971
938
|
*/
|
|
972
939
|
remarks?: string;
|
|
973
940
|
/**
|
|
974
|
-
* URL
|
|
941
|
+
* URL Safaricom calls with the final async result after processing.
|
|
975
942
|
* Must be publicly accessible. HTTPS required in production.
|
|
976
943
|
* Daraja field: ResultURL
|
|
977
944
|
*/
|
|
@@ -984,32 +951,52 @@ interface B2CRequest {
|
|
|
984
951
|
queueTimeOutUrl: string;
|
|
985
952
|
}
|
|
986
953
|
interface B2CResponse {
|
|
987
|
-
/**
|
|
954
|
+
/**
|
|
955
|
+
* Unique request identifier assigned by Daraja upon successful submission.
|
|
956
|
+
* Daraja field: OriginatorConversationID
|
|
957
|
+
*/
|
|
988
958
|
OriginatorConversationID: string;
|
|
989
|
-
/**
|
|
959
|
+
/**
|
|
960
|
+
* Unique request identifier assigned by M-Pesa.
|
|
961
|
+
* Daraja field: ConversationID
|
|
962
|
+
*/
|
|
990
963
|
ConversationID: string;
|
|
991
|
-
/**
|
|
964
|
+
/**
|
|
965
|
+
* "0" indicates the request was accepted for processing.
|
|
966
|
+
* Daraja field: ResponseCode
|
|
967
|
+
*/
|
|
992
968
|
ResponseCode: string;
|
|
993
|
-
/**
|
|
969
|
+
/**
|
|
970
|
+
* Human-readable submission status description.
|
|
971
|
+
* Daraja field: ResponseDescription
|
|
972
|
+
*/
|
|
994
973
|
ResponseDescription: string;
|
|
995
974
|
}
|
|
996
975
|
/**
|
|
997
|
-
*
|
|
976
|
+
* Documented result parameter keys for B2C Account Top Up transactions.
|
|
977
|
+
* Source: Safaricom Daraja B2C Account Top Up — Successful Result Parameters.
|
|
998
978
|
*
|
|
999
|
-
* `(string & {})` is used as
|
|
1000
|
-
* -
|
|
1001
|
-
* -
|
|
1002
|
-
* - The `no-redundant-type-constituents` ESLint rule is not triggered.
|
|
979
|
+
* `(string & {})` is used as a catch-all so:
|
|
980
|
+
* - Named literals appear in IntelliSense/autocomplete.
|
|
981
|
+
* - Future undocumented keys from Daraja are still accepted at runtime.
|
|
1003
982
|
*/
|
|
1004
|
-
type B2CResultParameterKey = 'DebitAccountBalance' | 'Amount' | '
|
|
983
|
+
type B2CResultParameterKey = 'DebitAccountBalance' | 'Amount' | 'Currency' | 'ReceiverPartyPublicName' | 'TransactionCompletedTime' | 'DebitPartyCharges' | (string & {});
|
|
1005
984
|
interface B2CResultParameter {
|
|
1006
985
|
Key: B2CResultParameterKey;
|
|
1007
986
|
Value: string | number;
|
|
1008
987
|
}
|
|
1009
988
|
interface B2CResult {
|
|
1010
989
|
Result: {
|
|
1011
|
-
/**
|
|
1012
|
-
|
|
990
|
+
/**
|
|
991
|
+
* Usually "0" for success. Docs show "0" (string) on success,
|
|
992
|
+
* numeric (e.g. 2001) on failure — typed as both for safety.
|
|
993
|
+
*/
|
|
994
|
+
ResultType: string | number;
|
|
995
|
+
/**
|
|
996
|
+
* "0" or 0 = success; non-zero = failure.
|
|
997
|
+
* Docs show string "0" on success, number 2001 on failure.
|
|
998
|
+
*/
|
|
999
|
+
ResultCode: string | number; /** Human-readable result description */
|
|
1013
1000
|
ResultDesc: string;
|
|
1014
1001
|
OriginatorConversationID: string;
|
|
1015
1002
|
ConversationID: string;
|
|
@@ -1028,6 +1015,20 @@ interface B2CResult {
|
|
|
1028
1015
|
};
|
|
1029
1016
|
};
|
|
1030
1017
|
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Documented Daraja error codes for the B2C Account Top Up API.
|
|
1020
|
+
*/
|
|
1021
|
+
declare const B2C_ERROR_CODES: {
|
|
1022
|
+
/** Internal server error */readonly INTERNAL_SERVER_ERROR: "500.003.1001"; /** Invalid or expired access token */
|
|
1023
|
+
readonly INVALID_ACCESS_TOKEN: "400.003.01"; /** Bad request — missing or malformed data */
|
|
1024
|
+
readonly BAD_REQUEST: "400.003.02"; /** Quota violation — too many requests */
|
|
1025
|
+
readonly QUOTA_VIOLATION: "500.003.03"; /** Spike arrest violation — server not responding */
|
|
1026
|
+
readonly SPIKE_ARREST: "500.003.02"; /** Resource not found — wrong endpoint */
|
|
1027
|
+
readonly NOT_FOUND: "404.003.01"; /** Invalid authentication header — wrong HTTP method */
|
|
1028
|
+
readonly INVALID_AUTH_HEADER: "404.001.04"; /** Invalid request payload — incorrect JSON format */
|
|
1029
|
+
readonly INVALID_PAYLOAD: "400.002.05";
|
|
1030
|
+
};
|
|
1031
|
+
type B2CErrorCode = (typeof B2C_ERROR_CODES)[keyof typeof B2C_ERROR_CODES];
|
|
1031
1032
|
interface B2CErrorResponse {
|
|
1032
1033
|
/** Unique request ID assigned by the API gateway */
|
|
1033
1034
|
requestId: string;
|
|
@@ -1036,36 +1037,55 @@ interface B2CErrorResponse {
|
|
|
1036
1037
|
/** Human-readable error message */
|
|
1037
1038
|
errorMessage: string;
|
|
1038
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Known B2C result codes documented by Safaricom Daraja.
|
|
1042
|
+
*/
|
|
1043
|
+
declare const B2C_RESULT_CODES: {
|
|
1044
|
+
/** Transaction processed successfully */readonly SUCCESS: 0; /** Initiator information is invalid */
|
|
1045
|
+
readonly INVALID_INITIATOR: 2001;
|
|
1046
|
+
};
|
|
1047
|
+
type B2CResultCode = (typeof B2C_RESULT_CODES)[keyof typeof B2C_RESULT_CODES];
|
|
1039
1048
|
//#endregion
|
|
1040
1049
|
//#region src/mpesa/b2c/payment.d.ts
|
|
1041
1050
|
/**
|
|
1042
|
-
* Initiates a B2C
|
|
1051
|
+
* Initiates a B2C Account Top Up payment request.
|
|
1043
1052
|
*
|
|
1044
|
-
* @param baseUrl
|
|
1045
|
-
* @param accessToken
|
|
1053
|
+
* @param baseUrl - Daraja base URL (sandbox or production)
|
|
1054
|
+
* @param accessToken - Valid OAuth Bearer token
|
|
1046
1055
|
* @param securityCredential - RSA-encrypted initiator password (base64)
|
|
1047
|
-
* @param initiatorName
|
|
1048
|
-
* @param request
|
|
1049
|
-
* @returns
|
|
1056
|
+
* @param initiatorName - M-Pesa API operator username with B2B role
|
|
1057
|
+
* @param request - B2C top-up request parameters
|
|
1058
|
+
* @returns Synchronous acknowledgement response from Daraja
|
|
1059
|
+
* @throws {PesafyError} VALIDATION_ERROR for invalid input before HTTP call
|
|
1060
|
+
* @throws {PesafyError} From httpRequest on network / API errors
|
|
1050
1061
|
*/
|
|
1051
1062
|
declare function initiateB2CPayment(baseUrl: string, accessToken: string, securityCredential: string, initiatorName: string, request: B2CRequest): Promise<B2CResponse>;
|
|
1052
1063
|
//#endregion
|
|
1053
1064
|
//#region src/mpesa/b2c/webhooks.d.ts
|
|
1054
1065
|
/**
|
|
1055
1066
|
* Runtime type guard — checks if a body looks like a B2C result callback.
|
|
1067
|
+
* Validates the minimum documented structure.
|
|
1056
1068
|
*/
|
|
1057
1069
|
declare function isB2CResult(body: unknown): body is B2CResult;
|
|
1058
1070
|
/**
|
|
1059
1071
|
* Returns true if the B2C result represents a successful transaction.
|
|
1072
|
+
* Handles both string "0" (documented in success sample) and number 0.
|
|
1060
1073
|
*/
|
|
1061
1074
|
declare function isB2CSuccess(result: B2CResult): boolean;
|
|
1062
1075
|
/**
|
|
1063
1076
|
* Returns true if the B2C result represents a failure.
|
|
1077
|
+
* Handles both string "0" (documented in success sample) and number 0.
|
|
1064
1078
|
*/
|
|
1065
1079
|
declare function isB2CFailure(result: B2CResult): boolean;
|
|
1080
|
+
/**
|
|
1081
|
+
* Returns true if the result code matches a known documented code.
|
|
1082
|
+
* Empty strings are explicitly rejected — Number('') coerces to 0 which
|
|
1083
|
+
* would otherwise incorrectly match B2C_RESULT_CODES.SUCCESS.
|
|
1084
|
+
*/
|
|
1085
|
+
declare function isKnownB2CResultCode(code: unknown): boolean;
|
|
1066
1086
|
/**
|
|
1067
1087
|
* Extracts the M-PESA transaction ID from a B2C result.
|
|
1068
|
-
*
|
|
1088
|
+
* Present on both success and failure (generic ID on failure).
|
|
1069
1089
|
*/
|
|
1070
1090
|
declare function getB2CTransactionId(result: B2CResult): string | null;
|
|
1071
1091
|
/**
|
|
@@ -1082,45 +1102,46 @@ declare function getB2COriginatorConversationId(result: B2CResult): string;
|
|
|
1082
1102
|
*/
|
|
1083
1103
|
declare function getB2CResultDesc(result: B2CResult): string;
|
|
1084
1104
|
/**
|
|
1085
|
-
* Extracts the transaction amount from
|
|
1105
|
+
* Extracts the transaction amount from B2C result parameters.
|
|
1106
|
+
* Documented field: "Amount"
|
|
1086
1107
|
* Returns null if not present (e.g. on failure).
|
|
1087
1108
|
*/
|
|
1088
1109
|
declare function getB2CAmount(result: B2CResult): number | null;
|
|
1089
1110
|
/**
|
|
1090
|
-
* Extracts the transaction
|
|
1091
|
-
*
|
|
1092
|
-
* Returns
|
|
1111
|
+
* Extracts the transaction currency from B2C result parameters.
|
|
1112
|
+
* Documented field: "Currency"
|
|
1113
|
+
* Returns "KES" as default when not present.
|
|
1093
1114
|
*/
|
|
1094
|
-
declare function
|
|
1095
|
-
/**
|
|
1096
|
-
* Extracts the debit party charges from B2C result parameters.
|
|
1097
|
-
* Returns null if not present or empty.
|
|
1098
|
-
*/
|
|
1099
|
-
declare function getB2CDebitPartyCharges(result: B2CResult): string | null;
|
|
1115
|
+
declare function getB2CCurrency(result: B2CResult): string;
|
|
1100
1116
|
/**
|
|
1101
1117
|
* Extracts the receiver's public name from B2C result parameters.
|
|
1118
|
+
* Documented field: "ReceiverPartyPublicName"
|
|
1102
1119
|
* Returns null if not present.
|
|
1103
1120
|
*/
|
|
1104
1121
|
declare function getB2CReceiverPublicName(result: B2CResult): string | null;
|
|
1105
1122
|
/**
|
|
1106
|
-
* Extracts the
|
|
1107
|
-
*
|
|
1123
|
+
* Extracts the transaction completion timestamp from B2C result parameters.
|
|
1124
|
+
* Documented field: "TransactionCompletedTime" — format: YYYYMMDDHHmmss
|
|
1125
|
+
* Returns null if not present.
|
|
1108
1126
|
*/
|
|
1109
|
-
declare function
|
|
1127
|
+
declare function getB2CTransactionCompletedTime(result: B2CResult): string | null;
|
|
1110
1128
|
/**
|
|
1111
1129
|
* Extracts the debit account balance from B2C result parameters.
|
|
1130
|
+
* Documented field: "DebitAccountBalance" (e.g. "{CurrencyCode=KES}")
|
|
1112
1131
|
* Returns null if not present.
|
|
1113
1132
|
*/
|
|
1114
1133
|
declare function getB2CDebitAccountBalance(result: B2CResult): string | null;
|
|
1115
1134
|
/**
|
|
1116
|
-
* Extracts the
|
|
1117
|
-
*
|
|
1135
|
+
* Extracts the debit party charges from B2C result parameters.
|
|
1136
|
+
* Documented field: "DebitPartyCharges"
|
|
1137
|
+
* Returns null if not present or empty.
|
|
1118
1138
|
*/
|
|
1119
|
-
declare function
|
|
1139
|
+
declare function getB2CDebitPartyCharges(result: B2CResult): string | null;
|
|
1120
1140
|
/**
|
|
1121
1141
|
* Extracts a named value from B2C result parameters.
|
|
1122
|
-
* Handles both single-object and array forms of ResultParameter
|
|
1123
|
-
*
|
|
1142
|
+
* Handles both single-object and array forms of ResultParameter
|
|
1143
|
+
* (Daraja returns either depending on how many parameters are present).
|
|
1144
|
+
* Returns undefined if key is absent or no ResultParameters exist.
|
|
1124
1145
|
*/
|
|
1125
1146
|
declare function getB2CResultParam(result: B2CResult, key: string): string | number | undefined;
|
|
1126
1147
|
//#endregion
|
|
@@ -1917,6 +1938,75 @@ declare const TAX_COMMAND_ID = "PayTaxToKRA";
|
|
|
1917
1938
|
*/
|
|
1918
1939
|
declare function remitTax(baseUrl: string, accessToken: string, securityCredential: string, initiatorName: string, request: TaxRemittanceRequest): Promise<TaxRemittanceResponse>;
|
|
1919
1940
|
//#endregion
|
|
1941
|
+
//#region src/mpesa/b2c-disbursement/types.d.ts
|
|
1942
|
+
/**
|
|
1943
|
+
* src/mpesa/b2c-disbursement/types.ts
|
|
1944
|
+
|
|
1945
|
+
// ── Command IDs ───────────────────────────────────────────────────────────────
|
|
1946
|
+
|
|
1947
|
+
/**
|
|
1948
|
+
* Supported CommandIDs for B2C Disbursement.
|
|
1949
|
+
* Source: Safaricom Daraja B2C API documentation.
|
|
1950
|
+
*/
|
|
1951
|
+
type B2CDisbursementCommandID = 'BusinessPayment' | 'SalaryPayment' | 'PromotionPayment';
|
|
1952
|
+
interface B2CDisbursementRequest {
|
|
1953
|
+
/**
|
|
1954
|
+
* Unique request ID from the merchant.
|
|
1955
|
+
* Daraja field: OriginatorConversationID
|
|
1956
|
+
*/
|
|
1957
|
+
originatorConversationId: string;
|
|
1958
|
+
/**
|
|
1959
|
+
* Transaction type.
|
|
1960
|
+
* Daraja field: CommandID
|
|
1961
|
+
*/
|
|
1962
|
+
commandId: B2CDisbursementCommandID;
|
|
1963
|
+
/**
|
|
1964
|
+
* Transaction amount in KES.
|
|
1965
|
+
* Daraja field: Amount
|
|
1966
|
+
*/
|
|
1967
|
+
amount: number;
|
|
1968
|
+
/**
|
|
1969
|
+
* Sending organisation shortcode.
|
|
1970
|
+
* Daraja field: PartyA
|
|
1971
|
+
*/
|
|
1972
|
+
partyA: string;
|
|
1973
|
+
/**
|
|
1974
|
+
* Receiving customer MSISDN (2547XXXXXXXX).
|
|
1975
|
+
* Daraja field: PartyB
|
|
1976
|
+
*/
|
|
1977
|
+
partyB: string;
|
|
1978
|
+
/**
|
|
1979
|
+
* Additional transaction info (2–100 characters).
|
|
1980
|
+
* Daraja field: Remarks
|
|
1981
|
+
*/
|
|
1982
|
+
remarks: string;
|
|
1983
|
+
/**
|
|
1984
|
+
* URL Safaricom calls with the async result.
|
|
1985
|
+
* Daraja field: ResultURL
|
|
1986
|
+
*/
|
|
1987
|
+
resultUrl: string;
|
|
1988
|
+
/**
|
|
1989
|
+
* URL Safaricom calls on queue timeout.
|
|
1990
|
+
* Daraja field: QueueTimeOutURL
|
|
1991
|
+
*/
|
|
1992
|
+
queueTimeOutUrl: string;
|
|
1993
|
+
/**
|
|
1994
|
+
* Optional additional info.
|
|
1995
|
+
* Daraja field: Occassion (sic — preserved from Daraja docs)
|
|
1996
|
+
*/
|
|
1997
|
+
occasion?: string;
|
|
1998
|
+
}
|
|
1999
|
+
interface B2CDisbursementResponse {
|
|
2000
|
+
/** Unique request ID assigned by M-Pesa */
|
|
2001
|
+
ConversationID: string;
|
|
2002
|
+
/** Merchant-supplied request ID echoed back */
|
|
2003
|
+
OriginatorConversationID: string;
|
|
2004
|
+
/** "0" = accepted */
|
|
2005
|
+
ResponseCode: string;
|
|
2006
|
+
/** Human-readable submission status */
|
|
2007
|
+
ResponseDescription: string;
|
|
2008
|
+
}
|
|
2009
|
+
//#endregion
|
|
1920
2010
|
//#region src/mpesa/index.d.ts
|
|
1921
2011
|
declare class Mpesa {
|
|
1922
2012
|
private readonly config;
|
|
@@ -1971,6 +2061,7 @@ declare class Mpesa {
|
|
|
1971
2061
|
remitTax(request: TaxRemittanceRequest): Promise<TaxRemittanceResponse>;
|
|
1972
2062
|
b2bExpressCheckout(request: B2BExpressCheckoutRequest): Promise<B2BExpressCheckoutResponse>;
|
|
1973
2063
|
b2cPayment(request: B2CRequest): Promise<B2CResponse>;
|
|
2064
|
+
b2cDisbursement(request: B2CDisbursementRequest): Promise<B2CDisbursementResponse>;
|
|
1974
2065
|
/**
|
|
1975
2066
|
* Opts in a shortcode for Bill Manager.
|
|
1976
2067
|
*
|
|
@@ -2168,4 +2259,4 @@ interface HttpResponse<T> {
|
|
|
2168
2259
|
*/
|
|
2169
2260
|
declare function httpRequest<T = unknown>(url: string, options: HttpRequestOptions): Promise<HttpResponse<T>>;
|
|
2170
2261
|
//#endregion
|
|
2171
|
-
export { type AccountBalanceData, type AccountBalanceRequest, type AccountBalanceResponse, type AccountBalanceResult, type B2BExpressCheckoutCallback, type B2BExpressCheckoutCallbackCancelled, type B2BExpressCheckoutCallbackSuccess, type B2BExpressCheckoutErrorCode, type B2BExpressCheckoutErrorResponse, type B2BExpressCheckoutRequest, type B2BExpressCheckoutResponse, type B2CCommandID, type B2CErrorResponse, type B2CRequest, type B2CResponse, type B2CResult, type B2CResultParameter, type BillManagerBulkInvoiceRequest, type BillManagerBulkInvoiceResponse, type BillManagerCancelInvoiceRequest, type BillManagerCancelInvoiceResponse, type BillManagerInvoiceItem, type BillManagerOptInRequest, type BillManagerOptInResponse, type BillManagerPaymentNotification, type BillManagerSingleInvoiceRequest, type BillManagerSingleInvoiceResponse, type C2BApiVersion, type C2BCommandID, type C2BConfirmationAck, type C2BConfirmationPayload, type C2BRegisterUrlRequest, type C2BRegisterUrlResponse, type C2BResponseType, type C2BSimulateRequest, type C2BSimulateResponse, type C2BValidationPayload, type C2BValidationResponse, type C2BValidationResultCode, type CheckoutRequestID, type ConversationID, DARAJA_BASE_URLS, type DeepReadonly, type DynamicQRRequest, type DynamicQRResponse, type Environment, type ErrorCode, type HttpRequestOptions, type HttpResponse, KRA_SHORTCODE, type KesAmount, Mpesa, type MpesaConfig, type MpesaReceiptNumber, type MsisdnKE, type NonEmptyString, type OriginatorConversationID, type ParsedAccount, type PaybillCode, PesafyError, type PesafyErrorOptions, type QRTransactionCode, type Result, type RetryOptions, type RetryResult, type ReversalRequest, type ReversalResponse, type ReversalResult, SAFARICOM_IPS, type ShortCode, type StkCallbackFailure, type StkCallbackInner, type StkCallbackMetadataItem, type StkCallbackSuccess, type StkPushCallback, type StkPushRequest, type StkPushResponse, type StkPushWebhook, type StkQueryRequest, type StkQueryResponse, type StrictPick, TAX_COMMAND_ID, type TaxRemittanceErrorResponse, type TaxRemittanceRequest, type TaxRemittanceResponse, type TaxRemittanceResult, type TaxRemittanceResultParameter, type TillCode, type TransactionStatusRequest, type TransactionStatusResponse, type TransactionStatusResult, type TransactionStatusResultParameter, type TransactionType, type WebhookEvent, type WebhookEventType, type WebhookHandlerOptions, type WebhookHandlerResult, acceptC2BValidation, acknowledgeC2BConfirmation, createError, encryptSecurityCredential, err, extractAmount, extractPhoneNumber, extractTransactionId, formatSafaricomPhone as formatPhoneNumber, formatSafaricomPhone, getAccountBalanceParam, getB2BAmount, getB2BConversationId, getB2BRequestId, getB2BTransactionId, getB2CAmount, getB2CConversationId, getB2CCurrency, getB2CDebitAccountBalance, getB2CDebitPartyCharges,
|
|
2262
|
+
export { type AccountBalanceData, type AccountBalanceRequest, type AccountBalanceResponse, type AccountBalanceResult, type B2BExpressCheckoutCallback, type B2BExpressCheckoutCallbackCancelled, type B2BExpressCheckoutCallbackSuccess, type B2BExpressCheckoutErrorCode, type B2BExpressCheckoutErrorResponse, type B2BExpressCheckoutRequest, type B2BExpressCheckoutResponse, type B2CCommandID, type B2CErrorCode, type B2CErrorResponse, type B2CRequest, type B2CResponse, type B2CResult, type B2CResultCode, type B2CResultParameter, type B2CResultParameterKey, B2C_ERROR_CODES, B2C_RESULT_CODES, type BillManagerBulkInvoiceRequest, type BillManagerBulkInvoiceResponse, type BillManagerCancelInvoiceRequest, type BillManagerCancelInvoiceResponse, type BillManagerInvoiceItem, type BillManagerOptInRequest, type BillManagerOptInResponse, type BillManagerPaymentNotification, type BillManagerSingleInvoiceRequest, type BillManagerSingleInvoiceResponse, type C2BApiVersion, type C2BCommandID, type C2BConfirmationAck, type C2BConfirmationPayload, type C2BRegisterUrlRequest, type C2BRegisterUrlResponse, type C2BResponseType, type C2BSimulateRequest, type C2BSimulateResponse, type C2BValidationPayload, type C2BValidationResponse, type C2BValidationResultCode, type CheckoutRequestID, type ConversationID, DARAJA_BASE_URLS, type DeepReadonly, type DynamicQRRequest, type DynamicQRResponse, type Environment, type ErrorCode, type HttpRequestOptions, type HttpResponse, KRA_SHORTCODE, type KesAmount, Mpesa, type MpesaConfig, type MpesaReceiptNumber, type MsisdnKE, type NonEmptyString, type OriginatorConversationID, type ParsedAccount, type PaybillCode, PesafyError, type PesafyErrorOptions, type QRTransactionCode, type Result, type RetryOptions, type RetryResult, type ReversalRequest, type ReversalResponse, type ReversalResult, SAFARICOM_IPS, type ShortCode, type StkCallbackFailure, type StkCallbackInner, type StkCallbackMetadataItem, type StkCallbackSuccess, type StkPushCallback, type StkPushRequest, type StkPushResponse, type StkPushWebhook, type StkQueryRequest, type StkQueryResponse, type StrictPick, TAX_COMMAND_ID, type TaxRemittanceErrorResponse, type TaxRemittanceRequest, type TaxRemittanceResponse, type TaxRemittanceResult, type TaxRemittanceResultParameter, type TillCode, type TransactionStatusRequest, type TransactionStatusResponse, type TransactionStatusResult, type TransactionStatusResultParameter, type TransactionType, type WebhookEvent, type WebhookEventType, type WebhookHandlerOptions, type WebhookHandlerResult, acceptC2BValidation, acknowledgeC2BConfirmation, createError, encryptSecurityCredential, err, extractAmount, extractPhoneNumber, extractTransactionId, formatSafaricomPhone as formatPhoneNumber, formatSafaricomPhone, getAccountBalanceParam, getB2BAmount, getB2BConversationId, getB2BRequestId, getB2BTransactionId, getB2CAmount, getB2CConversationId, getB2CCurrency, getB2CDebitAccountBalance, getB2CDebitPartyCharges, getB2COriginatorConversationId, getB2CReceiverPublicName, getB2CResultDesc, getB2CResultParam, getB2CTransactionCompletedTime, getB2CTransactionId, getC2BAccountRef, getC2BAmount, getC2BCustomerName, getC2BTransactionId, getCallbackValue, getReversalConversationId, getReversalTransactionId, getTimestamp, handleWebhook, httpRequest, initiateB2BExpressCheckout, initiateB2CPayment, isAccountBalanceSuccess, isB2BCheckoutCallback, isB2BCheckoutCancelled, isB2BCheckoutSuccess, isB2CFailure, isB2CResult, isB2CSuccess, isBuyGoodsPayment, isC2BPayload, isKnownB2CResultCode, isPaybillPayment, isPesafyError, isReversalSuccess, isStkCallbackSuccess, isSuccessfulCallback, ok, parseAccountBalance, parseStkPushWebhook, registerC2BUrls, rejectC2BValidation, remitTax, retryWithBackoff, simulateC2B, toKesAmount, toMsisdn, toNonEmpty, toPaybill, toShortCode, toTill, verifyWebhookIP };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{readFile as e}from"node:fs/promises";import{constants as t,publicEncrypt as n}from"node:crypto";var r=class e extends Error{code;statusCode;response;requestId;cause;retryable;constructor(t){super(t.message),Object.defineProperty(this,`name`,{value:`PesafyError`}),this.code=t.code,this.statusCode=t.statusCode,this.response=t.response,this.requestId=t.requestId,this.cause=t.cause,this.retryable=t.retryable??(t.code===`NETWORK_ERROR`||t.code===`TIMEOUT`||t.code===`RATE_LIMITED`||t.code===`REQUEST_FAILED`),Error.captureStackTrace&&Error.captureStackTrace(this,e)}get isValidation(){return this.code===`VALIDATION_ERROR`}get isAuth(){return this.code===`AUTH_FAILED`||this.code===`INVALID_CREDENTIALS`}toJSON(){return{name:this.name,code:this.code,message:this.message,statusCode:this.statusCode,requestId:this.requestId,retryable:this.retryable}}};function i(e){return new r(e)}function a(e){return e instanceof r}const o=new Set([429,500,502,503,504]);function s(e){return new Promise(t=>setTimeout(t,e))}function c(e){let t=e*.25;return e+(Math.random()*t*2-t)}async function l(e,t){let n=t.retries??4,i=t.retryDelay??2e3,a=t.timeout??3e4,l={"Content-Type":`application/json`,Accept:`application/json`,...t.headers};t.idempotencyKey&&(l[`Idempotency-Key`]=t.idempotencyKey);let u={method:t.method,headers:l,...t.body===void 0?{}:{body:JSON.stringify(t.body)}},d=null;for(let l=0;l<=n;l++){if(l>0){let r=c(i*2**(l-1));console.warn(`[pesafy] Retry ${l}/${n} → ${t.method} ${e} in ${Math.round(r)} ms`),await s(r)}let f=new AbortController,p=setTimeout(()=>f.abort(),a),m;try{m=await fetch(e,{...u,signal:f.signal})}catch(t){if(clearTimeout(p),d=t instanceof Error&&t.name===`AbortError`?new r({code:`TIMEOUT`,message:`Request to ${e} timed out after ${a} ms`,cause:t,retryable:!0}):new r({code:`NETWORK_ERROR`,message:`Network error: ${t instanceof Error?t.message:String(t)}`,cause:t,retryable:!0}),l<n)continue;throw d}finally{clearTimeout(p)}let h=``,g=null,_=m.headers.get(`content-type`)??``;try{h=await m.text(),h&&(g=_.includes(`application/json`)?JSON.parse(h):h)}catch{g=h||null}let v={};if(m.headers.forEach((e,t)=>{v[t]=e}),m.ok)return{data:g,status:m.status,headers:v};let y=o.has(m.status),b=typeof g==`object`&&g?g:{},x=b.errorMessage??b.ResponseDescription??b.resultDesc??h??`HTTP ${m.status}`;if(d=new r({code:y?`REQUEST_FAILED`:`API_ERROR`,message:x,statusCode:m.status,response:g,retryable:y,...typeof b.requestId==`string`?{requestId:b.requestId}:{}}),!(y&&l<n))throw d}throw d}var u=class{consumerKey;consumerSecret;baseUrl;cachedToken=null;tokenExpiresAt=0;constructor(e,t,n){this.consumerKey=e,this.consumerSecret=t,this.baseUrl=n}getBasicAuthHeader(){let e=`${this.consumerKey}:${this.consumerSecret}`;return`Basic ${Buffer.from(e,`utf-8`).toString(`base64`)}`}async getAccessToken(){let e=Date.now()/1e3;if(this.cachedToken&&this.tokenExpiresAt>e+60)return this.cachedToken;let t=await l(`${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`,{method:`GET`,headers:{Authorization:this.getBasicAuthHeader()}}),{access_token:n,expires_in:i}=t.data;if(!n)throw new r({code:`AUTH_FAILED`,message:`Daraja did not return an access token. Check your consumer key and secret.`,response:t.data});return this.cachedToken=n,this.tokenExpiresAt=e+(i??3600),this.cachedToken}clearCache(){this.cachedToken=null,this.tokenExpiresAt=0}};function d(e,i){try{let r=Buffer.from(e,`utf-8`);return n({key:i,padding:t.RSA_PKCS1_PADDING},r).toString(`base64`)}catch(e){throw new r({code:`ENCRYPTION_FAILED`,message:`Failed to encrypt security credential. Ensure the certificate PEM is valid and matches the environment (sandbox/production).`,cause:e})}}function f(e){let t=Math.round(e);if(!Number.isFinite(t)||t<1)throw TypeError(`KesAmount must be a whole number ≥ 1, got ${e}`);return t}function p(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw TypeError(`Cannot normalise "${e}" to 254XXXXXXXXX. Use 07XX…, 2547XX…, or +2547XX….`);if(n.length!==12)throw TypeError(`Phone "${e}" normalised to "${n}" — expected 12 digits.`);return n}function m(e){return String(e)}function h(e){return String(e)}function g(e){return String(e)}function _(e){return{ok:!0,data:e}}function v(e){return{ok:!1,error:e}}function y(e){if(!e.trim())throw TypeError(`String must not be empty`);return e}async function b(e,t,n,r,a){if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required.`});if(![`1`,`2`,`4`].includes(a.identifierType))throw i({code:`VALIDATION_ERROR`,message:`identifierType must be "1" (MSISDN), "2" (Till), or "4" (ShortCode).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let o={Initiator:r,SecurityCredential:n,CommandID:`AccountBalance`,PartyA:String(a.partyA),IdentifierType:a.identifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Account Balance Query`},{data:s}=await l(`${e}/mpesa/accountbalance/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}function x(e){let t=e.split(`|`),n=[];for(let e=0;e+2<t.length;e+=3){let r=t[e]?.trim(),i=t[e+1]?.trim(),a=t[e+2]?.trim();r&&i&&a!==void 0&&n.push({name:r,currency:i,amount:a})}return n}function ee(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function S(e){return e.Result.ResultCode===0}function te(){try{if(typeof crypto<`u`&&typeof crypto.randomUUID==`function`)return crypto.randomUUID()}catch{}return`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`}async function C(e,t,n){if(!n.primaryShortCode||!String(n.primaryShortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`primaryShortCode is required — the merchant's till number (debit party).`});if(!n.receiverShortCode||!String(n.receiverShortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — the vendor's Paybill account (credit party).`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount}).`});if(!n.paymentRef||!String(n.paymentRef).trim())throw i({code:`VALIDATION_ERROR`,message:`paymentRef is required — shown in the merchant's USSD prompt as the payment reference.`});if(!n.callbackUrl||!String(n.callbackUrl).trim())throw i({code:`VALIDATION_ERROR`,message:`callbackUrl is required — Daraja POSTs the transaction result here.`});if(!n.partnerName||!String(n.partnerName).trim())throw i({code:`VALIDATION_ERROR`,message:`partnerName is required — vendor's friendly name shown in the merchant's USSD prompt.`});let a={primaryShortCode:String(n.primaryShortCode),receiverShortCode:String(n.receiverShortCode),amount:String(r),paymentRef:n.paymentRef,callbackUrl:n.callbackUrl,partnerName:n.partnerName,RequestRefID:n.requestRefId??te()},{data:o}=await l(`${e}/v1/ussdpush/get-msisdn`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}const w={SUCCESS:`0`,CANCELLED:`4001`,KYC_FAIL:`4102`,NO_NOMINATED_NUMBER:`4104`,USSD_NETWORK_ERROR:`4201`,USSD_EXCEPTION_ERROR:`4203`};new Set(Object.values(w));function ne(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.resultCode==`string`&&typeof t.requestId==`string`&&typeof t.amount==`string`}function T(e){return e.resultCode===w.SUCCESS}function re(e){return e.resultCode===w.CANCELLED}function ie(e){return e.requestId}function ae(e){return Number(e.amount)}function oe(e){return T(e)?e.transactionId??null:null}function se(e){return T(e)?e.conversationID??null:null}async function E(e,t,n,r,a){if(!a.commandId)throw i({code:`VALIDATION_ERROR`,message:`commandId is required: "BusinessPayToBulk" | "BusinessPayment" | "SalaryPayment" | "PromotionPayment"`});let o=[`BusinessPayToBulk`,`BusinessPayment`,`SalaryPayment`,`PromotionPayment`];if(!o.includes(a.commandId))throw i({code:`VALIDATION_ERROR`,message:`commandId must be one of: ${o.join(`, `)}. Got: "${a.commandId}"`});let s=Math.round(a.amount);if(!Number.isFinite(s)||s<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount} which rounds to ${s}).`});if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required — your business shortcode from which money is deducted.`});if(!a.partyB?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyB is required — the recipient shortcode (BusinessPayToBulk) or customer MSISDN (other commands).`});if(!a.accountReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`accountReference is required — a reference for this transaction.`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the B2C result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let c={Initiator:r,SecurityCredential:n,CommandID:a.commandId,SenderIdentifierType:a.senderIdentifierType??`4`,RecieverIdentifierType:a.receiverIdentifierType??`4`,Amount:String(s),PartyA:String(a.partyA),PartyB:String(a.partyB),AccountReference:a.accountReference,Remarks:a.remarks??`B2C Payment`,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl};a.requester?.trim()&&(c.Requester=String(a.requester));let{data:u}=await l(`${e}/mpesa/b2b/v1/paymentrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:c});return u}function ce(e){if(!e||typeof e!=`object`)return!1;let t=e;if(!t.Result||typeof t.Result!=`object`)return!1;let n=t.Result;return typeof n.ResultCode==`number`&&typeof n.ConversationID==`string`}function le(e){return e.Result.ResultCode===0}function ue(e){return e.Result.ResultCode!==0}function de(e){return e.Result.TransactionID??null}function fe(e){return e.Result.ConversationID}function pe(e){return e.Result.OriginatorConversationID}function me(e){return e.Result.ResultDesc}function he(e){let t=M(e,`Amount`);return t===void 0?null:Number(t)}function ge(e){let t=M(e,`TransCompletedTime`);return t===void 0?null:String(t)}function D(e){let t=M(e,`DebitPartyCharges`);return t===void 0||t===``?null:String(t)}function O(e){let t=M(e,`ReceiverPartyPublicName`);return t===void 0?null:String(t)}function k(e){let t=M(e,`Currency`);return t===void 0?`KES`:String(t)}function A(e){let t=M(e,`DebitAccountBalance`);return t===void 0?null:String(t)}function j(e){let t=M(e,`InitiatorAccountCurrentBalance`);return t===void 0?null:String(t)}function M(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}async function N(e,t,n){if(!n.shortcode?.trim())throw i({code:`VALIDATION_ERROR`,message:`shortcode is required.`});if(!n.email?.trim())throw i({code:`VALIDATION_ERROR`,message:`email is required.`});if(!n.callbackUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`callbackUrl is required.`});let r={shortcode:n.shortcode,email:n.email,officialContact:n.officialContact,sendReminders:n.sendReminders,logo:n.logo??``,callbackUrl:n.callbackUrl},{data:a}=await l(`${e}/v1/billmanager-invoice/optin`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:r});return a}async function P(e,t,n){if(!n.externalReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`externalReference is required.`});if(!n.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA (customer MSISDN) is required.`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be ≥ 1 (got ${n.amount}).`});let a={externalReference:n.externalReference,billingPeriod:n.billingPeriod,invoiceName:n.invoiceName,dueDate:n.dueDate,accountReference:n.accountReference,amount:String(r),partyA:n.partyA,invoiceItems:n.invoiceItems?.map(e=>({itemName:e.itemName,amount:String(Math.round(e.amount))}))??[]},{data:o}=await l(`${e}/v1/billmanager-invoice/single-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function F(e,t,n){if(!n.invoices?.length)throw i({code:`VALIDATION_ERROR`,message:`invoices array must not be empty.`});if(n.invoices.length>1e3)throw i({code:`VALIDATION_ERROR`,message:`Maximum 1 000 invoices per bulk request.`});let{data:r}=await l(`${e}/v1/billmanager-invoice/bulk-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:n.invoices});return r}async function I(e,t,n){if(!n.externalReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`externalReference is required.`});let{data:r}=await l(`${e}/v1/billmanager-invoice/cancel-single-invoice`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:[{externalReference:n.externalReference}]});return r}const L=[`mpesa`,`safaricom`,`exec`,`exe`,`cmd`,`sql`,`query`],_e=[`Completed`,`Cancelled`];function R(e,t){if(!e||!e.trim())throw i({code:`VALIDATION_ERROR`,message:`${t} is required`});let n=e.toLowerCase();for(let e of L)if(n.includes(e))throw i({code:`VALIDATION_ERROR`,message:`${t} must not contain the keyword "${e}". Daraja rejects URLs containing: mpesa, safaricom, exec, exe, cmd, sql, query (and their variants).`})}async function z(e,t,n){if(!n.shortCode||!String(n.shortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!n.responseType)throw i({code:`VALIDATION_ERROR`,message:`responseType is required: "Completed" or "Cancelled" (sentence case)`});if(!_e.includes(n.responseType))throw i({code:`VALIDATION_ERROR`,message:`responseType must be exactly "Completed" or "Cancelled" (sentence case, correctly spelled). Got: "${String(n.responseType)}"`});R(n.confirmationUrl,`confirmationUrl`),R(n.validationUrl,`validationUrl`);let r=n.apiVersion??`v2`,a={ShortCode:String(n.shortCode),ResponseType:n.responseType,ConfirmationURL:n.confirmationUrl,ValidationURL:n.validationUrl},{data:o}=await l(`${e}/mpesa/c2b/${r}/registerurl`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function B(e,t,n){if(!e.includes(`sandbox`))throw i({code:`VALIDATION_ERROR`,message:`C2B simulate is only available in the Sandbox environment. In production, customers initiate payments via M-PESA App, USSD, or SIM Toolkit.`});if(!n.shortCode||!String(n.shortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(n.commandId!==`CustomerPayBillOnline`&&n.commandId!==`CustomerBuyGoodsOnline`)throw i({code:`VALIDATION_ERROR`,message:`commandId must be "CustomerPayBillOnline" or "CustomerBuyGoodsOnline". Got: "${String(n.commandId)}"`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount})`});if(!n.msisdn||!String(n.msisdn).trim())throw i({code:`VALIDATION_ERROR`,message:`msisdn is required. Sandbox test MSISDN: 254708374149`});let a=n.commandId===`CustomerBuyGoodsOnline`,o=n.apiVersion??`v2`,s={ShortCode:Number(n.shortCode),CommandID:n.commandId,Amount:r,Msisdn:Number(n.msisdn)};a||(s.BillRefNumber=n.billRefNumber??``);let{data:c}=await l(`${e}/mpesa/c2b/${o}/simulate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return c}function ve(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.TransID==`string`&&typeof t.BusinessShortCode==`string`&&typeof t.TransAmount==`string`}function ye(e){return{ResultCode:`0`,ResultDesc:`Accepted`,...e?{ThirdPartyTransID:e}:{}}}function be(e=`C2B00016`){return{ResultCode:e,ResultDesc:`Rejected`}}function xe(){return{ResultCode:0,ResultDesc:`Success`}}function Se(e){return Number(e.TransAmount)}function Ce(e){return e.TransID}function we(e){return e.BillRefNumber}function Te(e){return[e.FirstName,e.MiddleName,e.LastName].filter(Boolean).join(` `).trim()}function Ee(e){return e.TransactionType===`Pay Bill`}function De(e){return e.TransactionType===`Buy Goods`}const V=[`BG`,`WA`,`PB`,`SM`,`SB`];function Oe(e){return typeof e!=`string`||e.trim().length===0?`merchantName is required and must be a non-empty string`:null}function ke(e){return typeof e!=`string`||e.trim().length===0?`refNo (transaction reference) is required and must be a non-empty string`:null}function H(e){return typeof e!=`number`||!Number.isFinite(e)?`amount must be a finite number`:Math.round(e)<1?`amount must be at least 1 KES (got ${e})`:null}function Ae(e){return V.includes(e)?null:`trxCode must be one of: ${V.join(`, `)} (BG=Buy Goods, WA=Withdraw Cash, PB=Paybill, SM=Send Money, SB=Send to Business)`}function je(e){return typeof e!=`string`||e.trim().length===0?`cpi (Credit Party Identifier) is required and must be a non-empty string`:null}function Me(e){return e==null?null:typeof e!=`number`||!Number.isFinite(e)?`size must be a finite number when provided`:!Number.isInteger(e)||e<1?`size must be a positive integer (minimum 1)`:e>1e3?`size must not exceed 1000 pixels (got ${e})`:null}function Ne(e){if(typeof e!=`object`||!e)return{valid:!1,errors:{payload:`request payload must be a non-null object`}};let t=e,n={},r=Oe(t.merchantName);r&&(n.merchantName=r);let i=ke(t.refNo);i&&(n.refNo=i);let a=H(t.amount);a&&(n.amount=a);let o=Ae(t.trxCode);o&&(n.trxCode=o);let s=je(t.cpi);s&&(n.cpi=s);let c=Me(t.size);return c&&(n.size=c),Object.keys(n).length>0?{valid:!1,errors:n}:{valid:!0}}function Pe(e,t){switch(e){case`404.001.04`:return new r({code:`AUTH_FAILED`,message:`Daraja rejected the request due to an invalid authentication header. Ensure the Dynamic QR endpoint is called with POST and that the Authorization: Bearer <token> header is present. Daraja: "${t}"`,statusCode:404});case`400.003.01`:return new r({code:`AUTH_FAILED`,message:`The M-PESA access token is invalid or has expired. Call clearTokenCache() on the Mpesa instance to force a token refresh and retry the request. Daraja: "${t}"`,statusCode:401});case`400.002.05`:return new r({code:`VALIDATION_ERROR`,message:`Daraja rejected the request payload as malformed. Verify that all required fields (MerchantName, RefNo, Amount, TrxCode, CPI, Size) are present and have correct types. Daraja: "${t}"`,statusCode:400});default:return new r({code:`REQUEST_FAILED`,message:`Dynamic QR request failed (${e}): ${t}`,statusCode:400})}}function Fe(e){return typeof e==`object`&&!!e&&`errorCode`in e&&typeof e.errorCode==`string`}function Ie(e){return typeof e==`object`&&!!e&&`ResponseCode`in e&&`QRCode`in e&&typeof e.QRCode==`string`&&e.QRCode.length>0}async function Le(e,t,n){let i=Ne(n);if(!i.valid)throw new r({code:`VALIDATION_ERROR`,message:`Dynamic QR request validation failed:\n${Object.entries(i.errors).map(([e,t])=>` • ${e}: ${t}`).join(`
|
|
2
|
-
`)}`});if(!t||typeof t!=`string`||t.trim().length===0)throw new r({code:`AUTH_FAILED`,message:`accessToken is required. Obtain one via the Daraja Authorization API (GET /oauth/v1/generate?grant_type=client_credentials).`});let a=n.size??300,o=Math.round(n.amount),s={MerchantName:n.merchantName.trim(),RefNo:n.refNo.trim(),Amount:o,TrxCode:n.trxCode,CPI:n.cpi.trim(),Size:String(a)},{data:c}=await l(`${e}/mpesa/qrcode/v1/generate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});if(Fe(c))throw Pe(c.errorCode,c.errorMessage);if(!Ie(c))throw new r({code:`REQUEST_FAILED`,message:`Daraja returned an unexpected response structure for the Dynamic QR request. The response was missing required fields (ResponseCode, QRCode). Raw response: ${JSON.stringify(c).slice(0,300)}`});return c}async function Re(e,t,n,r,a){if(!a.transactionId?.trim())throw i({code:`VALIDATION_ERROR`,message:`transactionId is required.`});if(!a.receiverParty?.trim())throw i({code:`VALIDATION_ERROR`,message:`receiverParty is required.`});if(![`1`,`2`,`4`].includes(a.receiverIdentifierType))throw i({code:`VALIDATION_ERROR`,message:`receiverIdentifierType must be "1", "2", or "4".`});let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount}).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let s={Initiator:r,SecurityCredential:n,CommandID:`TransactionReversal`,TransactionID:a.transactionId,Amount:String(o),ReceiverParty:String(a.receiverParty),RecieverIdentifierType:a.receiverIdentifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Transaction Reversal`,Occasion:a.occasion??``},{data:c}=await l(`${e}/mpesa/reversal/v1/request`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return c}function ze(e){return e.Result.ResultCode===0}function Be(e){return e.Result.TransactionID??null}function Ve(e){return e.Result.ConversationID}const U={MIN_AMOUNT:1,MAX_AMOUNT:25e4},He={SUCCESS:0,INSUFFICIENT_BALANCE:1,CANCELLED_BY_USER:1032,PHONE_UNREACHABLE:1037,INVALID_PIN:2001};function W(e){return e.ResultCode===He.SUCCESS}function Ue(e,t){let n=e.Body.stkCallback;if(W(n))return n.CallbackMetadata.Item.find(e=>e.Name===t)?.Value}function G(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw new r({code:`INVALID_PHONE`,message:`Cannot parse "${e}". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`});if(n.length!==12)throw new r({code:`INVALID_PHONE`,message:`"${e}" normalised to "${n}" — expected 12 digits.`});return n}function K(e,t,n){return btoa(`${e}${t}${n}`)}function q(){let e=new Date,t=e=>e.toString().padStart(2,`0`);return[e.getFullYear(),t(e.getMonth()+1),t(e.getDate()),t(e.getHours()),t(e.getMinutes()),t(e.getSeconds())].join(``)}async function We(e,t,n){let i=Math.round(n.amount);if(!Number.isFinite(i)||i<U.MIN_AMOUNT)throw new r({code:`VALIDATION_ERROR`,message:`Amount must be at least KES ${U.MIN_AMOUNT} (got ${n.amount} which rounds to ${i}).`});if(i>U.MAX_AMOUNT)throw new r({code:`VALIDATION_ERROR`,message:`Amount must not exceed KES ${U.MAX_AMOUNT.toLocaleString()} per transaction as per Safaricom Daraja limits (got ${n.amount} which rounds to ${i}).`});let a=q(),o=n.partyB??n.shortCode,s={BusinessShortCode:n.shortCode,Password:K(n.shortCode,n.passKey,a),Timestamp:a,TransactionType:n.transactionType??`CustomerPayBillOnline`,Amount:i,PartyA:G(n.phoneNumber),PartyB:o,PhoneNumber:G(n.phoneNumber),CallBackURL:n.callbackUrl,AccountReference:n.accountReference.slice(0,12),TransactionDesc:n.transactionDesc.slice(0,13)},{data:c}=await l(`${e}/mpesa/stkpush/v1/processrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s,retries:5,retryDelay:3e3});return c}async function Ge(e,t,n){let r=q(),i={BusinessShortCode:n.shortCode,Password:K(n.shortCode,n.passKey,r),Timestamp:r,CheckoutRequestID:n.checkoutRequestId},{data:a}=await l(`${e}/mpesa/stkpushquery/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}const Ke=`572572`,J=`PayTaxToKRA`;async function Y(e,t,n,r,a){let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount} which rounds to ${o}).`});if(!a.partyA)throw i({code:`VALIDATION_ERROR`,message:`partyA is required — your M-PESA business shortcode from which tax is deducted.`});if(!a.accountReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`accountReference is required — the Payment Registration Number (PRN) issued by KRA.`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the tax remittance result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={Initiator:r,SecurityCredential:n,CommandID:J,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(o),PartyA:String(a.partyA),PartyB:a.partyB??`572572`,AccountReference:a.accountReference,Remarks:a.remarks??`Tax Remittance`,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl},{data:c}=await l(`${e}/mpesa/b2b/v1/remittax`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return c}async function qe(e,t,n,r,a){if(!a.transactionId)throw i({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!a.partyA)throw i({code:`VALIDATION_ERROR`,message:`partyA is required (your business shortcode, till number, or MSISDN)`});if(!a.identifierType)throw i({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" (MSISDN) | "2" (Till) | "4" (ShortCode)`});if(!a.resultUrl)throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the transaction result here`});if(!a.queueTimeOutUrl)throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on timeout`});let o={Initiator:r,SecurityCredential:n,CommandID:a.commandId??`TransactionStatusQuery`,TransactionID:a.transactionId,PartyA:a.partyA,IdentifierType:a.identifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Transaction Status Query`,Occasion:a.occasion??``},{data:s}=await l(`${e}/mpesa/transactionstatus/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}const X={sandbox:`https://sandbox.safaricom.co.ke`,production:`https://api.safaricom.co.ke`};var Je=class{config;tokenManager;baseUrl;constructor(e){if(!e.consumerKey||!e.consumerSecret)throw new r({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required.`});this.config=e,this.baseUrl=X[e.environment],this.tokenManager=new u(e.consumerKey,e.consumerSecret,this.baseUrl)}getToken(){return this.tokenManager.getAccessToken()}async buildSecurityCredential(){if(this.config.securityCredential)return this.config.securityCredential;if(!this.config.initiatorPassword)throw new r({code:`INVALID_CREDENTIALS`,message:`Provide securityCredential (pre-encrypted) OR (initiatorPassword + certificatePath/certificatePem).`});let t;if(this.config.certificatePem)t=this.config.certificatePem;else if(this.config.certificatePath)t=await e(this.config.certificatePath,`utf-8`);else throw new r({code:`INVALID_CREDENTIALS`,message:`certificatePath or certificatePem is required to encrypt the initiator password.`});return d(this.config.initiatorPassword,t)}requireInitiator(e){let t=this.config.initiatorName??``;if(!t)throw new r({code:`VALIDATION_ERROR`,message:`initiatorName is required for ${e}.`});return t}async stkPushSafe(e){try{return _(await this.stkPush(e))}catch(e){return v(e)}}async stkPush(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new r({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push.`});let i=await this.getToken();return We(this.baseUrl,i,{...e,shortCode:t,passKey:n})}async stkQuery(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new r({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query.`});let i=await this.getToken();return Ge(this.baseUrl,i,{...e,shortCode:t,passKey:n})}async transactionStatus(e){let t=this.requireInitiator(`Transaction Status`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return qe(this.baseUrl,n,r,t,e)}async accountBalance(e){let t=this.requireInitiator(`Account Balance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return b(this.baseUrl,n,r,t,e)}async reverseTransaction(e){let t=this.requireInitiator(`Reversal`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Re(this.baseUrl,n,r,t,e)}async generateDynamicQR(e){let t=await this.getToken();return Le(this.baseUrl,t,e)}async registerC2BUrls(e){let t=await this.getToken();return z(this.baseUrl,t,e)}async simulateC2B(e){let t=await this.getToken();return B(this.baseUrl,t,e)}async remitTax(e){let t=this.requireInitiator(`Tax Remittance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Y(this.baseUrl,n,r,t,e)}async b2bExpressCheckout(e){let t=await this.getToken();return C(this.baseUrl,t,e)}async b2cPayment(e){let t=this.requireInitiator(`B2C Payment`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return E(this.baseUrl,n,r,t,e)}async billManagerOptIn(e){let t=await this.getToken();return N(this.baseUrl,t,e)}async sendInvoice(e){let t=await this.getToken();return P(this.baseUrl,t,e)}async sendBulkInvoices(e){let t=await this.getToken();return F(this.baseUrl,t,e)}async cancelInvoice(e){let t=await this.getToken();return I(this.baseUrl,t,e)}clearTokenCache(){this.tokenManager.clearCache()}get environment(){return this.config.environment}};const Ye={maxRetries:1/0,initialDelay:1e3,maxDelay:36e5,backoffMultiplier:2,maxRetryDuration:720*60*60*1e3};async function Xe(e,t={}){let n={...Ye,...t},r=n.initialDelay,i=0,a=Date.now();for(;i<n.maxRetries;){if(i++,Date.now()-a>n.maxRetryDuration)return{success:!1,attempts:i,error:Error(`Max retry duration exceeded`)};try{return{success:!0,data:await e(),attempts:i}}catch(e){let t=e instanceof Error?e:Error(String(e));if(t.message.includes(`4`))return{success:!1,attempts:i,error:t};i<n.maxRetries&&(await new Promise(e=>setTimeout(e,r)),r=Math.min(r*n.backoffMultiplier,n.maxDelay))}}return{success:!1,attempts:i,error:Error(`Max retries exceeded`)}}const Z=[`196.201.214.200`,`196.201.214.206`,`196.201.213.114`,`196.201.214.207`,`196.201.214.208`,`196.201.213.44`,`196.201.212.127`,`196.201.212.138`,`196.201.212.129`,`196.201.212.136`,`196.201.212.74`,`196.201.212.69`];function Q(e,t=Z){return t.includes(e)}function $(e){try{let t=e;return t?.Body?.stkCallback?t:null}catch{return null}}function Ze(e,t={}){if(!t.skipIPCheck&&t.requestIP&&!Q(t.requestIP,t.allowedIPs))return{success:!1,eventType:null,data:null,error:`IP address ${t.requestIP} is not in the Safaricom whitelist`};let n=$(e);return n?{success:!0,eventType:`stk_push`,data:n}:{success:!1,eventType:null,data:null,error:`Unknown or malformed webhook payload`}}function Qe(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`MpesaReceiptNumber`);return t?String(t.Value):null}function $e(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`Amount`);return t?Number(t.Value):null}function et(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`PhoneNumber`);return t?String(t.Value):null}function tt(e){return e.Body?.stkCallback?.ResultCode===0}export{X as DARAJA_BASE_URLS,Ke as KRA_SHORTCODE,Je as Mpesa,r as PesafyError,Z as SAFARICOM_IPS,J as TAX_COMMAND_ID,ye as acceptC2BValidation,xe as acknowledgeC2BConfirmation,i as createError,d as encryptSecurityCredential,v as err,$e as extractAmount,et as extractPhoneNumber,Qe as extractTransactionId,G as formatPhoneNumber,G as formatSafaricomPhone,ee as getAccountBalanceParam,ae as getB2BAmount,se as getB2BConversationId,ie as getB2BRequestId,oe as getB2BTransactionId,he as getB2CAmount,fe as getB2CConversationId,k as getB2CCurrency,A as getB2CDebitAccountBalance,D as getB2CDebitPartyCharges,j as getB2CInitiatorAccountBalance,pe as getB2COriginatorConversationId,O as getB2CReceiverPublicName,me as getB2CResultDesc,M as getB2CResultParam,ge as getB2CTransactionCompletedTime,de as getB2CTransactionId,we as getC2BAccountRef,Se as getC2BAmount,Te as getC2BCustomerName,Ce as getC2BTransactionId,Ue as getCallbackValue,Ve as getReversalConversationId,Be as getReversalTransactionId,q as getTimestamp,Ze as handleWebhook,l as httpRequest,C as initiateB2BExpressCheckout,E as initiateB2CPayment,S as isAccountBalanceSuccess,ne as isB2BCheckoutCallback,re as isB2BCheckoutCancelled,T as isB2BCheckoutSuccess,ue as isB2CFailure,ce as isB2CResult,le as isB2CSuccess,De as isBuyGoodsPayment,ve as isC2BPayload,Ee as isPaybillPayment,a as isPesafyError,ze as isReversalSuccess,W as isStkCallbackSuccess,tt as isSuccessfulCallback,_ as ok,x as parseAccountBalance,$ as parseStkPushWebhook,z as registerC2BUrls,be as rejectC2BValidation,Y as remitTax,Xe as retryWithBackoff,B as simulateC2B,f as toKesAmount,p as toMsisdn,y as toNonEmpty,m as toPaybill,g as toShortCode,h as toTill,Q as verifyWebhookIP};
|
|
1
|
+
import{readFile as e}from"node:fs/promises";import{constants as t,publicEncrypt as n}from"node:crypto";var r=class e extends Error{code;statusCode;response;requestId;cause;retryable;constructor(t){super(t.message),Object.defineProperty(this,`name`,{value:`PesafyError`}),this.code=t.code,this.statusCode=t.statusCode,this.response=t.response,this.requestId=t.requestId,this.cause=t.cause,this.retryable=t.retryable??(t.code===`NETWORK_ERROR`||t.code===`TIMEOUT`||t.code===`RATE_LIMITED`||t.code===`REQUEST_FAILED`),Error.captureStackTrace&&Error.captureStackTrace(this,e)}get isValidation(){return this.code===`VALIDATION_ERROR`}get isAuth(){return this.code===`AUTH_FAILED`||this.code===`INVALID_CREDENTIALS`}toJSON(){return{name:this.name,code:this.code,message:this.message,statusCode:this.statusCode,requestId:this.requestId,retryable:this.retryable}}};function i(e){return new r(e)}function a(e){return e instanceof r}const o=new Set([429,500,502,503,504]);function s(e){return new Promise(t=>setTimeout(t,e))}function c(e){let t=e*.25;return e+(Math.random()*t*2-t)}async function l(e,t){let n=t.retries??4,i=t.retryDelay??2e3,a=t.timeout??3e4,l={"Content-Type":`application/json`,Accept:`application/json`,...t.headers};t.idempotencyKey&&(l[`Idempotency-Key`]=t.idempotencyKey);let u={method:t.method,headers:l,...t.body===void 0?{}:{body:JSON.stringify(t.body)}},d=null;for(let l=0;l<=n;l++){if(l>0){let r=c(i*2**(l-1));console.warn(`[pesafy] Retry ${l}/${n} → ${t.method} ${e} in ${Math.round(r)} ms`),await s(r)}let f=new AbortController,p=setTimeout(()=>f.abort(),a),m;try{m=await fetch(e,{...u,signal:f.signal})}catch(t){if(clearTimeout(p),d=t instanceof Error&&t.name===`AbortError`?new r({code:`TIMEOUT`,message:`Request to ${e} timed out after ${a} ms`,cause:t,retryable:!0}):new r({code:`NETWORK_ERROR`,message:`Network error: ${t instanceof Error?t.message:String(t)}`,cause:t,retryable:!0}),l<n)continue;throw d}finally{clearTimeout(p)}let h=``,g=null,_=m.headers.get(`content-type`)??``;try{h=await m.text(),h&&(g=_.includes(`application/json`)?JSON.parse(h):h)}catch{g=h||null}let v={};if(m.headers.forEach((e,t)=>{v[t]=e}),m.ok)return{data:g,status:m.status,headers:v};let y=o.has(m.status),b=typeof g==`object`&&g?g:{},x=b.errorMessage??b.ResponseDescription??b.resultDesc??h??`HTTP ${m.status}`;if(d=new r({code:y?`REQUEST_FAILED`:`API_ERROR`,message:x,statusCode:m.status,response:g,retryable:y,...typeof b.requestId==`string`?{requestId:b.requestId}:{}}),!(y&&l<n))throw d}throw d}var u=class{consumerKey;consumerSecret;baseUrl;cachedToken=null;tokenExpiresAt=0;constructor(e,t,n){this.consumerKey=e,this.consumerSecret=t,this.baseUrl=n}getBasicAuthHeader(){let e=`${this.consumerKey}:${this.consumerSecret}`;return`Basic ${Buffer.from(e,`utf-8`).toString(`base64`)}`}async getAccessToken(){let e=Date.now()/1e3;if(this.cachedToken&&this.tokenExpiresAt>e+60)return this.cachedToken;let t=await l(`${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`,{method:`GET`,headers:{Authorization:this.getBasicAuthHeader()}}),{access_token:n,expires_in:i}=t.data;if(!n)throw new r({code:`AUTH_FAILED`,message:`Daraja did not return an access token. Check your consumer key and secret.`,response:t.data});return this.cachedToken=n,this.tokenExpiresAt=e+(i??3600),this.cachedToken}clearCache(){this.cachedToken=null,this.tokenExpiresAt=0}};function d(e,i){try{let r=Buffer.from(e,`utf-8`);return n({key:i,padding:t.RSA_PKCS1_PADDING},r).toString(`base64`)}catch(e){throw new r({code:`ENCRYPTION_FAILED`,message:`Failed to encrypt security credential. Ensure the certificate PEM is valid and matches the environment (sandbox/production).`,cause:e})}}function f(e){let t=Math.round(e);if(!Number.isFinite(t)||t<1)throw TypeError(`KesAmount must be a whole number ≥ 1, got ${e}`);return t}function p(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw TypeError(`Cannot normalise "${e}" to 254XXXXXXXXX. Use 07XX…, 2547XX…, or +2547XX….`);if(n.length!==12)throw TypeError(`Phone "${e}" normalised to "${n}" — expected 12 digits.`);return n}function m(e){return String(e)}function h(e){return String(e)}function g(e){return String(e)}function _(e){return{ok:!0,data:e}}function v(e){return{ok:!1,error:e}}function y(e){if(!e.trim())throw TypeError(`String must not be empty`);return e}async function b(e,t,n,r,a){if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required.`});if(![`1`,`2`,`4`].includes(a.identifierType))throw i({code:`VALIDATION_ERROR`,message:`identifierType must be "1" (MSISDN), "2" (Till), or "4" (ShortCode).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let o={Initiator:r,SecurityCredential:n,CommandID:`AccountBalance`,PartyA:String(a.partyA),IdentifierType:a.identifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Account Balance Query`},{data:s}=await l(`${e}/mpesa/accountbalance/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}function x(e){let t=e.split(`|`),n=[];for(let e=0;e+2<t.length;e+=3){let r=t[e]?.trim(),i=t[e+1]?.trim(),a=t[e+2]?.trim();r&&i&&a!==void 0&&n.push({name:r,currency:i,amount:a})}return n}function S(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function ee(e){return e.Result.ResultCode===0}function te(){try{if(typeof crypto<`u`&&typeof crypto.randomUUID==`function`)return crypto.randomUUID()}catch{}return`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`}async function C(e,t,n){if(!n.primaryShortCode||!String(n.primaryShortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`primaryShortCode is required — the merchant's till number (debit party).`});if(!n.receiverShortCode||!String(n.receiverShortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — the vendor's Paybill account (credit party).`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount}).`});if(!n.paymentRef||!String(n.paymentRef).trim())throw i({code:`VALIDATION_ERROR`,message:`paymentRef is required — shown in the merchant's USSD prompt as the payment reference.`});if(!n.callbackUrl||!String(n.callbackUrl).trim())throw i({code:`VALIDATION_ERROR`,message:`callbackUrl is required — Daraja POSTs the transaction result here.`});if(!n.partnerName||!String(n.partnerName).trim())throw i({code:`VALIDATION_ERROR`,message:`partnerName is required — vendor's friendly name shown in the merchant's USSD prompt.`});let a={primaryShortCode:String(n.primaryShortCode),receiverShortCode:String(n.receiverShortCode),amount:String(r),paymentRef:n.paymentRef,callbackUrl:n.callbackUrl,partnerName:n.partnerName,RequestRefID:n.requestRefId??te()},{data:o}=await l(`${e}/v1/ussdpush/get-msisdn`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}const w={SUCCESS:`0`,CANCELLED:`4001`,KYC_FAIL:`4102`,NO_NOMINATED_NUMBER:`4104`,USSD_NETWORK_ERROR:`4201`,USSD_EXCEPTION_ERROR:`4203`};new Set(Object.values(w));function ne(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.resultCode==`string`&&typeof t.requestId==`string`&&typeof t.amount==`string`}function T(e){return e.resultCode===w.SUCCESS}function re(e){return e.resultCode===w.CANCELLED}function ie(e){return e.requestId}function ae(e){return Number(e.amount)}function oe(e){return T(e)?e.transactionId??null:null}function se(e){return T(e)?e.conversationID??null:null}async function E(e,t,n,r,a){if(!a.commandId||a.commandId!==`BusinessPayToBulk`)throw i({code:`VALIDATION_ERROR`,message:`commandId must be "BusinessPayToBulk". This is the only CommandID supported by the B2C Account Top Up API.`});let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount} which rounds to ${o}).`});if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required — the sender shortcode from which funds are deducted.`});if(!a.partyB?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyB is required — the receiver B2C shortcode that receives the funds.`});if(!a.accountReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`accountReference is required — a reference for this transaction.`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the async result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={Initiator:r,SecurityCredential:n,CommandID:a.commandId,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(o),PartyA:String(a.partyA),PartyB:String(a.partyB),AccountReference:a.accountReference,Remarks:a.remarks??`B2C Account Top Up`,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl};a.requester?.trim()&&(s.Requester=String(a.requester));let{data:c}=await l(`${e}/mpesa/b2b/v1/paymentrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return c}const ce={INTERNAL_SERVER_ERROR:`500.003.1001`,INVALID_ACCESS_TOKEN:`400.003.01`,BAD_REQUEST:`400.003.02`,QUOTA_VIOLATION:`500.003.03`,SPIKE_ARREST:`500.003.02`,NOT_FOUND:`404.003.01`,INVALID_AUTH_HEADER:`404.001.04`,INVALID_PAYLOAD:`400.002.05`},D={SUCCESS:0,INVALID_INITIATOR:2001};function le(e){if(!e||typeof e!=`object`)return!1;let t=e;if(!t.Result||typeof t.Result!=`object`)return!1;let n=t.Result;return(typeof n.ResultCode==`number`||typeof n.ResultCode==`string`)&&typeof n.ConversationID==`string`&&typeof n.OriginatorConversationID==`string`}function O(e){let t=e.Result.ResultCode;return t===0||t===`0`}function ue(e){return!O(e)}function de(e){if(typeof e!=`number`&&typeof e!=`string`||typeof e==`string`&&e.trim()===``)return!1;let t=Number(e);return Object.values(D).includes(t)}function fe(e){return e.Result.TransactionID??null}function pe(e){return e.Result.ConversationID}function me(e){return e.Result.OriginatorConversationID}function he(e){return e.Result.ResultDesc}function ge(e){let t=P(e,`Amount`);if(t===void 0)return null;let n=Number(t);return Number.isFinite(n)?n:null}function k(e){let t=P(e,`Currency`);return t===void 0||t===``?`KES`:String(t)}function A(e){let t=P(e,`ReceiverPartyPublicName`);return t===void 0?null:String(t)}function j(e){let t=P(e,`TransactionCompletedTime`);return t===void 0?null:String(t)}function M(e){let t=P(e,`DebitAccountBalance`);return t===void 0?null:String(t)}function N(e){let t=P(e,`DebitPartyCharges`);return t===void 0||t===``?null:String(t)}function P(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}async function F(e,t,n){if(!n.shortcode?.trim())throw i({code:`VALIDATION_ERROR`,message:`shortcode is required.`});if(!n.email?.trim())throw i({code:`VALIDATION_ERROR`,message:`email is required.`});if(!n.callbackUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`callbackUrl is required.`});let r={shortcode:n.shortcode,email:n.email,officialContact:n.officialContact,sendReminders:n.sendReminders,logo:n.logo??``,callbackUrl:n.callbackUrl},{data:a}=await l(`${e}/v1/billmanager-invoice/optin`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:r});return a}async function I(e,t,n){if(!n.externalReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`externalReference is required.`});if(!n.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA (customer MSISDN) is required.`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be ≥ 1 (got ${n.amount}).`});let a={externalReference:n.externalReference,billingPeriod:n.billingPeriod,invoiceName:n.invoiceName,dueDate:n.dueDate,accountReference:n.accountReference,amount:String(r),partyA:n.partyA,invoiceItems:n.invoiceItems?.map(e=>({itemName:e.itemName,amount:String(Math.round(e.amount))}))??[]},{data:o}=await l(`${e}/v1/billmanager-invoice/single-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function L(e,t,n){if(!n.invoices?.length)throw i({code:`VALIDATION_ERROR`,message:`invoices array must not be empty.`});if(n.invoices.length>1e3)throw i({code:`VALIDATION_ERROR`,message:`Maximum 1 000 invoices per bulk request.`});let{data:r}=await l(`${e}/v1/billmanager-invoice/bulk-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:n.invoices});return r}async function _e(e,t,n){if(!n.externalReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`externalReference is required.`});let{data:r}=await l(`${e}/v1/billmanager-invoice/cancel-single-invoice`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:[{externalReference:n.externalReference}]});return r}const ve=[`mpesa`,`safaricom`,`exec`,`exe`,`cmd`,`sql`,`query`],ye=[`Completed`,`Cancelled`];function R(e,t){if(!e||!e.trim())throw i({code:`VALIDATION_ERROR`,message:`${t} is required`});let n=e.toLowerCase();for(let e of ve)if(n.includes(e))throw i({code:`VALIDATION_ERROR`,message:`${t} must not contain the keyword "${e}". Daraja rejects URLs containing: mpesa, safaricom, exec, exe, cmd, sql, query (and their variants).`})}async function z(e,t,n){if(!n.shortCode||!String(n.shortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!n.responseType)throw i({code:`VALIDATION_ERROR`,message:`responseType is required: "Completed" or "Cancelled" (sentence case)`});if(!ye.includes(n.responseType))throw i({code:`VALIDATION_ERROR`,message:`responseType must be exactly "Completed" or "Cancelled" (sentence case, correctly spelled). Got: "${String(n.responseType)}"`});R(n.confirmationUrl,`confirmationUrl`),R(n.validationUrl,`validationUrl`);let r=n.apiVersion??`v2`,a={ShortCode:String(n.shortCode),ResponseType:n.responseType,ConfirmationURL:n.confirmationUrl,ValidationURL:n.validationUrl},{data:o}=await l(`${e}/mpesa/c2b/${r}/registerurl`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function B(e,t,n){if(!e.includes(`sandbox`))throw i({code:`VALIDATION_ERROR`,message:`C2B simulate is only available in the Sandbox environment. In production, customers initiate payments via M-PESA App, USSD, or SIM Toolkit.`});if(!n.shortCode||!String(n.shortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(n.commandId!==`CustomerPayBillOnline`&&n.commandId!==`CustomerBuyGoodsOnline`)throw i({code:`VALIDATION_ERROR`,message:`commandId must be "CustomerPayBillOnline" or "CustomerBuyGoodsOnline". Got: "${String(n.commandId)}"`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount})`});if(!n.msisdn||!String(n.msisdn).trim())throw i({code:`VALIDATION_ERROR`,message:`msisdn is required. Sandbox test MSISDN: 254708374149`});let a=n.commandId===`CustomerBuyGoodsOnline`,o=n.apiVersion??`v2`,s={ShortCode:Number(n.shortCode),CommandID:n.commandId,Amount:r,Msisdn:Number(n.msisdn)};a||(s.BillRefNumber=n.billRefNumber??``);let{data:c}=await l(`${e}/mpesa/c2b/${o}/simulate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return c}function be(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.TransID==`string`&&typeof t.BusinessShortCode==`string`&&typeof t.TransAmount==`string`}function xe(e){return{ResultCode:`0`,ResultDesc:`Accepted`,...e?{ThirdPartyTransID:e}:{}}}function Se(e=`C2B00016`){return{ResultCode:e,ResultDesc:`Rejected`}}function Ce(){return{ResultCode:0,ResultDesc:`Success`}}function we(e){return Number(e.TransAmount)}function Te(e){return e.TransID}function Ee(e){return e.BillRefNumber}function De(e){return[e.FirstName,e.MiddleName,e.LastName].filter(Boolean).join(` `).trim()}function Oe(e){return e.TransactionType===`Pay Bill`}function ke(e){return e.TransactionType===`Buy Goods`}const V=[`BG`,`WA`,`PB`,`SM`,`SB`];function Ae(e){return typeof e!=`string`||e.trim().length===0?`merchantName is required and must be a non-empty string`:null}function je(e){return typeof e!=`string`||e.trim().length===0?`refNo (transaction reference) is required and must be a non-empty string`:null}function H(e){return typeof e!=`number`||!Number.isFinite(e)?`amount must be a finite number`:Math.round(e)<1?`amount must be at least 1 KES (got ${e})`:null}function Me(e){return V.includes(e)?null:`trxCode must be one of: ${V.join(`, `)} (BG=Buy Goods, WA=Withdraw Cash, PB=Paybill, SM=Send Money, SB=Send to Business)`}function Ne(e){return typeof e!=`string`||e.trim().length===0?`cpi (Credit Party Identifier) is required and must be a non-empty string`:null}function Pe(e){return e==null?null:typeof e!=`number`||!Number.isFinite(e)?`size must be a finite number when provided`:!Number.isInteger(e)||e<1?`size must be a positive integer (minimum 1)`:e>1e3?`size must not exceed 1000 pixels (got ${e})`:null}function Fe(e){if(typeof e!=`object`||!e)return{valid:!1,errors:{payload:`request payload must be a non-null object`}};let t=e,n={},r=Ae(t.merchantName);r&&(n.merchantName=r);let i=je(t.refNo);i&&(n.refNo=i);let a=H(t.amount);a&&(n.amount=a);let o=Me(t.trxCode);o&&(n.trxCode=o);let s=Ne(t.cpi);s&&(n.cpi=s);let c=Pe(t.size);return c&&(n.size=c),Object.keys(n).length>0?{valid:!1,errors:n}:{valid:!0}}function Ie(e,t){switch(e){case`404.001.04`:return new r({code:`AUTH_FAILED`,message:`Daraja rejected the request due to an invalid authentication header. Ensure the Dynamic QR endpoint is called with POST and that the Authorization: Bearer <token> header is present. Daraja: "${t}"`,statusCode:404});case`400.003.01`:return new r({code:`AUTH_FAILED`,message:`The M-PESA access token is invalid or has expired. Call clearTokenCache() on the Mpesa instance to force a token refresh and retry the request. Daraja: "${t}"`,statusCode:401});case`400.002.05`:return new r({code:`VALIDATION_ERROR`,message:`Daraja rejected the request payload as malformed. Verify that all required fields (MerchantName, RefNo, Amount, TrxCode, CPI, Size) are present and have correct types. Daraja: "${t}"`,statusCode:400});default:return new r({code:`REQUEST_FAILED`,message:`Dynamic QR request failed (${e}): ${t}`,statusCode:400})}}function Le(e){return typeof e==`object`&&!!e&&`errorCode`in e&&typeof e.errorCode==`string`}function Re(e){return typeof e==`object`&&!!e&&`ResponseCode`in e&&`QRCode`in e&&typeof e.QRCode==`string`&&e.QRCode.length>0}async function ze(e,t,n){let i=Fe(n);if(!i.valid)throw new r({code:`VALIDATION_ERROR`,message:`Dynamic QR request validation failed:\n${Object.entries(i.errors).map(([e,t])=>` • ${e}: ${t}`).join(`
|
|
2
|
+
`)}`});if(!t||typeof t!=`string`||t.trim().length===0)throw new r({code:`AUTH_FAILED`,message:`accessToken is required. Obtain one via the Daraja Authorization API (GET /oauth/v1/generate?grant_type=client_credentials).`});let a=n.size??300,o=Math.round(n.amount),s={MerchantName:n.merchantName.trim(),RefNo:n.refNo.trim(),Amount:o,TrxCode:n.trxCode,CPI:n.cpi.trim(),Size:String(a)},{data:c}=await l(`${e}/mpesa/qrcode/v1/generate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});if(Le(c))throw Ie(c.errorCode,c.errorMessage);if(!Re(c))throw new r({code:`REQUEST_FAILED`,message:`Daraja returned an unexpected response structure for the Dynamic QR request. The response was missing required fields (ResponseCode, QRCode). Raw response: ${JSON.stringify(c).slice(0,300)}`});return c}async function Be(e,t,n,r,a){if(!a.transactionId?.trim())throw i({code:`VALIDATION_ERROR`,message:`transactionId is required.`});if(!a.receiverParty?.trim())throw i({code:`VALIDATION_ERROR`,message:`receiverParty is required.`});if(![`1`,`2`,`4`].includes(a.receiverIdentifierType))throw i({code:`VALIDATION_ERROR`,message:`receiverIdentifierType must be "1", "2", or "4".`});let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount}).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let s={Initiator:r,SecurityCredential:n,CommandID:`TransactionReversal`,TransactionID:a.transactionId,Amount:String(o),ReceiverParty:String(a.receiverParty),RecieverIdentifierType:a.receiverIdentifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Transaction Reversal`,Occasion:a.occasion??``},{data:c}=await l(`${e}/mpesa/reversal/v1/request`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return c}function Ve(e){return e.Result.ResultCode===0}function He(e){return e.Result.TransactionID??null}function Ue(e){return e.Result.ConversationID}const U={MIN_AMOUNT:1,MAX_AMOUNT:25e4},We={SUCCESS:0,INSUFFICIENT_BALANCE:1,CANCELLED_BY_USER:1032,PHONE_UNREACHABLE:1037,INVALID_PIN:2001};function W(e){return e.ResultCode===We.SUCCESS}function Ge(e,t){let n=e.Body.stkCallback;if(W(n))return n.CallbackMetadata.Item.find(e=>e.Name===t)?.Value}function G(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw new r({code:`INVALID_PHONE`,message:`Cannot parse "${e}". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`});if(n.length!==12)throw new r({code:`INVALID_PHONE`,message:`"${e}" normalised to "${n}" — expected 12 digits.`});return n}function K(e,t,n){return btoa(`${e}${t}${n}`)}function q(){let e=new Date,t=e=>e.toString().padStart(2,`0`);return[e.getFullYear(),t(e.getMonth()+1),t(e.getDate()),t(e.getHours()),t(e.getMinutes()),t(e.getSeconds())].join(``)}async function Ke(e,t,n){let i=Math.round(n.amount);if(!Number.isFinite(i)||i<U.MIN_AMOUNT)throw new r({code:`VALIDATION_ERROR`,message:`Amount must be at least KES ${U.MIN_AMOUNT} (got ${n.amount} which rounds to ${i}).`});if(i>U.MAX_AMOUNT)throw new r({code:`VALIDATION_ERROR`,message:`Amount must not exceed KES ${U.MAX_AMOUNT.toLocaleString()} per transaction as per Safaricom Daraja limits (got ${n.amount} which rounds to ${i}).`});let a=q(),o=n.partyB??n.shortCode,s={BusinessShortCode:n.shortCode,Password:K(n.shortCode,n.passKey,a),Timestamp:a,TransactionType:n.transactionType??`CustomerPayBillOnline`,Amount:i,PartyA:G(n.phoneNumber),PartyB:o,PhoneNumber:G(n.phoneNumber),CallBackURL:n.callbackUrl,AccountReference:n.accountReference.slice(0,12),TransactionDesc:n.transactionDesc.slice(0,13)},{data:c}=await l(`${e}/mpesa/stkpush/v1/processrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s,retries:5,retryDelay:3e3});return c}async function qe(e,t,n){let r=q(),i={BusinessShortCode:n.shortCode,Password:K(n.shortCode,n.passKey,r),Timestamp:r,CheckoutRequestID:n.checkoutRequestId},{data:a}=await l(`${e}/mpesa/stkpushquery/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}const Je=`572572`,J=`PayTaxToKRA`;async function Y(e,t,n,r,a){let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount} which rounds to ${o}).`});if(!a.partyA)throw i({code:`VALIDATION_ERROR`,message:`partyA is required — your M-PESA business shortcode from which tax is deducted.`});if(!a.accountReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`accountReference is required — the Payment Registration Number (PRN) issued by KRA.`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the tax remittance result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={Initiator:r,SecurityCredential:n,CommandID:J,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(o),PartyA:String(a.partyA),PartyB:a.partyB??`572572`,AccountReference:a.accountReference,Remarks:a.remarks??`Tax Remittance`,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl},{data:c}=await l(`${e}/mpesa/b2b/v1/remittax`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return c}async function Ye(e,t,n,r,a){if(!a.transactionId)throw i({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!a.partyA)throw i({code:`VALIDATION_ERROR`,message:`partyA is required (your business shortcode, till number, or MSISDN)`});if(!a.identifierType)throw i({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" (MSISDN) | "2" (Till) | "4" (ShortCode)`});if(!a.resultUrl)throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the transaction result here`});if(!a.queueTimeOutUrl)throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on timeout`});let o={Initiator:r,SecurityCredential:n,CommandID:a.commandId??`TransactionStatusQuery`,TransactionID:a.transactionId,PartyA:a.partyA,IdentifierType:a.identifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Transaction Status Query`,Occasion:a.occasion??``},{data:s}=await l(`${e}/mpesa/transactionstatus/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}const Xe=new Set([`BusinessPayment`,`SalaryPayment`,`PromotionPayment`]);async function Ze(e,t,n,r,a){if(!a.commandId||!Xe.has(a.commandId))throw i({code:`VALIDATION_ERROR`,message:`commandId must be one of: BusinessPayment, SalaryPayment, PromotionPayment. Got "${a.commandId}".`});let o=Math.round(a.amount);if(!Number.isFinite(o)||o<10)throw i({code:`VALIDATION_ERROR`,message:`amount must be ≥ 10 KES (got ${a.amount} which rounds to ${o}).`});if(!a.originatorConversationId?.trim())throw i({code:`VALIDATION_ERROR`,message:`originatorConversationId is required — a unique ID per request.`});if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required — the sending organisation shortcode.`});if(!a.partyB?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyB is required — the receiving customer MSISDN (2547XXXXXXXX).`});if(!a.remarks?.trim())throw i({code:`VALIDATION_ERROR`,message:`remarks is required (2–100 characters).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the async result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={OriginatorConversationID:a.originatorConversationId,InitiatorName:r,SecurityCredential:n,CommandID:a.commandId,Amount:o,PartyA:String(a.partyA),PartyB:String(a.partyB),Remarks:a.remarks,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl};a.occasion?.trim()&&(s.Occassion=a.occasion);let{data:c}=await l(`${e}/mpesa/b2c/v3/paymentrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return c}const X={sandbox:`https://sandbox.safaricom.co.ke`,production:`https://api.safaricom.co.ke`};var Qe=class{config;tokenManager;baseUrl;constructor(e){if(!e.consumerKey||!e.consumerSecret)throw new r({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required.`});this.config=e,this.baseUrl=X[e.environment],this.tokenManager=new u(e.consumerKey,e.consumerSecret,this.baseUrl)}getToken(){return this.tokenManager.getAccessToken()}async buildSecurityCredential(){if(this.config.securityCredential)return this.config.securityCredential;if(!this.config.initiatorPassword)throw new r({code:`INVALID_CREDENTIALS`,message:`Provide securityCredential (pre-encrypted) OR (initiatorPassword + certificatePath/certificatePem).`});let t;if(this.config.certificatePem)t=this.config.certificatePem;else if(this.config.certificatePath)t=await e(this.config.certificatePath,`utf-8`);else throw new r({code:`INVALID_CREDENTIALS`,message:`certificatePath or certificatePem is required to encrypt the initiator password.`});return d(this.config.initiatorPassword,t)}requireInitiator(e){let t=this.config.initiatorName??``;if(!t)throw new r({code:`VALIDATION_ERROR`,message:`initiatorName is required for ${e}.`});return t}async stkPushSafe(e){try{return _(await this.stkPush(e))}catch(e){return v(e)}}async stkPush(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new r({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push.`});let i=await this.getToken();return Ke(this.baseUrl,i,{...e,shortCode:t,passKey:n})}async stkQuery(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new r({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query.`});let i=await this.getToken();return qe(this.baseUrl,i,{...e,shortCode:t,passKey:n})}async transactionStatus(e){let t=this.requireInitiator(`Transaction Status`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Ye(this.baseUrl,n,r,t,e)}async accountBalance(e){let t=this.requireInitiator(`Account Balance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return b(this.baseUrl,n,r,t,e)}async reverseTransaction(e){let t=this.requireInitiator(`Reversal`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Be(this.baseUrl,n,r,t,e)}async generateDynamicQR(e){let t=await this.getToken();return ze(this.baseUrl,t,e)}async registerC2BUrls(e){let t=await this.getToken();return z(this.baseUrl,t,e)}async simulateC2B(e){let t=await this.getToken();return B(this.baseUrl,t,e)}async remitTax(e){let t=this.requireInitiator(`Tax Remittance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Y(this.baseUrl,n,r,t,e)}async b2bExpressCheckout(e){let t=await this.getToken();return C(this.baseUrl,t,e)}async b2cPayment(e){let t=this.requireInitiator(`B2C Payment`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return E(this.baseUrl,n,r,t,e)}async b2cDisbursement(e){let t=this.requireInitiator(`B2C Disbursement`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Ze(this.baseUrl,n,r,t,e)}async billManagerOptIn(e){let t=await this.getToken();return F(this.baseUrl,t,e)}async sendInvoice(e){let t=await this.getToken();return I(this.baseUrl,t,e)}async sendBulkInvoices(e){let t=await this.getToken();return L(this.baseUrl,t,e)}async cancelInvoice(e){let t=await this.getToken();return _e(this.baseUrl,t,e)}clearTokenCache(){this.tokenManager.clearCache()}get environment(){return this.config.environment}};const $e={maxRetries:1/0,initialDelay:1e3,maxDelay:36e5,backoffMultiplier:2,maxRetryDuration:720*60*60*1e3};async function et(e,t={}){let n={...$e,...t},r=n.initialDelay,i=0,a=Date.now();for(;i<n.maxRetries;){if(i++,Date.now()-a>n.maxRetryDuration)return{success:!1,attempts:i,error:Error(`Max retry duration exceeded`)};try{return{success:!0,data:await e(),attempts:i}}catch(e){let t=e instanceof Error?e:Error(String(e));if(t.message.includes(`4`))return{success:!1,attempts:i,error:t};i<n.maxRetries&&(await new Promise(e=>setTimeout(e,r)),r=Math.min(r*n.backoffMultiplier,n.maxDelay))}}return{success:!1,attempts:i,error:Error(`Max retries exceeded`)}}const Z=[`196.201.214.200`,`196.201.214.206`,`196.201.213.114`,`196.201.214.207`,`196.201.214.208`,`196.201.213.44`,`196.201.212.127`,`196.201.212.138`,`196.201.212.129`,`196.201.212.136`,`196.201.212.74`,`196.201.212.69`];function Q(e,t=Z){return t.includes(e)}function $(e){try{let t=e;return t?.Body?.stkCallback?t:null}catch{return null}}function tt(e,t={}){if(!t.skipIPCheck&&t.requestIP&&!Q(t.requestIP,t.allowedIPs))return{success:!1,eventType:null,data:null,error:`IP address ${t.requestIP} is not in the Safaricom whitelist`};let n=$(e);return n?{success:!0,eventType:`stk_push`,data:n}:{success:!1,eventType:null,data:null,error:`Unknown or malformed webhook payload`}}function nt(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`MpesaReceiptNumber`);return t?String(t.Value):null}function rt(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`Amount`);return t?Number(t.Value):null}function it(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`PhoneNumber`);return t?String(t.Value):null}function at(e){return e.Body?.stkCallback?.ResultCode===0}export{ce as B2C_ERROR_CODES,D as B2C_RESULT_CODES,X as DARAJA_BASE_URLS,Je as KRA_SHORTCODE,Qe as Mpesa,r as PesafyError,Z as SAFARICOM_IPS,J as TAX_COMMAND_ID,xe as acceptC2BValidation,Ce as acknowledgeC2BConfirmation,i as createError,d as encryptSecurityCredential,v as err,rt as extractAmount,it as extractPhoneNumber,nt as extractTransactionId,G as formatPhoneNumber,G as formatSafaricomPhone,S as getAccountBalanceParam,ae as getB2BAmount,se as getB2BConversationId,ie as getB2BRequestId,oe as getB2BTransactionId,ge as getB2CAmount,pe as getB2CConversationId,k as getB2CCurrency,M as getB2CDebitAccountBalance,N as getB2CDebitPartyCharges,me as getB2COriginatorConversationId,A as getB2CReceiverPublicName,he as getB2CResultDesc,P as getB2CResultParam,j as getB2CTransactionCompletedTime,fe as getB2CTransactionId,Ee as getC2BAccountRef,we as getC2BAmount,De as getC2BCustomerName,Te as getC2BTransactionId,Ge as getCallbackValue,Ue as getReversalConversationId,He as getReversalTransactionId,q as getTimestamp,tt as handleWebhook,l as httpRequest,C as initiateB2BExpressCheckout,E as initiateB2CPayment,ee as isAccountBalanceSuccess,ne as isB2BCheckoutCallback,re as isB2BCheckoutCancelled,T as isB2BCheckoutSuccess,ue as isB2CFailure,le as isB2CResult,O as isB2CSuccess,ke as isBuyGoodsPayment,be as isC2BPayload,de as isKnownB2CResultCode,Oe as isPaybillPayment,a as isPesafyError,Ve as isReversalSuccess,W as isStkCallbackSuccess,at as isSuccessfulCallback,_ as ok,x as parseAccountBalance,$ as parseStkPushWebhook,z as registerC2BUrls,Se as rejectC2BValidation,Y as remitTax,et as retryWithBackoff,B as simulateC2B,f as toKesAmount,p as toMsisdn,y as toNonEmpty,m as toPaybill,g as toShortCode,h as toTill,Q as verifyWebhookIP};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{readFile as e}from"node:fs/promises";import{constants as t,publicEncrypt as n}from"node:crypto";var r=class e extends Error{code;statusCode;response;requestId;cause;retryable;constructor(t){super(t.message),Object.defineProperty(this,`name`,{value:`PesafyError`}),this.code=t.code,this.statusCode=t.statusCode,this.response=t.response,this.requestId=t.requestId,this.cause=t.cause,this.retryable=t.retryable??(t.code===`NETWORK_ERROR`||t.code===`TIMEOUT`||t.code===`RATE_LIMITED`||t.code===`REQUEST_FAILED`),Error.captureStackTrace&&Error.captureStackTrace(this,e)}get isValidation(){return this.code===`VALIDATION_ERROR`}get isAuth(){return this.code===`AUTH_FAILED`||this.code===`INVALID_CREDENTIALS`}toJSON(){return{name:this.name,code:this.code,message:this.message,statusCode:this.statusCode,requestId:this.requestId,retryable:this.retryable}}};function i(e){return new r(e)}const a=new Set([429,500,502,503,504]);function o(e){return new Promise(t=>setTimeout(t,e))}function s(e){let t=e*.25;return e+(Math.random()*t*2-t)}async function c(e,t){let n=t.retries??4,i=t.retryDelay??2e3,c=t.timeout??3e4,l={"Content-Type":`application/json`,Accept:`application/json`,...t.headers};t.idempotencyKey&&(l[`Idempotency-Key`]=t.idempotencyKey);let u={method:t.method,headers:l,...t.body===void 0?{}:{body:JSON.stringify(t.body)}},d=null;for(let l=0;l<=n;l++){if(l>0){let r=s(i*2**(l-1));console.warn(`[pesafy] Retry ${l}/${n} → ${t.method} ${e} in ${Math.round(r)} ms`),await o(r)}let f=new AbortController,p=setTimeout(()=>f.abort(),c),m;try{m=await fetch(e,{...u,signal:f.signal})}catch(t){if(clearTimeout(p),d=t instanceof Error&&t.name===`AbortError`?new r({code:`TIMEOUT`,message:`Request to ${e} timed out after ${c} ms`,cause:t,retryable:!0}):new r({code:`NETWORK_ERROR`,message:`Network error: ${t instanceof Error?t.message:String(t)}`,cause:t,retryable:!0}),l<n)continue;throw d}finally{clearTimeout(p)}let h=``,g=null,_=m.headers.get(`content-type`)??``;try{h=await m.text(),h&&(g=_.includes(`application/json`)?JSON.parse(h):h)}catch{g=h||null}let v={};if(m.headers.forEach((e,t)=>{v[t]=e}),m.ok)return{data:g,status:m.status,headers:v};let y=a.has(m.status),b=typeof g==`object`&&g?g:{},x=b.errorMessage??b.ResponseDescription??b.resultDesc??h??`HTTP ${m.status}`;if(d=new r({code:y?`REQUEST_FAILED`:`API_ERROR`,message:x,statusCode:m.status,response:g,retryable:y,...typeof b.requestId==`string`?{requestId:b.requestId}:{}}),!(y&&l<n))throw d}throw d}var l=class{consumerKey;consumerSecret;baseUrl;cachedToken=null;tokenExpiresAt=0;constructor(e,t,n){this.consumerKey=e,this.consumerSecret=t,this.baseUrl=n}getBasicAuthHeader(){let e=`${this.consumerKey}:${this.consumerSecret}`;return`Basic ${Buffer.from(e,`utf-8`).toString(`base64`)}`}async getAccessToken(){let e=Date.now()/1e3;if(this.cachedToken&&this.tokenExpiresAt>e+60)return this.cachedToken;let t=await c(`${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`,{method:`GET`,headers:{Authorization:this.getBasicAuthHeader()}}),{access_token:n,expires_in:i}=t.data;if(!n)throw new r({code:`AUTH_FAILED`,message:`Daraja did not return an access token. Check your consumer key and secret.`,response:t.data});return this.cachedToken=n,this.tokenExpiresAt=e+(i??3600),this.cachedToken}clearCache(){this.cachedToken=null,this.tokenExpiresAt=0}};function u(e,i){try{let r=Buffer.from(e,`utf-8`);return n({key:i,padding:t.RSA_PKCS1_PADDING},r).toString(`base64`)}catch(e){throw new r({code:`ENCRYPTION_FAILED`,message:`Failed to encrypt security credential. Ensure the certificate PEM is valid and matches the environment (sandbox/production).`,cause:e})}}function d(e){return{ok:!0,data:e}}function f(e){return{ok:!1,error:e}}async function p(e,t,n,r,a){if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required.`});if(![`1`,`2`,`4`].includes(a.identifierType))throw i({code:`VALIDATION_ERROR`,message:`identifierType must be "1" (MSISDN), "2" (Till), or "4" (ShortCode).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let o={Initiator:r,SecurityCredential:n,CommandID:`AccountBalance`,PartyA:String(a.partyA),IdentifierType:a.identifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Account Balance Query`},{data:s}=await c(`${e}/mpesa/accountbalance/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}function m(){try{if(typeof crypto<`u`&&typeof crypto.randomUUID==`function`)return crypto.randomUUID()}catch{}return`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`}async function h(e,t,n){if(!n.primaryShortCode||!String(n.primaryShortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`primaryShortCode is required — the merchant's till number (debit party).`});if(!n.receiverShortCode||!String(n.receiverShortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — the vendor's Paybill account (credit party).`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount}).`});if(!n.paymentRef||!String(n.paymentRef).trim())throw i({code:`VALIDATION_ERROR`,message:`paymentRef is required — shown in the merchant's USSD prompt as the payment reference.`});if(!n.callbackUrl||!String(n.callbackUrl).trim())throw i({code:`VALIDATION_ERROR`,message:`callbackUrl is required — Daraja POSTs the transaction result here.`});if(!n.partnerName||!String(n.partnerName).trim())throw i({code:`VALIDATION_ERROR`,message:`partnerName is required — vendor's friendly name shown in the merchant's USSD prompt.`});let a={primaryShortCode:String(n.primaryShortCode),receiverShortCode:String(n.receiverShortCode),amount:String(r),paymentRef:n.paymentRef,callbackUrl:n.callbackUrl,partnerName:n.partnerName,RequestRefID:n.requestRefId??m()},{data:o}=await c(`${e}/v1/ussdpush/get-msisdn`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function g(e,t,n,r,a){if(!a.commandId)throw i({code:`VALIDATION_ERROR`,message:`commandId is required: "BusinessPayToBulk" | "BusinessPayment" | "SalaryPayment" | "PromotionPayment"`});let o=[`BusinessPayToBulk`,`BusinessPayment`,`SalaryPayment`,`PromotionPayment`];if(!o.includes(a.commandId))throw i({code:`VALIDATION_ERROR`,message:`commandId must be one of: ${o.join(`, `)}. Got: "${a.commandId}"`});let s=Math.round(a.amount);if(!Number.isFinite(s)||s<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount} which rounds to ${s}).`});if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required — your business shortcode from which money is deducted.`});if(!a.partyB?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyB is required — the recipient shortcode (BusinessPayToBulk) or customer MSISDN (other commands).`});if(!a.accountReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`accountReference is required — a reference for this transaction.`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the B2C result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let l={Initiator:r,SecurityCredential:n,CommandID:a.commandId,SenderIdentifierType:a.senderIdentifierType??`4`,RecieverIdentifierType:a.receiverIdentifierType??`4`,Amount:String(s),PartyA:String(a.partyA),PartyB:String(a.partyB),AccountReference:a.accountReference,Remarks:a.remarks??`B2C Payment`,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl};a.requester?.trim()&&(l.Requester=String(a.requester));let{data:u}=await c(`${e}/mpesa/b2b/v1/paymentrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:l});return u}async function _(e,t,n){if(!n.shortcode?.trim())throw i({code:`VALIDATION_ERROR`,message:`shortcode is required.`});if(!n.email?.trim())throw i({code:`VALIDATION_ERROR`,message:`email is required.`});if(!n.callbackUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`callbackUrl is required.`});let r={shortcode:n.shortcode,email:n.email,officialContact:n.officialContact,sendReminders:n.sendReminders,logo:n.logo??``,callbackUrl:n.callbackUrl},{data:a}=await c(`${e}/v1/billmanager-invoice/optin`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:r});return a}async function v(e,t,n){if(!n.externalReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`externalReference is required.`});if(!n.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA (customer MSISDN) is required.`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be ≥ 1 (got ${n.amount}).`});let a={externalReference:n.externalReference,billingPeriod:n.billingPeriod,invoiceName:n.invoiceName,dueDate:n.dueDate,accountReference:n.accountReference,amount:String(r),partyA:n.partyA,invoiceItems:n.invoiceItems?.map(e=>({itemName:e.itemName,amount:String(Math.round(e.amount))}))??[]},{data:o}=await c(`${e}/v1/billmanager-invoice/single-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function y(e,t,n){if(!n.invoices?.length)throw i({code:`VALIDATION_ERROR`,message:`invoices array must not be empty.`});if(n.invoices.length>1e3)throw i({code:`VALIDATION_ERROR`,message:`Maximum 1 000 invoices per bulk request.`});let{data:r}=await c(`${e}/v1/billmanager-invoice/bulk-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:n.invoices});return r}async function b(e,t,n){if(!n.externalReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`externalReference is required.`});let{data:r}=await c(`${e}/v1/billmanager-invoice/cancel-single-invoice`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:[{externalReference:n.externalReference}]});return r}const x=[`mpesa`,`safaricom`,`exec`,`exe`,`cmd`,`sql`,`query`],S=[`Completed`,`Cancelled`];function C(e,t){if(!e||!e.trim())throw i({code:`VALIDATION_ERROR`,message:`${t} is required`});let n=e.toLowerCase();for(let e of x)if(n.includes(e))throw i({code:`VALIDATION_ERROR`,message:`${t} must not contain the keyword "${e}". Daraja rejects URLs containing: mpesa, safaricom, exec, exe, cmd, sql, query (and their variants).`})}async function w(e,t,n){if(!n.shortCode||!String(n.shortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!n.responseType)throw i({code:`VALIDATION_ERROR`,message:`responseType is required: "Completed" or "Cancelled" (sentence case)`});if(!S.includes(n.responseType))throw i({code:`VALIDATION_ERROR`,message:`responseType must be exactly "Completed" or "Cancelled" (sentence case, correctly spelled). Got: "${String(n.responseType)}"`});C(n.confirmationUrl,`confirmationUrl`),C(n.validationUrl,`validationUrl`);let r=n.apiVersion??`v2`,a={ShortCode:String(n.shortCode),ResponseType:n.responseType,ConfirmationURL:n.confirmationUrl,ValidationURL:n.validationUrl},{data:o}=await c(`${e}/mpesa/c2b/${r}/registerurl`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function T(e,t,n){if(!e.includes(`sandbox`))throw i({code:`VALIDATION_ERROR`,message:`C2B simulate is only available in the Sandbox environment. In production, customers initiate payments via M-PESA App, USSD, or SIM Toolkit.`});if(!n.shortCode||!String(n.shortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(n.commandId!==`CustomerPayBillOnline`&&n.commandId!==`CustomerBuyGoodsOnline`)throw i({code:`VALIDATION_ERROR`,message:`commandId must be "CustomerPayBillOnline" or "CustomerBuyGoodsOnline". Got: "${String(n.commandId)}"`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount})`});if(!n.msisdn||!String(n.msisdn).trim())throw i({code:`VALIDATION_ERROR`,message:`msisdn is required. Sandbox test MSISDN: 254708374149`});let a=n.commandId===`CustomerBuyGoodsOnline`,o=n.apiVersion??`v2`,s={ShortCode:Number(n.shortCode),CommandID:n.commandId,Amount:r,Msisdn:Number(n.msisdn)};a||(s.BillRefNumber=n.billRefNumber??``);let{data:l}=await c(`${e}/mpesa/c2b/${o}/simulate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}const E=[`BG`,`WA`,`PB`,`SM`,`SB`];function D(e){return typeof e!=`string`||e.trim().length===0?`merchantName is required and must be a non-empty string`:null}function O(e){return typeof e!=`string`||e.trim().length===0?`refNo (transaction reference) is required and must be a non-empty string`:null}function k(e){return typeof e!=`number`||!Number.isFinite(e)?`amount must be a finite number`:Math.round(e)<1?`amount must be at least 1 KES (got ${e})`:null}function A(e){return E.includes(e)?null:`trxCode must be one of: ${E.join(`, `)} (BG=Buy Goods, WA=Withdraw Cash, PB=Paybill, SM=Send Money, SB=Send to Business)`}function j(e){return typeof e!=`string`||e.trim().length===0?`cpi (Credit Party Identifier) is required and must be a non-empty string`:null}function M(e){return e==null?null:typeof e!=`number`||!Number.isFinite(e)?`size must be a finite number when provided`:!Number.isInteger(e)||e<1?`size must be a positive integer (minimum 1)`:e>1e3?`size must not exceed 1000 pixels (got ${e})`:null}function N(e){if(typeof e!=`object`||!e)return{valid:!1,errors:{payload:`request payload must be a non-null object`}};let t=e,n={},r=D(t.merchantName);r&&(n.merchantName=r);let i=O(t.refNo);i&&(n.refNo=i);let a=k(t.amount);a&&(n.amount=a);let o=A(t.trxCode);o&&(n.trxCode=o);let s=j(t.cpi);s&&(n.cpi=s);let c=M(t.size);return c&&(n.size=c),Object.keys(n).length>0?{valid:!1,errors:n}:{valid:!0}}function P(e,t){switch(e){case`404.001.04`:return new r({code:`AUTH_FAILED`,message:`Daraja rejected the request due to an invalid authentication header. Ensure the Dynamic QR endpoint is called with POST and that the Authorization: Bearer <token> header is present. Daraja: "${t}"`,statusCode:404});case`400.003.01`:return new r({code:`AUTH_FAILED`,message:`The M-PESA access token is invalid or has expired. Call clearTokenCache() on the Mpesa instance to force a token refresh and retry the request. Daraja: "${t}"`,statusCode:401});case`400.002.05`:return new r({code:`VALIDATION_ERROR`,message:`Daraja rejected the request payload as malformed. Verify that all required fields (MerchantName, RefNo, Amount, TrxCode, CPI, Size) are present and have correct types. Daraja: "${t}"`,statusCode:400});default:return new r({code:`REQUEST_FAILED`,message:`Dynamic QR request failed (${e}): ${t}`,statusCode:400})}}function F(e){return typeof e==`object`&&!!e&&`errorCode`in e&&typeof e.errorCode==`string`}function I(e){return typeof e==`object`&&!!e&&`ResponseCode`in e&&`QRCode`in e&&typeof e.QRCode==`string`&&e.QRCode.length>0}async function L(e,t,n){let i=N(n);if(!i.valid)throw new r({code:`VALIDATION_ERROR`,message:`Dynamic QR request validation failed:\n${Object.entries(i.errors).map(([e,t])=>` • ${e}: ${t}`).join(`
|
|
2
|
-
`)}`});if(!t||typeof t!=`string`||t.trim().length===0)throw new r({code:`AUTH_FAILED`,message:`accessToken is required. Obtain one via the Daraja Authorization API (GET /oauth/v1/generate?grant_type=client_credentials).`});let a=n.size??300,o=Math.round(n.amount),s={MerchantName:n.merchantName.trim(),RefNo:n.refNo.trim(),Amount:o,TrxCode:n.trxCode,CPI:n.cpi.trim(),Size:String(a)},{data:l}=await c(`${e}/mpesa/qrcode/v1/generate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});if(F(l))throw P(l.errorCode,l.errorMessage);if(!I(l))throw new r({code:`REQUEST_FAILED`,message:`Daraja returned an unexpected response structure for the Dynamic QR request. The response was missing required fields (ResponseCode, QRCode). Raw response: ${JSON.stringify(l).slice(0,300)}`});return l}async function R(e,t,n,r,a){if(!a.transactionId?.trim())throw i({code:`VALIDATION_ERROR`,message:`transactionId is required.`});if(!a.receiverParty?.trim())throw i({code:`VALIDATION_ERROR`,message:`receiverParty is required.`});if(![`1`,`2`,`4`].includes(a.receiverIdentifierType))throw i({code:`VALIDATION_ERROR`,message:`receiverIdentifierType must be "1", "2", or "4".`});let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount}).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let s={Initiator:r,SecurityCredential:n,CommandID:`TransactionReversal`,TransactionID:a.transactionId,Amount:String(o),ReceiverParty:String(a.receiverParty),RecieverIdentifierType:a.receiverIdentifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Transaction Reversal`,Occasion:a.occasion??``},{data:l}=await c(`${e}/mpesa/reversal/v1/request`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}const z={MIN_AMOUNT:1,MAX_AMOUNT:25e4};function B(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw new r({code:`INVALID_PHONE`,message:`Cannot parse "${e}". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`});if(n.length!==12)throw new r({code:`INVALID_PHONE`,message:`"${e}" normalised to "${n}" — expected 12 digits.`});return n}function V(e,t,n){return btoa(`${e}${t}${n}`)}function H(){let e=new Date,t=e=>e.toString().padStart(2,`0`);return[e.getFullYear(),t(e.getMonth()+1),t(e.getDate()),t(e.getHours()),t(e.getMinutes()),t(e.getSeconds())].join(``)}async function U(e,t,n){let i=Math.round(n.amount);if(!Number.isFinite(i)||i<z.MIN_AMOUNT)throw new r({code:`VALIDATION_ERROR`,message:`Amount must be at least KES ${z.MIN_AMOUNT} (got ${n.amount} which rounds to ${i}).`});if(i>z.MAX_AMOUNT)throw new r({code:`VALIDATION_ERROR`,message:`Amount must not exceed KES ${z.MAX_AMOUNT.toLocaleString()} per transaction as per Safaricom Daraja limits (got ${n.amount} which rounds to ${i}).`});let a=H(),o=n.partyB??n.shortCode,s={BusinessShortCode:n.shortCode,Password:V(n.shortCode,n.passKey,a),Timestamp:a,TransactionType:n.transactionType??`CustomerPayBillOnline`,Amount:i,PartyA:B(n.phoneNumber),PartyB:o,PhoneNumber:B(n.phoneNumber),CallBackURL:n.callbackUrl,AccountReference:n.accountReference.slice(0,12),TransactionDesc:n.transactionDesc.slice(0,13)},{data:l}=await c(`${e}/mpesa/stkpush/v1/processrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s,retries:5,retryDelay:3e3});return l}async function W(e,t,n){let r=H(),i={BusinessShortCode:n.shortCode,Password:V(n.shortCode,n.passKey,r),Timestamp:r,CheckoutRequestID:n.checkoutRequestId},{data:a}=await c(`${e}/mpesa/stkpushquery/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}async function G(e,t,n,r,a){let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount} which rounds to ${o}).`});if(!a.partyA)throw i({code:`VALIDATION_ERROR`,message:`partyA is required — your M-PESA business shortcode from which tax is deducted.`});if(!a.accountReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`accountReference is required — the Payment Registration Number (PRN) issued by KRA.`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the tax remittance result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={Initiator:r,SecurityCredential:n,CommandID:`PayTaxToKRA`,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(o),PartyA:String(a.partyA),PartyB:a.partyB??`572572`,AccountReference:a.accountReference,Remarks:a.remarks??`Tax Remittance`,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl},{data:l}=await c(`${e}/mpesa/b2b/v1/remittax`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}async function K(e,t,n,r,a){if(!a.transactionId)throw i({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!a.partyA)throw i({code:`VALIDATION_ERROR`,message:`partyA is required (your business shortcode, till number, or MSISDN)`});if(!a.identifierType)throw i({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" (MSISDN) | "2" (Till) | "4" (ShortCode)`});if(!a.resultUrl)throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the transaction result here`});if(!a.queueTimeOutUrl)throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on timeout`});let o={Initiator:r,SecurityCredential:n,CommandID:a.commandId??`TransactionStatusQuery`,TransactionID:a.transactionId,PartyA:a.partyA,IdentifierType:a.identifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Transaction Status Query`,Occasion:a.occasion??``},{data:s}=await c(`${e}/mpesa/transactionstatus/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}const q={sandbox:`https://sandbox.safaricom.co.ke`,production:`https://api.safaricom.co.ke`};var J=class{config;tokenManager;baseUrl;constructor(e){if(!e.consumerKey||!e.consumerSecret)throw new r({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required.`});this.config=e,this.baseUrl=q[e.environment],this.tokenManager=new l(e.consumerKey,e.consumerSecret,this.baseUrl)}getToken(){return this.tokenManager.getAccessToken()}async buildSecurityCredential(){if(this.config.securityCredential)return this.config.securityCredential;if(!this.config.initiatorPassword)throw new r({code:`INVALID_CREDENTIALS`,message:`Provide securityCredential (pre-encrypted) OR (initiatorPassword + certificatePath/certificatePem).`});let t;if(this.config.certificatePem)t=this.config.certificatePem;else if(this.config.certificatePath)t=await e(this.config.certificatePath,`utf-8`);else throw new r({code:`INVALID_CREDENTIALS`,message:`certificatePath or certificatePem is required to encrypt the initiator password.`});return u(this.config.initiatorPassword,t)}requireInitiator(e){let t=this.config.initiatorName??``;if(!t)throw new r({code:`VALIDATION_ERROR`,message:`initiatorName is required for ${e}.`});return t}async stkPushSafe(e){try{return d(await this.stkPush(e))}catch(e){return f(e)}}async stkPush(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new r({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push.`});let i=await this.getToken();return U(this.baseUrl,i,{...e,shortCode:t,passKey:n})}async stkQuery(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new r({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query.`});let i=await this.getToken();return W(this.baseUrl,i,{...e,shortCode:t,passKey:n})}async transactionStatus(e){let t=this.requireInitiator(`Transaction Status`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return K(this.baseUrl,n,r,t,e)}async accountBalance(e){let t=this.requireInitiator(`Account Balance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return p(this.baseUrl,n,r,t,e)}async reverseTransaction(e){let t=this.requireInitiator(`Reversal`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return R(this.baseUrl,n,r,t,e)}async generateDynamicQR(e){let t=await this.getToken();return L(this.baseUrl,t,e)}async registerC2BUrls(e){let t=await this.getToken();return w(this.baseUrl,t,e)}async simulateC2B(e){let t=await this.getToken();return T(this.baseUrl,t,e)}async remitTax(e){let t=this.requireInitiator(`Tax Remittance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return G(this.baseUrl,n,r,t,e)}async b2bExpressCheckout(e){let t=await this.getToken();return h(this.baseUrl,t,e)}async b2cPayment(e){let t=this.requireInitiator(`B2C Payment`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return g(this.baseUrl,n,r,t,e)}async billManagerOptIn(e){let t=await this.getToken();return _(this.baseUrl,t,e)}async sendInvoice(e){let t=await this.getToken();return v(this.baseUrl,t,e)}async sendBulkInvoices(e){let t=await this.getToken();return y(this.baseUrl,t,e)}async cancelInvoice(e){let t=await this.getToken();return b(this.baseUrl,t,e)}clearTokenCache(){this.tokenManager.clearCache()}get environment(){return this.config.environment}};const Y=[`196.201.214.200`,`196.201.214.206`,`196.201.213.114`,`196.201.214.207`,`196.201.214.208`,`196.201.213.44`,`196.201.212.127`,`196.201.212.138`,`196.201.212.129`,`196.201.212.136`,`196.201.212.74`,`196.201.212.69`];function X(e,t=Y){return t.includes(e)}function Z(e){try{let t=e;return t?.Body?.stkCallback?t:null}catch{return null}}export{r as i,X as n,J as r,Z as t};
|
|
1
|
+
import{readFile as e}from"node:fs/promises";import{constants as t,publicEncrypt as n}from"node:crypto";var r=class e extends Error{code;statusCode;response;requestId;cause;retryable;constructor(t){super(t.message),Object.defineProperty(this,`name`,{value:`PesafyError`}),this.code=t.code,this.statusCode=t.statusCode,this.response=t.response,this.requestId=t.requestId,this.cause=t.cause,this.retryable=t.retryable??(t.code===`NETWORK_ERROR`||t.code===`TIMEOUT`||t.code===`RATE_LIMITED`||t.code===`REQUEST_FAILED`),Error.captureStackTrace&&Error.captureStackTrace(this,e)}get isValidation(){return this.code===`VALIDATION_ERROR`}get isAuth(){return this.code===`AUTH_FAILED`||this.code===`INVALID_CREDENTIALS`}toJSON(){return{name:this.name,code:this.code,message:this.message,statusCode:this.statusCode,requestId:this.requestId,retryable:this.retryable}}};function i(e){return new r(e)}const a=new Set([429,500,502,503,504]);function o(e){return new Promise(t=>setTimeout(t,e))}function s(e){let t=e*.25;return e+(Math.random()*t*2-t)}async function c(e,t){let n=t.retries??4,i=t.retryDelay??2e3,c=t.timeout??3e4,l={"Content-Type":`application/json`,Accept:`application/json`,...t.headers};t.idempotencyKey&&(l[`Idempotency-Key`]=t.idempotencyKey);let u={method:t.method,headers:l,...t.body===void 0?{}:{body:JSON.stringify(t.body)}},d=null;for(let l=0;l<=n;l++){if(l>0){let r=s(i*2**(l-1));console.warn(`[pesafy] Retry ${l}/${n} → ${t.method} ${e} in ${Math.round(r)} ms`),await o(r)}let f=new AbortController,p=setTimeout(()=>f.abort(),c),m;try{m=await fetch(e,{...u,signal:f.signal})}catch(t){if(clearTimeout(p),d=t instanceof Error&&t.name===`AbortError`?new r({code:`TIMEOUT`,message:`Request to ${e} timed out after ${c} ms`,cause:t,retryable:!0}):new r({code:`NETWORK_ERROR`,message:`Network error: ${t instanceof Error?t.message:String(t)}`,cause:t,retryable:!0}),l<n)continue;throw d}finally{clearTimeout(p)}let h=``,g=null,_=m.headers.get(`content-type`)??``;try{h=await m.text(),h&&(g=_.includes(`application/json`)?JSON.parse(h):h)}catch{g=h||null}let v={};if(m.headers.forEach((e,t)=>{v[t]=e}),m.ok)return{data:g,status:m.status,headers:v};let y=a.has(m.status),b=typeof g==`object`&&g?g:{},x=b.errorMessage??b.ResponseDescription??b.resultDesc??h??`HTTP ${m.status}`;if(d=new r({code:y?`REQUEST_FAILED`:`API_ERROR`,message:x,statusCode:m.status,response:g,retryable:y,...typeof b.requestId==`string`?{requestId:b.requestId}:{}}),!(y&&l<n))throw d}throw d}var l=class{consumerKey;consumerSecret;baseUrl;cachedToken=null;tokenExpiresAt=0;constructor(e,t,n){this.consumerKey=e,this.consumerSecret=t,this.baseUrl=n}getBasicAuthHeader(){let e=`${this.consumerKey}:${this.consumerSecret}`;return`Basic ${Buffer.from(e,`utf-8`).toString(`base64`)}`}async getAccessToken(){let e=Date.now()/1e3;if(this.cachedToken&&this.tokenExpiresAt>e+60)return this.cachedToken;let t=await c(`${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`,{method:`GET`,headers:{Authorization:this.getBasicAuthHeader()}}),{access_token:n,expires_in:i}=t.data;if(!n)throw new r({code:`AUTH_FAILED`,message:`Daraja did not return an access token. Check your consumer key and secret.`,response:t.data});return this.cachedToken=n,this.tokenExpiresAt=e+(i??3600),this.cachedToken}clearCache(){this.cachedToken=null,this.tokenExpiresAt=0}};function u(e,i){try{let r=Buffer.from(e,`utf-8`);return n({key:i,padding:t.RSA_PKCS1_PADDING},r).toString(`base64`)}catch(e){throw new r({code:`ENCRYPTION_FAILED`,message:`Failed to encrypt security credential. Ensure the certificate PEM is valid and matches the environment (sandbox/production).`,cause:e})}}function d(e){return{ok:!0,data:e}}function f(e){return{ok:!1,error:e}}async function p(e,t,n,r,a){if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required.`});if(![`1`,`2`,`4`].includes(a.identifierType))throw i({code:`VALIDATION_ERROR`,message:`identifierType must be "1" (MSISDN), "2" (Till), or "4" (ShortCode).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let o={Initiator:r,SecurityCredential:n,CommandID:`AccountBalance`,PartyA:String(a.partyA),IdentifierType:a.identifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Account Balance Query`},{data:s}=await c(`${e}/mpesa/accountbalance/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}function m(){try{if(typeof crypto<`u`&&typeof crypto.randomUUID==`function`)return crypto.randomUUID()}catch{}return`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`}async function h(e,t,n){if(!n.primaryShortCode||!String(n.primaryShortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`primaryShortCode is required — the merchant's till number (debit party).`});if(!n.receiverShortCode||!String(n.receiverShortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — the vendor's Paybill account (credit party).`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount}).`});if(!n.paymentRef||!String(n.paymentRef).trim())throw i({code:`VALIDATION_ERROR`,message:`paymentRef is required — shown in the merchant's USSD prompt as the payment reference.`});if(!n.callbackUrl||!String(n.callbackUrl).trim())throw i({code:`VALIDATION_ERROR`,message:`callbackUrl is required — Daraja POSTs the transaction result here.`});if(!n.partnerName||!String(n.partnerName).trim())throw i({code:`VALIDATION_ERROR`,message:`partnerName is required — vendor's friendly name shown in the merchant's USSD prompt.`});let a={primaryShortCode:String(n.primaryShortCode),receiverShortCode:String(n.receiverShortCode),amount:String(r),paymentRef:n.paymentRef,callbackUrl:n.callbackUrl,partnerName:n.partnerName,RequestRefID:n.requestRefId??m()},{data:o}=await c(`${e}/v1/ussdpush/get-msisdn`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function g(e,t,n,r,a){if(!a.commandId||a.commandId!==`BusinessPayToBulk`)throw i({code:`VALIDATION_ERROR`,message:`commandId must be "BusinessPayToBulk". This is the only CommandID supported by the B2C Account Top Up API.`});let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount} which rounds to ${o}).`});if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required — the sender shortcode from which funds are deducted.`});if(!a.partyB?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyB is required — the receiver B2C shortcode that receives the funds.`});if(!a.accountReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`accountReference is required — a reference for this transaction.`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the async result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={Initiator:r,SecurityCredential:n,CommandID:a.commandId,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(o),PartyA:String(a.partyA),PartyB:String(a.partyB),AccountReference:a.accountReference,Remarks:a.remarks??`B2C Account Top Up`,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl};a.requester?.trim()&&(s.Requester=String(a.requester));let{data:l}=await c(`${e}/mpesa/b2b/v1/paymentrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}async function _(e,t,n){if(!n.shortcode?.trim())throw i({code:`VALIDATION_ERROR`,message:`shortcode is required.`});if(!n.email?.trim())throw i({code:`VALIDATION_ERROR`,message:`email is required.`});if(!n.callbackUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`callbackUrl is required.`});let r={shortcode:n.shortcode,email:n.email,officialContact:n.officialContact,sendReminders:n.sendReminders,logo:n.logo??``,callbackUrl:n.callbackUrl},{data:a}=await c(`${e}/v1/billmanager-invoice/optin`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:r});return a}async function v(e,t,n){if(!n.externalReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`externalReference is required.`});if(!n.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA (customer MSISDN) is required.`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be ≥ 1 (got ${n.amount}).`});let a={externalReference:n.externalReference,billingPeriod:n.billingPeriod,invoiceName:n.invoiceName,dueDate:n.dueDate,accountReference:n.accountReference,amount:String(r),partyA:n.partyA,invoiceItems:n.invoiceItems?.map(e=>({itemName:e.itemName,amount:String(Math.round(e.amount))}))??[]},{data:o}=await c(`${e}/v1/billmanager-invoice/single-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function y(e,t,n){if(!n.invoices?.length)throw i({code:`VALIDATION_ERROR`,message:`invoices array must not be empty.`});if(n.invoices.length>1e3)throw i({code:`VALIDATION_ERROR`,message:`Maximum 1 000 invoices per bulk request.`});let{data:r}=await c(`${e}/v1/billmanager-invoice/bulk-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:n.invoices});return r}async function b(e,t,n){if(!n.externalReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`externalReference is required.`});let{data:r}=await c(`${e}/v1/billmanager-invoice/cancel-single-invoice`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:[{externalReference:n.externalReference}]});return r}const x=[`mpesa`,`safaricom`,`exec`,`exe`,`cmd`,`sql`,`query`],S=[`Completed`,`Cancelled`];function C(e,t){if(!e||!e.trim())throw i({code:`VALIDATION_ERROR`,message:`${t} is required`});let n=e.toLowerCase();for(let e of x)if(n.includes(e))throw i({code:`VALIDATION_ERROR`,message:`${t} must not contain the keyword "${e}". Daraja rejects URLs containing: mpesa, safaricom, exec, exe, cmd, sql, query (and their variants).`})}async function w(e,t,n){if(!n.shortCode||!String(n.shortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!n.responseType)throw i({code:`VALIDATION_ERROR`,message:`responseType is required: "Completed" or "Cancelled" (sentence case)`});if(!S.includes(n.responseType))throw i({code:`VALIDATION_ERROR`,message:`responseType must be exactly "Completed" or "Cancelled" (sentence case, correctly spelled). Got: "${String(n.responseType)}"`});C(n.confirmationUrl,`confirmationUrl`),C(n.validationUrl,`validationUrl`);let r=n.apiVersion??`v2`,a={ShortCode:String(n.shortCode),ResponseType:n.responseType,ConfirmationURL:n.confirmationUrl,ValidationURL:n.validationUrl},{data:o}=await c(`${e}/mpesa/c2b/${r}/registerurl`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function T(e,t,n){if(!e.includes(`sandbox`))throw i({code:`VALIDATION_ERROR`,message:`C2B simulate is only available in the Sandbox environment. In production, customers initiate payments via M-PESA App, USSD, or SIM Toolkit.`});if(!n.shortCode||!String(n.shortCode).trim())throw i({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(n.commandId!==`CustomerPayBillOnline`&&n.commandId!==`CustomerBuyGoodsOnline`)throw i({code:`VALIDATION_ERROR`,message:`commandId must be "CustomerPayBillOnline" or "CustomerBuyGoodsOnline". Got: "${String(n.commandId)}"`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount})`});if(!n.msisdn||!String(n.msisdn).trim())throw i({code:`VALIDATION_ERROR`,message:`msisdn is required. Sandbox test MSISDN: 254708374149`});let a=n.commandId===`CustomerBuyGoodsOnline`,o=n.apiVersion??`v2`,s={ShortCode:Number(n.shortCode),CommandID:n.commandId,Amount:r,Msisdn:Number(n.msisdn)};a||(s.BillRefNumber=n.billRefNumber??``);let{data:l}=await c(`${e}/mpesa/c2b/${o}/simulate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}const E=[`BG`,`WA`,`PB`,`SM`,`SB`];function D(e){return typeof e!=`string`||e.trim().length===0?`merchantName is required and must be a non-empty string`:null}function O(e){return typeof e!=`string`||e.trim().length===0?`refNo (transaction reference) is required and must be a non-empty string`:null}function k(e){return typeof e!=`number`||!Number.isFinite(e)?`amount must be a finite number`:Math.round(e)<1?`amount must be at least 1 KES (got ${e})`:null}function A(e){return E.includes(e)?null:`trxCode must be one of: ${E.join(`, `)} (BG=Buy Goods, WA=Withdraw Cash, PB=Paybill, SM=Send Money, SB=Send to Business)`}function j(e){return typeof e!=`string`||e.trim().length===0?`cpi (Credit Party Identifier) is required and must be a non-empty string`:null}function M(e){return e==null?null:typeof e!=`number`||!Number.isFinite(e)?`size must be a finite number when provided`:!Number.isInteger(e)||e<1?`size must be a positive integer (minimum 1)`:e>1e3?`size must not exceed 1000 pixels (got ${e})`:null}function N(e){if(typeof e!=`object`||!e)return{valid:!1,errors:{payload:`request payload must be a non-null object`}};let t=e,n={},r=D(t.merchantName);r&&(n.merchantName=r);let i=O(t.refNo);i&&(n.refNo=i);let a=k(t.amount);a&&(n.amount=a);let o=A(t.trxCode);o&&(n.trxCode=o);let s=j(t.cpi);s&&(n.cpi=s);let c=M(t.size);return c&&(n.size=c),Object.keys(n).length>0?{valid:!1,errors:n}:{valid:!0}}function P(e,t){switch(e){case`404.001.04`:return new r({code:`AUTH_FAILED`,message:`Daraja rejected the request due to an invalid authentication header. Ensure the Dynamic QR endpoint is called with POST and that the Authorization: Bearer <token> header is present. Daraja: "${t}"`,statusCode:404});case`400.003.01`:return new r({code:`AUTH_FAILED`,message:`The M-PESA access token is invalid or has expired. Call clearTokenCache() on the Mpesa instance to force a token refresh and retry the request. Daraja: "${t}"`,statusCode:401});case`400.002.05`:return new r({code:`VALIDATION_ERROR`,message:`Daraja rejected the request payload as malformed. Verify that all required fields (MerchantName, RefNo, Amount, TrxCode, CPI, Size) are present and have correct types. Daraja: "${t}"`,statusCode:400});default:return new r({code:`REQUEST_FAILED`,message:`Dynamic QR request failed (${e}): ${t}`,statusCode:400})}}function F(e){return typeof e==`object`&&!!e&&`errorCode`in e&&typeof e.errorCode==`string`}function I(e){return typeof e==`object`&&!!e&&`ResponseCode`in e&&`QRCode`in e&&typeof e.QRCode==`string`&&e.QRCode.length>0}async function L(e,t,n){let i=N(n);if(!i.valid)throw new r({code:`VALIDATION_ERROR`,message:`Dynamic QR request validation failed:\n${Object.entries(i.errors).map(([e,t])=>` • ${e}: ${t}`).join(`
|
|
2
|
+
`)}`});if(!t||typeof t!=`string`||t.trim().length===0)throw new r({code:`AUTH_FAILED`,message:`accessToken is required. Obtain one via the Daraja Authorization API (GET /oauth/v1/generate?grant_type=client_credentials).`});let a=n.size??300,o=Math.round(n.amount),s={MerchantName:n.merchantName.trim(),RefNo:n.refNo.trim(),Amount:o,TrxCode:n.trxCode,CPI:n.cpi.trim(),Size:String(a)},{data:l}=await c(`${e}/mpesa/qrcode/v1/generate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});if(F(l))throw P(l.errorCode,l.errorMessage);if(!I(l))throw new r({code:`REQUEST_FAILED`,message:`Daraja returned an unexpected response structure for the Dynamic QR request. The response was missing required fields (ResponseCode, QRCode). Raw response: ${JSON.stringify(l).slice(0,300)}`});return l}async function R(e,t,n,r,a){if(!a.transactionId?.trim())throw i({code:`VALIDATION_ERROR`,message:`transactionId is required.`});if(!a.receiverParty?.trim())throw i({code:`VALIDATION_ERROR`,message:`receiverParty is required.`});if(![`1`,`2`,`4`].includes(a.receiverIdentifierType))throw i({code:`VALIDATION_ERROR`,message:`receiverIdentifierType must be "1", "2", or "4".`});let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount}).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let s={Initiator:r,SecurityCredential:n,CommandID:`TransactionReversal`,TransactionID:a.transactionId,Amount:String(o),ReceiverParty:String(a.receiverParty),RecieverIdentifierType:a.receiverIdentifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Transaction Reversal`,Occasion:a.occasion??``},{data:l}=await c(`${e}/mpesa/reversal/v1/request`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}const z={MIN_AMOUNT:1,MAX_AMOUNT:25e4};function B(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw new r({code:`INVALID_PHONE`,message:`Cannot parse "${e}". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`});if(n.length!==12)throw new r({code:`INVALID_PHONE`,message:`"${e}" normalised to "${n}" — expected 12 digits.`});return n}function V(e,t,n){return btoa(`${e}${t}${n}`)}function H(){let e=new Date,t=e=>e.toString().padStart(2,`0`);return[e.getFullYear(),t(e.getMonth()+1),t(e.getDate()),t(e.getHours()),t(e.getMinutes()),t(e.getSeconds())].join(``)}async function U(e,t,n){let i=Math.round(n.amount);if(!Number.isFinite(i)||i<z.MIN_AMOUNT)throw new r({code:`VALIDATION_ERROR`,message:`Amount must be at least KES ${z.MIN_AMOUNT} (got ${n.amount} which rounds to ${i}).`});if(i>z.MAX_AMOUNT)throw new r({code:`VALIDATION_ERROR`,message:`Amount must not exceed KES ${z.MAX_AMOUNT.toLocaleString()} per transaction as per Safaricom Daraja limits (got ${n.amount} which rounds to ${i}).`});let a=H(),o=n.partyB??n.shortCode,s={BusinessShortCode:n.shortCode,Password:V(n.shortCode,n.passKey,a),Timestamp:a,TransactionType:n.transactionType??`CustomerPayBillOnline`,Amount:i,PartyA:B(n.phoneNumber),PartyB:o,PhoneNumber:B(n.phoneNumber),CallBackURL:n.callbackUrl,AccountReference:n.accountReference.slice(0,12),TransactionDesc:n.transactionDesc.slice(0,13)},{data:l}=await c(`${e}/mpesa/stkpush/v1/processrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s,retries:5,retryDelay:3e3});return l}async function W(e,t,n){let r=H(),i={BusinessShortCode:n.shortCode,Password:V(n.shortCode,n.passKey,r),Timestamp:r,CheckoutRequestID:n.checkoutRequestId},{data:a}=await c(`${e}/mpesa/stkpushquery/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}async function G(e,t,n,r,a){let o=Math.round(a.amount);if(!Number.isFinite(o)||o<1)throw i({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${a.amount} which rounds to ${o}).`});if(!a.partyA)throw i({code:`VALIDATION_ERROR`,message:`partyA is required — your M-PESA business shortcode from which tax is deducted.`});if(!a.accountReference?.trim())throw i({code:`VALIDATION_ERROR`,message:`accountReference is required — the Payment Registration Number (PRN) issued by KRA.`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the tax remittance result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={Initiator:r,SecurityCredential:n,CommandID:`PayTaxToKRA`,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(o),PartyA:String(a.partyA),PartyB:a.partyB??`572572`,AccountReference:a.accountReference,Remarks:a.remarks??`Tax Remittance`,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl},{data:l}=await c(`${e}/mpesa/b2b/v1/remittax`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}async function K(e,t,n,r,a){if(!a.transactionId)throw i({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!a.partyA)throw i({code:`VALIDATION_ERROR`,message:`partyA is required (your business shortcode, till number, or MSISDN)`});if(!a.identifierType)throw i({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" (MSISDN) | "2" (Till) | "4" (ShortCode)`});if(!a.resultUrl)throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the transaction result here`});if(!a.queueTimeOutUrl)throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on timeout`});let o={Initiator:r,SecurityCredential:n,CommandID:a.commandId??`TransactionStatusQuery`,TransactionID:a.transactionId,PartyA:a.partyA,IdentifierType:a.identifierType,ResultURL:a.resultUrl,QueueTimeOutURL:a.queueTimeOutUrl,Remarks:a.remarks??`Transaction Status Query`,Occasion:a.occasion??``},{data:s}=await c(`${e}/mpesa/transactionstatus/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}const q=new Set([`BusinessPayment`,`SalaryPayment`,`PromotionPayment`]);async function J(e,t,n,r,a){if(!a.commandId||!q.has(a.commandId))throw i({code:`VALIDATION_ERROR`,message:`commandId must be one of: BusinessPayment, SalaryPayment, PromotionPayment. Got "${a.commandId}".`});let o=Math.round(a.amount);if(!Number.isFinite(o)||o<10)throw i({code:`VALIDATION_ERROR`,message:`amount must be ≥ 10 KES (got ${a.amount} which rounds to ${o}).`});if(!a.originatorConversationId?.trim())throw i({code:`VALIDATION_ERROR`,message:`originatorConversationId is required — a unique ID per request.`});if(!a.partyA?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyA is required — the sending organisation shortcode.`});if(!a.partyB?.trim())throw i({code:`VALIDATION_ERROR`,message:`partyB is required — the receiving customer MSISDN (2547XXXXXXXX).`});if(!a.remarks?.trim())throw i({code:`VALIDATION_ERROR`,message:`remarks is required (2–100 characters).`});if(!a.resultUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the async result here.`});if(!a.queueTimeOutUrl?.trim())throw i({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={OriginatorConversationID:a.originatorConversationId,InitiatorName:r,SecurityCredential:n,CommandID:a.commandId,Amount:o,PartyA:String(a.partyA),PartyB:String(a.partyB),Remarks:a.remarks,QueueTimeOutURL:a.queueTimeOutUrl,ResultURL:a.resultUrl};a.occasion?.trim()&&(s.Occassion=a.occasion);let{data:l}=await c(`${e}/mpesa/b2c/v3/paymentrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}const Y={sandbox:`https://sandbox.safaricom.co.ke`,production:`https://api.safaricom.co.ke`};var X=class{config;tokenManager;baseUrl;constructor(e){if(!e.consumerKey||!e.consumerSecret)throw new r({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required.`});this.config=e,this.baseUrl=Y[e.environment],this.tokenManager=new l(e.consumerKey,e.consumerSecret,this.baseUrl)}getToken(){return this.tokenManager.getAccessToken()}async buildSecurityCredential(){if(this.config.securityCredential)return this.config.securityCredential;if(!this.config.initiatorPassword)throw new r({code:`INVALID_CREDENTIALS`,message:`Provide securityCredential (pre-encrypted) OR (initiatorPassword + certificatePath/certificatePem).`});let t;if(this.config.certificatePem)t=this.config.certificatePem;else if(this.config.certificatePath)t=await e(this.config.certificatePath,`utf-8`);else throw new r({code:`INVALID_CREDENTIALS`,message:`certificatePath or certificatePem is required to encrypt the initiator password.`});return u(this.config.initiatorPassword,t)}requireInitiator(e){let t=this.config.initiatorName??``;if(!t)throw new r({code:`VALIDATION_ERROR`,message:`initiatorName is required for ${e}.`});return t}async stkPushSafe(e){try{return d(await this.stkPush(e))}catch(e){return f(e)}}async stkPush(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new r({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push.`});let i=await this.getToken();return U(this.baseUrl,i,{...e,shortCode:t,passKey:n})}async stkQuery(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new r({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query.`});let i=await this.getToken();return W(this.baseUrl,i,{...e,shortCode:t,passKey:n})}async transactionStatus(e){let t=this.requireInitiator(`Transaction Status`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return K(this.baseUrl,n,r,t,e)}async accountBalance(e){let t=this.requireInitiator(`Account Balance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return p(this.baseUrl,n,r,t,e)}async reverseTransaction(e){let t=this.requireInitiator(`Reversal`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return R(this.baseUrl,n,r,t,e)}async generateDynamicQR(e){let t=await this.getToken();return L(this.baseUrl,t,e)}async registerC2BUrls(e){let t=await this.getToken();return w(this.baseUrl,t,e)}async simulateC2B(e){let t=await this.getToken();return T(this.baseUrl,t,e)}async remitTax(e){let t=this.requireInitiator(`Tax Remittance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return G(this.baseUrl,n,r,t,e)}async b2bExpressCheckout(e){let t=await this.getToken();return h(this.baseUrl,t,e)}async b2cPayment(e){let t=this.requireInitiator(`B2C Payment`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return g(this.baseUrl,n,r,t,e)}async b2cDisbursement(e){let t=this.requireInitiator(`B2C Disbursement`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return J(this.baseUrl,n,r,t,e)}async billManagerOptIn(e){let t=await this.getToken();return _(this.baseUrl,t,e)}async sendInvoice(e){let t=await this.getToken();return v(this.baseUrl,t,e)}async sendBulkInvoices(e){let t=await this.getToken();return y(this.baseUrl,t,e)}async cancelInvoice(e){let t=await this.getToken();return b(this.baseUrl,t,e)}clearTokenCache(){this.tokenManager.clearCache()}get environment(){return this.config.environment}};const Z=[`196.201.214.200`,`196.201.214.206`,`196.201.213.114`,`196.201.214.207`,`196.201.214.208`,`196.201.213.44`,`196.201.212.127`,`196.201.212.138`,`196.201.212.129`,`196.201.212.136`,`196.201.212.74`,`196.201.212.69`];function Q(e,t=Z){return t.includes(e)}function $(e){try{let t=e;return t?.Body?.stkCallback?t:null}catch{return null}}export{r as i,Q as n,X as r,$ as t};
|