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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # pesafy
2
2
 
3
+ ## 0.5.4
4
+
5
+ ### Patch Changes
6
+
7
+ - added b2c disbursement
8
+
3
9
  ## 0.5.3
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -11,7 +11,6 @@
11
11
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/)
12
12
  [![CI](https://github.com/levos-snr/pesafy/actions/workflows/ci.yml/badge.svg)](https://github.com/levos-snr/pesafy/actions/workflows/ci.yml)
13
13
  [![codecov](https://codecov.io/github/levos-snr/pesafy/graph/badge.svg?token=JYK2BS1ZZF)](https://codecov.io/github/levos-snr/pesafy)
14
- [![npm](https://img.shields.io/npm/v/pesafy)](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
- * Business to Customer (B2C) types
492
+ * src/mpesa/b2c/types.ts
493
493
  *
494
- * API: POST /mpesa/b2b/v1/paymentrequest
494
+ * Strictly aligned with Safaricom Daraja B2C Account Top Up API documentation.
495
+ * Endpoint: POST /mpesa/b2b/v1/paymentrequest
495
496
  *
496
- * B2C enables two flows:
497
- * 1. BusinessPayToBulk — load funds from MMF/Working account to a B2C shortcode
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
- * B2C CommandID values.
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 = 'BusinessPayment' | 'SalaryPayment' | 'PromotionPayment' | 'BusinessPayToBulk';
504
+ type B2CCommandID = 'BusinessPayToBulk';
516
505
  interface B2CRequest {
517
506
  /**
518
- * The type of transaction. Use "BusinessPayToBulk" for account top-up.
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
- * The transaction amount. Must be a whole number ≥ 1.
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
- * Your business shortcode from which money is deducted.
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
- * The recipient shortcode or MSISDN (credit party).
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
- * Type of the sender (PartyA) identifier.
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. The consumer's mobile number on behalf of whom you are paying.
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 where Safaricom POSTs the final result after processing.
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
- /** Unique request identifier assigned by Daraja upon successful submission */
558
+ /**
559
+ * Unique request identifier assigned by Daraja upon successful submission.
560
+ * Daraja field: OriginatorConversationID
561
+ */
592
562
  OriginatorConversationID: string;
593
- /** Unique request identifier assigned by M-Pesa upon successful submission */
563
+ /**
564
+ * Unique request identifier assigned by M-Pesa.
565
+ * Daraja field: ConversationID
566
+ */
594
567
  ConversationID: string;
595
- /** "0" = successful submission */
568
+ /**
569
+ * "0" indicates the request was accepted for processing.
570
+ * Daraja field: ResponseCode
571
+ */
596
572
  ResponseCode: string;
597
- /** Human-readable status, e.g. "Accept the service request successfully." */
573
+ /**
574
+ * Human-readable submission status description.
575
+ * Daraja field: ResponseDescription
576
+ */
598
577
  ResponseDescription: string;
599
578
  }
600
579
  /**
601
- * Known result parameter keys returned by Daraja for B2C transactions.
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 the catch-all so that:
604
- * - The named literals appear in IntelliSense / autocomplete.
605
- * - Any unknown future key Daraja may return is still accepted.
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' | 'DebitPartyAffectedAccountBalance' | 'TransCompletedTime' | 'DebitPartyCharges' | 'ReceiverPartyPublicName' | 'Currency' | 'InitiatorAccountCurrentBalance' | 'B2CRecipientIsRegisteredCustomer' | 'B2CChargesPaidAccountAvailableFunds' | 'B2CWorkingAccountAvailableFunds' | 'B2CUtilityAccountAvailableFunds' | (string & {});
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
- /** Usually "0" */ResultType: string | number; /** 0 = success */
616
- ResultCode: number; /** Human-readable result description */
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
  *
@@ -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
- * Business to Customer (B2C) types
888
+ * src/mpesa/b2c/types.ts
889
889
  *
890
- * API: POST /mpesa/b2b/v1/paymentrequest
890
+ * Strictly aligned with Safaricom Daraja B2C Account Top Up API documentation.
891
+ * Endpoint: POST /mpesa/b2b/v1/paymentrequest
891
892
  *
892
- * B2C enables two flows:
893
- * 1. BusinessPayToBulk — load funds from MMF/Working account to a B2C shortcode
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
- * B2C CommandID values.
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 = 'BusinessPayment' | 'SalaryPayment' | 'PromotionPayment' | 'BusinessPayToBulk';
900
+ type B2CCommandID = 'BusinessPayToBulk';
912
901
  interface B2CRequest {
913
902
  /**
914
- * The type of transaction. Use "BusinessPayToBulk" for account top-up.
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
- * The transaction amount. Must be a whole number ≥ 1.
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
- * Your business shortcode from which money is deducted.
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
- * The recipient shortcode or MSISDN (credit party).
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
- * Type of the sender (PartyA) identifier.
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. The consumer's mobile number on behalf of whom you are paying.
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 where Safaricom POSTs the final result after processing.
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
- /** Unique request identifier assigned by Daraja upon successful submission */
954
+ /**
955
+ * Unique request identifier assigned by Daraja upon successful submission.
956
+ * Daraja field: OriginatorConversationID
957
+ */
988
958
  OriginatorConversationID: string;
989
- /** Unique request identifier assigned by M-Pesa upon successful submission */
959
+ /**
960
+ * Unique request identifier assigned by M-Pesa.
961
+ * Daraja field: ConversationID
962
+ */
990
963
  ConversationID: string;
991
- /** "0" = successful submission */
964
+ /**
965
+ * "0" indicates the request was accepted for processing.
966
+ * Daraja field: ResponseCode
967
+ */
992
968
  ResponseCode: string;
993
- /** Human-readable status, e.g. "Accept the service request successfully." */
969
+ /**
970
+ * Human-readable submission status description.
971
+ * Daraja field: ResponseDescription
972
+ */
994
973
  ResponseDescription: string;
995
974
  }
996
975
  /**
997
- * Known result parameter keys returned by Daraja for B2C transactions.
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 the catch-all so that:
1000
- * - The named literals appear in IntelliSense / autocomplete.
1001
- * - Any unknown future key Daraja may return is still accepted.
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' | 'DebitPartyAffectedAccountBalance' | 'TransCompletedTime' | 'DebitPartyCharges' | 'ReceiverPartyPublicName' | 'Currency' | 'InitiatorAccountCurrentBalance' | 'B2CRecipientIsRegisteredCustomer' | 'B2CChargesPaidAccountAvailableFunds' | 'B2CWorkingAccountAvailableFunds' | 'B2CUtilityAccountAvailableFunds' | (string & {});
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
- /** Usually "0" */ResultType: string | number; /** 0 = success */
1012
- ResultCode: number; /** Human-readable result description */
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 payment (Business to Customer).
1051
+ * Initiates a B2C Account Top Up payment request.
1043
1052
  *
1044
- * @param baseUrl - Daraja base URL (sandbox or production)
1045
- * @param accessToken - Valid OAuth bearer token
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 - M-PESA org portal API operator username
1048
- * @param request - B2C payment parameters
1049
- * @returns - Daraja acknowledgement response
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
- * Returns null if not present.
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 the B2C result parameters.
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 completion time from B2C result parameters.
1091
- * Format: YYYYMMDDHHmmss
1092
- * Returns null if not present.
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 getB2CTransactionCompletedTime(result: B2CResult): string | null;
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 currency from B2C result parameters.
1107
- * Returns "KES" as default if not present.
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 getB2CCurrency(result: B2CResult): string;
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 initiator account current balance from B2C result parameters.
1117
- * Returns null if not present.
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 getB2CInitiatorAccountBalance(result: B2CResult): string | null;
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
- * Returns undefined if key is absent.
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, getB2CInitiatorAccountBalance, 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, isPaybillPayment, isPesafyError, isReversalSuccess, isStkCallbackSuccess, isSuccessfulCallback, ok, parseAccountBalance, parseStkPushWebhook, registerC2BUrls, rejectC2BValidation, remitTax, retryWithBackoff, simulateC2B, toKesAmount, toMsisdn, toNonEmpty, toPaybill, toShortCode, toTill, verifyWebhookIP };
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pesafy",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "private": false,
5
5
  "description": "Type-safe M-PESA Daraja SDK for Node.js, Bun, Deno, Cloudflare Workers, and all JS frameworks",
6
6
  "keywords": [