pesafy 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1 +1,4197 @@
1
- import{readFile as e}from"node:fs/promises";import{constants as t,publicEncrypt as n}from"node:crypto";import{z as r}from"zod";var i=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 a(e){return new i(e)}function o(e){return e instanceof i}function s(e,t){return t?.idempotency?{...e,idempotency:t.idempotency}:e}const c=new Set([429,500,502,503,504]);function l(e){return new Promise(t=>setTimeout(t,e))}function u(e){let t=e*.25;return e+(Math.random()*t*2-t)}async function d(e,t){let n=t.retries??4,r=t.retryDelay??2e3,a=t.timeout??3e4,o={"Content-Type":`application/json`,Accept:`application/json`,...t.headers},s=t.idempotency,d=t.idempotencyKey;if(t.method===`POST`&&s?.enabled){d=s.reserve(d);let e=s.headerName;o[e]=d}else d&&(o[`Idempotency-Key`]=d);let f={method:t.method,headers:o,...t.body===void 0?{}:{body:JSON.stringify(t.body)}},p=null;for(let o=0;o<=n;o++){if(o>0){let i=u(r*2**(o-1));console.warn(`[pesafy] Retry ${o}/${n} → ${t.method} ${e} in ${Math.round(i)} ms`),await l(i)}let m=new AbortController,h=setTimeout(()=>m.abort(),a),g;try{g=await fetch(e,{...f,signal:m.signal})}catch(t){if(clearTimeout(h),p=t instanceof Error&&t.name===`AbortError`?new i({code:`TIMEOUT`,message:`Request to ${e} timed out after ${a} ms`,cause:t,retryable:!0}):new i({code:`NETWORK_ERROR`,message:`Network error: ${t instanceof Error?t.message:String(t)}`,cause:t,retryable:!0}),o<n)continue;throw d&&s?.enabled&&s.release(d),p}finally{clearTimeout(h)}let _=``,v=null,y=g.headers.get(`content-type`)??``;try{_=await g.text(),_&&(v=y.includes(`application/json`)?JSON.parse(_):_)}catch{v=_||null}let b={};if(g.headers.forEach((e,t)=>{b[t]=e}),g.ok)return d&&s?.enabled&&s.complete(d),{data:v,status:g.status,headers:b};let x=c.has(g.status),S=typeof v==`object`&&v?v:{},ee=S.errorMessage??S.ResponseDescription??S.resultDesc??_??`HTTP ${g.status}`;if(p=new i({code:x?`REQUEST_FAILED`:`API_ERROR`,message:ee,statusCode:g.status,response:v,retryable:x,...typeof S.requestId==`string`?{requestId:S.requestId}:{}}),!(x&&o<n))throw d&&s?.enabled&&s.release(d),p}throw d&&s?.enabled&&s.release(d),p}const f={INVALID_AUTH_TYPE:`400.008.01`,INVALID_GRANT_TYPE:`400.008.02`};var p=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`)}`}mapAuthError(e){if(e instanceof i){if(e.code===`AUTH_FAILED`)throw e;let t=e.response;if(t&&typeof t==`object`){let n=t.errorCode??t.error_code;if(n===f.INVALID_AUTH_TYPE)throw new i({code:`AUTH_FAILED`,message:`Invalid authentication type (400.008.01). Use Basic authentication: Authorization: Basic <Base64(consumerKey:consumerSecret)>.`,...e.statusCode!==void 0&&{statusCode:e.statusCode},response:e.response});if(n===f.INVALID_GRANT_TYPE)throw new i({code:`AUTH_FAILED`,message:`Invalid grant type (400.008.02). Set grant_type=client_credentials in the request query parameters.`,...e.statusCode!==void 0&&{statusCode:e.statusCode},response:e.response})}throw e}throw e}async getAccessToken(){let e=Date.now()/1e3;if(this.cachedToken&&this.tokenExpiresAt>e+60)return this.cachedToken;let t=`${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`;try{let n=await d(t,{method:`GET`,headers:{Authorization:this.getBasicAuthHeader()}}),{access_token:r,expires_in:a}=n.data;if(!r)throw new i({code:`AUTH_FAILED`,message:`Daraja did not return an access token. Verify your consumer key and consumer secret.`,response:n.data});return this.cachedToken=r,this.tokenExpiresAt=e+(a??3600),this.cachedToken}catch(e){return this.mapAuthError(e)}}clearCache(){this.cachedToken=null,this.tokenExpiresAt=0}};function m(e,r){try{let i=Buffer.from(e,`utf-8`);return n({key:r,padding:t.RSA_PKCS1_PADDING},i).toString(`base64`)}catch(e){throw new i({code:`ENCRYPTION_FAILED`,message:`Failed to encrypt security credential. Ensure the certificate PEM is valid and matches the environment (sandbox/production).`,cause:e})}}function h(e){let t=crypto.randomUUID();return e?`${e}-${t}`:t}function g(){return h(`pesafy`)}function _(){return crypto.randomUUID()}var v=class{entries=new Map;get(e){return this.entries.get(e)}set(e,t){this.entries.set(e,t)}delete(e){this.entries.delete(e)}prune(e){let t=Date.now()-e;for(let[e,n]of this.entries)n.createdAt<t&&this.entries.delete(e)}},y=class{enabled;headerName;ttlMs;store;generateKey;constructor(e={}){this.enabled=e.enabled!==!1,this.headerName=e.headerName??`Idempotency-Key`,this.ttlMs=e.ttlMs??864e5,this.store=e.store??new v,this.generateKey=e.generateKey??h}reserve(e){if(!this.enabled)return e??this.generateKey();this.pruneExpired();let t=e??this.generateKey(),n=this.store.get(t);if(n){if(Date.now()-n.createdAt<this.ttlMs)throw new i({code:`IDEMPOTENCY_ERROR`,message:`Duplicate request detected for idempotency key "${t}".`});this.store.delete(t)}return this.store.set(t,{key:t,createdAt:Date.now()}),t}complete(e){if(!this.enabled)return;let t=this.store.get(e);t&&this.store.set(e,{...t,completedAt:Date.now()})}release(e){this.enabled&&this.store.delete(e)}pruneExpired(){this.store instanceof v&&this.store.prune(this.ttlMs)}};function b(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 x(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 S(e){return String(e)}function ee(e){return String(e)}function te(e){return String(e)}function C(e){return{ok:!0,data:e}}function w(e){return{ok:!1,error:e}}function ne(e){if(!e.trim())throw TypeError(`String must not be empty`);return e}function re(e,t=`Request`){return new i({code:`VALIDATION_ERROR`,message:`${t} validation failed: ${e.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`; `)}`,cause:e})}function T(e,t,n){let r=e.safeParse(t);if(!r.success)throw re(r.error,n);return r.data}const ie=r.enum([`sandbox`,`production`]),ae=r.string().min(10).regex(/^254\d{9}$/,`Must be Safaricom format 2547XXXXXXXX`),E=r.number().finite().positive().refine(e=>Math.round(e)>=1,{message:`amount must round to at least 1 KES`}),D=r.string().trim().min(1),O=r.string().url(),oe=r.object({errorMessage:r.string().optional(),ResponseDescription:r.string().optional(),resultDesc:r.string().optional(),requestId:r.string().optional()}).passthrough(),se=r.enum([`1`,`2`,`4`]),k=r.object({ConversationID:r.string().optional(),OriginatorConversationID:r.string().optional(),ResponseCode:r.string(),ResponseDescription:r.string()}).passthrough(),ce=r.object({transactionId:r.string().optional(),originalConversationId:r.string().optional(),partyA:D,identifierType:se,resultUrl:O,queueTimeOutUrl:O,commandId:r.literal(`TransactionStatusQuery`).optional(),remarks:r.string().optional(),occasion:r.string().optional()}).superRefine((e,t)=>{!e.transactionId?.trim()&&!e.originalConversationId?.trim()&&t.addIssue({code:`custom`,message:`Either transactionId (M-Pesa Receipt Number) or originalConversationId is required`,path:[`transactionId`]})}),le=k,ue=r.object({partyA:D,identifierType:se,resultUrl:O,queueTimeOutUrl:O,remarks:r.string().optional()}),de=k,fe=r.object({transactionId:D,receiverParty:D,receiverIdentifierType:r.literal(`11`).optional(),amount:E,resultUrl:O,queueTimeOutUrl:O,remarks:r.string().optional(),occasion:r.string().optional()}).superRefine((e,t)=>{e.receiverIdentifierType!==void 0&&e.receiverIdentifierType!==`11`&&t.addIssue({code:`custom`,message:`receiverIdentifierType must be "11" for the Reversals API`,path:[`receiverIdentifierType`]});let n=e.remarks??`Transaction Reversal`;(n.length<2||n.length>100)&&t.addIssue({code:`custom`,message:`remarks must be between 2 and 100 characters`,path:[`remarks`]})}),pe=k,me=r.object({amount:E,partyA:D,partyB:r.string().optional(),accountReference:D,resultUrl:O,queueTimeOutUrl:O,remarks:r.string().optional()}),he=k,ge=r.object({merchantName:D,refNo:D,amount:E,trxCode:r.enum([`BG`,`WA`,`PB`,`SM`,`SB`]),cpi:D,size:r.number().int().min(1).max(1e3).optional()}),_e=r.object({ResponseCode:r.string(),RequestID:r.string().optional(),ResponseDescription:r.string(),QRCode:r.string().optional()}).passthrough();async function ve(e,t,n,r,i,a){let o=T(ue,i,`Account Balance request`),c={Initiator:r,SecurityCredential:n,CommandID:`AccountBalance`,PartyA:String(o.partyA.trim()),IdentifierType:o.identifierType,ResultURL:o.resultUrl,QueueTimeOutURL:o.queueTimeOutUrl,Remarks:o.remarks??`Account Balance Query`},{data:l}=await d(`${e}/mpesa/accountbalance/v1/query`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:c},a));return T(de,l,`Account Balance response`)}const ye={DUPLICATE_DETECTED:15,INTERNAL_FAILURE:17,INITIATOR_CREDENTIAL_CHECK_FAILURE:18,MESSAGE_SEQUENCING_FAILURE:19,UNRESOLVED_INITIATOR:20,INITIATOR_TO_PRIMARY_PARTY_PERMISSION_FAILURE:21,INITIATOR_TO_RECEIVER_PARTY_PERMISSION_FAILURE:22,MISSING_MANDATORY_FIELDS:24,SYSTEM_OVERLOAD:100000001,THROTTLING_ERROR:100000002,INTERNAL_SERVER_ERROR:100000004,INVALID_INPUT_VALUE:100000005,SERVICE_ABNORMAL:100000007,API_STATUS_ABNORMAL:100000009,INSUFFICIENT_PERMISSIONS:100000010,REQUEST_RATE_EXCEEDED:100000011};function be(e){if(!e.trim())return[];let t=e.split(`|`),n=[];for(let e=0;e+2<t.length;e++){let r=t[e]?.trim(),i=t[e+1]?.trim(),a=t[e+2]?.trim();r&&i&&a!==void 0&&isNaN(Number(r))&&r.length>0&&n.push({name:r,currency:i,amount:a})}return n}function A(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function xe(e){return e.Result.TransactionID}function Se(e){return e.Result.ConversationID}function Ce(e){return e.Result.OriginatorConversationID}function we(e){let t=A(e,`BOCompletedTime`);return t==null?null:String(t)}function Te(e){let t=A(e,`AccountBalance`);return t==null?null:String(t)}function Ee(e){let t=e.Result.ReferenceData;if(!t)return null;let n=t.ReferenceItem;return n?Array.isArray(n)?n[0]??null:n:null}function De(e){let t=e.Result.ResultCode;return t===0||t===`0`}const Oe=r.object({ConversationID:r.string(),OriginatorConversationID:r.string(),ResponseCode:r.string(),ResponseDescription:r.string()}).passthrough(),ke=r.object({amount:E,partyA:D,partyB:D,accountReference:D,requester:r.string().optional(),remarks:r.string().optional(),resultUrl:O,queueTimeOutUrl:O,occasion:r.string().optional()}),Ae=ke.extend({commandId:r.literal(`BusinessBuyGoods`)}),je=ke.extend({commandId:r.literal(`BusinessPayBill`)}),Me=Oe,Ne=Oe,Pe=r.object({primaryShortCode:D,receiverShortCode:D,amount:E,paymentRef:D,callbackUrl:O,partnerName:D,requestRefId:D.optional()}),Fe=r.object({code:r.string(),status:r.string()}).passthrough(),Ie=r.object({ConversationID:r.string().optional(),OriginatorConversationID:r.string().optional(),ResponseCode:r.string(),ResponseDescription:r.string()}).passthrough();async function Le(e,t,n,r){let i=T(Pe,n,`B2B Express Checkout request`),a=Math.round(i.amount),o={primaryShortCode:String(i.primaryShortCode),receiverShortCode:String(i.receiverShortCode),amount:String(a),paymentRef:i.paymentRef,callbackUrl:i.callbackUrl,partnerName:i.partnerName,RequestRefID:i.requestRefId??_()},{data:c}=await d(`${e}/v1/ussdpush/get-msisdn`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o},r));return T(Fe,c,`B2B Express Checkout response`)}const j={SUCCESS:`0`,CANCELLED:`4001`,KYC_FAIL:`4102`,NO_NOMINATED_NUMBER:`4104`,USSD_NETWORK_ERROR:`4201`,USSD_EXCEPTION_ERROR:`4203`};new Set(Object.values(j));function Re(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 M(e){return e.resultCode===j.SUCCESS}function ze(e){return e.resultCode===j.CANCELLED}function Be(e){return e.requestId}function Ve(e){return Number(e.amount)}function He(e){return M(e)?e.transactionId??null:null}function Ue(e){return M(e)?e.conversationID??null:null}async function N(e,t,n,r,i,a){let o=T(Ae,i,`B2B Buy Goods request`),c=Math.round(o.amount),l={Initiator:r,SecurityCredential:n,CommandID:o.commandId,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(c),PartyA:String(o.partyA),PartyB:String(o.partyB),AccountReference:o.accountReference.slice(0,13),Remarks:o.remarks??`Business Buy Goods`,QueueTimeOutURL:o.queueTimeOutUrl,ResultURL:o.resultUrl};o.requester?.trim()&&(l.Requester=String(o.requester)),o.occasion?.trim()&&(l.Occassion=o.occasion);let{data:u}=await d(`${e}/mpesa/b2b/v1/paymentrequest`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:l},a));return T(Me,u,`B2B Buy Goods response`)}const We={SUCCESS:0,INSUFFICIENT_FUNDS:1,AMOUNT_TOO_SMALL:2,AMOUNT_TOO_LARGE:3,DAILY_LIMIT_EXCEEDED:4,MAX_BALANCE_EXCEEDED:8,INVALID_INITIATOR_INFO:2001,ACCOUNT_INACTIVE:2006,PRODUCT_NOT_PERMITTED:2028,CUSTOMER_NOT_REGISTERED:2040},Ge={INVALID_ACCESS_TOKEN:`400.003.01`,BAD_REQUEST:`400.003.02`,INTERNAL_SERVER_ERROR:`500.003.1001`,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`},Ke=new Set(Object.values(We));function qe(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 Je(e){let t=e.Result.ResultCode;return t===0||t===`0`}function Ye(e){return!Je(e)}function Xe(e){if(typeof e!=`number`&&typeof e!=`string`||typeof e==`string`&&e.trim()===``)return!1;let t=Number(e);return Number.isFinite(t)&&Ke.has(t)}function Ze(e){return e.Result.TransactionID}function Qe(e){return e.Result.ConversationID}function $e(e){return e.Result.OriginatorConversationID}function et(e){return e.Result.ResultDesc}function tt(e){return e.Result.ResultCode}function nt(e){let t=P(e,`Amount`);if(t===void 0)return null;let n=Number(t);return Number.isFinite(n)?n:null}function rt(e){let t=P(e,`TransCompletedTime`)??P(e,`BOCompletedTime`);return t===void 0?null:String(t)}function it(e){let t=P(e,`ReceiverPartyPublicName`);return t===void 0?null:String(t)}function at(e){let t=P(e,`DebitPartyCharges`);return t===void 0||t===``?null:String(t)}function ot(e){let t=P(e,`Currency`);return t===void 0||t===``?`KES`:String(t)}function st(e){let t=P(e,`DebitPartyAffectedAccountBalance`);return t===void 0?null:String(t)}function ct(e){let t=P(e,`DebitAccountBalance`);return t===void 0?null:String(t)}function lt(e){let t=P(e,`InitiatorAccountCurrentBalance`);return t===void 0?null:String(t)}function ut(e){let t=e.Result.ReferenceData?.ReferenceItem;return t?(Array.isArray(t)?t:[t]).find(e=>e.Key===`BillReferenceNumber`)?.Value??null:null}function dt(e){let t=e.Result.ReferenceData?.ReferenceItem;return t?(Array.isArray(t)?t:[t]).find(e=>e.Key===`QueueTimeoutURL`)?.Value??null:null}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 ft(e,t,n,r,i,a){let o=T(je,i,`B2B Pay Bill request`),c=Math.round(o.amount),l={Initiator:r,SecurityCredential:n,CommandID:o.commandId,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(c),PartyA:String(o.partyA),PartyB:String(o.partyB),AccountReference:o.accountReference.slice(0,13),Remarks:o.remarks??`Business Pay Bill`,QueueTimeOutURL:o.queueTimeOutUrl,ResultURL:o.resultUrl};o.requester?.trim()&&(l.Requester=String(o.requester)),o.occasion?.trim()&&(l.Occassion=o.occasion);let{data:u}=await d(`${e}/mpesa/b2b/v1/paymentrequest`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:l},a));return T(Ne,u,`B2B Pay Bill response`)}const pt={SUCCESS:0,INSUFFICIENT_FUNDS:1,AMOUNT_TOO_SMALL:2,AMOUNT_TOO_LARGE:3,DAILY_LIMIT_EXCEEDED:4,MAX_BALANCE_EXCEEDED:8,INVALID_INITIATOR_INFO:2001,ACCOUNT_INACTIVE:2006,PRODUCT_NOT_PERMITTED:2028,CUSTOMER_NOT_REGISTERED:2040},mt={INVALID_ACCESS_TOKEN:`400.003.01`,BAD_REQUEST:`400.003.02`,INTERNAL_SERVER_ERROR:`500.003.1001`,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`},ht=new Set(Object.values(pt));function gt(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 _t(e){let t=e.Result.ResultCode;return t===0||t===`0`}function vt(e){return!_t(e)}function yt(e){if(typeof e!=`number`&&typeof e!=`string`||typeof e==`string`&&e.trim()===``)return!1;let t=Number(e);return Number.isFinite(t)&&ht.has(t)}function bt(e){return e.Result.TransactionID}function xt(e){return e.Result.ConversationID}function St(e){return e.Result.OriginatorConversationID}function Ct(e){return e.Result.ResultDesc}function wt(e){return e.Result.ResultCode}function Tt(e){let t=F(e,`Amount`);if(t===void 0)return null;let n=Number(t);return Number.isFinite(n)?n:null}function Et(e){let t=F(e,`TransCompletedTime`)??F(e,`BOCompletedTime`);return t===void 0?null:String(t)}function Dt(e){let t=F(e,`ReceiverPartyPublicName`);return t===void 0?null:String(t)}function Ot(e){let t=F(e,`DebitPartyCharges`);return t===void 0||t===``?null:String(t)}function kt(e){let t=F(e,`Currency`);return t===void 0||t===``?`KES`:String(t)}function At(e){let t=F(e,`DebitPartyAffectedAccountBalance`);return t===void 0?null:String(t)}function jt(e){let t=F(e,`DebitAccountCurrentBalance`);return t===void 0?null:String(t)}function Mt(e){let t=F(e,`InitiatorAccountCurrentBalance`);return t===void 0?null:String(t)}function Nt(e){let t=e.Result.ReferenceData?.ReferenceItem;return t?(Array.isArray(t)?t:[t]).find(e=>e.Key===`BillReferenceNumber`)?.Value??null:null}function F(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}const Pt=r.object({ConversationID:r.string(),OriginatorConversationID:r.string(),ResponseCode:r.string(),ResponseDescription:r.string()}).passthrough(),Ft=r.object({commandId:r.literal(`BusinessPayToBulk`),amount:E,partyA:D,partyB:D,accountReference:D,requester:r.string().optional(),remarks:r.string().optional(),resultUrl:O,queueTimeOutUrl:O}),It=Pt,Lt=r.object({commandId:r.enum([`BusinessPayment`,`SalaryPayment`,`PromotionPayment`]),amount:r.number().finite().positive(),partyA:D,partyB:D,remarks:D,queueTimeOutUrl:O,resultUrl:O,originatorConversationId:D.optional(),occasion:r.string().optional()}),Rt=Pt;async function zt(e,t,n,r,i,a){let o=T(Ft,i,`B2C request`),c=Math.round(o.amount),l={Initiator:r,SecurityCredential:n,CommandID:o.commandId,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(c),PartyA:String(o.partyA),PartyB:String(o.partyB),AccountReference:o.accountReference,Remarks:o.remarks??`B2C Account Top Up`,QueueTimeOutURL:o.queueTimeOutUrl,ResultURL:o.resultUrl};o.requester?.trim()&&(l.Requester=String(o.requester));let{data:u}=await d(`${e}/mpesa/b2b/v1/paymentrequest`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:l},a));return T(It,u,`B2C response`)}const Bt={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`},Vt={SUCCESS:0,INVALID_INITIATOR:2001};function Ht(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 Ut(e){let t=e.Result.ResultCode;return t===0||t===`0`}function Wt(e){return!Ut(e)}function Gt(e){if(typeof e!=`number`&&typeof e!=`string`||typeof e==`string`&&e.trim()===``)return!1;let t=Number(e);return Object.values(Vt).includes(t)}function Kt(e){return e.Result.TransactionID??null}function qt(e){return e.Result.ConversationID}function Jt(e){return e.Result.OriginatorConversationID}function Yt(e){return e.Result.ResultDesc}function Xt(e){let t=I(e,`Amount`);if(t===void 0)return null;let n=Number(t);return Number.isFinite(n)?n:null}function Zt(e){let t=I(e,`Currency`);return t===void 0||t===``?`KES`:String(t)}function Qt(e){let t=I(e,`ReceiverPartyPublicName`);return t===void 0?null:String(t)}function $t(e){let t=I(e,`TransactionCompletedTime`);return t===void 0?null:String(t)}function en(e){let t=I(e,`DebitAccountBalance`);return t===void 0?null:String(t)}function tn(e){let t=I(e,`DebitPartyCharges`);return t===void 0||t===``?null:String(t)}function I(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}const nn=new Set([`BusinessPayment`,`SalaryPayment`,`PromotionPayment`]);async function rn(e,t,n,r,i,o){let c=i.originatorConversationId?.trim()||g(),l=T(Lt,{...i,originatorConversationId:c},`B2C Disbursement request`);if(!l.commandId||!nn.has(l.commandId))throw a({code:`VALIDATION_ERROR`,message:`commandId must be one of: BusinessPayment, SalaryPayment, PromotionPayment. Got "${l.commandId}".`});let u=Math.round(l.amount);if(!Number.isFinite(u)||u<10)throw a({code:`VALIDATION_ERROR`,message:`amount must be ≥ 10 KES (got ${l.amount} which rounds to ${u}).`});if(!l.partyA?.trim())throw a({code:`VALIDATION_ERROR`,message:`partyA is required — the sending organisation shortcode.`});if(!l.partyB?.trim())throw a({code:`VALIDATION_ERROR`,message:`partyB is required — the receiving customer MSISDN (2547XXXXXXXX).`});if(!l.remarks?.trim())throw a({code:`VALIDATION_ERROR`,message:`remarks is required (2–100 characters).`});if(!l.resultUrl?.trim())throw a({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the async result here.`});if(!l.queueTimeOutUrl?.trim())throw a({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let f={OriginatorConversationID:c,InitiatorName:r,SecurityCredential:n,CommandID:l.commandId,Amount:u,PartyA:String(l.partyA),PartyB:String(l.partyB),Remarks:l.remarks,QueueTimeOutURL:l.queueTimeOutUrl,ResultURL:l.resultUrl};l.occasion?.trim()&&(f.Occassion=l.occasion);let{data:p}=await d(`${e}/mpesa/b2c/v3/paymentrequest`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:f},o));return T(Rt,p,`B2C Disbursement response`)}const L={SUCCESS:0,INSUFFICIENT_BALANCE:1,AMOUNT_TOO_SMALL:2,AMOUNT_TOO_LARGE:3,DAILY_LIMIT_EXCEEDED:4,MAX_BALANCE_EXCEEDED:8,DEBIT_PARTY_INVALID:11,INITIATOR_NOT_ALLOWED:21,INVALID_INITIATOR_INFO:2001,ACCOUNT_INACTIVE:2006,PRODUCT_NOT_PERMITTED:2028,CUSTOMER_NOT_REGISTERED:2040,SECURITY_CREDENTIAL_LOCKED:8006,OPERATOR_DOES_NOT_EXIST:`SFC_IC0003`};function an(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 on(e){let t=e.Result.ResultCode;return t===0||t===`0`}function sn(e){return!on(e)}function cn(e){if(e==null||typeof e!=`number`&&typeof e!=`string`)return!1;if(typeof e==`string`&&e===L.OPERATOR_DOES_NOT_EXIST)return!0;if(typeof e==`string`&&e.trim()===``)return!1;let t=Number(e);return Object.values(L).filter(e=>typeof e==`number`).includes(t)}function ln(e){return e.Result.TransactionID??null}function un(e){return e.Result.ConversationID}function dn(e){return e.Result.OriginatorConversationID}function fn(e){return e.Result.ResultDesc}function pn(e){return e.Result.ResultCode}function R(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function mn(e){let t=R(e,`TransactionAmount`);if(t===void 0)return null;let n=Number(t);return Number.isFinite(n)?n:null}function hn(e){let t=R(e,`TransactionReceipt`);return t===void 0?null:String(t)}function gn(e){let t=R(e,`ReceiverPartyPublicName`);return t===void 0?null:String(t)}function _n(e){let t=R(e,`TransactionCompletedDateTime`);return t===void 0?null:String(t)}function vn(e){let t=R(e,`B2CUtilityAccountAvailableFunds`);if(t===void 0)return null;let n=Number(t);return Number.isFinite(n)?n:null}function yn(e){let t=R(e,`B2CWorkingAccountAvailableFunds`);if(t===void 0)return null;let n=Number(t);return Number.isFinite(n)?n:null}function bn(e){let t=R(e,`B2CRecipientIsRegisteredCustomer`);return t===void 0?null:String(t).toUpperCase()===`Y`}const xn=r.enum([`0`,`1`]),z=r.object({shortcode:D,email:D,officialContact:D,sendReminders:xn,logo:r.string().optional(),callbackUrl:O}),Sn=r.object({app_key:r.string().optional(),resmsg:r.string(),rescode:r.string()}).passthrough(),Cn=z,wn=r.object({resmsg:r.string(),rescode:r.string()}).passthrough(),Tn=r.object({itemName:D,amount:E}),B=r.object({externalReference:D,billedFullName:D,billedPhoneNumber:D,billedPeriod:D,invoiceName:D,dueDate:D,accountReference:D,amount:E,invoiceItems:r.array(Tn).optional()}),En=r.object({Status_Message:r.string().optional(),resmsg:r.string(),rescode:r.string()}).passthrough(),Dn=r.object({invoices:r.array(B).min(1).max(1e3)}),On=r.object({Status_Message:r.string().optional(),resmsg:r.string(),rescode:r.string()}).passthrough(),kn=r.object({externalReference:D}),An=r.object({Status_Message:r.string().optional(),resmsg:r.string(),rescode:r.string(),errors:r.array(r.unknown()).optional()}).passthrough(),jn=r.object({externalReferences:r.array(D).min(1)}),Mn=r.object({Status_Message:r.string().optional(),resmsg:r.string(),rescode:r.string(),errors:r.array(r.unknown()).optional()}).passthrough(),Nn=r.object({paymentDate:D,paidAmount:D,accountReference:D,transactionId:D,phoneNumber:D,fullName:D,invoiceName:D,externalReference:D}),Pn=r.object({resmsg:r.string(),rescode:r.string()}).passthrough();async function Fn(e,t,n,r){let i=T(z,n,`Bill Manager opt-in request`),a={shortcode:i.shortcode,email:i.email,officialContact:i.officialContact,sendReminders:i.sendReminders,logo:i.logo??``,callbackurl:i.callbackUrl},{data:o}=await d(`${e}/v1/billmanager-invoice/optin`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a},r));return T(Sn,o,`Bill Manager opt-in response`)}async function In(e,t,n,r){let i=T(Cn,n,`Bill Manager update opt-in request`),a={shortcode:i.shortcode,email:i.email,officialContact:i.officialContact,sendReminders:i.sendReminders,logo:i.logo??``,callbackurl:i.callbackUrl},{data:o}=await d(`${e}/v1/billmanager-invoice/change-optin-details`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a},r));return T(wn,o,`Bill Manager update opt-in response`)}async function Ln(e,t,n,r){let i=T(B,n,`Bill Manager single invoice request`),a=Math.round(i.amount),o={externalReference:i.externalReference,billedFullName:i.billedFullName,billedPhoneNumber:i.billedPhoneNumber,billedPeriod:i.billedPeriod,invoiceName:i.invoiceName,dueDate:i.dueDate,accountReference:i.accountReference,amount:String(a),invoiceItems:i.invoiceItems?.map(e=>({itemName:e.itemName,amount:String(Math.round(e.amount))}))??[]},{data:c}=await d(`${e}/v1/billmanager-invoice/single-invoicing`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o},r));return T(En,c,`Bill Manager single invoice response`)}async function V(e,t,n,r){let i=T(Dn,n,`Bill Manager bulk invoice request`).invoices.map(e=>({externalReference:e.externalReference,billedFullName:e.billedFullName,billedPhoneNumber:e.billedPhoneNumber,billedPeriod:e.billedPeriod,invoiceName:e.invoiceName,dueDate:e.dueDate,accountReference:e.accountReference,amount:String(Math.round(e.amount)),invoiceItems:e.invoiceItems?.map(e=>({itemName:e.itemName,amount:String(Math.round(e.amount))}))??[]})),{data:a}=await d(`${e}/v1/billmanager-invoice/bulk-invoicing`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i},r));return T(On,a,`Bill Manager bulk invoice response`)}async function Rn(e,t,n,r){let i=T(kn,n,`Bill Manager cancel invoice request`),{data:a}=await d(`${e}/v1/billmanager-invoice/cancel-single-invoice`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:{externalReference:i.externalReference}},r));return T(An,a,`Bill Manager cancel invoice response`)}async function zn(e,t,n,r){let i=T(jn,n,`Bill Manager cancel bulk invoices request`).externalReferences.map(e=>({externalReference:e})),{data:a}=await d(`${e}/v1/billmanager-invoice/cancel-bulk-invoices`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i},r));return T(Mn,a,`Bill Manager cancel bulk invoices response`)}async function Bn(e,t,n,r){let i=T(Nn,n,`Bill Manager reconciliation request`),a={paymentDate:i.paymentDate,paidAmount:i.paidAmount,accountReference:i.accountReference,transactionId:i.transactionId,phoneNumber:i.phoneNumber,fullName:i.fullName,invoiceName:i.invoiceName,externalReference:i.externalReference},{data:o}=await d(`${e}/v1/billmanager-invoice/reconciliation`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a},r));return T(Pn,o,`Bill Manager reconciliation response`)}const Vn=r.object({shortCode:D,responseType:r.enum([`Completed`,`Cancelled`]),confirmationUrl:O,validationUrl:O,apiVersion:r.enum([`v1`,`v2`]).optional()}),H=r.object({OriginatorCoversationID:r.string(),ResponseCode:r.string(),ResponseDescription:r.string()}).passthrough(),Hn=H,Un=H,Wn=r.object({shortCode:r.union([D,r.number()]),commandId:r.enum([`CustomerPayBillOnline`,`CustomerBuyGoodsOnline`]),amount:E,msisdn:r.union([D,r.number()]),billRefNumber:r.union([D,r.null()]).optional(),apiVersion:r.enum([`v1`,`v2`]).optional()}).superRefine((e,t)=>{e.commandId===`CustomerPayBillOnline`&&!e.billRefNumber?.trim()&&t.addIssue({code:`custom`,message:`billRefNumber is required for CustomerPayBillOnline`,path:[`billRefNumber`]})}),Gn=r.object({TransactionType:r.string(),TransID:r.string(),TransTime:r.string(),TransAmount:r.union([r.string(),r.number()]),BusinessShortCode:r.string(),BillRefNumber:r.string().optional(),MSISDN:r.string()}).passthrough(),Kn=[`mpesa`,`safaricom`,`exec`,`exe`,`cme`,`cmd`,`sql`,`query`];function qn(e,t){if(!e||!e.trim())throw a({code:`VALIDATION_ERROR`,message:`${t} is required`});let n=e.toLowerCase();for(let e of Kn)if(n.includes(e))throw a({code:`VALIDATION_ERROR`,message:`${t} must not contain the keyword "${e}". Daraja rejects URLs containing: mpesa, safaricom, exe, exec, cme (and variants: cmd, sql, query).`})}async function Jn(e,t,n,r){let i=T(Vn,n,`C2B Register URL request`);qn(i.confirmationUrl,`confirmationUrl`),qn(i.validationUrl,`validationUrl`);let a=i.apiVersion??`v2`,o={ShortCode:String(i.shortCode),ResponseType:i.responseType,ConfirmationURL:i.confirmationUrl,ValidationURL:i.validationUrl},{data:c}=await d(`${e}/mpesa/c2b/${a}/registerurl`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o},r));return T(Hn,c,`C2B Register URL response`)}async function Yn(e,t,n,r){let i=T(Wn,n,`C2B Simulate request`);if(!e.includes(`sandbox`))throw a({code:`VALIDATION_ERROR`,message:`C2B simulate is only available in the Sandbox environment (per Daraja docs). In production, customers initiate payments directly via M-PESA App, USSD, or SIM Toolkit.`});let o=Math.round(i.amount),c=i.commandId===`CustomerBuyGoodsOnline`,l=i.apiVersion??`v2`,u={ShortCode:Number(i.shortCode),CommandID:i.commandId,Amount:o,Msisdn:Number(i.msisdn)};c||(u.BillRefNumber=i.billRefNumber.trim());let{data:f}=await d(`${e}/mpesa/c2b/${l}/simulate`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:u},r));return T(Un,f,`C2B Simulate response`)}const Xn={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`,RESOURCE_NOT_FOUND:`404.003.01`,INVALID_AUTH_HEADER:`404.001.04`,INVALID_REQUEST_PAYLOAD:`400.002.05`},Zn={ACCEPT:`0`,INVALID_MSISDN:`C2B00011`,INVALID_ACCOUNT_NUMBER:`C2B00012`,INVALID_AMOUNT:`C2B00013`,INVALID_KYC_DETAILS:`C2B00014`,INVALID_SHORTCODE:`C2B00015`,OTHER_ERROR:`C2B00016`};function Qn(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 $n(e){return{ResultCode:`0`,ResultDesc:`Accepted`,...e?{ThirdPartyTransID:e}:{}}}function er(e=`C2B00016`){return{ResultCode:e,ResultDesc:`Rejected`}}function tr(){return{ResultCode:0,ResultDesc:`Success`}}function nr(e){return Number(e.TransAmount)}function rr(e){return e.TransID}function ir(e){return e.BillRefNumber}function ar(e){return[e.FirstName,e.MiddleName,e.LastName].filter(Boolean).join(` `).trim()}function or(e){return e.TransactionType===`Pay Bill`}function sr(e){return e.TransactionType===`Buy Goods`}const U=[`BG`,`WA`,`PB`,`SM`,`SB`],cr=1,lr=1e3,ur=1,dr=300;function fr(e){return typeof e!=`string`||e.trim().length===0?`merchantName is required and must be a non-empty string`:null}function pr(e){return typeof e!=`string`||e.trim().length===0?`refNo (transaction reference) is required and must be a non-empty string`:null}function mr(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 hr(e){return U.includes(e)?null:`trxCode must be one of: ${U.join(`, `)} (BG=Buy Goods, WA=Withdraw Cash, PB=Paybill, SM=Send Money, SB=Send to Business)`}function gr(e){return typeof e!=`string`||e.trim().length===0?`cpi (Credit Party Identifier) is required and must be a non-empty string`:null}function _r(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 ${lr} pixels (got ${e})`:null}function vr(e){if(typeof e!=`object`||!e)return{valid:!1,errors:{payload:`request payload must be a non-null object`}};let t=e,n={},r=fr(t.merchantName);r&&(n.merchantName=r);let i=pr(t.refNo);i&&(n.refNo=i);let a=mr(t.amount);a&&(n.amount=a);let o=hr(t.trxCode);o&&(n.trxCode=o);let s=gr(t.cpi);s&&(n.cpi=s);let c=_r(t.size);return c&&(n.size=c),Object.keys(n).length>0?{valid:!1,errors:n}:{valid:!0}}function yr(e,t){switch(e){case`404.001.04`:return new i({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 i({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 i({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 i({code:`REQUEST_FAILED`,message:`Dynamic QR request failed (${e}): ${t}`,statusCode:400})}}function br(e){return typeof e==`object`&&!!e&&`errorCode`in e&&typeof e.errorCode==`string`}function xr(e){return typeof e==`object`&&!!e&&`ResponseCode`in e&&`QRCode`in e&&typeof e.QRCode==`string`&&e.QRCode.length>0}async function Sr(e,t,n,r){let a=T(ge,n,`Dynamic QR request`);if(!t||typeof t!=`string`||t.trim().length===0)throw new i({code:`AUTH_FAILED`,message:`accessToken is required. Obtain one via the Daraja Authorization API (GET /oauth/v1/generate?grant_type=client_credentials).`});let o=a.size??300,c=Math.round(a.amount),l={MerchantName:a.merchantName.trim(),RefNo:a.refNo.trim(),Amount:c,TrxCode:a.trxCode,CPI:a.cpi.trim(),Size:String(o)},{data:u}=await d(`${e}/mpesa/qrcode/v1/generate`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:l},r));if(br(u))throw yr(u.errorCode,u.errorMessage);if(!xr(u))throw new i({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(u).slice(0,300)}`});return T(_e,u,`Dynamic QR response`)}const Cr=`TransactionReversal`,wr=`11`,W={SUCCESS:0,INSUFFICIENT_BALANCE:1,DEBIT_PARTY_INVALID_STATE:11,INITIATOR_NOT_ALLOWED:21,INITIATOR_INFORMATION_INVALID:2001,DECLINED_ACCOUNT_RULE:2006,NOT_PERMITTED:2028,SECURITY_CREDENTIAL_LOCKED:8006,ALREADY_REVERSED:`R000001`,INVALID_TRANSACTION_ID:`R000002`},Tr={INVALID_ACCESS_TOKEN:`404.001.03`,BAD_REQUEST:`400.002.02`,RESOURCE_NOT_FOUND:`404.001.01`,INTERNAL_SERVER_ERROR:`500.001.1001`,SPIKE_ARREST:`500.003.02`,QUOTA_VIOLATION:`500.003.03`};function Er(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 n.ResultCode!==void 0&&typeof n.ResultDesc==`string`&&typeof n.ConversationID==`string`}function Dr(e){return e.Result.ResultCode===W.SUCCESS}function Or(e){return!Dr(e)}function kr(e){return Object.values(W).includes(e)}function Ar(e){return e.Result.TransactionID??null}function jr(e){return e.Result.ConversationID}function Mr(e){return e.Result.OriginatorConversationID}function Nr(e){return e.Result.ResultCode}function Pr(e){return e.Result.ResultDesc}function G(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return n.find(e=>e.Key===t)?.Value}function Fr(e){let t=G(e,`Amount`);return t===void 0?void 0:Number(t)}function Ir(e){let t=G(e,`OriginalTransactionID`);return t===void 0?void 0:String(t)}function Lr(e){let t=G(e,`CreditPartyPublicName`);return t===void 0?void 0:String(t)}function Rr(e){let t=G(e,`DebitPartyPublicName`);return t===void 0?void 0:String(t)}function zr(e){let t=G(e,`DebitAccountBalance`);return t===void 0?void 0:String(t)}function Br(e){let t=G(e,`TransCompletedTime`);return t===void 0?void 0:Number(t)}function Vr(e){let t=G(e,`Charge`);return t===void 0?void 0:Number(t)}async function Hr(e,t,n,r,i,a){let o=T(fe,i,`Reversal request`),c=Math.round(o.amount),l=o.remarks??`Transaction Reversal`,u={Initiator:r,SecurityCredential:n,CommandID:Cr,TransactionID:o.transactionId,Amount:String(c),ReceiverParty:String(o.receiverParty),RecieverIdentifierType:`11`,ResultURL:o.resultUrl,QueueTimeOutURL:o.queueTimeOutUrl,Remarks:l};o.occasion!==void 0&&o.occasion!==null&&(u.Occasion=o.occasion);let{data:f}=await d(`${e}/mpesa/reversal/v1/request`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:u},a));return T(pe,f,`Reversal response`)}const Ur=r.enum([`CustomerPayBillOnline`,`CustomerBuyGoodsOnline`]),Wr=r.object({amount:r.number().finite({message:`amount must be a finite number (not NaN or Infinity)`}),phoneNumber:D,shortCode:D,passKey:D,callbackUrl:O,accountReference:D,transactionDesc:D,transactionType:Ur.optional(),partyB:r.string().optional()}),Gr=r.object({MerchantRequestID:r.string(),CheckoutRequestID:r.string(),ResponseCode:r.string(),ResponseDescription:r.string(),CustomerMessage:r.string().optional()}).passthrough(),Kr=r.object({checkoutRequestId:D,shortCode:D,passKey:D}),qr=r.object({ResponseCode:r.string(),ResponseDescription:r.string(),MerchantRequestID:r.string().optional(),CheckoutRequestID:r.string().optional(),ResultCode:r.union([r.string(),r.number()]).optional(),ResultDesc:r.string().optional()}).passthrough(),Jr=r.object({Body:r.object({stkCallback:r.object({MerchantRequestID:r.string(),CheckoutRequestID:r.string(),ResultCode:r.number(),ResultDesc:r.string(),CallbackMetadata:r.object({Item:r.array(r.object({Name:r.string(),Value:r.union([r.string(),r.number()])}))}).optional()}).passthrough()})}),K={MIN_AMOUNT:1,MAX_AMOUNT:25e4},q={SUCCESS:0,INSUFFICIENT_BALANCE:1,CANCELLED_BY_USER:1032,PHONE_UNREACHABLE:1037,INVALID_PIN:2001};function Yr(e){return Object.values(q).includes(e)}function Xr(e){return e.ResultCode===q.SUCCESS}function Zr(e,t){let n=e.Body.stkCallback;if(Xr(n))return n.CallbackMetadata.Item.find(e=>e.Name===t)?.Value}function J(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 i({code:`INVALID_PHONE`,message:`Cannot parse "${e}". Use 07XXXXXXXX, 2547XXXXXXXX, 2541XXXXXXXX (Airtel), or +2547XXXXXXXX.`});if(n.length!==12)throw new i({code:`INVALID_PHONE`,message:`"${e}" normalised to "${n}" — expected 12 digits.`});return n}function Qr(e,t,n){return btoa(`${e}${t}${n}`)}function Y(){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 $r(e,t,n,r){let a=T(Wr,n,`STK Push request`);if(!Number.isFinite(a.amount))throw new i({code:`VALIDATION_ERROR`,message:`amount must be a finite number (got ${a.amount}).`});let o=Math.round(a.amount);if(o<K.MIN_AMOUNT)throw new i({code:`VALIDATION_ERROR`,message:`Amount must be at least KES ${K.MIN_AMOUNT} (got ${a.amount} which rounds to ${o}).`});if(o>K.MAX_AMOUNT)throw new i({code:`VALIDATION_ERROR`,message:`Amount must not exceed KES ${K.MAX_AMOUNT.toLocaleString()} per transaction as per Safaricom Daraja limits (got ${a.amount} which rounds to ${o}).`});let c=Y(),l=a.partyB??a.shortCode,u={BusinessShortCode:a.shortCode,Password:Qr(a.shortCode,a.passKey,c),Timestamp:c,TransactionType:a.transactionType??`CustomerPayBillOnline`,Amount:o,PartyA:J(a.phoneNumber),PartyB:l,PhoneNumber:J(a.phoneNumber),CallBackURL:a.callbackUrl,AccountReference:a.accountReference.slice(0,12),TransactionDesc:a.transactionDesc.slice(0,13)},{data:f}=await d(`${e}/mpesa/stkpush/v1/processrequest`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:u,retries:5,retryDelay:3e3},r));return T(Gr,f,`STK Push response`)}async function ei(e,t,n,r){let i=T(Kr,n,`STK Query request`),a=Y(),o={BusinessShortCode:i.shortCode,Password:Qr(i.shortCode,i.passKey,a),Timestamp:a,CheckoutRequestID:i.checkoutRequestId},{data:c}=await d(`${e}/mpesa/stkpushquery/v1/query`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o},r));return T(qr,c,`STK Query response`)}const ti=`572572`,ni=`PayTaxToKRA`;async function ri(e,t,n,r,i,a){let o=T(me,i,`Tax Remittance request`),c=Math.round(o.amount),l={Initiator:r,SecurityCredential:n,CommandID:ni,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(c),PartyA:String(o.partyA),PartyB:o.partyB??`572572`,AccountReference:o.accountReference,Remarks:o.remarks??`Tax Remittance`,QueueTimeOutURL:o.queueTimeOutUrl,ResultURL:o.resultUrl},{data:u}=await d(`${e}/mpesa/b2b/v1/remittax`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:l},a));return T(he,u,`Tax Remittance response`)}function ii(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ai(e){return typeof e==`string`?Number(e):e}function oi(e){if(!ii(e))return!1;let t=e;if(!ii(t.Result))return!1;let n=t.Result;return n.ResultCode!==void 0&&n.ResultCode!==null&&typeof n.ConversationID==`string`&&typeof n.OriginatorConversationID==`string`}function si(e){return ai(e.Result.ResultCode)===0}function ci(e){return!si(e)}function X(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n==null)return;if(Array.isArray(n))return n.find(e=>e.Key===t)?.Value;let r=n;return r.Key===t?r.Value:void 0}function li(e){return e.Result.ResultCode}function ui(e){return e.Result.ResultDesc}function di(e){return e.Result.TransactionID}function fi(e){return e.Result.ConversationID}function pi(e){return e.Result.OriginatorConversationID}function mi(e){let t=X(e,`Amount`);if(t==null)return null;let n=typeof t==`number`?t:parseFloat(String(t));return Number.isNaN(n)?null:n}function hi(e){let t=X(e,`TransactionCompletedTime`);return t===void 0?null:String(t)}function gi(e){let t=X(e,`ReceiverPartyPublicName`);return t===void 0?null:String(t)}async function _i(e,t,n,r,i,a){let o=T(ce,i,`Transaction Status request`),c={Initiator:r,SecurityCredential:n,CommandID:o.commandId??`TransactionStatusQuery`,TransactionID:o.transactionId??``,OriginalConversationID:o.originalConversationId??``,PartyA:o.partyA,IdentifierType:o.identifierType,ResultURL:o.resultUrl,QueueTimeOutURL:o.queueTimeOutUrl,Remarks:o.remarks??`Transaction Status Query`,Occasion:o.occasion??``},{data:l}=await d(`${e}/mpesa/transactionstatus/v1/query`,s({method:`POST`,headers:{Authorization:`Bearer ${t}`},body:c},a));return T(le,l,`Transaction Status response`)}const vi={INVALID_ACCESS_TOKEN:`400.003.01`,BAD_REQUEST:`400.003.02`,INTERNAL_SERVER_ERROR:`500.003.1001`,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`},yi={SUCCESS:0,INVALID_INITIATOR:2001},bi=new Set(Object.values(yi));function xi(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 Si(e){let t=e.Result.ResultCode;return t===0||t===`0`}function Ci(e){return!Si(e)}function wi(e){if(typeof e!=`number`&&typeof e!=`string`||typeof e==`string`&&e.trim()===``)return!1;let t=Number(e);return Number.isFinite(t)&&bi.has(t)}function Ti(e){return e.Result.TransactionID}function Ei(e){return e.Result.ConversationID}function Di(e){return e.Result.OriginatorConversationID}function Oi(e){return e.Result.ResultDesc}function ki(e){return e.Result.ResultCode}function Ai(e){let t=Z(e,`Amount`);if(t===void 0)return null;let n=Number(t);return Number.isFinite(n)?n:null}function ji(e){let t=Z(e,`ReceiptNo`);return t===void 0?null:String(t)}function Mi(e){let t=Z(e,`TransactionStatus`);return t===void 0?null:String(t)}function Ni(e){let t=Z(e,`DebitPartyName`);return t===void 0?null:String(t)}function Pi(e){let t=Z(e,`CreditPartyName`);return t===void 0?null:String(t)}function Fi(e){let t=Z(e,`DebitAccountBalance`);return t===void 0?null:String(t)}function Ii(e){let t=Z(e,`TransactionDate`);return t===void 0?null:String(t)}function Z(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}const Li={sandbox:`https://sandbox.safaricom.co.ke`,production:`https://api.safaricom.co.ke`},Ri={maxRetries:1/0,initialDelay:1e3,maxDelay:36e5,backoffMultiplier:2,maxRetryDuration:720*60*60*1e3};async function zi(e,t={}){let n={...Ri,...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 Bi={sha256:`SHA-256`,sha512:`SHA-512`};function Vi(e){let t=e.trim();if(!/^[0-9a-fA-F]+$/.test(t)||t.length%2!=0)return null;let n=new Uint8Array(t.length/2);for(let e=0;e<n.length;e++)n[e]=Number.parseInt(t.slice(e*2,e*2+2),16);return n}function Hi(e){return[...new Uint8Array(e)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}function Ui(e,t){if(e.length!==t.length)return!1;let n=0;for(let r=0;r<e.length;r++)n|=e[r]^t[r];return n===0}async function Wi(e,t,n,r=`sha256`){if(!n||!t)return!1;let i=globalThis.crypto?.subtle;if(!i)return!1;let a=typeof e==`string`?new TextEncoder().encode(e):e instanceof Uint8Array?e:new Uint8Array(e),o=Vi(t);if(!o)return!1;try{let e=await i.importKey(`raw`,new TextEncoder().encode(n),{name:`HMAC`,hash:Bi[r]},!1,[`sign`]),t=Vi(Hi(await i.sign(`HMAC`,e,a)));return t?Ui(o,t):!1}catch{return!1}}const Q=[`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 $(e,t=Q){return t.includes(e)}function Gi(e){return Jr.safeParse(e).success?e:null}async function Ki(e){let t=[],n=e.allowedIPs??Q,r=e.skipIPCheck===!0||$(e.requestIP,n);r||t.push(`Request IP is not in the Safaricom allowlist.`);let i=!0;return e.secret&&e.signature&&e.rawBody!==void 0?(i=await Wi(e.rawBody,e.signature,e.secret,e.hmacAlgorithm),i||t.push(`Webhook HMAC signature verification failed.`)):e.requireHMAC&&(i=!1,t.push(`HMAC verification required but secret, signature, or rawBody missing.`)),{valid:r&&i,ipValid:r,hmacValid:i,errors:t}}function qi(e,t={}){if(!t.skipIPCheck&&t.requestIP&&!$(t.requestIP,t.allowedIPs))return{success:!1,eventType:null,data:null,error:`IP address ${t.requestIP} is not in the Safaricom whitelist`};let n=Gi(e);return n?{success:!0,eventType:`stk_push`,data:n}:{success:!1,eventType:null,data:null,error:`Unknown or malformed webhook payload`}}function Ji(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`MpesaReceiptNumber`);return t?String(t.Value):null}function Yi(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`Amount`);return t?Number(t.Value):null}function Xi(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`PhoneNumber`);return t?String(t.Value):null}function Zi(e){return e.Body?.stkCallback?.ResultCode===0}var Qi=class{config;tokenManager;baseUrl;idempotencyManager;constructor(e){if(!e.consumerKey||!e.consumerSecret)throw new i({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required.`});this.config=e,this.baseUrl=Li[e.environment],this.tokenManager=new p(e.consumerKey,e.consumerSecret,this.baseUrl),this.idempotencyManager=new y(e.idempotency)}darajaHttp(){return{idempotency:this.idempotencyManager}}getToken(){return this.tokenManager.getAccessToken()}async buildSecurityCredential(){if(this.config.securityCredential)return this.config.securityCredential;if(!this.config.initiatorPassword)throw new i({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 i({code:`INVALID_CREDENTIALS`,message:`certificatePath or certificatePem is required to encrypt the initiator password.`});return m(this.config.initiatorPassword,t)}requireInitiator(e){let t=this.config.initiatorName??``;if(!t)throw new i({code:`VALIDATION_ERROR`,message:`initiatorName is required for ${e}.`});return t}async stkPushSafe(e){try{return C(await this.stkPush(e))}catch(e){return w(e)}}async accountBalanceSafe(e){try{return C(await this.accountBalance(e))}catch(e){return w(e)}}async stkPush(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new i({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push.`});let r=await this.getToken();return $r(this.baseUrl,r,{...e,shortCode:t,passKey:n},this.darajaHttp())}async stkQuery(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new i({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query.`});let r=await this.getToken();return ei(this.baseUrl,r,{...e,shortCode:t,passKey:n},this.darajaHttp())}async transactionStatus(e){let t=this.requireInitiator(`Transaction Status`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return _i(this.baseUrl,n,r,t,e,this.darajaHttp())}async accountBalance(e){let t=this.requireInitiator(`Account Balance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return ve(this.baseUrl,n,r,t,e,this.darajaHttp())}async reverseTransaction(e){let t=this.requireInitiator(`Reversal`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Hr(this.baseUrl,n,r,t,e,this.darajaHttp())}async generateDynamicQR(e){let t=await this.getToken();return Sr(this.baseUrl,t,e,this.darajaHttp())}async registerC2BUrls(e){let t=await this.getToken();return Jn(this.baseUrl,t,e,this.darajaHttp())}async simulateC2B(e){let t=await this.getToken();return Yn(this.baseUrl,t,e,this.darajaHttp())}async remitTax(e){let t=this.requireInitiator(`Tax Remittance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return ri(this.baseUrl,n,r,t,e,this.darajaHttp())}async b2bExpressCheckout(e){let t=await this.getToken();return Le(this.baseUrl,t,e,this.darajaHttp())}async b2bBuyGoods(e){let t=this.requireInitiator(`B2B Buy Goods`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return N(this.baseUrl,n,r,t,e,this.darajaHttp())}async b2bPayBill(e){let t=this.requireInitiator(`B2B Pay Bill`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return ft(this.baseUrl,n,r,t,e,this.darajaHttp())}async b2cPayment(e){let t=this.requireInitiator(`B2C Payment`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return zt(this.baseUrl,n,r,t,e,this.darajaHttp())}async b2cDisbursement(e){let t=this.requireInitiator(`B2C Disbursement`),n={...e,originatorConversationId:e.originatorConversationId??g()},[r,i]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return rn(this.baseUrl,r,i,t,n,this.darajaHttp())}async billManagerOptIn(e){let t=await this.getToken();return Fn(this.baseUrl,t,e,this.darajaHttp())}async updateOptIn(e){let t=await this.getToken();return In(this.baseUrl,t,e,this.darajaHttp())}async sendInvoice(e){let t=await this.getToken();return Ln(this.baseUrl,t,e,this.darajaHttp())}async sendBulkInvoices(e){let t=await this.getToken();return V(this.baseUrl,t,e,this.darajaHttp())}async cancelInvoice(e){let t=await this.getToken();return Rn(this.baseUrl,t,e,this.darajaHttp())}async cancelBulkInvoices(e){let t=await this.getToken();return zn(this.baseUrl,t,e,this.darajaHttp())}async reconcilePayment(e){let t=await this.getToken();return Bn(this.baseUrl,t,e,this.darajaHttp())}clearTokenCache(){this.tokenManager.clearCache()}get environment(){return this.config.environment}};const $i=r.object({Result:r.object({ResultType:r.number(),ResultCode:r.number(),ResultDesc:r.string(),OriginatorConversationID:r.string().optional(),ConversationID:r.string().optional(),TransactionID:r.string().optional()})}).passthrough(),ea=r.object({TransID:r.string(),TransAmount:r.union([r.string(),r.number()]),MSISDN:r.string(),BusinessShortCode:r.string()}).passthrough();export{ye as ACCOUNT_BALANCE_ERROR_CODES,f as AUTH_ERROR_CODES,ue as AccountBalanceRequestSchema,de as AccountBalanceResponseSchema,k as AsyncApiResponseSchema,Ae as B2BBuyGoodsRequestSchema,Me as B2BBuyGoodsResponseSchema,Pe as B2BExpressCheckoutRequestSchema,Fe as B2BExpressCheckoutResponseSchema,je as B2BPayBillRequestSchema,Ne as B2BPayBillResponseSchema,Ie as B2BResponseSchema,Ge as B2B_BUY_GOODS_ERROR_CODES,We as B2B_BUY_GOODS_RESULT_CODES,mt as B2B_PAY_BILL_ERROR_CODES,pt as B2B_PAY_BILL_RESULT_CODES,j as B2B_RESULT_CODES,Lt as B2CDisbursementRequestSchema,Rt as B2CDisbursementResponseSchema,Ft as B2CRequestSchema,It as B2CResponseSchema,$i as B2CResultWebhookSchema,L as B2C_DISBURSEMENT_RESULT_CODES,Bt as B2C_ERROR_CODES,Vt as B2C_RESULT_CODES,Dn as BillManagerBulkInvoiceRequestSchema,On as BillManagerBulkInvoiceResponseSchema,jn as BillManagerCancelBulkInvoiceRequestSchema,Mn as BillManagerCancelBulkInvoiceResponseSchema,kn as BillManagerCancelInvoiceRequestSchema,An as BillManagerCancelInvoiceResponseSchema,Tn as BillManagerInvoiceItemSchema,z as BillManagerOptInRequestSchema,Sn as BillManagerOptInResponseSchema,Nn as BillManagerReconciliationRequestSchema,Pn as BillManagerReconciliationResponseSchema,B as BillManagerSingleInvoiceRequestSchema,En as BillManagerSingleInvoiceResponseSchema,Cn as BillManagerUpdateOptInRequestSchema,wn as BillManagerUpdateOptInResponseSchema,H as C2BBaseResponseSchema,ea as C2BConfirmationWebhookSchema,Vn as C2BRegisterUrlRequestSchema,Hn as C2BRegisterUrlResponseSchema,Wn as C2BSimulateRequestSchema,Un as C2BSimulateResponseSchema,Gn as C2BValidationWebhookSchema,Xn as C2B_REGISTER_URL_ERROR_CODES,Zn as C2B_VALIDATION_RESULT_CODES,Li as DARAJA_BASE_URLS,dr as DEFAULT_QR_SIZE,oe as DarajaErrorResponseSchema,ge as DynamicQRRequestSchema,_e as DynamicQRResponseSchema,ie as EnvironmentSchema,y as IdempotencyManager,v as InMemoryIdempotencyStore,ti as KRA_SHORTCODE,E as KesAmountSchema,lr as MAX_QR_SIZE,cr as MIN_AMOUNT,ur as MIN_QR_SIZE,Qi as Mpesa,ae as MsisdnSchema,D as NonEmptyStringSchema,i as PesafyError,U as QR_TRANSACTION_CODES,Cr as REVERSAL_COMMAND_ID,Tr as REVERSAL_ERROR_CODES,wr as REVERSAL_RECEIVER_IDENTIFIER_TYPE,W as REVERSAL_RESULT_CODES,fe as ReversalRequestSchema,pe as ReversalResponseSchema,Q as SAFARICOM_IPS,K as STK_PUSH_LIMITS,q as STK_RESULT_CODES,Wr as StkPushRequestSchema,Gr as StkPushResponseSchema,Jr as StkPushWebhookSchema,Kr as StkQueryRequestSchema,qr as StkQueryResponseSchema,ni as TAX_COMMAND_ID,vi as TRANSACTION_STATUS_ERROR_CODES,yi as TRANSACTION_STATUS_RESULT_CODES,me as TaxRemittanceRequestSchema,he as TaxRemittanceResponseSchema,p as TokenManager,ce as TransactionStatusRequestSchema,le as TransactionStatusResponseSchema,Ur as TransactionTypeSchema,O as UrlSchema,$n as acceptC2BValidation,tr as acknowledgeC2BConfirmation,Fn as billManagerOptIn,zn as cancelBulkInvoices,Rn as cancelInvoice,a as createError,m as encryptSecurityCredential,w as err,Yi as extractAmount,Xi as extractPhoneNumber,Ji as extractTransactionId,J as formatPhoneNumber,J as formatSafaricomPhone,Sr as generateDynamicQR,h as generateIdempotencyKey,g as generateOriginatorConversationId,_ as generateRequestRefId,we as getAccountBalanceCompletedTime,Se as getAccountBalanceConversationId,Ce as getAccountBalanceOriginatorConversationId,A as getAccountBalanceParam,Te as getAccountBalanceRawBalance,Ee as getAccountBalanceReferenceItem,xe as getAccountBalanceTransactionId,Ve as getB2BAmount,nt as getB2BBuyGoodsAmount,ut as getB2BBuyGoodsBillReferenceNumber,rt as getB2BBuyGoodsCompletedTime,Qe as getB2BBuyGoodsConversationId,ot as getB2BBuyGoodsCurrency,ct as getB2BBuyGoodsDebitAccountBalance,st as getB2BBuyGoodsDebitPartyAffectedBalance,at as getB2BBuyGoodsDebitPartyCharges,lt as getB2BBuyGoodsInitiatorBalance,$e as getB2BBuyGoodsOriginatorConversationId,dt as getB2BBuyGoodsQueueTimeoutUrl,it as getB2BBuyGoodsReceiverName,tt as getB2BBuyGoodsResultCode,et as getB2BBuyGoodsResultDesc,P as getB2BBuyGoodsResultParam,Ze as getB2BBuyGoodsTransactionId,Ue as getB2BConversationId,Tt as getB2BPayBillAmount,Nt as getB2BPayBillBillReferenceNumber,Et as getB2BPayBillCompletedTime,xt as getB2BPayBillConversationId,kt as getB2BPayBillCurrency,jt as getB2BPayBillDebitAccountBalance,At as getB2BPayBillDebitPartyAffectedBalance,Ot as getB2BPayBillDebitPartyCharges,Mt as getB2BPayBillInitiatorBalance,St as getB2BPayBillOriginatorConversationId,Dt as getB2BPayBillReceiverName,wt as getB2BPayBillResultCode,Ct as getB2BPayBillResultDesc,F as getB2BPayBillResultParam,bt as getB2BPayBillTransactionId,Be as getB2BRequestId,He as getB2BTransactionId,Xt as getB2CAmount,qt as getB2CConversationId,Zt as getB2CCurrency,en as getB2CDebitAccountBalance,tn as getB2CDebitPartyCharges,mn as getB2CDisbursementAmount,_n as getB2CDisbursementCompletedTime,un as getB2CDisbursementConversationId,dn as getB2CDisbursementOriginatorConversationId,hn as getB2CDisbursementReceiptNumber,gn as getB2CDisbursementReceiverName,pn as getB2CDisbursementResultCode,fn as getB2CDisbursementResultDesc,R as getB2CDisbursementResultParam,ln as getB2CDisbursementTransactionId,vn as getB2CDisbursementUtilityBalance,yn as getB2CDisbursementWorkingBalance,Jt as getB2COriginatorConversationId,Qt as getB2CReceiverPublicName,Yt as getB2CResultDesc,I as getB2CResultParam,$t as getB2CTransactionCompletedTime,Kt as getB2CTransactionId,ir as getC2BAccountRef,nr as getC2BAmount,ar as getC2BCustomerName,rr as getC2BTransactionId,Zr as getCallbackValue,Fr as getReversalAmount,Vr as getReversalCharge,Br as getReversalCompletedTime,jr as getReversalConversationId,Lr as getReversalCreditPartyPublicName,zr as getReversalDebitAccountBalance,Rr as getReversalDebitPartyPublicName,Ir as getReversalOriginalTransactionId,Mr as getReversalOriginatorConversationId,Nr as getReversalResultCode,Pr as getReversalResultDesc,G as getReversalResultParam,Ar as getReversalTransactionId,mi as getTaxAmount,hi as getTaxCompletedTime,fi as getTaxConversationId,pi as getTaxOriginatorConversationId,gi as getTaxReceiverName,li as getTaxResultCode,ui as getTaxResultDesc,X as getTaxResultParam,di as getTaxTransactionId,Y as getTimestamp,Ai as getTransactionStatusAmount,Ei as getTransactionStatusConversationId,Pi as getTransactionStatusCreditPartyName,Fi as getTransactionStatusDebitAccountBalance,Ni as getTransactionStatusDebitPartyName,Di as getTransactionStatusOriginatorConversationId,ji as getTransactionStatusReceiptNo,ki as getTransactionStatusResultCode,Oi as getTransactionStatusResultDesc,Z as getTransactionStatusResultParam,Mi as getTransactionStatusStatus,Ii as getTransactionStatusTransactionDate,Ti as getTransactionStatusTransactionId,qi as handleWebhook,d as httpRequest,N as initiateB2BBuyGoods,Le as initiateB2BExpressCheckout,ft as initiateB2BPayBill,rn as initiateB2CDisbursement,zt as initiateB2CPayment,De as isAccountBalanceSuccess,Ye as isB2BBuyGoodsFailure,qe as isB2BBuyGoodsResult,Je as isB2BBuyGoodsSuccess,Re as isB2BCheckoutCallback,ze as isB2BCheckoutCancelled,M as isB2BCheckoutSuccess,vt as isB2BPayBillFailure,gt as isB2BPayBillResult,_t as isB2BPayBillSuccess,sn as isB2CDisbursementFailure,bn as isB2CDisbursementRecipientRegistered,an as isB2CDisbursementResult,on as isB2CDisbursementSuccess,Wt as isB2CFailure,Ht as isB2CResult,Ut as isB2CSuccess,sr as isBuyGoodsPayment,Qn as isC2BPayload,Xe as isKnownB2BBuyGoodsResultCode,yt as isKnownB2BPayBillResultCode,cn as isKnownB2CDisbursementResultCode,Gt as isKnownB2CResultCode,kr as isKnownReversalResultCode,Yr as isKnownStkResultCode,wi as isKnownTransactionStatusResultCode,or as isPaybillPayment,o as isPesafyError,Or as isReversalFailure,Er as isReversalResult,Dr as isReversalSuccess,Xr as isStkCallbackSuccess,Zi as isSuccessfulCallback,ci as isTaxRemittanceFailure,oi as isTaxRemittanceResult,si as isTaxRemittanceSuccess,Ci as isTransactionStatusFailure,xi as isTransactionStatusResult,Si as isTransactionStatusSuccess,C as ok,be as parseAccountBalance,Gi as parseStkPushWebhook,ve as queryAccountBalance,_i as queryTransactionStatus,Bn as reconcilePayment,Jn as registerC2BUrls,er as rejectC2BValidation,ri as remitTax,Hr as requestReversal,zi as retryWithBackoff,V as sendBulkInvoices,Ln as sendSingleInvoice,Yn as simulateC2B,b as toKesAmount,x as toMsisdn,ne as toNonEmpty,S as toPaybill,te as toShortCode,ee as toTill,In as updateOptIn,mr as validateAmount,gr as validateCpi,vr as validateDynamicQRRequest,fr as validateMerchantName,pr as validateRefNo,_r as validateSize,hr as validateTrxCode,Ki as verifyWebhook,Wi as verifyWebhookHMAC,$ as verifyWebhookIP};
1
+ import { readFile } from "node:fs/promises";
2
+ import { constants, publicEncrypt } from "node:crypto";
3
+ import { z } from "zod";
4
+
5
+ //#region src/utils/errors/index.ts
6
+ var PesafyError = class PesafyError extends Error {
7
+ code;
8
+ statusCode;
9
+ response;
10
+ requestId;
11
+ cause;
12
+ retryable;
13
+ constructor(options) {
14
+ super(options.message);
15
+ Object.defineProperty(this, "name", { value: "PesafyError" });
16
+ this.code = options.code;
17
+ this.statusCode = options.statusCode;
18
+ this.response = options.response;
19
+ this.requestId = options.requestId;
20
+ this.cause = options.cause;
21
+ this.retryable = options.retryable ?? (options.code === "NETWORK_ERROR" || options.code === "TIMEOUT" || options.code === "RATE_LIMITED" || options.code === "REQUEST_FAILED");
22
+ if (Error.captureStackTrace) Error.captureStackTrace(this, PesafyError);
23
+ }
24
+ /** Returns true if this is a validation error (user bug — do not retry) */
25
+ get isValidation() {
26
+ return this.code === "VALIDATION_ERROR";
27
+ }
28
+ /** Returns true if this is an auth error */
29
+ get isAuth() {
30
+ return this.code === "AUTH_FAILED" || this.code === "INVALID_CREDENTIALS";
31
+ }
32
+ toJSON() {
33
+ return {
34
+ name: this.name,
35
+ code: this.code,
36
+ message: this.message,
37
+ statusCode: this.statusCode,
38
+ requestId: this.requestId,
39
+ retryable: this.retryable
40
+ };
41
+ }
42
+ };
43
+ /** Convenience factory */
44
+ function createError(options) {
45
+ return new PesafyError(options);
46
+ }
47
+ /** Type guard */
48
+ function isPesafyError(err) {
49
+ return err instanceof PesafyError;
50
+ }
51
+
52
+ //#endregion
53
+ //#region src/utils/http/index.ts
54
+ /** Merge explicit Daraja HTTP options into an httpRequest options object. */
55
+ function withDarajaHttp(options, http) {
56
+ if (!http?.idempotency) return options;
57
+ return {
58
+ ...options,
59
+ idempotency: http.idempotency
60
+ };
61
+ }
62
+ const RETRYABLE_STATUSES = new Set([
63
+ 429,
64
+ 500,
65
+ 502,
66
+ 503,
67
+ 504
68
+ ]);
69
+ function sleep(ms) {
70
+ return new Promise((r) => setTimeout(r, ms));
71
+ }
72
+ function withJitter(base) {
73
+ const spread = base * .25;
74
+ return base + (Math.random() * spread * 2 - spread);
75
+ }
76
+ /** Origin + path only (no query) for safe retry logs. */
77
+ function logSafeUrl(url) {
78
+ try {
79
+ const u = new URL(url);
80
+ return `${u.origin}${u.pathname}`;
81
+ } catch {
82
+ return url.split("?")[0] ?? url;
83
+ }
84
+ }
85
+ /**
86
+ * Sends an HTTP request to Daraja and returns parsed JSON.
87
+ * Automatically retries transient failures with exponential back-off.
88
+ *
89
+ * @throws {PesafyError} on non-retryable errors or exhausted retries.
90
+ */
91
+ async function httpRequest(url, options) {
92
+ const maxRetries = options.retries ?? 4;
93
+ const baseDelay = options.retryDelay ?? 2e3;
94
+ const timeout = options.timeout ?? 3e4;
95
+ const headers = {
96
+ "Content-Type": "application/json",
97
+ Accept: "application/json",
98
+ ...options.headers
99
+ };
100
+ const manager = options.idempotency;
101
+ let idempotencyKey = options.idempotencyKey;
102
+ if (options.method === "POST" && manager?.enabled) {
103
+ idempotencyKey = manager.reserve(idempotencyKey);
104
+ const headerName = manager.headerName;
105
+ headers[headerName] = idempotencyKey;
106
+ } else if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
107
+ const init = {
108
+ method: options.method,
109
+ headers,
110
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
111
+ };
112
+ let lastError = null;
113
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
114
+ if (attempt > 0) {
115
+ const delay = withJitter(baseDelay * Math.pow(2, attempt - 1));
116
+ console.warn(`[pesafy] Retry ${attempt}/${maxRetries} → ${options.method} ${logSafeUrl(url)} in ${Math.round(delay)} ms`);
117
+ await sleep(delay);
118
+ }
119
+ const controller = new AbortController();
120
+ const tid = setTimeout(() => controller.abort(), timeout);
121
+ let response;
122
+ try {
123
+ response = await fetch(url, {
124
+ ...init,
125
+ signal: controller.signal
126
+ });
127
+ } catch (err) {
128
+ clearTimeout(tid);
129
+ if (err instanceof Error && err.name === "AbortError") lastError = new PesafyError({
130
+ code: "TIMEOUT",
131
+ message: `Request to ${url} timed out after ${timeout} ms`,
132
+ cause: err,
133
+ retryable: true
134
+ });
135
+ else lastError = new PesafyError({
136
+ code: "NETWORK_ERROR",
137
+ message: `Network error: ${err instanceof Error ? err.message : String(err)}`,
138
+ cause: err,
139
+ retryable: true
140
+ });
141
+ if (attempt < maxRetries) continue;
142
+ if (idempotencyKey && manager?.enabled) manager.release(idempotencyKey);
143
+ throw lastError;
144
+ } finally {
145
+ clearTimeout(tid);
146
+ }
147
+ let rawText = "";
148
+ let data = null;
149
+ const ct = response.headers.get("content-type") ?? "";
150
+ try {
151
+ rawText = await response.text();
152
+ if (rawText) data = ct.includes("application/json") ? JSON.parse(rawText) : rawText;
153
+ } catch {
154
+ data = rawText || null;
155
+ }
156
+ const responseHeaders = {};
157
+ response.headers.forEach((v, k) => {
158
+ responseHeaders[k] = v;
159
+ });
160
+ if (response.ok) {
161
+ if (idempotencyKey && manager?.enabled) manager.complete(idempotencyKey);
162
+ return {
163
+ data,
164
+ status: response.status,
165
+ headers: responseHeaders
166
+ };
167
+ }
168
+ const isTransient = RETRYABLE_STATUSES.has(response.status);
169
+ const daraja = typeof data === "object" && data !== null ? data : {};
170
+ const message = daraja["errorMessage"] ?? daraja["ResponseDescription"] ?? daraja["resultDesc"] ?? rawText ?? `HTTP ${response.status}`;
171
+ lastError = new PesafyError({
172
+ code: isTransient ? "REQUEST_FAILED" : "API_ERROR",
173
+ message,
174
+ statusCode: response.status,
175
+ response: data,
176
+ retryable: isTransient,
177
+ ...typeof daraja["requestId"] === "string" ? { requestId: daraja["requestId"] } : {}
178
+ });
179
+ if (isTransient && attempt < maxRetries) continue;
180
+ if (idempotencyKey && manager?.enabled) manager.release(idempotencyKey);
181
+ throw lastError;
182
+ }
183
+ if (idempotencyKey && manager?.enabled) manager.release(idempotencyKey);
184
+ throw lastError;
185
+ }
186
+
187
+ //#endregion
188
+ //#region src/core/auth/types.ts
189
+ /**
190
+ * Daraja Authorization API error codes
191
+ * Documented at: https://developer.safaricom.co.ke/APIs/Authorization
192
+ *
193
+ * These are returned in the `errorCode` field of a 400 error response body
194
+ * when the OAuth token request is malformed.
195
+ */
196
+ const AUTH_ERROR_CODES = {
197
+ INVALID_AUTH_TYPE: "400.008.01",
198
+ INVALID_GRANT_TYPE: "400.008.02"
199
+ };
200
+
201
+ //#endregion
202
+ //#region src/core/auth/token-manager.ts
203
+ /** Refresh the token this many seconds before it actually expires */
204
+ const TOKEN_BUFFER_SECONDS = 60;
205
+ var TokenManager = class {
206
+ consumerKey;
207
+ consumerSecret;
208
+ baseUrl;
209
+ cachedToken = null;
210
+ tokenExpiresAt = 0;
211
+ constructor(consumerKey, consumerSecret, baseUrl) {
212
+ this.consumerKey = consumerKey;
213
+ this.consumerSecret = consumerSecret;
214
+ this.baseUrl = baseUrl;
215
+ }
216
+ getBasicAuthHeader() {
217
+ const credentials = `${this.consumerKey}:${this.consumerSecret}`;
218
+ return `Basic ${Buffer.from(credentials, "utf-8").toString("base64")}`;
219
+ }
220
+ /**
221
+ * Maps Daraja-specific auth error codes (400.008.01 / 400.008.02) to
222
+ * descriptive PesafyError messages so callers get actionable feedback.
223
+ *
224
+ * Always throws — the `never` return type signals this to TypeScript.
225
+ */
226
+ mapAuthError(error) {
227
+ if (error instanceof PesafyError) {
228
+ if (error.code === "AUTH_FAILED") throw error;
229
+ const raw = error.response;
230
+ if (raw && typeof raw === "object") {
231
+ const errorCode = raw["errorCode"] ?? raw["error_code"];
232
+ if (errorCode === AUTH_ERROR_CODES.INVALID_AUTH_TYPE) throw new PesafyError({
233
+ code: "AUTH_FAILED",
234
+ message: "Invalid authentication type (400.008.01). Use Basic authentication: Authorization: Basic <Base64(consumerKey:consumerSecret)>.",
235
+ ...error.statusCode !== void 0 && { statusCode: error.statusCode },
236
+ response: error.response
237
+ });
238
+ if (errorCode === AUTH_ERROR_CODES.INVALID_GRANT_TYPE) throw new PesafyError({
239
+ code: "AUTH_FAILED",
240
+ message: "Invalid grant type (400.008.02). Set grant_type=client_credentials in the request query parameters.",
241
+ ...error.statusCode !== void 0 && { statusCode: error.statusCode },
242
+ response: error.response
243
+ });
244
+ }
245
+ throw error;
246
+ }
247
+ throw error;
248
+ }
249
+ /**
250
+ * Returns a valid access token, fetching a new one when the cached token
251
+ * is absent or within TOKEN_BUFFER_SECONDS of expiry.
252
+ *
253
+ * Daraja endpoint: GET /oauth/v1/generate?grant_type=client_credentials
254
+ * Auth: Basic Base64(consumerKey:consumerSecret)
255
+ * Token lifetime: 3599 seconds (Daraja docs)
256
+ */
257
+ async getAccessToken() {
258
+ const now = Date.now() / 1e3;
259
+ if (this.cachedToken && this.tokenExpiresAt > now + TOKEN_BUFFER_SECONDS) return this.cachedToken;
260
+ const url = `${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`;
261
+ try {
262
+ const response = await httpRequest(url, {
263
+ method: "GET",
264
+ headers: { Authorization: this.getBasicAuthHeader() }
265
+ });
266
+ const { access_token, expires_in } = response.data;
267
+ if (!access_token) throw new PesafyError({
268
+ code: "AUTH_FAILED",
269
+ message: "Daraja did not return an access token. Verify your consumer key and consumer secret.",
270
+ response: response.data
271
+ });
272
+ this.cachedToken = access_token;
273
+ this.tokenExpiresAt = now + (expires_in ?? 3600);
274
+ return this.cachedToken;
275
+ } catch (error) {
276
+ return this.mapAuthError(error);
277
+ }
278
+ }
279
+ /** Force token refresh on the next call (e.g. after a 401 response) */
280
+ clearCache() {
281
+ this.cachedToken = null;
282
+ this.tokenExpiresAt = 0;
283
+ }
284
+ };
285
+
286
+ //#endregion
287
+ //#region src/core/encryption/security-credentials.ts
288
+ function encryptSecurityCredential(initiatorPassword, certificatePem) {
289
+ try {
290
+ const passwordBuffer = Buffer.from(initiatorPassword, "utf-8");
291
+ return publicEncrypt({
292
+ key: certificatePem,
293
+ padding: constants.RSA_PKCS1_PADDING
294
+ }, passwordBuffer).toString("base64");
295
+ } catch (error) {
296
+ throw new PesafyError({
297
+ code: "ENCRYPTION_FAILED",
298
+ message: "Failed to encrypt security credential. Ensure the certificate PEM is valid and matches the environment (sandbox/production).",
299
+ cause: error
300
+ });
301
+ }
302
+ }
303
+
304
+ //#endregion
305
+ //#region src/core/idempotency/generate-key.ts
306
+ /**
307
+ * Generate idempotency keys for Daraja mutating requests.
308
+ */
309
+ /** UUID v4 idempotency key, optionally prefixed for debugging. */
310
+ function generateIdempotencyKey(prefix) {
311
+ const id = crypto.randomUUID();
312
+ return prefix ? `${prefix}-${id}` : id;
313
+ }
314
+ /** Daraja OriginatorConversationID — unique per async API request. */
315
+ function generateOriginatorConversationId() {
316
+ return generateIdempotencyKey("pesafy");
317
+ }
318
+ /** B2B Express RequestRefID — unique per checkout request. */
319
+ function generateRequestRefId() {
320
+ return crypto.randomUUID();
321
+ }
322
+
323
+ //#endregion
324
+ //#region src/core/idempotency/store.ts
325
+ var InMemoryIdempotencyStore = class {
326
+ entries = /* @__PURE__ */ new Map();
327
+ get(key) {
328
+ return this.entries.get(key);
329
+ }
330
+ set(key, entry) {
331
+ this.entries.set(key, entry);
332
+ }
333
+ delete(key) {
334
+ this.entries.delete(key);
335
+ }
336
+ /** Remove entries older than ttlMs. */
337
+ prune(ttlMs) {
338
+ const cutoff = Date.now() - ttlMs;
339
+ for (const [key, entry] of this.entries) if (entry.createdAt < cutoff) this.entries.delete(key);
340
+ }
341
+ };
342
+
343
+ //#endregion
344
+ //#region src/core/idempotency/manager.ts
345
+ const DEFAULT_TTL_MS = 1440 * 60 * 1e3;
346
+ var IdempotencyManager = class {
347
+ enabled;
348
+ headerName;
349
+ ttlMs;
350
+ store;
351
+ generateKey;
352
+ constructor(config = {}) {
353
+ this.enabled = config.enabled !== false;
354
+ this.headerName = config.headerName ?? "Idempotency-Key";
355
+ this.ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
356
+ this.store = config.store ?? new InMemoryIdempotencyStore();
357
+ this.generateKey = config.generateKey ?? generateIdempotencyKey;
358
+ }
359
+ /**
360
+ * Reserve an idempotency key before the HTTP call.
361
+ * @throws PesafyError with IDEMPOTENCY_ERROR if duplicate in-flight/completed within TTL.
362
+ */
363
+ reserve(key) {
364
+ if (!this.enabled) return key ?? this.generateKey();
365
+ this.pruneExpired();
366
+ const resolved = key ?? this.generateKey();
367
+ const existing = this.store.get(resolved);
368
+ if (existing) {
369
+ if (Date.now() - existing.createdAt < this.ttlMs) throw new PesafyError({
370
+ code: "IDEMPOTENCY_ERROR",
371
+ message: `Duplicate request detected for idempotency key "${resolved}".`
372
+ });
373
+ this.store.delete(resolved);
374
+ }
375
+ this.store.set(resolved, {
376
+ key: resolved,
377
+ createdAt: Date.now()
378
+ });
379
+ return resolved;
380
+ }
381
+ /** Mark key as successfully completed. */
382
+ complete(key) {
383
+ if (!this.enabled) return;
384
+ const entry = this.store.get(key);
385
+ if (entry) this.store.set(key, {
386
+ ...entry,
387
+ completedAt: Date.now()
388
+ });
389
+ }
390
+ /** Release reservation on failure so callers can retry with same key. */
391
+ release(key) {
392
+ if (!this.enabled) return;
393
+ this.store.delete(key);
394
+ }
395
+ pruneExpired() {
396
+ if (this.store instanceof InMemoryIdempotencyStore) this.store.prune(this.ttlMs);
397
+ }
398
+ };
399
+
400
+ //#endregion
401
+ //#region src/types/branded.ts
402
+ /**
403
+ * Creates a validated KesAmount.
404
+ * @throws {TypeError} if amount is not a whole number ≥ 1
405
+ */
406
+ function toKesAmount(value) {
407
+ const rounded = Math.round(value);
408
+ if (!Number.isFinite(rounded) || rounded < 1) throw new TypeError(`KesAmount must be a whole number ≥ 1, got ${value}`);
409
+ return rounded;
410
+ }
411
+ /**
412
+ * Creates a validated MsisdnKE from any common Kenyan phone format.
413
+ */
414
+ function toMsisdn(phone) {
415
+ const digits = phone.replace(/\D/g, "");
416
+ let normalised;
417
+ if (digits.startsWith("254") && digits.length === 12) normalised = digits;
418
+ else if (digits.startsWith("0") && digits.length === 10) normalised = `254${digits.slice(1)}`;
419
+ else if (digits.length === 9) normalised = `254${digits}`;
420
+ else throw new TypeError(`Cannot normalise "${phone}" to 254XXXXXXXXX. Use 07XX…, 2547XX…, or +2547XX….`);
421
+ if (normalised.length !== 12) throw new TypeError(`Phone "${phone}" normalised to "${normalised}" — expected 12 digits.`);
422
+ return normalised;
423
+ }
424
+ function toPaybill(code) {
425
+ return String(code);
426
+ }
427
+ function toTill(code) {
428
+ return String(code);
429
+ }
430
+ function toShortCode(code) {
431
+ return String(code);
432
+ }
433
+ function ok(data) {
434
+ return {
435
+ ok: true,
436
+ data
437
+ };
438
+ }
439
+ function err(error) {
440
+ return {
441
+ ok: false,
442
+ error
443
+ };
444
+ }
445
+ function toNonEmpty(s) {
446
+ if (!s.trim()) throw new TypeError("String must not be empty");
447
+ return s;
448
+ }
449
+
450
+ //#endregion
451
+ //#region src/core/validation/zod-error.ts
452
+ /** Map Zod validation failures to PesafyError. */
453
+ function zodToPesafyError(error, label = "Request") {
454
+ return new PesafyError({
455
+ code: "VALIDATION_ERROR",
456
+ message: `${label} validation failed: ${error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`,
457
+ cause: error
458
+ });
459
+ }
460
+ /** Parse with Zod; throw PesafyError on failure. */
461
+ function parseWithSchema(schema, data, label) {
462
+ const result = schema.safeParse(data);
463
+ if (!result.success) throw zodToPesafyError(result.error, label);
464
+ return result.data;
465
+ }
466
+
467
+ //#endregion
468
+ //#region src/schemas/common.ts
469
+ const EnvironmentSchema = z.enum(["sandbox", "production"]);
470
+ const MsisdnSchema = z.string().min(10).regex(/^254\d{9}$/, "Must be Safaricom format 2547XXXXXXXX");
471
+ const KesAmountSchema = z.number().finite().positive().refine((n) => Math.round(n) >= 1, { message: "amount must round to at least 1 KES" });
472
+ const NonEmptyStringSchema = z.string().trim().min(1);
473
+ const UrlSchema = z.string().url();
474
+ const DarajaErrorResponseSchema = z.object({
475
+ errorMessage: z.string().optional(),
476
+ ResponseDescription: z.string().optional(),
477
+ resultDesc: z.string().optional(),
478
+ requestId: z.string().optional()
479
+ }).passthrough();
480
+
481
+ //#endregion
482
+ //#region src/schemas/async-apis.ts
483
+ const IdentifierTypeSchema = z.enum([
484
+ "1",
485
+ "2",
486
+ "4"
487
+ ]);
488
+ const AsyncApiResponseSchema = z.object({
489
+ ConversationID: z.string().optional(),
490
+ OriginatorConversationID: z.string().optional(),
491
+ ResponseCode: z.string(),
492
+ ResponseDescription: z.string()
493
+ }).passthrough();
494
+ const TransactionStatusRequestSchema = z.object({
495
+ transactionId: z.string().optional(),
496
+ originalConversationId: z.string().optional(),
497
+ partyA: NonEmptyStringSchema,
498
+ identifierType: IdentifierTypeSchema,
499
+ resultUrl: UrlSchema,
500
+ queueTimeOutUrl: UrlSchema,
501
+ commandId: z.literal("TransactionStatusQuery").optional(),
502
+ remarks: z.string().optional(),
503
+ occasion: z.string().optional()
504
+ }).superRefine((data, ctx) => {
505
+ if (!data.transactionId?.trim() && !data.originalConversationId?.trim()) ctx.addIssue({
506
+ code: "custom",
507
+ message: "Either transactionId (M-Pesa Receipt Number) or originalConversationId is required",
508
+ path: ["transactionId"]
509
+ });
510
+ });
511
+ const TransactionStatusResponseSchema = AsyncApiResponseSchema;
512
+ const AccountBalanceRequestSchema = z.object({
513
+ partyA: NonEmptyStringSchema,
514
+ identifierType: IdentifierTypeSchema,
515
+ resultUrl: UrlSchema,
516
+ queueTimeOutUrl: UrlSchema,
517
+ remarks: z.string().optional()
518
+ });
519
+ const AccountBalanceResponseSchema = AsyncApiResponseSchema;
520
+ const ReversalRequestSchema = z.object({
521
+ transactionId: NonEmptyStringSchema,
522
+ receiverParty: NonEmptyStringSchema,
523
+ receiverIdentifierType: z.literal("11").optional(),
524
+ amount: KesAmountSchema,
525
+ resultUrl: UrlSchema,
526
+ queueTimeOutUrl: UrlSchema,
527
+ remarks: z.string().optional(),
528
+ occasion: z.string().optional()
529
+ }).superRefine((data, ctx) => {
530
+ if (data.receiverIdentifierType !== void 0 && data.receiverIdentifierType !== "11") ctx.addIssue({
531
+ code: "custom",
532
+ message: "receiverIdentifierType must be \"11\" for the Reversals API",
533
+ path: ["receiverIdentifierType"]
534
+ });
535
+ const remarks = data.remarks ?? "Transaction Reversal";
536
+ if (remarks.length < 2 || remarks.length > 100) ctx.addIssue({
537
+ code: "custom",
538
+ message: "remarks must be between 2 and 100 characters",
539
+ path: ["remarks"]
540
+ });
541
+ });
542
+ const ReversalResponseSchema = AsyncApiResponseSchema;
543
+ const TaxRemittanceRequestSchema = z.object({
544
+ amount: KesAmountSchema,
545
+ partyA: NonEmptyStringSchema,
546
+ partyB: z.string().optional(),
547
+ accountReference: NonEmptyStringSchema,
548
+ resultUrl: UrlSchema,
549
+ queueTimeOutUrl: UrlSchema,
550
+ remarks: z.string().optional()
551
+ });
552
+ const TaxRemittanceResponseSchema = AsyncApiResponseSchema;
553
+ const DynamicQRRequestSchema = z.object({
554
+ merchantName: NonEmptyStringSchema,
555
+ refNo: NonEmptyStringSchema,
556
+ amount: KesAmountSchema,
557
+ trxCode: z.enum([
558
+ "BG",
559
+ "WA",
560
+ "PB",
561
+ "SM",
562
+ "SB"
563
+ ]),
564
+ cpi: NonEmptyStringSchema,
565
+ size: z.number().int().min(1).max(1e3).optional()
566
+ });
567
+ const DynamicQRResponseSchema = z.object({
568
+ ResponseCode: z.string(),
569
+ RequestID: z.string().optional(),
570
+ ResponseDescription: z.string(),
571
+ QRCode: z.string().optional()
572
+ }).passthrough();
573
+
574
+ //#endregion
575
+ //#region src/mpesa/account-balance/query.ts
576
+ /**
577
+ * Account Balance Query — checks the balance of an M-PESA shortcode.
578
+ *
579
+ * API: POST /mpesa/accountbalance/v1/query
580
+ *
581
+ * This is ASYNCHRONOUS. The sync response only confirms receipt.
582
+ * Balance data arrives via POST to your ResultURL.
583
+ *
584
+ * Required org portal role: "Balance Query ORG API" (Account Balance ORG API initiator)
585
+ *
586
+ * Ref: https://sandbox.safaricom.co.ke/mpesa/accountbalance/v1/query
587
+ */
588
+ async function queryAccountBalance(baseUrl, accessToken, securityCredential, initiatorName, request, http) {
589
+ const validated = parseWithSchema(AccountBalanceRequestSchema, request, "Account Balance request");
590
+ const payload = {
591
+ Initiator: initiatorName,
592
+ SecurityCredential: securityCredential,
593
+ CommandID: "AccountBalance",
594
+ PartyA: String(validated.partyA.trim()),
595
+ IdentifierType: validated.identifierType,
596
+ ResultURL: validated.resultUrl,
597
+ QueueTimeOutURL: validated.queueTimeOutUrl,
598
+ Remarks: validated.remarks ?? "Account Balance Query"
599
+ };
600
+ const { data } = await httpRequest(`${baseUrl}/mpesa/accountbalance/v1/query`, withDarajaHttp({
601
+ method: "POST",
602
+ headers: { Authorization: `Bearer ${accessToken}` },
603
+ body: payload
604
+ }, http));
605
+ return parseWithSchema(AccountBalanceResponseSchema, data, "Account Balance response");
606
+ }
607
+
608
+ //#endregion
609
+ //#region src/mpesa/account-balance/types.ts
610
+ /**
611
+ * Account Balance Query types
612
+ *
613
+ * API: POST /mpesa/accountbalance/v1/query
614
+ *
615
+ * Queries the current balance of an M-PESA shortcode.
616
+ * This is ASYNCHRONOUS — the sync response is only acknowledgement.
617
+ * Balance data is POSTed to your ResultURL callback.
618
+ *
619
+ * Required org portal role: "Balance Query ORG API" (Account Balance ORG API initiator)
620
+ *
621
+ * Ref: Account Balance — Daraja Developer Portal
622
+ */
623
+ /**
624
+ * Documented Account Balance API error/result codes.
625
+ * These appear in the async callback ResultCode field.
626
+ *
627
+ * Ref: Account Balance — Error Codes section
628
+ */
629
+ const ACCOUNT_BALANCE_ERROR_CODES = {
630
+ DUPLICATE_DETECTED: 15,
631
+ INTERNAL_FAILURE: 17,
632
+ INITIATOR_CREDENTIAL_CHECK_FAILURE: 18,
633
+ MESSAGE_SEQUENCING_FAILURE: 19,
634
+ UNRESOLVED_INITIATOR: 20,
635
+ INITIATOR_TO_PRIMARY_PARTY_PERMISSION_FAILURE: 21,
636
+ INITIATOR_TO_RECEIVER_PARTY_PERMISSION_FAILURE: 22,
637
+ MISSING_MANDATORY_FIELDS: 24,
638
+ SYSTEM_OVERLOAD: 100000001,
639
+ THROTTLING_ERROR: 100000002,
640
+ INTERNAL_SERVER_ERROR: 100000004,
641
+ INVALID_INPUT_VALUE: 100000005,
642
+ SERVICE_ABNORMAL: 100000007,
643
+ API_STATUS_ABNORMAL: 100000009,
644
+ INSUFFICIENT_PERMISSIONS: 100000010,
645
+ REQUEST_RATE_EXCEEDED: 100000011
646
+ };
647
+ /**
648
+ * Parses the raw Daraja AccountBalance string into structured account objects.
649
+ *
650
+ * Daraja returns each account as 3 consecutive pipe-separated fields:
651
+ * AccountName | Currency | Amount
652
+ * Multiple accounts are concatenated, with additional balance sub-fields interleaved.
653
+ *
654
+ * The parser extracts triplets where the first element is a non-numeric account name.
655
+ *
656
+ * Example input:
657
+ * "Working Account|KES|700000.00|KES|0.00|KES|0.00|Utility Account|KES|228037.00|"
658
+ *
659
+ * Example output:
660
+ * [
661
+ * { name: "Working Account", currency: "KES", amount: "700000.00" },
662
+ * { name: "Utility Account", currency: "KES", amount: "228037.00" },
663
+ * ]
664
+ */
665
+ function parseAccountBalance(raw) {
666
+ if (!raw.trim()) return [];
667
+ const parts = raw.split("|");
668
+ const accounts = [];
669
+ for (let i = 0; i + 2 < parts.length; i++) {
670
+ const candidate = parts[i]?.trim();
671
+ const currency = parts[i + 1]?.trim();
672
+ const amount = parts[i + 2]?.trim();
673
+ if (candidate && currency && amount !== void 0 && isNaN(Number(candidate)) && candidate.length > 0) accounts.push({
674
+ name: candidate,
675
+ currency,
676
+ amount
677
+ });
678
+ }
679
+ return accounts;
680
+ }
681
+ /**
682
+ * Extracts a result parameter value from an AccountBalanceResult by key.
683
+ * Handles both array and single-object forms of ResultParameter (Daraja inconsistency).
684
+ *
685
+ * Documented keys: "AccountBalance", "BOCompletedTime"
686
+ */
687
+ function getAccountBalanceParam(result, key) {
688
+ const params = result.Result.ResultParameters?.ResultParameter;
689
+ if (!params) return void 0;
690
+ return (Array.isArray(params) ? params : [params]).find((p) => p.Key === key)?.Value;
691
+ }
692
+ /**
693
+ * Returns the M-PESA TransactionID from the result.
694
+ */
695
+ function getAccountBalanceTransactionId(result) {
696
+ return result.Result.TransactionID;
697
+ }
698
+ /**
699
+ * Returns the ConversationID from the result.
700
+ */
701
+ function getAccountBalanceConversationId(result) {
702
+ return result.Result.ConversationID;
703
+ }
704
+ /**
705
+ * Returns the OriginatorConversationID from the result.
706
+ * This correlates the callback to the original request.
707
+ */
708
+ function getAccountBalanceOriginatorConversationId(result) {
709
+ return result.Result.OriginatorConversationID;
710
+ }
711
+ /**
712
+ * Returns the BOCompletedTime from the result parameters, or null if not present.
713
+ * Format: YYYYMMDDHHmmss (e.g. "20200109125710")
714
+ */
715
+ function getAccountBalanceCompletedTime(result) {
716
+ const value = getAccountBalanceParam(result, "BOCompletedTime");
717
+ if (value === void 0 || value === null) return null;
718
+ return String(value);
719
+ }
720
+ /**
721
+ * Returns the raw AccountBalance pipe-delimited string from the result, or null if absent.
722
+ * Use parseAccountBalance() to parse this into structured account objects.
723
+ */
724
+ function getAccountBalanceRawBalance(result) {
725
+ const value = getAccountBalanceParam(result, "AccountBalance");
726
+ if (value === void 0 || value === null) return null;
727
+ return String(value);
728
+ }
729
+ /**
730
+ * Returns the ReferenceItem from the async callback, or null if not present.
731
+ * Key is typically "QueueTimeoutURL" as documented by Daraja.
732
+ */
733
+ function getAccountBalanceReferenceItem(result) {
734
+ const refData = result.Result.ReferenceData;
735
+ if (!refData) return null;
736
+ const item = refData.ReferenceItem;
737
+ if (!item) return null;
738
+ return Array.isArray(item) ? item[0] ?? null : item;
739
+ }
740
+ /**
741
+ * Returns true if the Account Balance result indicates success (ResultCode 0).
742
+ * Handles both numeric 0 and string "0" (Daraja inconsistency).
743
+ */
744
+ function isAccountBalanceSuccess(result) {
745
+ const code = result.Result.ResultCode;
746
+ return code === 0 || code === "0";
747
+ }
748
+
749
+ //#endregion
750
+ //#region src/schemas/b2b.ts
751
+ const B2BAsyncResponseSchema = z.object({
752
+ ConversationID: z.string(),
753
+ OriginatorConversationID: z.string(),
754
+ ResponseCode: z.string(),
755
+ ResponseDescription: z.string()
756
+ }).passthrough();
757
+ const B2BPaymentBaseSchema = z.object({
758
+ amount: KesAmountSchema,
759
+ partyA: NonEmptyStringSchema,
760
+ partyB: NonEmptyStringSchema,
761
+ accountReference: NonEmptyStringSchema,
762
+ requester: z.string().optional(),
763
+ remarks: z.string().optional(),
764
+ resultUrl: UrlSchema,
765
+ queueTimeOutUrl: UrlSchema,
766
+ occasion: z.string().optional()
767
+ });
768
+ const B2BBuyGoodsRequestSchema = B2BPaymentBaseSchema.extend({ commandId: z.literal("BusinessBuyGoods") });
769
+ const B2BPayBillRequestSchema = B2BPaymentBaseSchema.extend({ commandId: z.literal("BusinessPayBill") });
770
+ const B2BBuyGoodsResponseSchema = B2BAsyncResponseSchema;
771
+ const B2BPayBillResponseSchema = B2BAsyncResponseSchema;
772
+ const B2BExpressCheckoutRequestSchema = z.object({
773
+ primaryShortCode: NonEmptyStringSchema,
774
+ receiverShortCode: NonEmptyStringSchema,
775
+ amount: KesAmountSchema,
776
+ paymentRef: NonEmptyStringSchema,
777
+ callbackUrl: UrlSchema,
778
+ partnerName: NonEmptyStringSchema,
779
+ requestRefId: NonEmptyStringSchema.optional()
780
+ });
781
+ const B2BExpressCheckoutResponseSchema = z.object({
782
+ code: z.string(),
783
+ status: z.string()
784
+ }).passthrough();
785
+ const B2BResponseSchema = z.object({
786
+ ConversationID: z.string().optional(),
787
+ OriginatorConversationID: z.string().optional(),
788
+ ResponseCode: z.string(),
789
+ ResponseDescription: z.string()
790
+ }).passthrough();
791
+
792
+ //#endregion
793
+ //#region src/mpesa/b2b-express-checkout/initiate.ts
794
+ /**
795
+ * src/mpesa/b2b-express-checkout/initiate.ts
796
+ *
797
+ * B2B Express Checkout USSD Push to Till implementation.
798
+ */
799
+ async function initiateB2BExpressCheckout(baseUrl, accessToken, request, http) {
800
+ const validated = parseWithSchema(B2BExpressCheckoutRequestSchema, request, "B2B Express Checkout request");
801
+ const amount = Math.round(validated.amount);
802
+ const payload = {
803
+ primaryShortCode: String(validated.primaryShortCode),
804
+ receiverShortCode: String(validated.receiverShortCode),
805
+ amount: String(amount),
806
+ paymentRef: validated.paymentRef,
807
+ callbackUrl: validated.callbackUrl,
808
+ partnerName: validated.partnerName,
809
+ RequestRefID: validated.requestRefId ?? generateRequestRefId()
810
+ };
811
+ const { data } = await httpRequest(`${baseUrl}/v1/ussdpush/get-msisdn`, withDarajaHttp({
812
+ method: "POST",
813
+ headers: { Authorization: `Bearer ${accessToken}` },
814
+ body: payload
815
+ }, http));
816
+ return parseWithSchema(B2BExpressCheckoutResponseSchema, data, "B2B Express Checkout response");
817
+ }
818
+
819
+ //#endregion
820
+ //#region src/mpesa/b2b-express-checkout/types.ts
821
+ /**
822
+ * Known B2B Express Checkout result codes.
823
+ *
824
+ * SUCCESS (0) — Transaction completed successfully
825
+ * CANCELLED(4001) — Merchant cancelled the USSD prompt
826
+ * KYC_FAIL (4102) — Merchant KYC failure; provide valid KYC
827
+ * NO_NUMBER(4104) — Missing nominated number; configure in M-PESA portal
828
+ * NET_ERROR(4201) — USSD network error; retry on stable network
829
+ * USSD_ERR (4203) — USSD exception error; retry on stable network
830
+ */
831
+ const B2B_RESULT_CODES = {
832
+ SUCCESS: "0",
833
+ CANCELLED: "4001",
834
+ KYC_FAIL: "4102",
835
+ NO_NOMINATED_NUMBER: "4104",
836
+ USSD_NETWORK_ERROR: "4201",
837
+ USSD_EXCEPTION_ERROR: "4203"
838
+ };
839
+
840
+ //#endregion
841
+ //#region src/mpesa/b2b-express-checkout/webhooks.ts
842
+ /**
843
+ * src/mpesa/b2b-express-checkout/webhooks.ts
844
+ *
845
+ * B2B Express Checkout callback (webhook) helpers.
846
+ * Strictly aligned with Safaricom Daraja B2B Express Checkout documentation.
847
+ *
848
+ * All callbacks are discriminated on `resultCode`:
849
+ * "0" → success (B2BExpressCheckoutCallbackSuccess)
850
+ * "4001" → cancelled (B2BExpressCheckoutCallbackCancelled)
851
+ * other → failed (B2BExpressCheckoutCallbackFailed)
852
+ */
853
+ const KNOWN_RESULT_CODES$3 = new Set(Object.values(B2B_RESULT_CODES));
854
+ /**
855
+ * Runtime type guard — returns true if `body` looks like a valid B2B
856
+ * Express Checkout callback payload.
857
+ *
858
+ * Use this in your callback route before casting the body:
859
+ * @example
860
+ * app.post('/mpesa/b2b/callback', (req, res) => {
861
+ * if (!isB2BCheckoutCallback(req.body)) {
862
+ * return res.status(400).json({ error: 'unrecognised payload' })
863
+ * }
864
+ * // safe to use as B2BExpressCheckoutCallback
865
+ * })
866
+ */
867
+ function isB2BCheckoutCallback(body) {
868
+ if (!body || typeof body !== "object") return false;
869
+ const b = body;
870
+ return typeof b["resultCode"] === "string" && typeof b["requestId"] === "string" && typeof b["amount"] === "string";
871
+ }
872
+ /**
873
+ * Returns true when the B2B callback represents a SUCCESSFUL transaction.
874
+ * A successful callback has resultCode "0" and includes transactionId.
875
+ *
876
+ * Per Daraja docs:
877
+ * {
878
+ * "resultCode": "0",
879
+ * "resultDesc": "The service request is processed successfully.",
880
+ * "transactionId": "RDQ01NFT1Q",
881
+ * "status": "SUCCESS",
882
+ * ...
883
+ * }
884
+ */
885
+ function isB2BCheckoutSuccess(callback) {
886
+ return callback.resultCode === B2B_RESULT_CODES.SUCCESS;
887
+ }
888
+ /**
889
+ * Returns true when the merchant CANCELLED the USSD prompt.
890
+ * resultCode "4001" = "User cancelled transaction".
891
+ *
892
+ * Per Daraja docs:
893
+ * {
894
+ * "resultCode": "4001",
895
+ * "resultDesc": "User cancelled transaction",
896
+ * "paymentReference": "MAndbubry3hi",
897
+ * ...
898
+ * }
899
+ */
900
+ function isB2BCheckoutCancelled(callback) {
901
+ return callback.resultCode === B2B_RESULT_CODES.CANCELLED;
902
+ }
903
+ /**
904
+ * Returns the requestId from any B2B callback.
905
+ * Use this to correlate the callback with your original request.
906
+ */
907
+ function getB2BRequestId(callback) {
908
+ return callback.requestId;
909
+ }
910
+ /**
911
+ * Returns the transaction amount as a number from any B2B callback.
912
+ * Daraja sends amount as a string (e.g. "71.0"); this converts it to number.
913
+ */
914
+ function getB2BAmount(callback) {
915
+ return Number(callback.amount);
916
+ }
917
+ /**
918
+ * Returns the M-PESA receipt number from a SUCCESSFUL callback.
919
+ * Returns null if the callback is not a success.
920
+ */
921
+ function getB2BTransactionId(callback) {
922
+ if (!isB2BCheckoutSuccess(callback)) return null;
923
+ return callback.transactionId ?? null;
924
+ }
925
+ /**
926
+ * Returns the M-PESA conversationID from a SUCCESSFUL callback.
927
+ * Returns null if the callback is not a success.
928
+ */
929
+ function getB2BConversationId(callback) {
930
+ if (!isB2BCheckoutSuccess(callback)) return null;
931
+ return callback.conversationID ?? null;
932
+ }
933
+
934
+ //#endregion
935
+ //#region src/mpesa/b2b-buy-goods/payment.ts
936
+ /**
937
+ * src/mpesa/b2b-buy-goods/payment.ts
938
+ *
939
+ * Initiates a Business Buy Goods payment via Safaricom Daraja.
940
+ * Endpoint: POST /mpesa/b2b/v1/paymentrequest
941
+ *
942
+ * Strictly follows the Safaricom Daraja Business Buy Goods API documentation:
943
+ * - CommandID must be "BusinessBuyGoods"
944
+ * - SenderIdentifierType is always "4" (hardcoded per docs)
945
+ * - RecieverIdentifierType is always "4" (hardcoded per docs)
946
+ * - Amount is sent as a string per the JSON spec
947
+ * - AccountReference is truncated to max 13 characters per docs
948
+ * - Requester and Occassion are optional
949
+ */
950
+ /** Daraja Business Buy Goods endpoint — same as Pay Bill per docs */
951
+ const B2B_BUY_GOODS_ENDPOINT = "/mpesa/b2b/v1/paymentrequest";
952
+ /**
953
+ * Per documentation: SenderIdentifierType and RecieverIdentifierType
954
+ * must always be "4" (Organisation ShortCode). Not configurable.
955
+ */
956
+ const IDENTIFIER_TYPE$2 = "4";
957
+ /**
958
+ * Initiates a Business Buy Goods payment request.
959
+ *
960
+ * Moves money from your MMF/Working account to the recipient's merchant account
961
+ * (till number, merchant store number, or Merchant HO).
962
+ * The sync response is acknowledgement only — the result arrives via resultUrl.
963
+ *
964
+ * @param baseUrl - Daraja base URL (sandbox or production)
965
+ * @param accessToken - Valid OAuth Bearer token
966
+ * @param securityCredential - RSA-encrypted initiator password (base64)
967
+ * @param initiatorName - M-Pesa API operator username with B2B role
968
+ * @param request - Business Buy Goods request parameters
969
+ * @returns Synchronous acknowledgement response from Daraja
970
+ * @throws {PesafyError} VALIDATION_ERROR for invalid input before HTTP call
971
+ * @throws {PesafyError} From httpRequest on network / API errors
972
+ */
973
+ async function initiateB2BBuyGoods(baseUrl, accessToken, securityCredential, initiatorName, request, http) {
974
+ const validated = parseWithSchema(B2BBuyGoodsRequestSchema, request, "B2B Buy Goods request");
975
+ const amount = Math.round(validated.amount);
976
+ const payload = {
977
+ Initiator: initiatorName,
978
+ SecurityCredential: securityCredential,
979
+ CommandID: validated.commandId,
980
+ SenderIdentifierType: IDENTIFIER_TYPE$2,
981
+ RecieverIdentifierType: IDENTIFIER_TYPE$2,
982
+ Amount: String(amount),
983
+ PartyA: String(validated.partyA),
984
+ PartyB: String(validated.partyB),
985
+ AccountReference: validated.accountReference.slice(0, 13),
986
+ Remarks: validated.remarks ?? "Business Buy Goods",
987
+ QueueTimeOutURL: validated.queueTimeOutUrl,
988
+ ResultURL: validated.resultUrl
989
+ };
990
+ if (validated.requester?.trim()) payload["Requester"] = String(validated.requester);
991
+ if (validated.occasion?.trim()) payload["Occassion"] = validated.occasion;
992
+ const { data } = await httpRequest(`${baseUrl}${B2B_BUY_GOODS_ENDPOINT}`, withDarajaHttp({
993
+ method: "POST",
994
+ headers: { Authorization: `Bearer ${accessToken}` },
995
+ body: payload
996
+ }, http));
997
+ return parseWithSchema(B2BBuyGoodsResponseSchema, data, "B2B Buy Goods response");
998
+ }
999
+
1000
+ //#endregion
1001
+ //#region src/mpesa/b2b-buy-goods/types.ts
1002
+ /**
1003
+ * Known Business Buy Goods result codes documented by Safaricom Daraja.
1004
+ */
1005
+ const B2B_BUY_GOODS_RESULT_CODES = {
1006
+ SUCCESS: 0,
1007
+ INSUFFICIENT_FUNDS: 1,
1008
+ AMOUNT_TOO_SMALL: 2,
1009
+ AMOUNT_TOO_LARGE: 3,
1010
+ DAILY_LIMIT_EXCEEDED: 4,
1011
+ MAX_BALANCE_EXCEEDED: 8,
1012
+ INVALID_INITIATOR_INFO: 2001,
1013
+ ACCOUNT_INACTIVE: 2006,
1014
+ PRODUCT_NOT_PERMITTED: 2028,
1015
+ CUSTOMER_NOT_REGISTERED: 2040
1016
+ };
1017
+ /**
1018
+ * Documented Daraja error codes for the Business Buy Goods API.
1019
+ */
1020
+ const B2B_BUY_GOODS_ERROR_CODES = {
1021
+ INVALID_ACCESS_TOKEN: "400.003.01",
1022
+ BAD_REQUEST: "400.003.02",
1023
+ INTERNAL_SERVER_ERROR: "500.003.1001",
1024
+ QUOTA_VIOLATION: "500.003.03",
1025
+ SPIKE_ARREST: "500.003.02",
1026
+ NOT_FOUND: "404.003.01",
1027
+ INVALID_AUTH_HEADER: "404.001.04",
1028
+ INVALID_PAYLOAD: "400.002.05"
1029
+ };
1030
+
1031
+ //#endregion
1032
+ //#region src/mpesa/b2b-buy-goods/webhooks.ts
1033
+ /**
1034
+ * src/mpesa/b2b-buy-goods/webhooks.ts
1035
+ *
1036
+ * Type guards and payload extractors for Business Buy Goods result callbacks.
1037
+ *
1038
+ * Result parameters are aligned strictly to Safaricom Daraja documentation:
1039
+ * - Amount
1040
+ * - TransCompletedTime
1041
+ * - ReceiverPartyPublicName
1042
+ * - DebitPartyCharges
1043
+ * - Currency
1044
+ * - DebitPartyAffectedAccountBalance
1045
+ * - DebitAccountBalance
1046
+ * - InitiatorAccountCurrentBalance
1047
+ * - BillReferenceNumber (from ReferenceData)
1048
+ * - QueueTimeoutURL (from ReferenceData)
1049
+ *
1050
+ * Docs note: ResultCode is "0" (string) on success but 2001 (number) on
1051
+ * failure — both forms are handled.
1052
+ */
1053
+ const KNOWN_RESULT_CODES$2 = new Set(Object.values(B2B_BUY_GOODS_RESULT_CODES));
1054
+ /**
1055
+ * Runtime type guard — checks if a body looks like a B2B Buy Goods result callback.
1056
+ * Validates the minimum documented structure.
1057
+ */
1058
+ function isB2BBuyGoodsResult(body) {
1059
+ if (!body || typeof body !== "object") return false;
1060
+ const b = body;
1061
+ if (!b["Result"] || typeof b["Result"] !== "object") return false;
1062
+ const result = b["Result"];
1063
+ return (typeof result["ResultCode"] === "number" || typeof result["ResultCode"] === "string") && typeof result["ConversationID"] === "string" && typeof result["OriginatorConversationID"] === "string";
1064
+ }
1065
+ /**
1066
+ * Returns true if the B2B Buy Goods result represents a successful transaction.
1067
+ * Handles both string "0" (documented in success sample) and number 0.
1068
+ */
1069
+ function isB2BBuyGoodsSuccess(result) {
1070
+ const code = result.Result.ResultCode;
1071
+ return code === 0 || code === "0";
1072
+ }
1073
+ /**
1074
+ * Returns true if the B2B Buy Goods result represents a failure.
1075
+ */
1076
+ function isB2BBuyGoodsFailure(result) {
1077
+ return !isB2BBuyGoodsSuccess(result);
1078
+ }
1079
+ /**
1080
+ * Returns true if the result code is among the documented codes.
1081
+ * Handles both numeric and string representations.
1082
+ * Empty strings are explicitly rejected.
1083
+ */
1084
+ function isKnownB2BBuyGoodsResultCode(code) {
1085
+ if (typeof code !== "number" && typeof code !== "string") return false;
1086
+ if (typeof code === "string" && code.trim() === "") return false;
1087
+ const numeric = Number(code);
1088
+ return Number.isFinite(numeric) && KNOWN_RESULT_CODES$2.has(numeric);
1089
+ }
1090
+ /**
1091
+ * Extracts the M-PESA transaction ID from a B2B Buy Goods result.
1092
+ * Present on both success and failure (generic ID on failure).
1093
+ */
1094
+ function getB2BBuyGoodsTransactionId(result) {
1095
+ return result.Result.TransactionID;
1096
+ }
1097
+ /**
1098
+ * Extracts the ConversationID from a B2B Buy Goods result.
1099
+ */
1100
+ function getB2BBuyGoodsConversationId(result) {
1101
+ return result.Result.ConversationID;
1102
+ }
1103
+ /**
1104
+ * Extracts the OriginatorConversationID from a B2B Buy Goods result.
1105
+ * Use this to correlate with the original API call response.
1106
+ */
1107
+ function getB2BBuyGoodsOriginatorConversationId(result) {
1108
+ return result.Result.OriginatorConversationID;
1109
+ }
1110
+ /**
1111
+ * Extracts the result description (human-readable status).
1112
+ */
1113
+ function getB2BBuyGoodsResultDesc(result) {
1114
+ return result.Result.ResultDesc;
1115
+ }
1116
+ /**
1117
+ * Extracts the result code from a B2B Buy Goods result.
1118
+ */
1119
+ function getB2BBuyGoodsResultCode(result) {
1120
+ return result.Result.ResultCode;
1121
+ }
1122
+ /**
1123
+ * Extracts the transaction amount from B2B Buy Goods result parameters.
1124
+ * Documented field: "Amount"
1125
+ * Returns null if not present (e.g. on failure).
1126
+ */
1127
+ function getB2BBuyGoodsAmount(result) {
1128
+ const value = getB2BBuyGoodsResultParam(result, "Amount");
1129
+ if (value === void 0) return null;
1130
+ const num = Number(value);
1131
+ return Number.isFinite(num) ? num : null;
1132
+ }
1133
+ /**
1134
+ * Extracts the transaction completion time.
1135
+ * Documented field: "TransCompletedTime" — format: YYYYMMDDHHmmss
1136
+ * Falls back to "BOCompletedTime" (present on some failure callbacks).
1137
+ * Returns null if not present.
1138
+ */
1139
+ function getB2BBuyGoodsCompletedTime(result) {
1140
+ const value = getB2BBuyGoodsResultParam(result, "TransCompletedTime") ?? getB2BBuyGoodsResultParam(result, "BOCompletedTime");
1141
+ if (value === void 0) return null;
1142
+ return String(value);
1143
+ }
1144
+ /**
1145
+ * Extracts the receiver's public name from result parameters.
1146
+ * Documented field: "ReceiverPartyPublicName"
1147
+ * Returns null if not present.
1148
+ */
1149
+ function getB2BBuyGoodsReceiverName(result) {
1150
+ const value = getB2BBuyGoodsResultParam(result, "ReceiverPartyPublicName");
1151
+ if (value === void 0) return null;
1152
+ return String(value);
1153
+ }
1154
+ /**
1155
+ * Extracts debit party charges from result parameters.
1156
+ * Documented field: "DebitPartyCharges"
1157
+ * Returns null if not present or empty string (no charges applied).
1158
+ */
1159
+ function getB2BBuyGoodsDebitPartyCharges(result) {
1160
+ const value = getB2BBuyGoodsResultParam(result, "DebitPartyCharges");
1161
+ if (value === void 0 || value === "") return null;
1162
+ return String(value);
1163
+ }
1164
+ /**
1165
+ * Extracts the transaction currency from result parameters.
1166
+ * Documented field: "Currency"
1167
+ * Returns "KES" as default when not present.
1168
+ */
1169
+ function getB2BBuyGoodsCurrency(result) {
1170
+ const value = getB2BBuyGoodsResultParam(result, "Currency");
1171
+ if (value === void 0 || value === "") return "KES";
1172
+ return String(value);
1173
+ }
1174
+ /**
1175
+ * Extracts the debit party affected account balance.
1176
+ * Documented field: "DebitPartyAffectedAccountBalance"
1177
+ * Format: "Working Account|KES|346568.83|6186.83|340382.00|0.00"
1178
+ * Returns null if not present.
1179
+ */
1180
+ function getB2BBuyGoodsDebitPartyAffectedBalance(result) {
1181
+ const value = getB2BBuyGoodsResultParam(result, "DebitPartyAffectedAccountBalance");
1182
+ if (value === void 0) return null;
1183
+ return String(value);
1184
+ }
1185
+ /**
1186
+ * Extracts the debit account balance.
1187
+ * Documented field: "DebitAccountBalance"
1188
+ * Format: "{Amount={CurrencyCode=KES, MinimumAmount=618683, BasicAmount=6186.83}}"
1189
+ * Returns null if not present.
1190
+ */
1191
+ function getB2BBuyGoodsDebitAccountBalance(result) {
1192
+ const value = getB2BBuyGoodsResultParam(result, "DebitAccountBalance");
1193
+ if (value === void 0) return null;
1194
+ return String(value);
1195
+ }
1196
+ /**
1197
+ * Extracts the initiator account current balance.
1198
+ * Documented field: "InitiatorAccountCurrentBalance"
1199
+ * Returns null if not present.
1200
+ */
1201
+ function getB2BBuyGoodsInitiatorBalance(result) {
1202
+ const value = getB2BBuyGoodsResultParam(result, "InitiatorAccountCurrentBalance");
1203
+ if (value === void 0) return null;
1204
+ return String(value);
1205
+ }
1206
+ /**
1207
+ * Extracts the Bill Reference Number from ReferenceData.
1208
+ * Documented field: "BillReferenceNumber" (in ReferenceData, not ResultParameters)
1209
+ * Returns null if not present.
1210
+ */
1211
+ function getB2BBuyGoodsBillReferenceNumber(result) {
1212
+ const refData = result.Result.ReferenceData?.ReferenceItem;
1213
+ if (!refData) return null;
1214
+ return (Array.isArray(refData) ? refData : [refData]).find((i) => i.Key === "BillReferenceNumber")?.Value ?? null;
1215
+ }
1216
+ /**
1217
+ * Extracts the QueueTimeoutURL from ReferenceData.
1218
+ * Documented field: "QueueTimeoutURL" (in ReferenceData)
1219
+ * Returns null if not present.
1220
+ */
1221
+ function getB2BBuyGoodsQueueTimeoutUrl(result) {
1222
+ const refData = result.Result.ReferenceData?.ReferenceItem;
1223
+ if (!refData) return null;
1224
+ return (Array.isArray(refData) ? refData : [refData]).find((i) => i.Key === "QueueTimeoutURL")?.Value ?? null;
1225
+ }
1226
+ /**
1227
+ * Extracts a named value from B2B Buy Goods result parameters.
1228
+ * Handles both single-object and array forms of ResultParameter
1229
+ * (Daraja returns either depending on how many parameters are present).
1230
+ * Returns undefined if key is absent or no ResultParameters exist.
1231
+ */
1232
+ function getB2BBuyGoodsResultParam(result, key) {
1233
+ const params = result.Result.ResultParameters?.ResultParameter;
1234
+ if (!params) return void 0;
1235
+ return (Array.isArray(params) ? params : [params]).find((p) => p.Key === key)?.Value;
1236
+ }
1237
+
1238
+ //#endregion
1239
+ //#region src/mpesa/b2b-pay-bill/payment.ts
1240
+ /**
1241
+ * src/mpesa/b2b-pay-bill/payment.ts
1242
+ *
1243
+ * Initiates a Business Pay Bill payment via Safaricom Daraja.
1244
+ * Endpoint: POST /mpesa/b2b/v1/paymentrequest
1245
+ *
1246
+ * Strictly follows the Safaricom Daraja Business Pay Bill API documentation:
1247
+ * - CommandID must be "BusinessPayBill"
1248
+ * - SenderIdentifierType is always "4" (hardcoded per docs)
1249
+ * - RecieverIdentifierType is always "4" (hardcoded per docs)
1250
+ * - Amount is sent as a string per the JSON spec
1251
+ * - AccountReference is truncated to max 13 characters per docs
1252
+ * - Requester and Occassion are optional
1253
+ */
1254
+ /** Daraja Business Pay Bill endpoint */
1255
+ const B2B_PAY_BILL_ENDPOINT = "/mpesa/b2b/v1/paymentrequest";
1256
+ /**
1257
+ * Per documentation: SenderIdentifierType and RecieverIdentifierType
1258
+ * must always be "4" (Organisation ShortCode). Not configurable.
1259
+ */
1260
+ const IDENTIFIER_TYPE$1 = "4";
1261
+ /**
1262
+ * Initiates a Business Pay Bill payment request.
1263
+ *
1264
+ * Moves money from your MMF/Working account to the recipient's utility account.
1265
+ * The sync response is acknowledgement only — the result arrives via resultUrl.
1266
+ *
1267
+ * @param baseUrl - Daraja base URL (sandbox or production)
1268
+ * @param accessToken - Valid OAuth Bearer token
1269
+ * @param securityCredential - RSA-encrypted initiator password (base64)
1270
+ * @param initiatorName - M-Pesa API operator username with B2B role
1271
+ * @param request - Business Pay Bill request parameters
1272
+ * @returns Synchronous acknowledgement response from Daraja
1273
+ * @throws {PesafyError} VALIDATION_ERROR for invalid input before HTTP call
1274
+ * @throws {PesafyError} From httpRequest on network / API errors
1275
+ */
1276
+ async function initiateB2BPayBill(baseUrl, accessToken, securityCredential, initiatorName, request, http) {
1277
+ const validated = parseWithSchema(B2BPayBillRequestSchema, request, "B2B Pay Bill request");
1278
+ const amount = Math.round(validated.amount);
1279
+ const payload = {
1280
+ Initiator: initiatorName,
1281
+ SecurityCredential: securityCredential,
1282
+ CommandID: validated.commandId,
1283
+ SenderIdentifierType: IDENTIFIER_TYPE$1,
1284
+ RecieverIdentifierType: IDENTIFIER_TYPE$1,
1285
+ Amount: String(amount),
1286
+ PartyA: String(validated.partyA),
1287
+ PartyB: String(validated.partyB),
1288
+ AccountReference: validated.accountReference.slice(0, 13),
1289
+ Remarks: validated.remarks ?? "Business Pay Bill",
1290
+ QueueTimeOutURL: validated.queueTimeOutUrl,
1291
+ ResultURL: validated.resultUrl
1292
+ };
1293
+ if (validated.requester?.trim()) payload["Requester"] = String(validated.requester);
1294
+ if (validated.occasion?.trim()) payload["Occassion"] = validated.occasion;
1295
+ const { data } = await httpRequest(`${baseUrl}${B2B_PAY_BILL_ENDPOINT}`, withDarajaHttp({
1296
+ method: "POST",
1297
+ headers: { Authorization: `Bearer ${accessToken}` },
1298
+ body: payload
1299
+ }, http));
1300
+ return parseWithSchema(B2BPayBillResponseSchema, data, "B2B Pay Bill response");
1301
+ }
1302
+
1303
+ //#endregion
1304
+ //#region src/mpesa/b2b-pay-bill/types.ts
1305
+ /**
1306
+ * Known Business Pay Bill result codes documented by Safaricom Daraja.
1307
+ */
1308
+ const B2B_PAY_BILL_RESULT_CODES = {
1309
+ SUCCESS: 0,
1310
+ INSUFFICIENT_FUNDS: 1,
1311
+ AMOUNT_TOO_SMALL: 2,
1312
+ AMOUNT_TOO_LARGE: 3,
1313
+ DAILY_LIMIT_EXCEEDED: 4,
1314
+ MAX_BALANCE_EXCEEDED: 8,
1315
+ INVALID_INITIATOR_INFO: 2001,
1316
+ ACCOUNT_INACTIVE: 2006,
1317
+ PRODUCT_NOT_PERMITTED: 2028,
1318
+ CUSTOMER_NOT_REGISTERED: 2040
1319
+ };
1320
+ /**
1321
+ * Documented Daraja error codes for the Business Pay Bill API.
1322
+ */
1323
+ const B2B_PAY_BILL_ERROR_CODES = {
1324
+ INVALID_ACCESS_TOKEN: "400.003.01",
1325
+ BAD_REQUEST: "400.003.02",
1326
+ INTERNAL_SERVER_ERROR: "500.003.1001",
1327
+ QUOTA_VIOLATION: "500.003.03",
1328
+ SPIKE_ARREST: "500.003.02",
1329
+ NOT_FOUND: "404.003.01",
1330
+ INVALID_AUTH_HEADER: "404.001.04",
1331
+ INVALID_PAYLOAD: "400.002.05"
1332
+ };
1333
+
1334
+ //#endregion
1335
+ //#region src/mpesa/b2b-pay-bill/webhooks.ts
1336
+ /**
1337
+ * src/mpesa/b2b-pay-bill/webhooks.ts
1338
+ *
1339
+ * Type guards and payload extractors for Business Pay Bill result callbacks.
1340
+ *
1341
+ * Result parameters are aligned strictly to Safaricom Daraja documentation:
1342
+ * - Amount
1343
+ * - TransCompletedTime
1344
+ * - ReceiverPartyPublicName
1345
+ * - DebitPartyCharges
1346
+ * - Currency
1347
+ * - DebitPartyAffectedAccountBalance
1348
+ * - DebitAccountCurrentBalance
1349
+ * - InitiatorAccountCurrentBalance
1350
+ * - BillReferenceNumber (from ReferenceData)
1351
+ *
1352
+ * Docs note: ResultCode is "0" (string) on success but 2001 (number) on
1353
+ * failure — both forms are handled.
1354
+ */
1355
+ const KNOWN_RESULT_CODES$1 = new Set(Object.values(B2B_PAY_BILL_RESULT_CODES));
1356
+ /**
1357
+ * Runtime type guard — checks if a body looks like a B2B Pay Bill result callback.
1358
+ * Validates the minimum documented structure.
1359
+ */
1360
+ function isB2BPayBillResult(body) {
1361
+ if (!body || typeof body !== "object") return false;
1362
+ const b = body;
1363
+ if (!b["Result"] || typeof b["Result"] !== "object") return false;
1364
+ const result = b["Result"];
1365
+ return (typeof result["ResultCode"] === "number" || typeof result["ResultCode"] === "string") && typeof result["ConversationID"] === "string" && typeof result["OriginatorConversationID"] === "string";
1366
+ }
1367
+ /**
1368
+ * Returns true if the B2B Pay Bill result represents a successful transaction.
1369
+ * Handles both string "0" (documented in success sample) and number 0.
1370
+ */
1371
+ function isB2BPayBillSuccess(result) {
1372
+ const code = result.Result.ResultCode;
1373
+ return code === 0 || code === "0";
1374
+ }
1375
+ /**
1376
+ * Returns true if the B2B Pay Bill result represents a failure.
1377
+ */
1378
+ function isB2BPayBillFailure(result) {
1379
+ return !isB2BPayBillSuccess(result);
1380
+ }
1381
+ /**
1382
+ * Returns true if the result code is among the documented codes.
1383
+ * Handles both numeric and string representations.
1384
+ * Empty strings are explicitly rejected.
1385
+ */
1386
+ function isKnownB2BPayBillResultCode(code) {
1387
+ if (typeof code !== "number" && typeof code !== "string") return false;
1388
+ if (typeof code === "string" && code.trim() === "") return false;
1389
+ const numeric = Number(code);
1390
+ return Number.isFinite(numeric) && KNOWN_RESULT_CODES$1.has(numeric);
1391
+ }
1392
+ /**
1393
+ * Extracts the M-PESA transaction ID from a B2B Pay Bill result.
1394
+ * Present on both success and failure (generic ID on failure).
1395
+ */
1396
+ function getB2BPayBillTransactionId(result) {
1397
+ return result.Result.TransactionID;
1398
+ }
1399
+ /**
1400
+ * Extracts the ConversationID from a B2B Pay Bill result.
1401
+ */
1402
+ function getB2BPayBillConversationId(result) {
1403
+ return result.Result.ConversationID;
1404
+ }
1405
+ /**
1406
+ * Extracts the OriginatorConversationID from a B2B Pay Bill result.
1407
+ * Use this to correlate with the original API call response.
1408
+ */
1409
+ function getB2BPayBillOriginatorConversationId(result) {
1410
+ return result.Result.OriginatorConversationID;
1411
+ }
1412
+ /**
1413
+ * Extracts the result description (human-readable status).
1414
+ */
1415
+ function getB2BPayBillResultDesc(result) {
1416
+ return result.Result.ResultDesc;
1417
+ }
1418
+ /**
1419
+ * Extracts the numeric result code from a B2B Pay Bill result.
1420
+ */
1421
+ function getB2BPayBillResultCode(result) {
1422
+ return result.Result.ResultCode;
1423
+ }
1424
+ /**
1425
+ * Extracts the transaction amount from B2B Pay Bill result parameters.
1426
+ * Documented field: "Amount"
1427
+ * Returns null if not present (e.g. on failure).
1428
+ */
1429
+ function getB2BPayBillAmount(result) {
1430
+ const value = getB2BPayBillResultParam(result, "Amount");
1431
+ if (value === void 0) return null;
1432
+ const num = Number(value);
1433
+ return Number.isFinite(num) ? num : null;
1434
+ }
1435
+ /**
1436
+ * Extracts the transaction completion time.
1437
+ * Documented field: "TransCompletedTime" — format: YYYYMMDDHHmmss
1438
+ * Returns null if not present.
1439
+ */
1440
+ function getB2BPayBillCompletedTime(result) {
1441
+ const value = getB2BPayBillResultParam(result, "TransCompletedTime") ?? getB2BPayBillResultParam(result, "BOCompletedTime");
1442
+ if (value === void 0) return null;
1443
+ return String(value);
1444
+ }
1445
+ /**
1446
+ * Extracts the receiver's public name from result parameters.
1447
+ * Documented field: "ReceiverPartyPublicName"
1448
+ * Returns null if not present.
1449
+ */
1450
+ function getB2BPayBillReceiverName(result) {
1451
+ const value = getB2BPayBillResultParam(result, "ReceiverPartyPublicName");
1452
+ if (value === void 0) return null;
1453
+ return String(value);
1454
+ }
1455
+ /**
1456
+ * Extracts debit party charges from result parameters.
1457
+ * Documented field: "DebitPartyCharges"
1458
+ * Returns null if not present or empty.
1459
+ */
1460
+ function getB2BPayBillDebitPartyCharges(result) {
1461
+ const value = getB2BPayBillResultParam(result, "DebitPartyCharges");
1462
+ if (value === void 0 || value === "") return null;
1463
+ return String(value);
1464
+ }
1465
+ /**
1466
+ * Extracts the transaction currency from result parameters.
1467
+ * Documented field: "Currency"
1468
+ * Returns "KES" as default when not present.
1469
+ */
1470
+ function getB2BPayBillCurrency(result) {
1471
+ const value = getB2BPayBillResultParam(result, "Currency");
1472
+ if (value === void 0 || value === "") return "KES";
1473
+ return String(value);
1474
+ }
1475
+ /**
1476
+ * Extracts the debit party affected account balance.
1477
+ * Documented field: "DebitPartyAffectedAccountBalance"
1478
+ * Returns null if not present.
1479
+ */
1480
+ function getB2BPayBillDebitPartyAffectedBalance(result) {
1481
+ const value = getB2BPayBillResultParam(result, "DebitPartyAffectedAccountBalance");
1482
+ if (value === void 0) return null;
1483
+ return String(value);
1484
+ }
1485
+ /**
1486
+ * Extracts the debit account current balance.
1487
+ * Documented field: "DebitAccountCurrentBalance"
1488
+ * Returns null if not present.
1489
+ */
1490
+ function getB2BPayBillDebitAccountBalance(result) {
1491
+ const value = getB2BPayBillResultParam(result, "DebitAccountCurrentBalance");
1492
+ if (value === void 0) return null;
1493
+ return String(value);
1494
+ }
1495
+ /**
1496
+ * Extracts the initiator account current balance.
1497
+ * Documented field: "InitiatorAccountCurrentBalance"
1498
+ * Returns null if not present.
1499
+ */
1500
+ function getB2BPayBillInitiatorBalance(result) {
1501
+ const value = getB2BPayBillResultParam(result, "InitiatorAccountCurrentBalance");
1502
+ if (value === void 0) return null;
1503
+ return String(value);
1504
+ }
1505
+ /**
1506
+ * Extracts the Bill Reference Number from ReferenceData.
1507
+ * Documented field: "BillReferenceNumber" (in ReferenceData, not ResultParameters)
1508
+ * Returns null if not present.
1509
+ */
1510
+ function getB2BPayBillBillReferenceNumber(result) {
1511
+ const refData = result.Result.ReferenceData?.ReferenceItem;
1512
+ if (!refData) return null;
1513
+ return (Array.isArray(refData) ? refData : [refData]).find((i) => i.Key === "BillReferenceNumber")?.Value ?? null;
1514
+ }
1515
+ /**
1516
+ * Extracts a named value from B2B Pay Bill result parameters.
1517
+ * Handles both single-object and array forms of ResultParameter
1518
+ * (Daraja returns either depending on how many parameters are present).
1519
+ * Returns undefined if key is absent or no ResultParameters exist.
1520
+ */
1521
+ function getB2BPayBillResultParam(result, key) {
1522
+ const params = result.Result.ResultParameters?.ResultParameter;
1523
+ if (!params) return void 0;
1524
+ return (Array.isArray(params) ? params : [params]).find((p) => p.Key === key)?.Value;
1525
+ }
1526
+
1527
+ //#endregion
1528
+ //#region src/schemas/b2c.ts
1529
+ const B2CAsyncResponseSchema = z.object({
1530
+ ConversationID: z.string(),
1531
+ OriginatorConversationID: z.string(),
1532
+ ResponseCode: z.string(),
1533
+ ResponseDescription: z.string()
1534
+ }).passthrough();
1535
+ /** B2C Account Top Up (BusinessPayToBulk) */
1536
+ const B2CRequestSchema = z.object({
1537
+ commandId: z.literal("BusinessPayToBulk"),
1538
+ amount: KesAmountSchema,
1539
+ partyA: NonEmptyStringSchema,
1540
+ partyB: NonEmptyStringSchema,
1541
+ accountReference: NonEmptyStringSchema,
1542
+ requester: z.string().optional(),
1543
+ remarks: z.string().optional(),
1544
+ resultUrl: UrlSchema,
1545
+ queueTimeOutUrl: UrlSchema
1546
+ });
1547
+ const B2CResponseSchema = B2CAsyncResponseSchema;
1548
+ const B2CDisbursementRequestSchema = z.object({
1549
+ commandId: z.enum([
1550
+ "BusinessPayment",
1551
+ "SalaryPayment",
1552
+ "PromotionPayment"
1553
+ ]),
1554
+ amount: z.number().finite().positive(),
1555
+ partyA: NonEmptyStringSchema,
1556
+ partyB: NonEmptyStringSchema,
1557
+ remarks: NonEmptyStringSchema,
1558
+ queueTimeOutUrl: UrlSchema,
1559
+ resultUrl: UrlSchema,
1560
+ originatorConversationId: NonEmptyStringSchema.optional(),
1561
+ occasion: z.string().optional()
1562
+ });
1563
+ const B2CDisbursementResponseSchema = B2CAsyncResponseSchema;
1564
+
1565
+ //#endregion
1566
+ //#region src/mpesa/b2c/payment.ts
1567
+ /**
1568
+ * src/mpesa/b2c/payment.ts
1569
+ *
1570
+ * Initiates a B2C Account Top Up via Safaricom Daraja.
1571
+ * Endpoint: POST /mpesa/b2b/v1/paymentrequest
1572
+ *
1573
+ * Strictly follows the Safaricom Daraja B2C Account Top Up API documentation:
1574
+ * - CommandID must be "BusinessPayToBulk"
1575
+ * - SenderIdentifierType is always "4" (hardcoded per docs)
1576
+ * - RecieverIdentifierType is always "4" (hardcoded per docs)
1577
+ * - Amount is sent as a string per the JSON spec
1578
+ * - Requester is optional
1579
+ */
1580
+ /** The only endpoint documented for this API */
1581
+ const B2C_ENDPOINT = "/mpesa/b2b/v1/paymentrequest";
1582
+ /**
1583
+ * Per documentation: SenderIdentifierType and RecieverIdentifierType
1584
+ * must always be "4" (Organisation ShortCode). Not configurable.
1585
+ */
1586
+ const IDENTIFIER_TYPE = "4";
1587
+ /**
1588
+ * Initiates a B2C Account Top Up payment request.
1589
+ *
1590
+ * @param baseUrl - Daraja base URL (sandbox or production)
1591
+ * @param accessToken - Valid OAuth Bearer token
1592
+ * @param securityCredential - RSA-encrypted initiator password (base64)
1593
+ * @param initiatorName - M-Pesa API operator username with B2B role
1594
+ * @param request - B2C top-up request parameters
1595
+ * @returns Synchronous acknowledgement response from Daraja
1596
+ * @throws {PesafyError} VALIDATION_ERROR for invalid input before HTTP call
1597
+ * @throws {PesafyError} From httpRequest on network / API errors
1598
+ */
1599
+ async function initiateB2CPayment(baseUrl, accessToken, securityCredential, initiatorName, request, http) {
1600
+ const validated = parseWithSchema(B2CRequestSchema, request, "B2C request");
1601
+ const amount = Math.round(validated.amount);
1602
+ const payload = {
1603
+ Initiator: initiatorName,
1604
+ SecurityCredential: securityCredential,
1605
+ CommandID: validated.commandId,
1606
+ SenderIdentifierType: IDENTIFIER_TYPE,
1607
+ RecieverIdentifierType: IDENTIFIER_TYPE,
1608
+ Amount: String(amount),
1609
+ PartyA: String(validated.partyA),
1610
+ PartyB: String(validated.partyB),
1611
+ AccountReference: validated.accountReference,
1612
+ Remarks: validated.remarks ?? "B2C Account Top Up",
1613
+ QueueTimeOutURL: validated.queueTimeOutUrl,
1614
+ ResultURL: validated.resultUrl
1615
+ };
1616
+ if (validated.requester?.trim()) payload["Requester"] = String(validated.requester);
1617
+ const { data } = await httpRequest(`${baseUrl}${B2C_ENDPOINT}`, withDarajaHttp({
1618
+ method: "POST",
1619
+ headers: { Authorization: `Bearer ${accessToken}` },
1620
+ body: payload
1621
+ }, http));
1622
+ return parseWithSchema(B2CResponseSchema, data, "B2C response");
1623
+ }
1624
+
1625
+ //#endregion
1626
+ //#region src/mpesa/b2c/types.ts
1627
+ /**
1628
+ * Documented Daraja error codes for the B2C Account Top Up API.
1629
+ */
1630
+ const B2C_ERROR_CODES = {
1631
+ INTERNAL_SERVER_ERROR: "500.003.1001",
1632
+ INVALID_ACCESS_TOKEN: "400.003.01",
1633
+ BAD_REQUEST: "400.003.02",
1634
+ QUOTA_VIOLATION: "500.003.03",
1635
+ SPIKE_ARREST: "500.003.02",
1636
+ NOT_FOUND: "404.003.01",
1637
+ INVALID_AUTH_HEADER: "404.001.04",
1638
+ INVALID_PAYLOAD: "400.002.05"
1639
+ };
1640
+ /**
1641
+ * Known B2C result codes documented by Safaricom Daraja.
1642
+ */
1643
+ const B2C_RESULT_CODES = {
1644
+ SUCCESS: 0,
1645
+ INVALID_INITIATOR: 2001
1646
+ };
1647
+
1648
+ //#endregion
1649
+ //#region src/mpesa/b2c/webhooks.ts
1650
+ /**
1651
+ * src/mpesa/b2c/webhooks.ts
1652
+ *
1653
+ * Type guards and payload extractors for B2C Account Top Up result callbacks.
1654
+ *
1655
+ * Result parameters are aligned strictly to Safaricom Daraja documentation:
1656
+ * - DebitAccountBalance
1657
+ * - Amount
1658
+ * - Currency
1659
+ * - ReceiverPartyPublicName
1660
+ * - TransactionCompletedTime
1661
+ * - DebitPartyCharges
1662
+ *
1663
+ * Docs note: ResultCode is "0" (string) on success but 2001 (number) on
1664
+ * failure — both forms are handled by the type guard.
1665
+ */
1666
+ /**
1667
+ * Runtime type guard — checks if a body looks like a B2C result callback.
1668
+ * Validates the minimum documented structure.
1669
+ */
1670
+ function isB2CResult(body) {
1671
+ if (!body || typeof body !== "object") return false;
1672
+ const b = body;
1673
+ if (!b["Result"] || typeof b["Result"] !== "object") return false;
1674
+ const result = b["Result"];
1675
+ return (typeof result["ResultCode"] === "number" || typeof result["ResultCode"] === "string") && typeof result["ConversationID"] === "string" && typeof result["OriginatorConversationID"] === "string";
1676
+ }
1677
+ /**
1678
+ * Returns true if the B2C result represents a successful transaction.
1679
+ * Handles both string "0" (documented in success sample) and number 0.
1680
+ */
1681
+ function isB2CSuccess(result) {
1682
+ const code = result.Result.ResultCode;
1683
+ return code === 0 || code === "0";
1684
+ }
1685
+ /**
1686
+ * Returns true if the B2C result represents a failure.
1687
+ * Handles both string "0" (documented in success sample) and number 0.
1688
+ */
1689
+ function isB2CFailure(result) {
1690
+ return !isB2CSuccess(result);
1691
+ }
1692
+ /**
1693
+ * Returns true if the result code matches a known documented code.
1694
+ * Empty strings are explicitly rejected — Number('') coerces to 0 which
1695
+ * would otherwise incorrectly match B2C_RESULT_CODES.SUCCESS.
1696
+ */
1697
+ function isKnownB2CResultCode(code) {
1698
+ if (typeof code !== "number" && typeof code !== "string") return false;
1699
+ if (typeof code === "string" && code.trim() === "") return false;
1700
+ const numeric = Number(code);
1701
+ return Object.values(B2C_RESULT_CODES).includes(numeric);
1702
+ }
1703
+ /**
1704
+ * Extracts the M-PESA transaction ID from a B2C result.
1705
+ * Present on both success and failure (generic ID on failure).
1706
+ */
1707
+ function getB2CTransactionId(result) {
1708
+ return result.Result.TransactionID ?? null;
1709
+ }
1710
+ /**
1711
+ * Extracts the ConversationID from a B2C result.
1712
+ */
1713
+ function getB2CConversationId(result) {
1714
+ return result.Result.ConversationID;
1715
+ }
1716
+ /**
1717
+ * Extracts the OriginatorConversationID from a B2C result.
1718
+ * Use this to correlate with the original API call response.
1719
+ */
1720
+ function getB2COriginatorConversationId(result) {
1721
+ return result.Result.OriginatorConversationID;
1722
+ }
1723
+ /**
1724
+ * Extracts the result description (human-readable status).
1725
+ */
1726
+ function getB2CResultDesc(result) {
1727
+ return result.Result.ResultDesc;
1728
+ }
1729
+ /**
1730
+ * Extracts the transaction amount from B2C result parameters.
1731
+ * Documented field: "Amount"
1732
+ * Returns null if not present (e.g. on failure).
1733
+ */
1734
+ function getB2CAmount(result) {
1735
+ const value = getB2CResultParam(result, "Amount");
1736
+ if (value === void 0) return null;
1737
+ const num = Number(value);
1738
+ return Number.isFinite(num) ? num : null;
1739
+ }
1740
+ /**
1741
+ * Extracts the transaction currency from B2C result parameters.
1742
+ * Documented field: "Currency"
1743
+ * Returns "KES" as default when not present.
1744
+ */
1745
+ function getB2CCurrency(result) {
1746
+ const value = getB2CResultParam(result, "Currency");
1747
+ if (value === void 0 || value === "") return "KES";
1748
+ return String(value);
1749
+ }
1750
+ /**
1751
+ * Extracts the receiver's public name from B2C result parameters.
1752
+ * Documented field: "ReceiverPartyPublicName"
1753
+ * Returns null if not present.
1754
+ */
1755
+ function getB2CReceiverPublicName(result) {
1756
+ const value = getB2CResultParam(result, "ReceiverPartyPublicName");
1757
+ if (value === void 0) return null;
1758
+ return String(value);
1759
+ }
1760
+ /**
1761
+ * Extracts the transaction completion timestamp from B2C result parameters.
1762
+ * Documented field: "TransactionCompletedTime" — format: YYYYMMDDHHmmss
1763
+ * Returns null if not present.
1764
+ */
1765
+ function getB2CTransactionCompletedTime(result) {
1766
+ const value = getB2CResultParam(result, "TransactionCompletedTime");
1767
+ if (value === void 0) return null;
1768
+ return String(value);
1769
+ }
1770
+ /**
1771
+ * Extracts the debit account balance from B2C result parameters.
1772
+ * Documented field: "DebitAccountBalance" (e.g. "{CurrencyCode=KES}")
1773
+ * Returns null if not present.
1774
+ */
1775
+ function getB2CDebitAccountBalance(result) {
1776
+ const value = getB2CResultParam(result, "DebitAccountBalance");
1777
+ if (value === void 0) return null;
1778
+ return String(value);
1779
+ }
1780
+ /**
1781
+ * Extracts the debit party charges from B2C result parameters.
1782
+ * Documented field: "DebitPartyCharges"
1783
+ * Returns null if not present or empty.
1784
+ */
1785
+ function getB2CDebitPartyCharges(result) {
1786
+ const value = getB2CResultParam(result, "DebitPartyCharges");
1787
+ if (value === void 0 || value === "") return null;
1788
+ return String(value);
1789
+ }
1790
+ /**
1791
+ * Extracts a named value from B2C result parameters.
1792
+ * Handles both single-object and array forms of ResultParameter
1793
+ * (Daraja returns either depending on how many parameters are present).
1794
+ * Returns undefined if key is absent or no ResultParameters exist.
1795
+ */
1796
+ function getB2CResultParam(result, key) {
1797
+ const params = result.Result.ResultParameters?.ResultParameter;
1798
+ if (!params) return void 0;
1799
+ return (Array.isArray(params) ? params : [params]).find((p) => p.Key === key)?.Value;
1800
+ }
1801
+
1802
+ //#endregion
1803
+ //#region src/mpesa/b2c-disbursement/payment.ts
1804
+ /**
1805
+ * src/mpesa/b2c-disbursement/payment.ts
1806
+ *
1807
+ * Initiates a B2C Disbursement payment (Salary / Cashback / Promotion).
1808
+ * Endpoint: POST /mpesa/b2c/v3/paymentrequest
1809
+ */
1810
+ const B2C_DISBURSEMENT_ENDPOINT = "/mpesa/b2c/v3/paymentrequest";
1811
+ const VALID_COMMAND_IDS = new Set([
1812
+ "BusinessPayment",
1813
+ "SalaryPayment",
1814
+ "PromotionPayment"
1815
+ ]);
1816
+ /**
1817
+ * Initiates a B2C disbursement payment request.
1818
+ *
1819
+ * @param baseUrl - Daraja base URL (sandbox or production)
1820
+ * @param accessToken - Valid OAuth Bearer token
1821
+ * @param securityCredential - RSA-encrypted initiator password (base64)
1822
+ * @param initiatorName - M-Pesa API operator username
1823
+ * @param request - B2C disbursement request parameters
1824
+ */
1825
+ async function initiateB2CDisbursement(baseUrl, accessToken, securityCredential, initiatorName, request, http) {
1826
+ const originatorConversationId = request.originatorConversationId?.trim() || generateOriginatorConversationId();
1827
+ const validated = parseWithSchema(B2CDisbursementRequestSchema, {
1828
+ ...request,
1829
+ originatorConversationId
1830
+ }, "B2C Disbursement request");
1831
+ if (!validated.commandId || !VALID_COMMAND_IDS.has(validated.commandId)) throw createError({
1832
+ code: "VALIDATION_ERROR",
1833
+ message: `commandId must be one of: BusinessPayment, SalaryPayment, PromotionPayment. Got "${validated.commandId}".`
1834
+ });
1835
+ const amount = Math.round(validated.amount);
1836
+ if (!Number.isFinite(amount) || amount < 10) throw createError({
1837
+ code: "VALIDATION_ERROR",
1838
+ message: `amount must be ≥ 10 KES (got ${validated.amount} which rounds to ${amount}).`
1839
+ });
1840
+ if (!validated.partyA?.trim()) throw createError({
1841
+ code: "VALIDATION_ERROR",
1842
+ message: "partyA is required — the sending organisation shortcode."
1843
+ });
1844
+ if (!validated.partyB?.trim()) throw createError({
1845
+ code: "VALIDATION_ERROR",
1846
+ message: "partyB is required — the receiving customer MSISDN (2547XXXXXXXX)."
1847
+ });
1848
+ if (!validated.remarks?.trim()) throw createError({
1849
+ code: "VALIDATION_ERROR",
1850
+ message: "remarks is required (2–100 characters)."
1851
+ });
1852
+ if (!validated.resultUrl?.trim()) throw createError({
1853
+ code: "VALIDATION_ERROR",
1854
+ message: "resultUrl is required — Safaricom POSTs the async result here."
1855
+ });
1856
+ if (!validated.queueTimeOutUrl?.trim()) throw createError({
1857
+ code: "VALIDATION_ERROR",
1858
+ message: "queueTimeOutUrl is required — Safaricom calls this on request timeout."
1859
+ });
1860
+ const payload = {
1861
+ OriginatorConversationID: originatorConversationId,
1862
+ InitiatorName: initiatorName,
1863
+ SecurityCredential: securityCredential,
1864
+ CommandID: validated.commandId,
1865
+ Amount: amount,
1866
+ PartyA: String(validated.partyA),
1867
+ PartyB: String(validated.partyB),
1868
+ Remarks: validated.remarks,
1869
+ QueueTimeOutURL: validated.queueTimeOutUrl,
1870
+ ResultURL: validated.resultUrl
1871
+ };
1872
+ if (validated.occasion?.trim()) payload["Occassion"] = validated.occasion;
1873
+ const { data } = await httpRequest(`${baseUrl}${B2C_DISBURSEMENT_ENDPOINT}`, withDarajaHttp({
1874
+ method: "POST",
1875
+ headers: { Authorization: `Bearer ${accessToken}` },
1876
+ body: payload
1877
+ }, http));
1878
+ return parseWithSchema(B2CDisbursementResponseSchema, data, "B2C Disbursement response");
1879
+ }
1880
+
1881
+ //#endregion
1882
+ //#region src/mpesa/b2c-disbursement/types.ts
1883
+ /**
1884
+ * All documented B2C result codes.
1885
+ * Note: SFC_IC0003 is a string — all others are numeric.
1886
+ */
1887
+ const B2C_DISBURSEMENT_RESULT_CODES = {
1888
+ SUCCESS: 0,
1889
+ INSUFFICIENT_BALANCE: 1,
1890
+ AMOUNT_TOO_SMALL: 2,
1891
+ AMOUNT_TOO_LARGE: 3,
1892
+ DAILY_LIMIT_EXCEEDED: 4,
1893
+ MAX_BALANCE_EXCEEDED: 8,
1894
+ DEBIT_PARTY_INVALID: 11,
1895
+ INITIATOR_NOT_ALLOWED: 21,
1896
+ INVALID_INITIATOR_INFO: 2001,
1897
+ ACCOUNT_INACTIVE: 2006,
1898
+ PRODUCT_NOT_PERMITTED: 2028,
1899
+ CUSTOMER_NOT_REGISTERED: 2040,
1900
+ SECURITY_CREDENTIAL_LOCKED: 8006,
1901
+ OPERATOR_DOES_NOT_EXIST: "SFC_IC0003"
1902
+ };
1903
+
1904
+ //#endregion
1905
+ //#region src/mpesa/b2c-disbursement/webhooks.ts
1906
+ /**
1907
+ * src/mpesa/b2c-disbursement/webhooks.ts
1908
+ *
1909
+ * Type guards and payload extractors for B2C Disbursement result callbacks.
1910
+ */
1911
+ function isB2CDisbursementResult(body) {
1912
+ if (!body || typeof body !== "object") return false;
1913
+ const b = body;
1914
+ if (!b["Result"] || typeof b["Result"] !== "object") return false;
1915
+ const result = b["Result"];
1916
+ return (typeof result["ResultCode"] === "number" || typeof result["ResultCode"] === "string") && typeof result["ConversationID"] === "string" && typeof result["OriginatorConversationID"] === "string";
1917
+ }
1918
+ function isB2CDisbursementSuccess(result) {
1919
+ const code = result.Result.ResultCode;
1920
+ return code === 0 || code === "0";
1921
+ }
1922
+ function isB2CDisbursementFailure(result) {
1923
+ return !isB2CDisbursementSuccess(result);
1924
+ }
1925
+ /**
1926
+ * Returns true if the result code is among the 14 documented codes.
1927
+ * Handles both numeric codes and the string code "SFC_IC0003".
1928
+ * Rejects empty strings (Number('') → 0 which would falsely match SUCCESS).
1929
+ */
1930
+ function isKnownB2CDisbursementResultCode(code) {
1931
+ if (code === null || code === void 0) return false;
1932
+ if (typeof code !== "number" && typeof code !== "string") return false;
1933
+ if (typeof code === "string" && code === B2C_DISBURSEMENT_RESULT_CODES.OPERATOR_DOES_NOT_EXIST) return true;
1934
+ if (typeof code === "string" && code.trim() === "") return false;
1935
+ const numeric = Number(code);
1936
+ return Object.values(B2C_DISBURSEMENT_RESULT_CODES).filter((v) => typeof v === "number").includes(numeric);
1937
+ }
1938
+ function getB2CDisbursementTransactionId(result) {
1939
+ return result.Result.TransactionID ?? null;
1940
+ }
1941
+ function getB2CDisbursementConversationId(result) {
1942
+ return result.Result.ConversationID;
1943
+ }
1944
+ function getB2CDisbursementOriginatorConversationId(result) {
1945
+ return result.Result.OriginatorConversationID;
1946
+ }
1947
+ function getB2CDisbursementResultDesc(result) {
1948
+ return result.Result.ResultDesc;
1949
+ }
1950
+ function getB2CDisbursementResultCode(result) {
1951
+ return result.Result.ResultCode;
1952
+ }
1953
+ function getB2CDisbursementResultParam(result, key) {
1954
+ const params = result.Result.ResultParameters?.ResultParameter;
1955
+ if (!params) return void 0;
1956
+ return (Array.isArray(params) ? params : [params]).find((p) => p.Key === key)?.Value;
1957
+ }
1958
+ function getB2CDisbursementAmount(result) {
1959
+ const value = getB2CDisbursementResultParam(result, "TransactionAmount");
1960
+ if (value === void 0) return null;
1961
+ const num = Number(value);
1962
+ return Number.isFinite(num) ? num : null;
1963
+ }
1964
+ function getB2CDisbursementReceiptNumber(result) {
1965
+ const value = getB2CDisbursementResultParam(result, "TransactionReceipt");
1966
+ if (value === void 0) return null;
1967
+ return String(value);
1968
+ }
1969
+ function getB2CDisbursementReceiverName(result) {
1970
+ const value = getB2CDisbursementResultParam(result, "ReceiverPartyPublicName");
1971
+ if (value === void 0) return null;
1972
+ return String(value);
1973
+ }
1974
+ function getB2CDisbursementCompletedTime(result) {
1975
+ const value = getB2CDisbursementResultParam(result, "TransactionCompletedDateTime");
1976
+ if (value === void 0) return null;
1977
+ return String(value);
1978
+ }
1979
+ function getB2CDisbursementUtilityBalance(result) {
1980
+ const value = getB2CDisbursementResultParam(result, "B2CUtilityAccountAvailableFunds");
1981
+ if (value === void 0) return null;
1982
+ const num = Number(value);
1983
+ return Number.isFinite(num) ? num : null;
1984
+ }
1985
+ function getB2CDisbursementWorkingBalance(result) {
1986
+ const value = getB2CDisbursementResultParam(result, "B2CWorkingAccountAvailableFunds");
1987
+ if (value === void 0) return null;
1988
+ const num = Number(value);
1989
+ return Number.isFinite(num) ? num : null;
1990
+ }
1991
+ function isB2CDisbursementRecipientRegistered(result) {
1992
+ const value = getB2CDisbursementResultParam(result, "B2CRecipientIsRegisteredCustomer");
1993
+ if (value === void 0) return null;
1994
+ return String(value).toUpperCase() === "Y";
1995
+ }
1996
+
1997
+ //#endregion
1998
+ //#region src/schemas/bill-manager.ts
1999
+ const SendRemindersSchema = z.enum(["0", "1"]);
2000
+ const BillManagerOptInRequestSchema = z.object({
2001
+ shortcode: NonEmptyStringSchema,
2002
+ email: NonEmptyStringSchema,
2003
+ officialContact: NonEmptyStringSchema,
2004
+ sendReminders: SendRemindersSchema,
2005
+ logo: z.string().optional(),
2006
+ callbackUrl: UrlSchema
2007
+ });
2008
+ const BillManagerOptInResponseSchema = z.object({
2009
+ app_key: z.string().optional(),
2010
+ resmsg: z.string(),
2011
+ rescode: z.string()
2012
+ }).passthrough();
2013
+ const BillManagerUpdateOptInRequestSchema = BillManagerOptInRequestSchema;
2014
+ const BillManagerUpdateOptInResponseSchema = z.object({
2015
+ resmsg: z.string(),
2016
+ rescode: z.string()
2017
+ }).passthrough();
2018
+ const BillManagerInvoiceItemSchema = z.object({
2019
+ itemName: NonEmptyStringSchema,
2020
+ amount: KesAmountSchema
2021
+ });
2022
+ const BillManagerSingleInvoiceRequestSchema = z.object({
2023
+ externalReference: NonEmptyStringSchema,
2024
+ billedFullName: NonEmptyStringSchema,
2025
+ billedPhoneNumber: NonEmptyStringSchema,
2026
+ billedPeriod: NonEmptyStringSchema,
2027
+ invoiceName: NonEmptyStringSchema,
2028
+ dueDate: NonEmptyStringSchema,
2029
+ accountReference: NonEmptyStringSchema,
2030
+ amount: KesAmountSchema,
2031
+ invoiceItems: z.array(BillManagerInvoiceItemSchema).optional()
2032
+ });
2033
+ const BillManagerSingleInvoiceResponseSchema = z.object({
2034
+ Status_Message: z.string().optional(),
2035
+ resmsg: z.string(),
2036
+ rescode: z.string()
2037
+ }).passthrough();
2038
+ const BillManagerBulkInvoiceRequestSchema = z.object({ invoices: z.array(BillManagerSingleInvoiceRequestSchema).min(1).max(1e3) });
2039
+ const BillManagerBulkInvoiceResponseSchema = z.object({
2040
+ Status_Message: z.string().optional(),
2041
+ resmsg: z.string(),
2042
+ rescode: z.string()
2043
+ }).passthrough();
2044
+ const BillManagerCancelInvoiceRequestSchema = z.object({ externalReference: NonEmptyStringSchema });
2045
+ const BillManagerCancelInvoiceResponseSchema = z.object({
2046
+ Status_Message: z.string().optional(),
2047
+ resmsg: z.string(),
2048
+ rescode: z.string(),
2049
+ errors: z.array(z.unknown()).optional()
2050
+ }).passthrough();
2051
+ const BillManagerCancelBulkInvoiceRequestSchema = z.object({ externalReferences: z.array(NonEmptyStringSchema).min(1) });
2052
+ const BillManagerCancelBulkInvoiceResponseSchema = z.object({
2053
+ Status_Message: z.string().optional(),
2054
+ resmsg: z.string(),
2055
+ rescode: z.string(),
2056
+ errors: z.array(z.unknown()).optional()
2057
+ }).passthrough();
2058
+ const BillManagerReconciliationRequestSchema = z.object({
2059
+ paymentDate: NonEmptyStringSchema,
2060
+ paidAmount: NonEmptyStringSchema,
2061
+ accountReference: NonEmptyStringSchema,
2062
+ transactionId: NonEmptyStringSchema,
2063
+ phoneNumber: NonEmptyStringSchema,
2064
+ fullName: NonEmptyStringSchema,
2065
+ invoiceName: NonEmptyStringSchema,
2066
+ externalReference: NonEmptyStringSchema
2067
+ });
2068
+ const BillManagerReconciliationResponseSchema = z.object({
2069
+ resmsg: z.string(),
2070
+ rescode: z.string()
2071
+ }).passthrough();
2072
+
2073
+ //#endregion
2074
+ //#region src/mpesa/bill-manager/invoice.ts
2075
+ /**
2076
+ * src/mpesa/bill-manager/invoice.ts
2077
+ *
2078
+ * Bill Manager — opt-in, invoice creation/cancellation, and payment reconciliation.
2079
+ *
2080
+ * Strictly aligned with Safaricom Daraja Bill Manager API documentation.
2081
+ *
2082
+ * APIs:
2083
+ * POST /v1/billmanager-invoice/optin — Opt-in shortcode
2084
+ * POST /v1/billmanager-invoice/change-optin-details — Update opt-in details
2085
+ * POST /v1/billmanager-invoice/single-invoicing — Send a single invoice
2086
+ * POST /v1/billmanager-invoice/bulk-invoicing — Send bulk invoices (up to 1000)
2087
+ * POST /v1/billmanager-invoice/cancel-single-invoice — Cancel a single invoice
2088
+ * POST /v1/billmanager-invoice/cancel-bulk-invoices — Cancel multiple invoices
2089
+ * POST /v1/billmanager-invoice/reconciliation — Acknowledge a payment
2090
+ */
2091
+ async function billManagerOptIn(baseUrl, accessToken, request, http) {
2092
+ const validated = parseWithSchema(BillManagerOptInRequestSchema, request, "Bill Manager opt-in request");
2093
+ const payload = {
2094
+ shortcode: validated.shortcode,
2095
+ email: validated.email,
2096
+ officialContact: validated.officialContact,
2097
+ sendReminders: validated.sendReminders,
2098
+ logo: validated.logo ?? "",
2099
+ callbackurl: validated.callbackUrl
2100
+ };
2101
+ const { data } = await httpRequest(`${baseUrl}/v1/billmanager-invoice/optin`, withDarajaHttp({
2102
+ method: "POST",
2103
+ headers: { Authorization: `Bearer ${accessToken}` },
2104
+ body: payload
2105
+ }, http));
2106
+ return parseWithSchema(BillManagerOptInResponseSchema, data, "Bill Manager opt-in response");
2107
+ }
2108
+ async function updateOptIn(baseUrl, accessToken, request, http) {
2109
+ const validated = parseWithSchema(BillManagerUpdateOptInRequestSchema, request, "Bill Manager update opt-in request");
2110
+ const payload = {
2111
+ shortcode: validated.shortcode,
2112
+ email: validated.email,
2113
+ officialContact: validated.officialContact,
2114
+ sendReminders: validated.sendReminders,
2115
+ logo: validated.logo ?? "",
2116
+ callbackurl: validated.callbackUrl
2117
+ };
2118
+ const { data } = await httpRequest(`${baseUrl}/v1/billmanager-invoice/change-optin-details`, withDarajaHttp({
2119
+ method: "POST",
2120
+ headers: { Authorization: `Bearer ${accessToken}` },
2121
+ body: payload
2122
+ }, http));
2123
+ return parseWithSchema(BillManagerUpdateOptInResponseSchema, data, "Bill Manager update opt-in response");
2124
+ }
2125
+ async function sendSingleInvoice(baseUrl, accessToken, request, http) {
2126
+ const validated = parseWithSchema(BillManagerSingleInvoiceRequestSchema, request, "Bill Manager single invoice request");
2127
+ const amount = Math.round(validated.amount);
2128
+ const payload = {
2129
+ externalReference: validated.externalReference,
2130
+ billedFullName: validated.billedFullName,
2131
+ billedPhoneNumber: validated.billedPhoneNumber,
2132
+ billedPeriod: validated.billedPeriod,
2133
+ invoiceName: validated.invoiceName,
2134
+ dueDate: validated.dueDate,
2135
+ accountReference: validated.accountReference,
2136
+ amount: String(amount),
2137
+ invoiceItems: validated.invoiceItems?.map((i) => ({
2138
+ itemName: i.itemName,
2139
+ amount: String(Math.round(i.amount))
2140
+ })) ?? []
2141
+ };
2142
+ const { data } = await httpRequest(`${baseUrl}/v1/billmanager-invoice/single-invoicing`, withDarajaHttp({
2143
+ method: "POST",
2144
+ headers: { Authorization: `Bearer ${accessToken}` },
2145
+ body: payload
2146
+ }, http));
2147
+ return parseWithSchema(BillManagerSingleInvoiceResponseSchema, data, "Bill Manager single invoice response");
2148
+ }
2149
+ async function sendBulkInvoices(baseUrl, accessToken, request, http) {
2150
+ const payload = parseWithSchema(BillManagerBulkInvoiceRequestSchema, request, "Bill Manager bulk invoice request").invoices.map((inv) => ({
2151
+ externalReference: inv.externalReference,
2152
+ billedFullName: inv.billedFullName,
2153
+ billedPhoneNumber: inv.billedPhoneNumber,
2154
+ billedPeriod: inv.billedPeriod,
2155
+ invoiceName: inv.invoiceName,
2156
+ dueDate: inv.dueDate,
2157
+ accountReference: inv.accountReference,
2158
+ amount: String(Math.round(inv.amount)),
2159
+ invoiceItems: inv.invoiceItems?.map((item) => ({
2160
+ itemName: item.itemName,
2161
+ amount: String(Math.round(item.amount))
2162
+ })) ?? []
2163
+ }));
2164
+ const { data } = await httpRequest(`${baseUrl}/v1/billmanager-invoice/bulk-invoicing`, withDarajaHttp({
2165
+ method: "POST",
2166
+ headers: { Authorization: `Bearer ${accessToken}` },
2167
+ body: payload
2168
+ }, http));
2169
+ return parseWithSchema(BillManagerBulkInvoiceResponseSchema, data, "Bill Manager bulk invoice response");
2170
+ }
2171
+ async function cancelInvoice(baseUrl, accessToken, request, http) {
2172
+ const validated = parseWithSchema(BillManagerCancelInvoiceRequestSchema, request, "Bill Manager cancel invoice request");
2173
+ const { data } = await httpRequest(`${baseUrl}/v1/billmanager-invoice/cancel-single-invoice`, withDarajaHttp({
2174
+ method: "POST",
2175
+ headers: { Authorization: `Bearer ${accessToken}` },
2176
+ body: { externalReference: validated.externalReference }
2177
+ }, http));
2178
+ return parseWithSchema(BillManagerCancelInvoiceResponseSchema, data, "Bill Manager cancel invoice response");
2179
+ }
2180
+ async function cancelBulkInvoices(baseUrl, accessToken, request, http) {
2181
+ const payload = parseWithSchema(BillManagerCancelBulkInvoiceRequestSchema, request, "Bill Manager cancel bulk invoices request").externalReferences.map((ref) => ({ externalReference: ref }));
2182
+ const { data } = await httpRequest(`${baseUrl}/v1/billmanager-invoice/cancel-bulk-invoices`, withDarajaHttp({
2183
+ method: "POST",
2184
+ headers: { Authorization: `Bearer ${accessToken}` },
2185
+ body: payload
2186
+ }, http));
2187
+ return parseWithSchema(BillManagerCancelBulkInvoiceResponseSchema, data, "Bill Manager cancel bulk invoices response");
2188
+ }
2189
+ async function reconcilePayment(baseUrl, accessToken, request, http) {
2190
+ const validated = parseWithSchema(BillManagerReconciliationRequestSchema, request, "Bill Manager reconciliation request");
2191
+ const payload = {
2192
+ paymentDate: validated.paymentDate,
2193
+ paidAmount: validated.paidAmount,
2194
+ accountReference: validated.accountReference,
2195
+ transactionId: validated.transactionId,
2196
+ phoneNumber: validated.phoneNumber,
2197
+ fullName: validated.fullName,
2198
+ invoiceName: validated.invoiceName,
2199
+ externalReference: validated.externalReference
2200
+ };
2201
+ const { data } = await httpRequest(`${baseUrl}/v1/billmanager-invoice/reconciliation`, withDarajaHttp({
2202
+ method: "POST",
2203
+ headers: { Authorization: `Bearer ${accessToken}` },
2204
+ body: payload
2205
+ }, http));
2206
+ return parseWithSchema(BillManagerReconciliationResponseSchema, data, "Bill Manager reconciliation response");
2207
+ }
2208
+
2209
+ //#endregion
2210
+ //#region src/schemas/c2b.ts
2211
+ const C2BRegisterUrlRequestSchema = z.object({
2212
+ shortCode: NonEmptyStringSchema,
2213
+ responseType: z.enum(["Completed", "Cancelled"]),
2214
+ confirmationUrl: UrlSchema,
2215
+ validationUrl: UrlSchema,
2216
+ apiVersion: z.enum(["v1", "v2"]).optional()
2217
+ });
2218
+ const C2BBaseResponseSchema = z.object({
2219
+ OriginatorCoversationID: z.string(),
2220
+ ResponseCode: z.string(),
2221
+ ResponseDescription: z.string()
2222
+ }).passthrough();
2223
+ const C2BRegisterUrlResponseSchema = C2BBaseResponseSchema;
2224
+ const C2BSimulateResponseSchema = C2BBaseResponseSchema;
2225
+ const C2BSimulateRequestSchema = z.object({
2226
+ shortCode: z.union([NonEmptyStringSchema, z.number()]),
2227
+ commandId: z.enum(["CustomerPayBillOnline", "CustomerBuyGoodsOnline"]),
2228
+ amount: KesAmountSchema,
2229
+ msisdn: z.union([NonEmptyStringSchema, z.number()]),
2230
+ billRefNumber: z.union([NonEmptyStringSchema, z.null()]).optional(),
2231
+ apiVersion: z.enum(["v1", "v2"]).optional()
2232
+ }).superRefine((data, ctx) => {
2233
+ if (data.commandId === "CustomerPayBillOnline" && !data.billRefNumber?.trim()) ctx.addIssue({
2234
+ code: "custom",
2235
+ message: "billRefNumber is required for CustomerPayBillOnline",
2236
+ path: ["billRefNumber"]
2237
+ });
2238
+ });
2239
+ const C2BValidationWebhookSchema = z.object({
2240
+ TransactionType: z.string(),
2241
+ TransID: z.string(),
2242
+ TransTime: z.string(),
2243
+ TransAmount: z.union([z.string(), z.number()]),
2244
+ BusinessShortCode: z.string(),
2245
+ BillRefNumber: z.string().optional(),
2246
+ MSISDN: z.string()
2247
+ }).passthrough();
2248
+
2249
+ //#endregion
2250
+ //#region src/mpesa/c2b/register-url.ts
2251
+ /**
2252
+ * src/mpesa/c2b/register-url.ts
2253
+ *
2254
+ * C2B Register URL implementation.
2255
+ * Strictly aligned with Safaricom Daraja C2B Register URL API documentation.
2256
+ *
2257
+ * Endpoint (v1 — documented primary):
2258
+ * Sandbox: POST https://sandbox.safaricom.co.ke/mpesa/c2b/v1/registerurl
2259
+ * Production: POST https://api.safaricom.co.ke/mpesa/c2b/v1/registerurl
2260
+ *
2261
+ * Also supports v2 via the apiVersion option.
2262
+ */
2263
+ /**
2264
+ * Forbidden URL keywords per Daraja documentation:
2265
+ * "Avoid keywords such as M-PESA, M-Pesa, Safaricom, exe, exec, cme, or variants in your URLs."
2266
+ *
2267
+ * We lowercase-compare, so "MPESA", "Mpesa", "mPeSa" are all caught.
2268
+ *
2269
+ * Additional blocked keywords (documented variants): cmd, sql, query
2270
+ */
2271
+ const FORBIDDEN_URL_KEYWORDS = [
2272
+ "mpesa",
2273
+ "safaricom",
2274
+ "exec",
2275
+ "exe",
2276
+ "cme",
2277
+ "cmd",
2278
+ "sql",
2279
+ "query"
2280
+ ];
2281
+ /**
2282
+ * Validates a callback URL against Daraja's documented URL requirements.
2283
+ * Throws PesafyError if the URL violates any documented rule.
2284
+ */
2285
+ function validateCallbackUrl(url, fieldName) {
2286
+ if (!url || !url.trim()) throw createError({
2287
+ code: "VALIDATION_ERROR",
2288
+ message: `${fieldName} is required`
2289
+ });
2290
+ const lower = url.toLowerCase();
2291
+ for (const keyword of FORBIDDEN_URL_KEYWORDS) if (lower.includes(keyword)) throw createError({
2292
+ code: "VALIDATION_ERROR",
2293
+ message: `${fieldName} must not contain the keyword "${keyword}". Daraja rejects URLs containing: mpesa, safaricom, exe, exec, cme (and variants: cmd, sql, query).`
2294
+ });
2295
+ }
2296
+ /**
2297
+ * Registers C2B Confirmation and Validation URLs with Safaricom.
2298
+ *
2299
+ * Per Daraja documentation:
2300
+ * - Sandbox: may be called multiple times (URLs can be overwritten).
2301
+ * - Production: one-time call. To change URLs, delete existing on the portal
2302
+ * or email apisupport@safaricom.co.ke, then re-register.
2303
+ * - ResponseType must be sentence-case: "Completed" or "Cancelled".
2304
+ * - Both URLs must be publicly accessible and internet-reachable.
2305
+ * - Production requires HTTPS; Sandbox allows HTTP.
2306
+ * - Do not use public URL testers (ngrok, mockbin, requestbin) — they are blocked.
2307
+ * - The Validation URL is only called when external validation is enabled.
2308
+ * To activate, email apisupport@safaricom.co.ke.
2309
+ * - If M-PESA cannot reach your Validation URL within ~8 seconds, it defaults
2310
+ * to the ResponseType action set during registration.
2311
+ *
2312
+ * @param baseUrl - Daraja environment base URL
2313
+ * @param accessToken - Valid OAuth bearer token from Authorization API
2314
+ * @param request - Registration parameters
2315
+ * @returns - Daraja registration response (ResponseCode "0" = success)
2316
+ */
2317
+ async function registerC2BUrls(baseUrl, accessToken, request, http) {
2318
+ const validated = parseWithSchema(C2BRegisterUrlRequestSchema, request, "C2B Register URL request");
2319
+ validateCallbackUrl(validated.confirmationUrl, "confirmationUrl");
2320
+ validateCallbackUrl(validated.validationUrl, "validationUrl");
2321
+ const version = validated.apiVersion ?? "v2";
2322
+ const payload = {
2323
+ ShortCode: String(validated.shortCode),
2324
+ ResponseType: validated.responseType,
2325
+ ConfirmationURL: validated.confirmationUrl,
2326
+ ValidationURL: validated.validationUrl
2327
+ };
2328
+ const { data } = await httpRequest(`${baseUrl}/mpesa/c2b/${version}/registerurl`, withDarajaHttp({
2329
+ method: "POST",
2330
+ headers: { Authorization: `Bearer ${accessToken}` },
2331
+ body: payload
2332
+ }, http));
2333
+ return parseWithSchema(C2BRegisterUrlResponseSchema, data, "C2B Register URL response");
2334
+ }
2335
+
2336
+ //#endregion
2337
+ //#region src/mpesa/c2b/simulate.ts
2338
+ /**
2339
+ * src/mpesa/c2b/simulate.ts
2340
+ *
2341
+ * C2B Simulate implementation (Sandbox ONLY).
2342
+ * Strictly aligned with Safaricom Daraja C2B API documentation.
2343
+ *
2344
+ * Per docs: "NB: Simulation is not supported on production."
2345
+ *
2346
+ * Endpoint (v2, sandbox only):
2347
+ * POST https://sandbox.safaricom.co.ke/mpesa/c2b/v2/simulate
2348
+ */
2349
+ /**
2350
+ * Simulates a C2B customer payment. SANDBOX ONLY.
2351
+ *
2352
+ * Daraja payload shape:
2353
+ * {
2354
+ * "ShortCode": 600984, ← numeric
2355
+ * "CommandID": "CustomerPayBillOnline",
2356
+ * "Amount": 1, ← numeric, whole number ≥ 1
2357
+ * "Msisdn": 254708374149, ← numeric
2358
+ * "BillRefNumber": "AccountRef" ← Paybill only; OMIT for BuyGoods
2359
+ * }
2360
+ *
2361
+ * CRITICAL — BillRefNumber handling (per docs):
2362
+ * "Account reference for Customer paybills and null for customer buy goods"
2363
+ * We omit the key entirely for BuyGoods (not null, not "") because Daraja
2364
+ * validates field presence and rejects even null/empty values for Buy Goods.
2365
+ *
2366
+ * @param baseUrl - Must be the sandbox base URL
2367
+ * @param accessToken - Valid OAuth bearer token from Authorization API
2368
+ * @param request - Simulation parameters
2369
+ * @returns - Daraja simulate response (ResponseCode "0" = accepted)
2370
+ */
2371
+ async function simulateC2B(baseUrl, accessToken, request, http) {
2372
+ const validated = parseWithSchema(C2BSimulateRequestSchema, request, "C2B Simulate request");
2373
+ if (!baseUrl.includes("sandbox")) throw createError({
2374
+ code: "VALIDATION_ERROR",
2375
+ message: "C2B simulate is only available in the Sandbox environment (per Daraja docs). In production, customers initiate payments directly via M-PESA App, USSD, or SIM Toolkit."
2376
+ });
2377
+ const amount = Math.round(validated.amount);
2378
+ const isBuyGoods = validated.commandId === "CustomerBuyGoodsOnline";
2379
+ const version = validated.apiVersion ?? "v2";
2380
+ const payload = {
2381
+ ShortCode: Number(validated.shortCode),
2382
+ CommandID: validated.commandId,
2383
+ Amount: amount,
2384
+ Msisdn: Number(validated.msisdn)
2385
+ };
2386
+ if (!isBuyGoods) payload["BillRefNumber"] = validated.billRefNumber.trim();
2387
+ const { data } = await httpRequest(`${baseUrl}/mpesa/c2b/${version}/simulate`, withDarajaHttp({
2388
+ method: "POST",
2389
+ headers: { Authorization: `Bearer ${accessToken}` },
2390
+ body: payload
2391
+ }, http));
2392
+ return parseWithSchema(C2BSimulateResponseSchema, data, "C2B Simulate response");
2393
+ }
2394
+
2395
+ //#endregion
2396
+ //#region src/mpesa/c2b/types.ts
2397
+ /**
2398
+ * Error codes returned by the C2B Register URL API.
2399
+ *
2400
+ * Note: Multiple error conditions share the code "500.003.1001" —
2401
+ * the `errorMessage` field in the response distinguishes them.
2402
+ *
2403
+ * Per Daraja documentation:
2404
+ * - 500.003.1001 → Internal Server Error / URLs already registered / Duplicate notification
2405
+ * - 400.003.01 → Invalid Access Token
2406
+ * - 400.003.02 → Bad Request (missing or malformed payload)
2407
+ * - 500.003.03 → Quota Violation (too many requests)
2408
+ * - 500.003.02 → Spike Arrest Violation (endpoint errors causing a traffic spike)
2409
+ * - 404.003.01 → Resource Not Found (wrong endpoint URL)
2410
+ * - 404.001.04 → Invalid Authenticator Header (used GET instead of POST)
2411
+ * - 400.002.05 → Invalid Request Payload (typo or schema mismatch)
2412
+ */
2413
+ const C2B_REGISTER_URL_ERROR_CODES = {
2414
+ INTERNAL_SERVER_ERROR: "500.003.1001",
2415
+ INVALID_ACCESS_TOKEN: "400.003.01",
2416
+ BAD_REQUEST: "400.003.02",
2417
+ QUOTA_VIOLATION: "500.003.03",
2418
+ SPIKE_ARREST: "500.003.02",
2419
+ RESOURCE_NOT_FOUND: "404.003.01",
2420
+ INVALID_AUTH_HEADER: "404.001.04",
2421
+ INVALID_REQUEST_PAYLOAD: "400.002.05"
2422
+ };
2423
+ /**
2424
+ * Result codes your ValidationURL must return to M-PESA.
2425
+ *
2426
+ * Per Daraja documentation:
2427
+ * ResultCode "0" → Accept the payment
2428
+ * ResultCode "C2B00011" → Reject: Invalid MSISDN
2429
+ * ResultCode "C2B00012" → Reject: Invalid Account Number
2430
+ * ResultCode "C2B00013" → Reject: Invalid Amount
2431
+ * ResultCode "C2B00014" → Reject: Invalid KYC Details
2432
+ * ResultCode "C2B00015" → Reject: Invalid Shortcode
2433
+ * ResultCode "C2B00016" → Reject: Other Error
2434
+ */
2435
+ const C2B_VALIDATION_RESULT_CODES = {
2436
+ ACCEPT: "0",
2437
+ INVALID_MSISDN: "C2B00011",
2438
+ INVALID_ACCOUNT_NUMBER: "C2B00012",
2439
+ INVALID_AMOUNT: "C2B00013",
2440
+ INVALID_KYC_DETAILS: "C2B00014",
2441
+ INVALID_SHORTCODE: "C2B00015",
2442
+ OTHER_ERROR: "C2B00016"
2443
+ };
2444
+
2445
+ //#endregion
2446
+ //#region src/mpesa/c2b/webhooks.ts
2447
+ /**
2448
+ * Returns true if `body` looks like a valid C2B callback payload.
2449
+ * Works for both Validation and Confirmation payloads.
2450
+ *
2451
+ * Checks the minimum required fields from the Daraja docs payload sample:
2452
+ * - TransID
2453
+ * - BusinessShortCode
2454
+ * - TransAmount
2455
+ */
2456
+ function isC2BPayload(body) {
2457
+ if (!body || typeof body !== "object") return false;
2458
+ const b = body;
2459
+ return typeof b["TransID"] === "string" && typeof b["BusinessShortCode"] === "string" && typeof b["TransAmount"] === "string";
2460
+ }
2461
+ /**
2462
+ * Builds an "accept" validation response.
2463
+ *
2464
+ * Per Daraja docs (to accept the payment):
2465
+ * { "ResultCode": "0", "ResultDesc": "Accepted" }
2466
+ *
2467
+ * @param thirdPartyTransID - Optional: echo back the ThirdPartyTransID received
2468
+ * in the validation request. M-PESA includes it in the confirmation callback.
2469
+ */
2470
+ function acceptC2BValidation(thirdPartyTransID) {
2471
+ return {
2472
+ ResultCode: "0",
2473
+ ResultDesc: "Accepted",
2474
+ ...thirdPartyTransID ? { ThirdPartyTransID: thirdPartyTransID } : {}
2475
+ };
2476
+ }
2477
+ /**
2478
+ * Builds a "reject" validation response.
2479
+ *
2480
+ * Per Daraja docs (to reject the payment):
2481
+ * { "ResultCode": "C2B00011", "ResultDesc": "Rejected" }
2482
+ *
2483
+ * Documented result codes and their meanings:
2484
+ * C2B00011 — Invalid MSISDN
2485
+ * C2B00012 — Invalid Account Number
2486
+ * C2B00013 — Invalid Amount
2487
+ * C2B00014 — Invalid KYC Details
2488
+ * C2B00015 — Invalid Shortcode
2489
+ * C2B00016 — Other Error (default)
2490
+ *
2491
+ * @param resultCode - Must NOT be "0". Defaults to "C2B00016" (Other Error).
2492
+ */
2493
+ function rejectC2BValidation(resultCode = "C2B00016") {
2494
+ return {
2495
+ ResultCode: resultCode,
2496
+ ResultDesc: "Rejected"
2497
+ };
2498
+ }
2499
+ /**
2500
+ * Builds the confirmation acknowledgement your ConfirmationURL must return.
2501
+ * Always respond with ResultCode 0 to acknowledge receipt.
2502
+ *
2503
+ * Per docs process flow: M-PESA expects an acknowledgement response.
2504
+ * Failure to acknowledge may cause M-PESA to retry the confirmation.
2505
+ */
2506
+ function acknowledgeC2BConfirmation() {
2507
+ return {
2508
+ ResultCode: 0,
2509
+ ResultDesc: "Success"
2510
+ };
2511
+ }
2512
+ /**
2513
+ * Extracts the transaction amount as a number from a C2B callback payload.
2514
+ * TransAmount is a string in the payload (e.g. "10" per docs sample).
2515
+ */
2516
+ function getC2BAmount(payload) {
2517
+ return Number(payload.TransAmount);
2518
+ }
2519
+ /**
2520
+ * Extracts the M-PESA transaction ID (receipt) from a C2B callback payload.
2521
+ * Example from docs: "RKTQDM7W6S"
2522
+ */
2523
+ function getC2BTransactionId(payload) {
2524
+ return payload.TransID;
2525
+ }
2526
+ /**
2527
+ * Extracts the account reference (BillRefNumber) from a C2B callback payload.
2528
+ * Example from docs: "invoice008"
2529
+ * Empty string for Buy Goods payments (no account reference).
2530
+ */
2531
+ function getC2BAccountRef(payload) {
2532
+ return payload.BillRefNumber;
2533
+ }
2534
+ /**
2535
+ * Returns the customer's full name from a C2B callback payload.
2536
+ * Joins FirstName, MiddleName, LastName; skips empty parts.
2537
+ * Per docs: customer names "can be empty".
2538
+ */
2539
+ function getC2BCustomerName(payload) {
2540
+ return [
2541
+ payload.FirstName,
2542
+ payload.MiddleName,
2543
+ payload.LastName
2544
+ ].filter(Boolean).join(" ").trim();
2545
+ }
2546
+ /**
2547
+ * Returns true if the payload is a Paybill payment.
2548
+ *
2549
+ * Per Daraja docs, the callback TransactionType for Paybill is "Pay Bill"
2550
+ * (NOT "CustomerPayBillOnline" which is the request CommandID).
2551
+ */
2552
+ function isPaybillPayment(payload) {
2553
+ return payload.TransactionType === "Pay Bill";
2554
+ }
2555
+ /**
2556
+ * Returns true if the payload is a Buy Goods (Till) payment.
2557
+ *
2558
+ * Per Daraja docs, the callback TransactionType for Buy Goods is "Buy Goods"
2559
+ * (NOT "CustomerBuyGoodsOnline" which is the request CommandID).
2560
+ */
2561
+ function isBuyGoodsPayment(payload) {
2562
+ return payload.TransactionType === "Buy Goods";
2563
+ }
2564
+
2565
+ //#endregion
2566
+ //#region src/mpesa/dynamic-qr/types.ts
2567
+ /** All valid QR transaction codes as a readonly tuple — useful for validation. */
2568
+ const QR_TRANSACTION_CODES = [
2569
+ "BG",
2570
+ "WA",
2571
+ "PB",
2572
+ "SM",
2573
+ "SB"
2574
+ ];
2575
+
2576
+ //#endregion
2577
+ //#region src/mpesa/dynamic-qr/validators.ts
2578
+ /** Minimum amount Daraja accepts (whole KES). */
2579
+ const MIN_AMOUNT = 1;
2580
+ /** Maximum allowable QR size in pixels (sanity guard — Daraja has no documented limit). */
2581
+ const MAX_QR_SIZE = 1e3;
2582
+ /** Minimum QR size in pixels. */
2583
+ const MIN_QR_SIZE = 1;
2584
+ /** Default QR size when `size` is omitted from the request. */
2585
+ const DEFAULT_QR_SIZE = 300;
2586
+ /**
2587
+ * Validates `merchantName`.
2588
+ * @returns Error message string, or `null` if valid.
2589
+ */
2590
+ function validateMerchantName(value) {
2591
+ if (typeof value !== "string" || value.trim().length === 0) return "merchantName is required and must be a non-empty string";
2592
+ return null;
2593
+ }
2594
+ /**
2595
+ * Validates `refNo`.
2596
+ * @returns Error message string, or `null` if valid.
2597
+ */
2598
+ function validateRefNo(value) {
2599
+ if (typeof value !== "string" || value.trim().length === 0) return "refNo (transaction reference) is required and must be a non-empty string";
2600
+ return null;
2601
+ }
2602
+ /**
2603
+ * Validates `amount`.
2604
+ * @returns Error message string, or `null` if valid.
2605
+ */
2606
+ function validateAmount(value) {
2607
+ if (typeof value !== "number" || !Number.isFinite(value)) return "amount must be a finite number";
2608
+ if (Math.round(value) < 1) return `amount must be at least ${1} KES (got ${value})`;
2609
+ return null;
2610
+ }
2611
+ /**
2612
+ * Validates `trxCode`.
2613
+ * @returns Error message string, or `null` if valid.
2614
+ */
2615
+ function validateTrxCode(value) {
2616
+ if (!QR_TRANSACTION_CODES.includes(value)) return `trxCode must be one of: ${QR_TRANSACTION_CODES.join(", ")} (BG=Buy Goods, WA=Withdraw Cash, PB=Paybill, SM=Send Money, SB=Send to Business)`;
2617
+ return null;
2618
+ }
2619
+ /**
2620
+ * Validates `cpi` (Credit Party Identifier).
2621
+ * @returns Error message string, or `null` if valid.
2622
+ */
2623
+ function validateCpi(value) {
2624
+ if (typeof value !== "string" || value.trim().length === 0) return "cpi (Credit Party Identifier) is required and must be a non-empty string";
2625
+ return null;
2626
+ }
2627
+ /**
2628
+ * Validates `size` (optional).
2629
+ * @returns Error message string, or `null` if valid.
2630
+ */
2631
+ function validateSize(value) {
2632
+ if (value === void 0 || value === null) return null;
2633
+ if (typeof value !== "number" || !Number.isFinite(value)) return "size must be a finite number when provided";
2634
+ if (!Number.isInteger(value) || value < 1) return `size must be a positive integer (minimum ${1})`;
2635
+ if (value > 1e3) return `size must not exceed ${MAX_QR_SIZE} pixels (got ${value})`;
2636
+ return null;
2637
+ }
2638
+ /**
2639
+ * Validates a full {@link DynamicQRRequest} payload.
2640
+ *
2641
+ * Runs all field validators and collects every error so callers receive a
2642
+ * complete picture rather than failing on the first error only.
2643
+ *
2644
+ * Accepts `null`, `undefined`, or any non-object — returns `{ valid: false }`
2645
+ * with an appropriate error rather than throwing.
2646
+ *
2647
+ * @example
2648
+ * const result = validateDynamicQRRequest(payload)
2649
+ * if (!result.valid) {
2650
+ * console.error(result.errors)
2651
+ * }
2652
+ */
2653
+ function validateDynamicQRRequest(payload) {
2654
+ if (payload === null || payload === void 0 || typeof payload !== "object") return {
2655
+ valid: false,
2656
+ errors: { payload: "request payload must be a non-null object" }
2657
+ };
2658
+ const p = payload;
2659
+ const errors = {};
2660
+ const merchantNameError = validateMerchantName(p.merchantName);
2661
+ if (merchantNameError) errors["merchantName"] = merchantNameError;
2662
+ const refNoError = validateRefNo(p.refNo);
2663
+ if (refNoError) errors["refNo"] = refNoError;
2664
+ const amountError = validateAmount(p.amount);
2665
+ if (amountError) errors["amount"] = amountError;
2666
+ const trxCodeError = validateTrxCode(p.trxCode);
2667
+ if (trxCodeError) errors["trxCode"] = trxCodeError;
2668
+ const cpiError = validateCpi(p.cpi);
2669
+ if (cpiError) errors["cpi"] = cpiError;
2670
+ const sizeError = validateSize(p.size);
2671
+ if (sizeError) errors["size"] = sizeError;
2672
+ if (Object.keys(errors).length > 0) return {
2673
+ valid: false,
2674
+ errors
2675
+ };
2676
+ return { valid: true };
2677
+ }
2678
+
2679
+ //#endregion
2680
+ //#region src/mpesa/dynamic-qr/generate.ts
2681
+ /**
2682
+ * src/mpesa/dynamic-qr/generate.ts
2683
+ *
2684
+ * Core logic for the Safaricom Daraja Dynamic QR Code API.
2685
+ *
2686
+ * API: POST /mpesa/qrcode/v1/generate
2687
+ *
2688
+ * Error codes from Daraja docs:
2689
+ * 404.001.04 — Invalid Authentication Header
2690
+ * 400.002.05 — Invalid Request Payload
2691
+ * 400.003.01 — Invalid Access Token
2692
+ */
2693
+ /**
2694
+ * Maps Daraja-specific error codes to structured PesafyErrors with
2695
+ * actionable developer guidance.
2696
+ *
2697
+ * @internal
2698
+ */
2699
+ function mapDarajaError(errorCode, errorMessage) {
2700
+ switch (errorCode) {
2701
+ case "404.001.04": return new PesafyError({
2702
+ code: "AUTH_FAILED",
2703
+ 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: "${errorMessage}"`,
2704
+ statusCode: 404
2705
+ });
2706
+ case "400.003.01": return new PesafyError({
2707
+ code: "AUTH_FAILED",
2708
+ 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: "${errorMessage}"`,
2709
+ statusCode: 401
2710
+ });
2711
+ case "400.002.05": return new PesafyError({
2712
+ code: "VALIDATION_ERROR",
2713
+ 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: "${errorMessage}"`,
2714
+ statusCode: 400
2715
+ });
2716
+ default: return new PesafyError({
2717
+ code: "REQUEST_FAILED",
2718
+ message: `Dynamic QR request failed (${errorCode}): ${errorMessage}`,
2719
+ statusCode: 400
2720
+ });
2721
+ }
2722
+ }
2723
+ /**
2724
+ * Returns `true` when the raw response body looks like a Daraja error object.
2725
+ * @internal
2726
+ */
2727
+ function isDarajaError(body) {
2728
+ return typeof body === "object" && body !== null && "errorCode" in body && typeof body.errorCode === "string";
2729
+ }
2730
+ /**
2731
+ * Returns `true` when the response has the required QR success fields.
2732
+ * @internal
2733
+ */
2734
+ function isDarajaSuccess(body) {
2735
+ return typeof body === "object" && body !== null && "ResponseCode" in body && "QRCode" in body && typeof body.QRCode === "string" && body.QRCode.length > 0;
2736
+ }
2737
+ /**
2738
+ * Generates a Dynamic M-PESA QR Code via the Safaricom Daraja API.
2739
+ *
2740
+ * The QR code can be rendered directly in a browser as a base64 PNG:
2741
+ * ```html
2742
+ * <img src="data:image/png;base64,{response.QRCode}" />
2743
+ * ```
2744
+ * Or written to disk:
2745
+ * ```ts
2746
+ * import { writeFileSync } from 'node:fs'
2747
+ * writeFileSync('qr.png', Buffer.from(response.QRCode, 'base64'))
2748
+ * ```
2749
+ *
2750
+ * @param baseUrl - Daraja base URL (`https://sandbox.safaricom.co.ke` or
2751
+ * `https://api.safaricom.co.ke`)
2752
+ * @param accessToken - Valid Daraja OAuth2 Bearer token
2753
+ * @param request - QR generation parameters (see {@link DynamicQRRequest})
2754
+ * @returns - Daraja response including the base64-encoded QR image
2755
+ *
2756
+ * @throws {PesafyError} `VALIDATION_ERROR` — payload failed pre-flight checks
2757
+ * @throws {PesafyError} `AUTH_FAILED` — bad/expired token or wrong headers
2758
+ * @throws {PesafyError} `REQUEST_FAILED` — unexpected Daraja error
2759
+ *
2760
+ * @example
2761
+ * ```ts
2762
+ * const response = await generateDynamicQR(
2763
+ * 'https://sandbox.safaricom.co.ke',
2764
+ * accessToken,
2765
+ * {
2766
+ * merchantName: 'Test Supermarket',
2767
+ * refNo: 'INV-001',
2768
+ * amount: 500,
2769
+ * trxCode: 'BG',
2770
+ * cpi: '373132',
2771
+ * size: 300,
2772
+ * },
2773
+ * )
2774
+ * console.log(response.QRCode) // base64 PNG
2775
+ * ```
2776
+ */
2777
+ async function generateDynamicQR(baseUrl, accessToken, request, http) {
2778
+ const validated = parseWithSchema(DynamicQRRequestSchema, request, "Dynamic QR request");
2779
+ if (!accessToken || typeof accessToken !== "string" || accessToken.trim().length === 0) throw new PesafyError({
2780
+ code: "AUTH_FAILED",
2781
+ message: "accessToken is required. Obtain one via the Daraja Authorization API (GET /oauth/v1/generate?grant_type=client_credentials)."
2782
+ });
2783
+ const size = validated.size ?? 300;
2784
+ const amount = Math.round(validated.amount);
2785
+ const payload = {
2786
+ MerchantName: validated.merchantName.trim(),
2787
+ RefNo: validated.refNo.trim(),
2788
+ Amount: amount,
2789
+ TrxCode: validated.trxCode,
2790
+ CPI: validated.cpi.trim(),
2791
+ Size: String(size)
2792
+ };
2793
+ const { data } = await httpRequest(`${baseUrl}/mpesa/qrcode/v1/generate`, withDarajaHttp({
2794
+ method: "POST",
2795
+ headers: { Authorization: `Bearer ${accessToken}` },
2796
+ body: payload
2797
+ }, http));
2798
+ if (isDarajaError(data)) throw mapDarajaError(data.errorCode, data.errorMessage);
2799
+ if (!isDarajaSuccess(data)) throw new PesafyError({
2800
+ code: "REQUEST_FAILED",
2801
+ message: `Daraja returned an unexpected response structure for the Dynamic QR request. The response was missing required fields (ResponseCode, QRCode). Raw response: ${JSON.stringify(data).slice(0, 300)}`
2802
+ });
2803
+ return parseWithSchema(DynamicQRResponseSchema, data, "Dynamic QR response");
2804
+ }
2805
+
2806
+ //#endregion
2807
+ //#region src/mpesa/reversal/types.ts
2808
+ /**
2809
+ * src/mpesa/reversal/types.ts
2810
+ *
2811
+ * Transaction Reversal types, constants, and helpers.
2812
+ *
2813
+ * API: POST /mpesa/reversal/v1/request
2814
+ *
2815
+ * Reverses a completed M-PESA C2B transaction. The API is ASYNCHRONOUS —
2816
+ * the synchronous response is only an acknowledgement. The actual reversal
2817
+ * result is POSTed to your ResultURL after processing.
2818
+ *
2819
+ * Required org portal role: "Org Reversals Initiator"
2820
+ *
2821
+ * Per Daraja docs:
2822
+ * RecieverIdentifierType MUST always be "11" for reversals.
2823
+ * CommandID MUST always be "TransactionReversal".
2824
+ *
2825
+ * Ref: Reversals — Safaricom Daraja Developer Portal
2826
+ */
2827
+ /**
2828
+ * CommandID for the Reversals API.
2829
+ * Only "TransactionReversal" is allowed per Daraja docs.
2830
+ */
2831
+ const REVERSAL_COMMAND_ID = "TransactionReversal";
2832
+ /**
2833
+ * RecieverIdentifierType for the Reversals API.
2834
+ * Per Daraja docs: "Type of Organization (should be '11')".
2835
+ * This value is fixed for all reversal requests.
2836
+ */
2837
+ const REVERSAL_RECEIVER_IDENTIFIER_TYPE = "11";
2838
+ /**
2839
+ * Result codes returned in the async callback POSTed to your ResultURL.
2840
+ *
2841
+ * Per Daraja Reversals documentation:
2842
+ *
2843
+ * | ResultCode | Meaning |
2844
+ * |------------|----------------------------------------------------|
2845
+ * | 0 | Success — transaction reversed |
2846
+ * | 1 | Insufficient balance |
2847
+ * | 11 | DebitParty in invalid state (shortcode not active) |
2848
+ * | 21 | Initiator not allowed to initiate reversals |
2849
+ * | 2001 | Initiator information invalid (bad credentials) |
2850
+ * | 2006 | Declined due to account rule (shortcode inactive) |
2851
+ * | 2028 | Not permitted (shortcode lacks reversal permission)|
2852
+ * | 8006 | Security credential locked |
2853
+ * | R000001 | Transaction already reversed |
2854
+ * | R000002 | OriginalTransactionID is invalid / does not exist |
2855
+ */
2856
+ const REVERSAL_RESULT_CODES = {
2857
+ SUCCESS: 0,
2858
+ INSUFFICIENT_BALANCE: 1,
2859
+ DEBIT_PARTY_INVALID_STATE: 11,
2860
+ INITIATOR_NOT_ALLOWED: 21,
2861
+ INITIATOR_INFORMATION_INVALID: 2001,
2862
+ DECLINED_ACCOUNT_RULE: 2006,
2863
+ NOT_PERMITTED: 2028,
2864
+ SECURITY_CREDENTIAL_LOCKED: 8006,
2865
+ ALREADY_REVERSED: "R000001",
2866
+ INVALID_TRANSACTION_ID: "R000002"
2867
+ };
2868
+ /**
2869
+ * API-level error codes returned in the synchronous HTTP error response.
2870
+ *
2871
+ * Per Daraja Reversals error response documentation:
2872
+ *
2873
+ * | errorCode | Meaning | HTTP |
2874
+ * |-----------------|-------------------------------------------------|------|
2875
+ * | 404.001.03 | Invalid Access Token | 404 |
2876
+ * | 400.002.02 | Bad Request — Invalid payload field | 400 |
2877
+ * | 404.001.01 | Resource not found (wrong endpoint) | 404 |
2878
+ * | 500.001.1001 | Internal Server Error | 500 |
2879
+ * | 500.003.02 | Spike Arrest Violation (TPS limit exceeded) | 500 |
2880
+ * | 500.003.03 | Quota Violation (API request limit exceeded) | 500 |
2881
+ */
2882
+ const REVERSAL_ERROR_CODES = {
2883
+ INVALID_ACCESS_TOKEN: "404.001.03",
2884
+ BAD_REQUEST: "400.002.02",
2885
+ RESOURCE_NOT_FOUND: "404.001.01",
2886
+ INTERNAL_SERVER_ERROR: "500.001.1001",
2887
+ SPIKE_ARREST: "500.003.02",
2888
+ QUOTA_VIOLATION: "500.003.03"
2889
+ };
2890
+ /**
2891
+ * Returns true when the payload matches the shape of a ReversalResult.
2892
+ * Works for both success and failure callbacks.
2893
+ */
2894
+ function isReversalResult(body) {
2895
+ if (!body || typeof body !== "object") return false;
2896
+ const b = body;
2897
+ if (!b["Result"] || typeof b["Result"] !== "object") return false;
2898
+ const r = b["Result"];
2899
+ return typeof r["ResultCode"] !== "undefined" && typeof r["ResultDesc"] === "string" && typeof r["ConversationID"] === "string";
2900
+ }
2901
+ /**
2902
+ * Returns true when the reversal result indicates a successful reversal.
2903
+ * A successful reversal has ResultCode === 0 (number).
2904
+ */
2905
+ function isReversalSuccess(result) {
2906
+ return result.Result.ResultCode === REVERSAL_RESULT_CODES.SUCCESS;
2907
+ }
2908
+ /**
2909
+ * Returns true when the reversal result indicates a failure.
2910
+ */
2911
+ function isReversalFailure(result) {
2912
+ return !isReversalSuccess(result);
2913
+ }
2914
+ /**
2915
+ * Returns true when the specified result code is a known documented code.
2916
+ */
2917
+ function isKnownReversalResultCode(code) {
2918
+ return Object.values(REVERSAL_RESULT_CODES).includes(code);
2919
+ }
2920
+ /**
2921
+ * Extracts the M-PESA receipt number for the reversal transaction.
2922
+ * Returns null if the result does not contain a TransactionID.
2923
+ */
2924
+ function getReversalTransactionId(result) {
2925
+ return result.Result.TransactionID ?? null;
2926
+ }
2927
+ /**
2928
+ * Extracts the ConversationID from the reversal result.
2929
+ */
2930
+ function getReversalConversationId(result) {
2931
+ return result.Result.ConversationID;
2932
+ }
2933
+ /**
2934
+ * Extracts the OriginatorConversationID from the reversal result.
2935
+ */
2936
+ function getReversalOriginatorConversationId(result) {
2937
+ return result.Result.OriginatorConversationID;
2938
+ }
2939
+ /**
2940
+ * Extracts the ResultCode from the reversal result.
2941
+ * Returns 0 for success, or a string/number error code for failure.
2942
+ */
2943
+ function getReversalResultCode(result) {
2944
+ return result.Result.ResultCode;
2945
+ }
2946
+ /**
2947
+ * Extracts the ResultDesc from the reversal result.
2948
+ */
2949
+ function getReversalResultDesc(result) {
2950
+ return result.Result.ResultDesc;
2951
+ }
2952
+ /**
2953
+ * Extracts a named parameter value from the ResultParameters of a successful
2954
+ * reversal callback. Returns undefined if absent or if the reversal failed.
2955
+ *
2956
+ * @example
2957
+ * const amount = getReversalResultParam(result, 'Amount')
2958
+ * const origTxId = getReversalResultParam(result, 'OriginalTransactionID')
2959
+ */
2960
+ function getReversalResultParam(result, key) {
2961
+ const params = result.Result.ResultParameters?.ResultParameter;
2962
+ if (!params) return void 0;
2963
+ return params.find((p) => p.Key === key)?.Value;
2964
+ }
2965
+ /**
2966
+ * Extracts the reversed transaction amount from a successful reversal callback.
2967
+ */
2968
+ function getReversalAmount(result) {
2969
+ const val = getReversalResultParam(result, "Amount");
2970
+ return val !== void 0 ? Number(val) : void 0;
2971
+ }
2972
+ /**
2973
+ * Extracts the original TransactionID that was reversed.
2974
+ * This is the M-PESA receipt number of the original C2B transaction.
2975
+ */
2976
+ function getReversalOriginalTransactionId(result) {
2977
+ const val = getReversalResultParam(result, "OriginalTransactionID");
2978
+ return val !== void 0 ? String(val) : void 0;
2979
+ }
2980
+ /**
2981
+ * Extracts the CreditPartyPublicName from a successful reversal callback.
2982
+ * Format: "254705912645 - NICHOLAS JOHN SONGOK"
2983
+ */
2984
+ function getReversalCreditPartyPublicName(result) {
2985
+ const val = getReversalResultParam(result, "CreditPartyPublicName");
2986
+ return val !== void 0 ? String(val) : void 0;
2987
+ }
2988
+ /**
2989
+ * Extracts the DebitPartyPublicName from a successful reversal callback.
2990
+ * Format: "600992 - Safaricom Daraja 992"
2991
+ */
2992
+ function getReversalDebitPartyPublicName(result) {
2993
+ const val = getReversalResultParam(result, "DebitPartyPublicName");
2994
+ return val !== void 0 ? String(val) : void 0;
2995
+ }
2996
+ /**
2997
+ * Extracts the DebitAccountBalance from a successful reversal callback.
2998
+ * Format: "Utility Account|KES|7722179.62|7722179.62|0.00|0.00"
2999
+ */
3000
+ function getReversalDebitAccountBalance(result) {
3001
+ const val = getReversalResultParam(result, "DebitAccountBalance");
3002
+ return val !== void 0 ? String(val) : void 0;
3003
+ }
3004
+ /**
3005
+ * Extracts the reversal completion timestamp.
3006
+ * Format: YYYYMMDDHHmmss (e.g. 20211114132711)
3007
+ */
3008
+ function getReversalCompletedTime(result) {
3009
+ const val = getReversalResultParam(result, "TransCompletedTime");
3010
+ return val !== void 0 ? Number(val) : void 0;
3011
+ }
3012
+ /**
3013
+ * Extracts the Charge from a successful reversal callback.
3014
+ * Per docs: usually 0.00 for reversals.
3015
+ */
3016
+ function getReversalCharge(result) {
3017
+ const val = getReversalResultParam(result, "Charge");
3018
+ return val !== void 0 ? Number(val) : void 0;
3019
+ }
3020
+
3021
+ //#endregion
3022
+ //#region src/mpesa/reversal/request.ts
3023
+ /**
3024
+ * src/mpesa/reversal/request.ts
3025
+ *
3026
+ * Transaction Reversal — reverses a completed M-PESA C2B transaction.
3027
+ *
3028
+ * API: POST /mpesa/reversal/v1/request
3029
+ *
3030
+ * ASYNCHRONOUS: The synchronous response is acknowledgement only.
3031
+ * The actual reversal result is POSTed to your ResultURL after processing.
3032
+ *
3033
+ * Per Daraja docs:
3034
+ * - CommandID is always "TransactionReversal"
3035
+ * - RecieverIdentifierType is always "11" (Organisation ShortCode for reversals)
3036
+ * - Amount is sent as a string per the Daraja sample payload
3037
+ * - Remarks must be 2–100 characters
3038
+ * - Cannot be used for B2C reversals (those are done on the M-PESA portal)
3039
+ *
3040
+ * Required org portal role: "Org Reversals Initiator"
3041
+ */
3042
+ async function requestReversal(baseUrl, accessToken, securityCredential, initiatorName, request, http) {
3043
+ const validated = parseWithSchema(ReversalRequestSchema, request, "Reversal request");
3044
+ const amount = Math.round(validated.amount);
3045
+ const remarks = validated.remarks ?? "Transaction Reversal";
3046
+ const payload = {
3047
+ Initiator: initiatorName,
3048
+ SecurityCredential: securityCredential,
3049
+ CommandID: REVERSAL_COMMAND_ID,
3050
+ TransactionID: validated.transactionId,
3051
+ Amount: String(amount),
3052
+ ReceiverParty: String(validated.receiverParty),
3053
+ RecieverIdentifierType: "11",
3054
+ ResultURL: validated.resultUrl,
3055
+ QueueTimeOutURL: validated.queueTimeOutUrl,
3056
+ Remarks: remarks
3057
+ };
3058
+ if (validated.occasion !== void 0 && validated.occasion !== null) payload["Occasion"] = validated.occasion;
3059
+ const { data } = await httpRequest(`${baseUrl}/mpesa/reversal/v1/request`, withDarajaHttp({
3060
+ method: "POST",
3061
+ headers: { Authorization: `Bearer ${accessToken}` },
3062
+ body: payload
3063
+ }, http));
3064
+ return parseWithSchema(ReversalResponseSchema, data, "Reversal response");
3065
+ }
3066
+
3067
+ //#endregion
3068
+ //#region src/schemas/stk-push.ts
3069
+ const TransactionTypeSchema = z.enum(["CustomerPayBillOnline", "CustomerBuyGoodsOnline"]);
3070
+ const StkPushRequestSchema = z.object({
3071
+ amount: z.number().finite({ message: "amount must be a finite number (not NaN or Infinity)" }),
3072
+ phoneNumber: NonEmptyStringSchema,
3073
+ shortCode: NonEmptyStringSchema,
3074
+ passKey: NonEmptyStringSchema,
3075
+ callbackUrl: UrlSchema,
3076
+ accountReference: NonEmptyStringSchema,
3077
+ transactionDesc: NonEmptyStringSchema,
3078
+ transactionType: TransactionTypeSchema.optional(),
3079
+ partyB: z.string().optional()
3080
+ });
3081
+ const StkPushResponseSchema = z.object({
3082
+ MerchantRequestID: z.string(),
3083
+ CheckoutRequestID: z.string(),
3084
+ ResponseCode: z.string(),
3085
+ ResponseDescription: z.string(),
3086
+ CustomerMessage: z.string().optional()
3087
+ }).passthrough();
3088
+ const StkQueryRequestSchema = z.object({
3089
+ checkoutRequestId: NonEmptyStringSchema,
3090
+ shortCode: NonEmptyStringSchema,
3091
+ passKey: NonEmptyStringSchema
3092
+ });
3093
+ const StkQueryResponseSchema = z.object({
3094
+ ResponseCode: z.string(),
3095
+ ResponseDescription: z.string(),
3096
+ MerchantRequestID: z.string().optional(),
3097
+ CheckoutRequestID: z.string().optional(),
3098
+ ResultCode: z.union([z.string(), z.number()]).optional(),
3099
+ ResultDesc: z.string().optional()
3100
+ }).passthrough();
3101
+ const StkPushWebhookSchema = z.object({ Body: z.object({ stkCallback: z.object({
3102
+ MerchantRequestID: z.string(),
3103
+ CheckoutRequestID: z.string(),
3104
+ ResultCode: z.number(),
3105
+ ResultDesc: z.string(),
3106
+ CallbackMetadata: z.object({ Item: z.array(z.object({
3107
+ Name: z.string(),
3108
+ Value: z.union([z.string(), z.number()])
3109
+ })) }).optional()
3110
+ }).passthrough() }) });
3111
+
3112
+ //#endregion
3113
+ //#region src/mpesa/stk-push/types.ts
3114
+ /**
3115
+ * M-PESA transaction limits as documented by Safaricom Daraja.
3116
+ *
3117
+ * | Limit | Value |
3118
+ * |------------------|-----------|
3119
+ * | Min per tx | KES 1 |
3120
+ * | Max per tx | KES 250 000|
3121
+ * | Max daily | KES 500 000|
3122
+ * | Max balance | KES 500 000|
3123
+ */
3124
+ const STK_PUSH_LIMITS = {
3125
+ MIN_AMOUNT: 1,
3126
+ MAX_AMOUNT: 25e4
3127
+ };
3128
+ /**
3129
+ * All documented STK Push / Query result codes.
3130
+ *
3131
+ * These codes appear in:
3132
+ * - STK Query response → `ResultCode` field
3133
+ * - STK Callback body → `Body.stkCallback.ResultCode`
3134
+ *
3135
+ * | Code | Meaning |
3136
+ * |------|--------------------------|
3137
+ * | 0 | Transaction successful |
3138
+ * | 1 | Insufficient balance |
3139
+ * | 1032 | Cancelled by user |
3140
+ * | 1037 | Phone unreachable |
3141
+ * | 2001 | Invalid PIN |
3142
+ */
3143
+ const STK_RESULT_CODES = {
3144
+ SUCCESS: 0,
3145
+ INSUFFICIENT_BALANCE: 1,
3146
+ CANCELLED_BY_USER: 1032,
3147
+ PHONE_UNREACHABLE: 1037,
3148
+ INVALID_PIN: 2001
3149
+ };
3150
+ /**
3151
+ * Returns true when `code` is one of the documented STK result codes.
3152
+ * Useful for narrowing unknown values from the Daraja response.
3153
+ */
3154
+ function isKnownStkResultCode(code) {
3155
+ return Object.values(STK_RESULT_CODES).includes(code);
3156
+ }
3157
+ /**
3158
+ * Narrows `StkCallbackInner` to the success shape.
3159
+ * A callback is successful when `ResultCode === 0`.
3160
+ *
3161
+ * @example
3162
+ * if (isStkCallbackSuccess(callback.Body.stkCallback)) {
3163
+ * const receipt = getCallbackValue(callback, 'MpesaReceiptNumber')
3164
+ * }
3165
+ */
3166
+ function isStkCallbackSuccess(cb) {
3167
+ return cb.ResultCode === STK_RESULT_CODES.SUCCESS;
3168
+ }
3169
+ /**
3170
+ * Extracts a named value from a successful callback's metadata.
3171
+ * Returns `undefined` if the key is absent or the transaction failed.
3172
+ *
3173
+ * @example
3174
+ * const receipt = getCallbackValue(callback, 'MpesaReceiptNumber') // "NLJ7RT61SV"
3175
+ * const amount = getCallbackValue(callback, 'Amount') // 1
3176
+ * const phone = getCallbackValue(callback, 'PhoneNumber') // 254708374149
3177
+ * const date = getCallbackValue(callback, 'TransactionDate') // 20191219102115
3178
+ */
3179
+ function getCallbackValue(callback, name) {
3180
+ const inner = callback.Body.stkCallback;
3181
+ if (!isStkCallbackSuccess(inner)) return void 0;
3182
+ return inner.CallbackMetadata.Item.find((i) => i.Name === name)?.Value;
3183
+ }
3184
+
3185
+ //#endregion
3186
+ //#region src/utils/phone/index.ts
3187
+ /** Normalises any common Kenyan phone format to 254XXXXXXXXX (12 digits) */
3188
+ function formatSafaricomPhone(phone) {
3189
+ const digits = phone.replace(/\D/g, "");
3190
+ let n;
3191
+ if (digits.startsWith("254") && digits.length === 12) n = digits;
3192
+ else if (digits.startsWith("0") && digits.length === 10) n = `254${digits.slice(1)}`;
3193
+ else if (digits.length === 9) n = `254${digits}`;
3194
+ else throw new PesafyError({
3195
+ code: "INVALID_PHONE",
3196
+ message: `Cannot parse "${phone}". Use 07XXXXXXXX, 2547XXXXXXXX, 2541XXXXXXXX (Airtel), or +2547XXXXXXXX.`
3197
+ });
3198
+ /* v8 ignore next 6 -- unreachable: all branches above assign exactly 12 digits */
3199
+ if (n.length !== 12) throw new PesafyError({
3200
+ code: "INVALID_PHONE",
3201
+ message: `"${phone}" normalised to "${n}" — expected 12 digits.`
3202
+ });
3203
+ return n;
3204
+ }
3205
+
3206
+ //#endregion
3207
+ //#region src/mpesa/stk-push/utils.ts
3208
+ /**
3209
+ * Generates the STK Push password.
3210
+ * Formula: Base64( Shortcode + Passkey + Timestamp )
3211
+ *
3212
+ * Uses btoa() — works in Node.js ≥18, Bun, browsers, and edge runtimes.
3213
+ */
3214
+ function getStkPushPassword(shortCode, passKey, timestamp) {
3215
+ return btoa(`${shortCode}${passKey}${timestamp}`);
3216
+ }
3217
+ /**
3218
+ * Returns a Daraja-compatible timestamp: YYYYMMDDHHmmss
3219
+ *
3220
+ * Call this ONCE per request and reuse the result.
3221
+ */
3222
+ function getTimestamp() {
3223
+ const now = /* @__PURE__ */ new Date();
3224
+ const pad = (n) => n.toString().padStart(2, "0");
3225
+ return [
3226
+ now.getFullYear(),
3227
+ pad(now.getMonth() + 1),
3228
+ pad(now.getDate()),
3229
+ pad(now.getHours()),
3230
+ pad(now.getMinutes()),
3231
+ pad(now.getSeconds())
3232
+ ].join("");
3233
+ }
3234
+
3235
+ //#endregion
3236
+ //#region src/mpesa/stk-push/stk-push.ts
3237
+ /**
3238
+ * src/mpesa/stk-push/stk-push.ts
3239
+ *
3240
+ * Initiates an STK Push (M-PESA Express) payment.
3241
+ *
3242
+ * Daraja endpoint: POST /mpesa/stkpush/v1/processrequest
3243
+ *
3244
+ * Transaction limits (Daraja docs):
3245
+ * Min per transaction: KES 1
3246
+ * Max per transaction: KES 250,000
3247
+ */
3248
+ async function processStkPush(baseUrl, accessToken, request, http) {
3249
+ const validated = parseWithSchema(StkPushRequestSchema, request, "STK Push request");
3250
+ if (!Number.isFinite(validated.amount)) throw new PesafyError({
3251
+ code: "VALIDATION_ERROR",
3252
+ message: `amount must be a finite number (got ${validated.amount}).`
3253
+ });
3254
+ const amount = Math.round(validated.amount);
3255
+ if (amount < STK_PUSH_LIMITS.MIN_AMOUNT) throw new PesafyError({
3256
+ code: "VALIDATION_ERROR",
3257
+ message: `Amount must be at least KES ${STK_PUSH_LIMITS.MIN_AMOUNT} (got ${validated.amount} which rounds to ${amount}).`
3258
+ });
3259
+ if (amount > STK_PUSH_LIMITS.MAX_AMOUNT) throw new PesafyError({
3260
+ code: "VALIDATION_ERROR",
3261
+ message: `Amount must not exceed KES ${STK_PUSH_LIMITS.MAX_AMOUNT.toLocaleString()} per transaction as per Safaricom Daraja limits (got ${validated.amount} which rounds to ${amount}).`
3262
+ });
3263
+ const timestamp = getTimestamp();
3264
+ const partyB = validated.partyB ?? validated.shortCode;
3265
+ const body = {
3266
+ BusinessShortCode: validated.shortCode,
3267
+ Password: getStkPushPassword(validated.shortCode, validated.passKey, timestamp),
3268
+ Timestamp: timestamp,
3269
+ TransactionType: validated.transactionType ?? "CustomerPayBillOnline",
3270
+ Amount: amount,
3271
+ PartyA: formatSafaricomPhone(validated.phoneNumber),
3272
+ PartyB: partyB,
3273
+ PhoneNumber: formatSafaricomPhone(validated.phoneNumber),
3274
+ CallBackURL: validated.callbackUrl,
3275
+ AccountReference: validated.accountReference.slice(0, 12),
3276
+ TransactionDesc: validated.transactionDesc.slice(0, 13)
3277
+ };
3278
+ const { data } = await httpRequest(`${baseUrl}/mpesa/stkpush/v1/processrequest`, withDarajaHttp({
3279
+ method: "POST",
3280
+ headers: { Authorization: `Bearer ${accessToken}` },
3281
+ body,
3282
+ retries: 5,
3283
+ retryDelay: 3e3
3284
+ }, http));
3285
+ return parseWithSchema(StkPushResponseSchema, data, "STK Push response");
3286
+ }
3287
+
3288
+ //#endregion
3289
+ //#region src/mpesa/stk-push/stk-query.ts
3290
+ async function queryStkPush(baseUrl, accessToken, request, http) {
3291
+ const validated = parseWithSchema(StkQueryRequestSchema, request, "STK Query request");
3292
+ const timestamp = getTimestamp();
3293
+ const body = {
3294
+ BusinessShortCode: validated.shortCode,
3295
+ Password: getStkPushPassword(validated.shortCode, validated.passKey, timestamp),
3296
+ Timestamp: timestamp,
3297
+ CheckoutRequestID: validated.checkoutRequestId
3298
+ };
3299
+ const { data } = await httpRequest(`${baseUrl}/mpesa/stkpushquery/v1/query`, withDarajaHttp({
3300
+ method: "POST",
3301
+ headers: { Authorization: `Bearer ${accessToken}` },
3302
+ body
3303
+ }, http));
3304
+ return parseWithSchema(StkQueryResponseSchema, data, "STK Query response");
3305
+ }
3306
+
3307
+ //#endregion
3308
+ //#region src/mpesa/tax-remittance/remit-tax.ts
3309
+ /** KRA's M-PESA shortcode — the only allowed PartyB for tax remittance */
3310
+ const KRA_SHORTCODE = "572572";
3311
+ /** The only CommandID accepted by the Tax Remittance API */
3312
+ const TAX_COMMAND_ID = "PayTaxToKRA";
3313
+ /**
3314
+ * Remits tax to Kenya Revenue Authority (KRA) via M-PESA.
3315
+ *
3316
+ * Endpoint: POST /mpesa/b2b/v1/remittax
3317
+ *
3318
+ * @param baseUrl - Daraja base URL (sandbox or production)
3319
+ * @param accessToken - Valid OAuth bearer token
3320
+ * @param securityCredential - RSA-encrypted initiator password (base64)
3321
+ * @param initiatorName - M-PESA org portal API operator username
3322
+ * @param request - Tax remittance parameters
3323
+ * @returns - Daraja synchronous acknowledgement response
3324
+ *
3325
+ * @example
3326
+ * const response = await remitTax(
3327
+ * 'https://sandbox.safaricom.co.ke',
3328
+ * accessToken,
3329
+ * securityCredential,
3330
+ * 'TaxPayer',
3331
+ * {
3332
+ * amount: 239,
3333
+ * partyA: '888880',
3334
+ * accountReference: '353353',
3335
+ * resultUrl: 'https://example.org/b2b/remittax/result/',
3336
+ * queueTimeOutUrl: 'https://example.org/b2b/remittax/queue/',
3337
+ * },
3338
+ * )
3339
+ */
3340
+ async function remitTax(baseUrl, accessToken, securityCredential, initiatorName, request, http) {
3341
+ const validated = parseWithSchema(TaxRemittanceRequestSchema, request, "Tax Remittance request");
3342
+ const amount = Math.round(validated.amount);
3343
+ const payload = {
3344
+ Initiator: initiatorName,
3345
+ SecurityCredential: securityCredential,
3346
+ CommandID: TAX_COMMAND_ID,
3347
+ SenderIdentifierType: "4",
3348
+ RecieverIdentifierType: "4",
3349
+ Amount: String(amount),
3350
+ PartyA: String(validated.partyA),
3351
+ PartyB: validated.partyB ?? "572572",
3352
+ AccountReference: validated.accountReference,
3353
+ Remarks: validated.remarks ?? "Tax Remittance",
3354
+ QueueTimeOutURL: validated.queueTimeOutUrl,
3355
+ ResultURL: validated.resultUrl
3356
+ };
3357
+ const { data } = await httpRequest(`${baseUrl}/mpesa/b2b/v1/remittax`, withDarajaHttp({
3358
+ method: "POST",
3359
+ headers: { Authorization: `Bearer ${accessToken}` },
3360
+ body: payload
3361
+ }, http));
3362
+ return parseWithSchema(TaxRemittanceResponseSchema, data, "Tax Remittance response");
3363
+ }
3364
+
3365
+ //#endregion
3366
+ //#region src/mpesa/tax-remittance/webhooks.ts
3367
+ function isObject(value) {
3368
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3369
+ }
3370
+ function normaliseResultCode(code) {
3371
+ return typeof code === "string" ? Number(code) : code;
3372
+ }
3373
+ /**
3374
+ * Returns `true` when the value is a valid Tax Remittance async result payload
3375
+ * (the shape POSTed by Safaricom to your ResultURL).
3376
+ *
3377
+ * Checks the minimum required fields per the Daraja documentation:
3378
+ * - Result.ResultCode
3379
+ * - Result.ConversationID
3380
+ * - Result.OriginatorConversationID
3381
+ */
3382
+ function isTaxRemittanceResult(value) {
3383
+ if (!isObject(value)) return false;
3384
+ const root = value;
3385
+ if (!isObject(root["Result"])) return false;
3386
+ const result = root["Result"];
3387
+ return result["ResultCode"] !== void 0 && result["ResultCode"] !== null && typeof result["ConversationID"] === "string" && typeof result["OriginatorConversationID"] === "string";
3388
+ }
3389
+ /**
3390
+ * Returns `true` when the Tax Remittance result indicates a successful
3391
+ * transaction — i.e. ResultCode is 0 or "0".
3392
+ */
3393
+ function isTaxRemittanceSuccess(result) {
3394
+ return normaliseResultCode(result.Result.ResultCode) === 0;
3395
+ }
3396
+ /**
3397
+ * Returns `true` when the Tax Remittance result indicates a failure
3398
+ * — i.e. ResultCode is non-zero.
3399
+ *
3400
+ * `isTaxRemittanceSuccess` and `isTaxRemittanceFailure` are mutually exclusive.
3401
+ */
3402
+ function isTaxRemittanceFailure(result) {
3403
+ return !isTaxRemittanceSuccess(result);
3404
+ }
3405
+ /**
3406
+ * Extracts a single value from the Result's ResultParameter collection.
3407
+ *
3408
+ * Handles both the array form and the single-object form that Daraja may return.
3409
+ * Returns `undefined` when the key is not found or ResultParameters is absent.
3410
+ *
3411
+ * Documented result parameter keys:
3412
+ * - `Amount`
3413
+ * - `TransactionCompletedTime`
3414
+ * - `ReceiverPartyPublicName`
3415
+ */
3416
+ function getTaxResultParam(result, key) {
3417
+ const params = result.Result.ResultParameters?.ResultParameter;
3418
+ if (params === void 0 || params === null) return void 0;
3419
+ if (Array.isArray(params)) return params.find((p) => p.Key === key)?.Value;
3420
+ const single = params;
3421
+ return single.Key === key ? single.Value : void 0;
3422
+ }
3423
+ /**
3424
+ * Returns the ResultCode as-is (string or number) from the result payload.
3425
+ */
3426
+ function getTaxResultCode(result) {
3427
+ return result.Result.ResultCode;
3428
+ }
3429
+ /**
3430
+ * Returns the ResultDesc from the result payload.
3431
+ */
3432
+ function getTaxResultDesc(result) {
3433
+ return result.Result.ResultDesc;
3434
+ }
3435
+ /**
3436
+ * Returns the TransactionID (M-PESA receipt number) from the result payload.
3437
+ */
3438
+ function getTaxTransactionId(result) {
3439
+ return result.Result.TransactionID;
3440
+ }
3441
+ /**
3442
+ * Returns the ConversationID from the result payload.
3443
+ */
3444
+ function getTaxConversationId(result) {
3445
+ return result.Result.ConversationID;
3446
+ }
3447
+ /**
3448
+ * Returns the OriginatorConversationID from the result payload.
3449
+ */
3450
+ function getTaxOriginatorConversationId(result) {
3451
+ return result.Result.OriginatorConversationID;
3452
+ }
3453
+ /**
3454
+ * Returns the remitted Amount as a number, or `null` when ResultParameters
3455
+ * is absent (e.g. on failure callbacks).
3456
+ *
3457
+ * Documented key: `Amount` — Daraja returns it as a string like "190.00".
3458
+ */
3459
+ function getTaxAmount(result) {
3460
+ const raw = getTaxResultParam(result, "Amount");
3461
+ if (raw === void 0 || raw === null) return null;
3462
+ const parsed = typeof raw === "number" ? raw : parseFloat(String(raw));
3463
+ return Number.isNaN(parsed) ? null : parsed;
3464
+ }
3465
+ /**
3466
+ * Returns the TransactionCompletedTime string from ResultParameters,
3467
+ * or `null` when absent.
3468
+ *
3469
+ * Documented key: `TransactionCompletedTime` — format e.g. "20221110110717".
3470
+ */
3471
+ function getTaxCompletedTime(result) {
3472
+ const raw = getTaxResultParam(result, "TransactionCompletedTime");
3473
+ return raw !== void 0 ? String(raw) : null;
3474
+ }
3475
+ /**
3476
+ * Returns the ReceiverPartyPublicName from ResultParameters, or `null` when absent.
3477
+ *
3478
+ * Documented key: `ReceiverPartyPublicName` — e.g. "00000 Tax Collecting Company".
3479
+ */
3480
+ function getTaxReceiverName(result) {
3481
+ const raw = getTaxResultParam(result, "ReceiverPartyPublicName");
3482
+ return raw !== void 0 ? String(raw) : null;
3483
+ }
3484
+
3485
+ //#endregion
3486
+ //#region src/mpesa/transaction-status/query.ts
3487
+ /**
3488
+ * Transaction Status Query implementation
3489
+ *
3490
+ * API: POST /mpesa/transactionstatus/v1/query
3491
+ *
3492
+ * This is ASYNCHRONOUS. The synchronous response only acknowledges receipt.
3493
+ * Final results arrive via POST to your ResultURL.
3494
+ *
3495
+ * Required M-PESA org portal role: "Transaction Status query ORG API"
3496
+ *
3497
+ * Reconciliation options (at least one required):
3498
+ * - transactionId — M-Pesa Receipt Number (e.g. "NEF61H8J60")
3499
+ * - originalConversationId — OriginatorConversationID from the original call
3500
+ */
3501
+ async function queryTransactionStatus(baseUrl, token, securityCredential, initiator, request, http) {
3502
+ const validated = parseWithSchema(TransactionStatusRequestSchema, request, "Transaction Status request");
3503
+ const payload = {
3504
+ Initiator: initiator,
3505
+ SecurityCredential: securityCredential,
3506
+ CommandID: validated.commandId ?? "TransactionStatusQuery",
3507
+ TransactionID: validated.transactionId ?? "",
3508
+ OriginalConversationID: validated.originalConversationId ?? "",
3509
+ PartyA: validated.partyA,
3510
+ IdentifierType: validated.identifierType,
3511
+ ResultURL: validated.resultUrl,
3512
+ QueueTimeOutURL: validated.queueTimeOutUrl,
3513
+ Remarks: validated.remarks ?? "Transaction Status Query",
3514
+ Occasion: validated.occasion ?? ""
3515
+ };
3516
+ const { data } = await httpRequest(`${baseUrl}/mpesa/transactionstatus/v1/query`, withDarajaHttp({
3517
+ method: "POST",
3518
+ headers: { Authorization: `Bearer ${token}` },
3519
+ body: payload
3520
+ }, http));
3521
+ return parseWithSchema(TransactionStatusResponseSchema, data, "Transaction Status response");
3522
+ }
3523
+
3524
+ //#endregion
3525
+ //#region src/mpesa/transaction-status/types.ts
3526
+ /**
3527
+ * Daraja Transaction Status API error codes.
3528
+ * Returned in the errorCode field of error response bodies.
3529
+ *
3530
+ * @see https://developer.safaricom.co.ke/APIs/TransactionStatus
3531
+ */
3532
+ const TRANSACTION_STATUS_ERROR_CODES = {
3533
+ INVALID_ACCESS_TOKEN: "400.003.01",
3534
+ BAD_REQUEST: "400.003.02",
3535
+ INTERNAL_SERVER_ERROR: "500.003.1001",
3536
+ QUOTA_VIOLATION: "500.003.03",
3537
+ SPIKE_ARREST: "500.003.02",
3538
+ NOT_FOUND: "404.003.01",
3539
+ INVALID_AUTH_HEADER: "404.001.04",
3540
+ INVALID_PAYLOAD: "400.002.05"
3541
+ };
3542
+ /**
3543
+ * Documented result codes for the Transaction Status async callback.
3544
+ * ResultCode 0 = success; anything else is a failure.
3545
+ */
3546
+ const TRANSACTION_STATUS_RESULT_CODES = {
3547
+ SUCCESS: 0,
3548
+ INVALID_INITIATOR: 2001
3549
+ };
3550
+
3551
+ //#endregion
3552
+ //#region src/mpesa/transaction-status/webhooks.ts
3553
+ /**
3554
+ * Type guards and payload extractors for Transaction Status result callbacks.
3555
+ *
3556
+ * Documented result parameters (POSTed to your ResultURL):
3557
+ * - DebitPartyName
3558
+ * - TransactionStatus
3559
+ * - Amount
3560
+ * - ReceiptNo
3561
+ * - DebitAccountBalance
3562
+ * - TransactionDate
3563
+ * - CreditPartyName
3564
+ *
3565
+ * Docs note: ResultCode is 0 (number) on success. Daraja may return either
3566
+ * a number or a string depending on the transaction type — both forms handled.
3567
+ *
3568
+ * @see https://developer.safaricom.co.ke/APIs/TransactionStatus
3569
+ */
3570
+ const KNOWN_RESULT_CODES = new Set(Object.values(TRANSACTION_STATUS_RESULT_CODES));
3571
+ /**
3572
+ * Runtime type guard — checks if a body looks like a Transaction Status
3573
+ * result callback. Validates the minimum documented structure.
3574
+ */
3575
+ function isTransactionStatusResult(body) {
3576
+ if (!body || typeof body !== "object") return false;
3577
+ const b = body;
3578
+ if (!b["Result"] || typeof b["Result"] !== "object") return false;
3579
+ const result = b["Result"];
3580
+ return (typeof result["ResultCode"] === "number" || typeof result["ResultCode"] === "string") && typeof result["ConversationID"] === "string" && typeof result["OriginatorConversationID"] === "string";
3581
+ }
3582
+ /**
3583
+ * Returns true if the Transaction Status result represents a successful query.
3584
+ * Handles both string "0" and number 0 (Daraja inconsistency).
3585
+ */
3586
+ function isTransactionStatusSuccess(result) {
3587
+ const code = result.Result.ResultCode;
3588
+ return code === 0 || code === "0";
3589
+ }
3590
+ /**
3591
+ * Returns true if the Transaction Status result represents a failure.
3592
+ */
3593
+ function isTransactionStatusFailure(result) {
3594
+ return !isTransactionStatusSuccess(result);
3595
+ }
3596
+ /**
3597
+ * Returns true if the result code is among the documented codes.
3598
+ * Handles both numeric and string representations.
3599
+ */
3600
+ function isKnownTransactionStatusResultCode(code) {
3601
+ if (typeof code !== "number" && typeof code !== "string") return false;
3602
+ if (typeof code === "string" && code.trim() === "") return false;
3603
+ const numeric = Number(code);
3604
+ return Number.isFinite(numeric) && KNOWN_RESULT_CODES.has(numeric);
3605
+ }
3606
+ /**
3607
+ * Extracts the M-PESA transaction ID from a Transaction Status result.
3608
+ */
3609
+ function getTransactionStatusTransactionId(result) {
3610
+ return result.Result.TransactionID;
3611
+ }
3612
+ /**
3613
+ * Extracts the ConversationID from a Transaction Status result.
3614
+ */
3615
+ function getTransactionStatusConversationId(result) {
3616
+ return result.Result.ConversationID;
3617
+ }
3618
+ /**
3619
+ * Extracts the OriginatorConversationID from a Transaction Status result.
3620
+ * Use this to correlate with the original API call response.
3621
+ */
3622
+ function getTransactionStatusOriginatorConversationId(result) {
3623
+ return result.Result.OriginatorConversationID;
3624
+ }
3625
+ /**
3626
+ * Extracts the human-readable result description.
3627
+ */
3628
+ function getTransactionStatusResultDesc(result) {
3629
+ return result.Result.ResultDesc;
3630
+ }
3631
+ /**
3632
+ * Extracts the result code from a Transaction Status result.
3633
+ */
3634
+ function getTransactionStatusResultCode(result) {
3635
+ return result.Result.ResultCode;
3636
+ }
3637
+ /**
3638
+ * Extracts the transaction amount from result parameters.
3639
+ * Documented field: "Amount"
3640
+ * Returns null if not present (e.g. on failure).
3641
+ */
3642
+ function getTransactionStatusAmount(result) {
3643
+ const value = getTransactionStatusResultParam(result, "Amount");
3644
+ if (value === void 0) return null;
3645
+ const num = Number(value);
3646
+ return Number.isFinite(num) ? num : null;
3647
+ }
3648
+ /**
3649
+ * Extracts the M-Pesa receipt number from result parameters.
3650
+ * Documented field: "ReceiptNo"
3651
+ * Returns null if not present.
3652
+ */
3653
+ function getTransactionStatusReceiptNo(result) {
3654
+ const value = getTransactionStatusResultParam(result, "ReceiptNo");
3655
+ if (value === void 0) return null;
3656
+ return String(value);
3657
+ }
3658
+ /**
3659
+ * Extracts the transaction status string from result parameters.
3660
+ * Documented field: "TransactionStatus" — e.g. "Completed"
3661
+ * Returns null if not present.
3662
+ */
3663
+ function getTransactionStatusStatus(result) {
3664
+ const value = getTransactionStatusResultParam(result, "TransactionStatus");
3665
+ if (value === void 0) return null;
3666
+ return String(value);
3667
+ }
3668
+ /**
3669
+ * Extracts the debit party name from result parameters.
3670
+ * Documented field: "DebitPartyName" — e.g. "600310 Safaricom333"
3671
+ * Returns null if not present.
3672
+ */
3673
+ function getTransactionStatusDebitPartyName(result) {
3674
+ const value = getTransactionStatusResultParam(result, "DebitPartyName");
3675
+ if (value === void 0) return null;
3676
+ return String(value);
3677
+ }
3678
+ /**
3679
+ * Extracts the credit party name from result parameters.
3680
+ * Documented field: "CreditPartyName"
3681
+ * Returns null if not present.
3682
+ */
3683
+ function getTransactionStatusCreditPartyName(result) {
3684
+ const value = getTransactionStatusResultParam(result, "CreditPartyName");
3685
+ if (value === void 0) return null;
3686
+ return String(value);
3687
+ }
3688
+ /**
3689
+ * Extracts the debit account balance from result parameters.
3690
+ * Documented field: "DebitAccountBalance"
3691
+ * Returns null if not present.
3692
+ */
3693
+ function getTransactionStatusDebitAccountBalance(result) {
3694
+ const value = getTransactionStatusResultParam(result, "DebitAccountBalance");
3695
+ if (value === void 0) return null;
3696
+ return String(value);
3697
+ }
3698
+ /**
3699
+ * Extracts the transaction date from result parameters.
3700
+ * Documented field: "TransactionDate"
3701
+ * Returns null if not present.
3702
+ */
3703
+ function getTransactionStatusTransactionDate(result) {
3704
+ const value = getTransactionStatusResultParam(result, "TransactionDate");
3705
+ if (value === void 0) return null;
3706
+ return String(value);
3707
+ }
3708
+ /**
3709
+ * Extracts a named value from Transaction Status result parameters.
3710
+ * Handles both single-object and array forms of ResultParameter
3711
+ * (Daraja returns either depending on how many parameters are present).
3712
+ * Returns undefined if key is absent or no ResultParameters exist.
3713
+ */
3714
+ function getTransactionStatusResultParam(result, key) {
3715
+ const params = result.Result.ResultParameters?.ResultParameter;
3716
+ if (!params) return void 0;
3717
+ return (Array.isArray(params) ? params : [params]).find((p) => p.Key === key)?.Value;
3718
+ }
3719
+
3720
+ //#endregion
3721
+ //#region src/mpesa/types.ts
3722
+ const DARAJA_BASE_URLS = {
3723
+ sandbox: "https://sandbox.safaricom.co.ke",
3724
+ production: "https://api.safaricom.co.ke"
3725
+ };
3726
+
3727
+ //#endregion
3728
+ //#region src/mpesa/webhooks/retry.ts
3729
+ const DEFAULT_OPTIONS = {
3730
+ maxRetries: Infinity,
3731
+ initialDelay: 1e3,
3732
+ maxDelay: 36e5,
3733
+ backoffMultiplier: 2,
3734
+ maxRetryDuration: 720 * 60 * 60 * 1e3
3735
+ };
3736
+ /**
3737
+ * Retries `fn` with exponential backoff until it resolves, or limits are hit.
3738
+ *
3739
+ * `maxRetries` = number of *retries* after the first attempt.
3740
+ * 0 → one attempt total, no retries
3741
+ * N → up to N+1 attempts total
3742
+ */
3743
+ async function retryWithBackoff(fn, options = {}) {
3744
+ const opts = {
3745
+ ...DEFAULT_OPTIONS,
3746
+ ...options
3747
+ };
3748
+ let delay = opts.initialDelay;
3749
+ let attempts = 0;
3750
+ const startTime = Date.now();
3751
+ while (attempts <= opts.maxRetries) {
3752
+ attempts++;
3753
+ if (Date.now() - startTime > opts.maxRetryDuration) return {
3754
+ success: false,
3755
+ attempts,
3756
+ error: /* @__PURE__ */ new Error("Max retry duration exceeded")
3757
+ };
3758
+ try {
3759
+ return {
3760
+ success: true,
3761
+ data: await fn(),
3762
+ attempts
3763
+ };
3764
+ } catch (error) {
3765
+ const err = error instanceof Error ? error : new Error(String(error));
3766
+ if (err.message.includes("4")) return {
3767
+ success: false,
3768
+ attempts,
3769
+ error: err
3770
+ };
3771
+ if (attempts <= opts.maxRetries) {
3772
+ await new Promise((resolve) => setTimeout(resolve, delay));
3773
+ delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelay);
3774
+ }
3775
+ }
3776
+ }
3777
+ return {
3778
+ success: false,
3779
+ attempts,
3780
+ error: /* @__PURE__ */ new Error("Max retries exceeded")
3781
+ };
3782
+ }
3783
+
3784
+ //#endregion
3785
+ //#region src/mpesa/webhooks/hmac-verifier.ts
3786
+ /** Default algorithm until Safaricom documents the official scheme. */
3787
+ const DEFAULT_WEBHOOK_HMAC_ALGORITHM = "sha256";
3788
+ const ALGO_MAP = {
3789
+ sha256: "SHA-256",
3790
+ sha512: "SHA-512"
3791
+ };
3792
+ function hexToBytes(hex) {
3793
+ const trimmed = hex.trim();
3794
+ if (!/^[0-9a-fA-F]+$/.test(trimmed) || trimmed.length % 2 !== 0) return null;
3795
+ const bytes = new Uint8Array(trimmed.length / 2);
3796
+ for (let i = 0; i < bytes.length; i++) bytes[i] = Number.parseInt(trimmed.slice(i * 2, i * 2 + 2), 16);
3797
+ return bytes;
3798
+ }
3799
+ function bytesToHex(bytes) {
3800
+ return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
3801
+ }
3802
+ /** Constant-time comparison for equal-length byte arrays. */
3803
+ function timingSafeEqualBytes(a, b) {
3804
+ if (a.length !== b.length) return false;
3805
+ let diff = 0;
3806
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
3807
+ return diff === 0;
3808
+ }
3809
+ /**
3810
+ * Verify webhook HMAC signature (preview — opt-in until Safaricom publishes spec).
3811
+ * Uses Web Crypto (`globalThis.crypto.subtle`) for Node, Bun, Deno, and Workers.
3812
+ */
3813
+ async function verifyWebhookHMAC(payload, signature, secret, algorithm = DEFAULT_WEBHOOK_HMAC_ALGORITHM) {
3814
+ if (!secret || !signature) return false;
3815
+ const subtle = globalThis.crypto?.subtle;
3816
+ if (!subtle) return false;
3817
+ const body = typeof payload === "string" ? new TextEncoder().encode(payload) : payload instanceof Uint8Array ? payload : new Uint8Array(payload);
3818
+ const sigBytes = hexToBytes(signature);
3819
+ if (!sigBytes) return false;
3820
+ try {
3821
+ const key = await subtle.importKey("raw", new TextEncoder().encode(secret), {
3822
+ name: "HMAC",
3823
+ hash: ALGO_MAP[algorithm]
3824
+ }, false, ["sign"]);
3825
+ const expected = hexToBytes(bytesToHex(await subtle.sign("HMAC", key, body)));
3826
+ if (!expected) return false;
3827
+ return timingSafeEqualBytes(sigBytes, expected);
3828
+ } catch {
3829
+ return false;
3830
+ }
3831
+ }
3832
+
3833
+ //#endregion
3834
+ //#region src/mpesa/webhooks/signature-verifier.ts
3835
+ /**
3836
+ * Webhook verification utilities
3837
+ *
3838
+ * Daraja does NOT use HMAC webhook signatures like Stripe.
3839
+ * Instead, verify that callbacks come from whitelisted Safaricom IPs.
3840
+ *
3841
+ * Official Safaricom IP whitelist (from Getting Started docs):
3842
+ * 196.201.214.200
3843
+ * 196.201.214.206
3844
+ * 196.201.213.114
3845
+ * 196.201.214.207
3846
+ * 196.201.214.208
3847
+ * 196.201.213.44
3848
+ * 196.201.212.127
3849
+ * 196.201.212.138
3850
+ * 196.201.212.129
3851
+ * 196.201.212.136
3852
+ * 196.201.212.74
3853
+ * 196.201.212.69
3854
+ */
3855
+ /** Official Safaricom API Gateway IP addresses */
3856
+ const SAFARICOM_IPS = [
3857
+ "196.201.214.200",
3858
+ "196.201.214.206",
3859
+ "196.201.213.114",
3860
+ "196.201.214.207",
3861
+ "196.201.214.208",
3862
+ "196.201.213.44",
3863
+ "196.201.212.127",
3864
+ "196.201.212.138",
3865
+ "196.201.212.129",
3866
+ "196.201.212.136",
3867
+ "196.201.212.74",
3868
+ "196.201.212.69"
3869
+ ];
3870
+ /**
3871
+ * Returns true if requestIP is in the allowed list.
3872
+ * Defaults to the official Safaricom IP whitelist.
3873
+ */
3874
+ function verifyWebhookIP(requestIP, allowedIPs = SAFARICOM_IPS) {
3875
+ return allowedIPs.includes(requestIP);
3876
+ }
3877
+ /**
3878
+ * Parses and validates an STK Push webhook body.
3879
+ * Returns the typed payload or null if it doesn't match the expected shape.
3880
+ */
3881
+ function parseStkPushWebhook(body) {
3882
+ if (!StkPushWebhookSchema.safeParse(body).success) return null;
3883
+ return body;
3884
+ }
3885
+ /**
3886
+ * Combined webhook verification: IP allowlist (default) + optional HMAC.
3887
+ */
3888
+ async function verifyWebhook(options) {
3889
+ const errors = [];
3890
+ const allowed = options.allowedIPs ?? SAFARICOM_IPS;
3891
+ const ipValid = options.skipIPCheck === true || verifyWebhookIP(options.requestIP, allowed);
3892
+ if (!ipValid) errors.push("Request IP is not in the Safaricom allowlist.");
3893
+ let hmacValid = true;
3894
+ if (options.secret && options.signature && options.rawBody !== void 0) {
3895
+ hmacValid = await verifyWebhookHMAC(options.rawBody, options.signature, options.secret, options.hmacAlgorithm);
3896
+ if (!hmacValid) errors.push("Webhook HMAC signature verification failed.");
3897
+ } else if (options.requireHMAC) {
3898
+ hmacValid = false;
3899
+ errors.push("HMAC verification required but secret, signature, or rawBody missing.");
3900
+ }
3901
+ return {
3902
+ valid: ipValid && hmacValid,
3903
+ ipValid,
3904
+ hmacValid,
3905
+ errors
3906
+ };
3907
+ }
3908
+
3909
+ //#endregion
3910
+ //#region src/mpesa/webhooks/webhook-handler.ts
3911
+ /**
3912
+ * High-level webhook event handler
3913
+ */
3914
+ /**
3915
+ * Parses and validates an inbound Daraja webhook payload.
3916
+ *
3917
+ * @example
3918
+ * // Express route
3919
+ * app.post("/mpesa/callback", (req, res) => {
3920
+ * const result = handleWebhook(req.body, { requestIP: req.ip });
3921
+ * if (!result.success) return res.status(400).json({ error: result.error });
3922
+ * // process result.data (StkPushWebhook)
3923
+ * res.json({ ResultCode: 0, ResultDesc: "Accepted" });
3924
+ * });
3925
+ */
3926
+ function handleWebhook(body, options = {}) {
3927
+ if (!options.skipIPCheck && options.requestIP) {
3928
+ if (!verifyWebhookIP(options.requestIP, options.allowedIPs)) return {
3929
+ success: false,
3930
+ eventType: null,
3931
+ data: null,
3932
+ error: `IP address ${options.requestIP} is not in the Safaricom whitelist`
3933
+ };
3934
+ }
3935
+ const stkPush = parseStkPushWebhook(body);
3936
+ if (stkPush) return {
3937
+ success: true,
3938
+ eventType: "stk_push",
3939
+ data: stkPush
3940
+ };
3941
+ return {
3942
+ success: false,
3943
+ eventType: null,
3944
+ data: null,
3945
+ error: "Unknown or malformed webhook payload"
3946
+ };
3947
+ }
3948
+ /** Extracts the M-Pesa receipt number from a successful STK Push callback */
3949
+ function extractTransactionId(webhook) {
3950
+ const item = (webhook.Body?.stkCallback?.CallbackMetadata?.Item)?.find((i) => i.Name === "MpesaReceiptNumber");
3951
+ return item ? String(item.Value) : null;
3952
+ }
3953
+ /** Extracts the transaction amount from a successful STK Push callback */
3954
+ function extractAmount(webhook) {
3955
+ const item = (webhook.Body?.stkCallback?.CallbackMetadata?.Item)?.find((i) => i.Name === "Amount");
3956
+ return item ? Number(item.Value) : null;
3957
+ }
3958
+ /** Extracts the phone number from a successful STK Push callback */
3959
+ function extractPhoneNumber(webhook) {
3960
+ const item = (webhook.Body?.stkCallback?.CallbackMetadata?.Item)?.find((i) => i.Name === "PhoneNumber");
3961
+ return item ? String(item.Value) : null;
3962
+ }
3963
+ /** Returns true if the STK Push callback represents a successful transaction */
3964
+ function isSuccessfulCallback(webhook) {
3965
+ return webhook.Body?.stkCallback?.ResultCode === 0;
3966
+ }
3967
+
3968
+ //#endregion
3969
+ //#region src/mpesa/index.ts
3970
+ /**
3971
+ * src/mpesa/index.ts
3972
+ *
3973
+ * Primary M-PESA module entry point.
3974
+ *
3975
+ * Exports:
3976
+ * 1. Mpesa class — the main SDK client
3977
+ * 2. All submodule APIs, types, constants, and helpers
3978
+ *
3979
+ * Submodules re-exported here:
3980
+ * - account-balance
3981
+ * - b2b-buy-goods
3982
+ * - b2b-express-checkout
3983
+ * - b2b-pay-bill
3984
+ * - b2c (Account Top Up)
3985
+ * - b2c-disbursement
3986
+ * - bill-manager
3987
+ * - c2b
3988
+ * - dynamic-qr
3989
+ * - reversal
3990
+ * - stk-push
3991
+ * - tax-remittance
3992
+ * - transaction-status
3993
+ * - webhooks
3994
+ */
3995
+ var Mpesa = class {
3996
+ config;
3997
+ tokenManager;
3998
+ baseUrl;
3999
+ idempotencyManager;
4000
+ constructor(config) {
4001
+ if (!config.consumerKey || !config.consumerSecret) throw new PesafyError({
4002
+ code: "INVALID_CREDENTIALS",
4003
+ message: "consumerKey and consumerSecret are required."
4004
+ });
4005
+ this.config = config;
4006
+ this.baseUrl = DARAJA_BASE_URLS[config.environment];
4007
+ this.tokenManager = new TokenManager(config.consumerKey, config.consumerSecret, this.baseUrl);
4008
+ this.idempotencyManager = new IdempotencyManager(config.idempotency);
4009
+ }
4010
+ /** Idempotency options passed to all outbound Daraja HTTP calls. */
4011
+ darajaHttp() {
4012
+ return { idempotency: this.idempotencyManager };
4013
+ }
4014
+ getToken() {
4015
+ return this.tokenManager.getAccessToken();
4016
+ }
4017
+ async buildSecurityCredential() {
4018
+ if (this.config.securityCredential) return this.config.securityCredential;
4019
+ if (!this.config.initiatorPassword) throw new PesafyError({
4020
+ code: "INVALID_CREDENTIALS",
4021
+ message: "Provide securityCredential (pre-encrypted) OR (initiatorPassword + certificatePath/certificatePem)."
4022
+ });
4023
+ let cert;
4024
+ if (this.config.certificatePem) cert = this.config.certificatePem;
4025
+ else if (this.config.certificatePath) cert = await readFile(this.config.certificatePath, "utf-8");
4026
+ else throw new PesafyError({
4027
+ code: "INVALID_CREDENTIALS",
4028
+ message: "certificatePath or certificatePem is required to encrypt the initiator password."
4029
+ });
4030
+ return encryptSecurityCredential(this.config.initiatorPassword, cert);
4031
+ }
4032
+ requireInitiator(forApi) {
4033
+ const name = this.config.initiatorName ?? "";
4034
+ if (!name) throw new PesafyError({
4035
+ code: "VALIDATION_ERROR",
4036
+ message: `initiatorName is required for ${forApi}.`
4037
+ });
4038
+ return name;
4039
+ }
4040
+ async stkPushSafe(request) {
4041
+ try {
4042
+ return ok(await this.stkPush(request));
4043
+ } catch (e) {
4044
+ return err(e);
4045
+ }
4046
+ }
4047
+ async accountBalanceSafe(request) {
4048
+ try {
4049
+ return ok(await this.accountBalance(request));
4050
+ } catch (e) {
4051
+ return err(e);
4052
+ }
4053
+ }
4054
+ async stkPush(request) {
4055
+ const shortCode = this.config.lipaNaMpesaShortCode ?? "";
4056
+ const passKey = this.config.lipaNaMpesaPassKey ?? "";
4057
+ if (!shortCode || !passKey) throw new PesafyError({
4058
+ code: "VALIDATION_ERROR",
4059
+ message: "lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push."
4060
+ });
4061
+ const token = await this.getToken();
4062
+ return processStkPush(this.baseUrl, token, {
4063
+ ...request,
4064
+ shortCode,
4065
+ passKey
4066
+ }, this.darajaHttp());
4067
+ }
4068
+ async stkQuery(request) {
4069
+ const shortCode = this.config.lipaNaMpesaShortCode ?? "";
4070
+ const passKey = this.config.lipaNaMpesaPassKey ?? "";
4071
+ if (!shortCode || !passKey) throw new PesafyError({
4072
+ code: "VALIDATION_ERROR",
4073
+ message: "lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query."
4074
+ });
4075
+ const token = await this.getToken();
4076
+ return queryStkPush(this.baseUrl, token, {
4077
+ ...request,
4078
+ shortCode,
4079
+ passKey
4080
+ }, this.darajaHttp());
4081
+ }
4082
+ async transactionStatus(request) {
4083
+ const initiator = this.requireInitiator("Transaction Status");
4084
+ const [token, cred] = await Promise.all([this.getToken(), this.buildSecurityCredential()]);
4085
+ return queryTransactionStatus(this.baseUrl, token, cred, initiator, request, this.darajaHttp());
4086
+ }
4087
+ async accountBalance(request) {
4088
+ const initiator = this.requireInitiator("Account Balance");
4089
+ const [token, cred] = await Promise.all([this.getToken(), this.buildSecurityCredential()]);
4090
+ return queryAccountBalance(this.baseUrl, token, cred, initiator, request, this.darajaHttp());
4091
+ }
4092
+ async reverseTransaction(request) {
4093
+ const initiator = this.requireInitiator("Reversal");
4094
+ const [token, cred] = await Promise.all([this.getToken(), this.buildSecurityCredential()]);
4095
+ return requestReversal(this.baseUrl, token, cred, initiator, request, this.darajaHttp());
4096
+ }
4097
+ async generateDynamicQR(request) {
4098
+ const token = await this.getToken();
4099
+ return generateDynamicQR(this.baseUrl, token, request, this.darajaHttp());
4100
+ }
4101
+ async registerC2BUrls(request) {
4102
+ const token = await this.getToken();
4103
+ return registerC2BUrls(this.baseUrl, token, request, this.darajaHttp());
4104
+ }
4105
+ async simulateC2B(request) {
4106
+ const token = await this.getToken();
4107
+ return simulateC2B(this.baseUrl, token, request, this.darajaHttp());
4108
+ }
4109
+ async remitTax(request) {
4110
+ const initiator = this.requireInitiator("Tax Remittance");
4111
+ const [token, cred] = await Promise.all([this.getToken(), this.buildSecurityCredential()]);
4112
+ return remitTax(this.baseUrl, token, cred, initiator, request, this.darajaHttp());
4113
+ }
4114
+ async b2bExpressCheckout(request) {
4115
+ const token = await this.getToken();
4116
+ return initiateB2BExpressCheckout(this.baseUrl, token, request, this.darajaHttp());
4117
+ }
4118
+ async b2bBuyGoods(request) {
4119
+ const initiator = this.requireInitiator("B2B Buy Goods");
4120
+ const [token, cred] = await Promise.all([this.getToken(), this.buildSecurityCredential()]);
4121
+ return initiateB2BBuyGoods(this.baseUrl, token, cred, initiator, request, this.darajaHttp());
4122
+ }
4123
+ async b2bPayBill(request) {
4124
+ const initiator = this.requireInitiator("B2B Pay Bill");
4125
+ const [token, cred] = await Promise.all([this.getToken(), this.buildSecurityCredential()]);
4126
+ return initiateB2BPayBill(this.baseUrl, token, cred, initiator, request, this.darajaHttp());
4127
+ }
4128
+ async b2cPayment(request) {
4129
+ const initiator = this.requireInitiator("B2C Payment");
4130
+ const [token, cred] = await Promise.all([this.getToken(), this.buildSecurityCredential()]);
4131
+ return initiateB2CPayment(this.baseUrl, token, cred, initiator, request, this.darajaHttp());
4132
+ }
4133
+ async b2cDisbursement(request) {
4134
+ const initiator = this.requireInitiator("B2C Disbursement");
4135
+ const req = {
4136
+ ...request,
4137
+ originatorConversationId: request.originatorConversationId ?? generateOriginatorConversationId()
4138
+ };
4139
+ const [token, cred] = await Promise.all([this.getToken(), this.buildSecurityCredential()]);
4140
+ return initiateB2CDisbursement(this.baseUrl, token, cred, initiator, req, this.darajaHttp());
4141
+ }
4142
+ async billManagerOptIn(request) {
4143
+ const token = await this.getToken();
4144
+ return billManagerOptIn(this.baseUrl, token, request, this.darajaHttp());
4145
+ }
4146
+ async updateOptIn(request) {
4147
+ const token = await this.getToken();
4148
+ return updateOptIn(this.baseUrl, token, request, this.darajaHttp());
4149
+ }
4150
+ async sendInvoice(request) {
4151
+ const token = await this.getToken();
4152
+ return sendSingleInvoice(this.baseUrl, token, request, this.darajaHttp());
4153
+ }
4154
+ async sendBulkInvoices(request) {
4155
+ const token = await this.getToken();
4156
+ return sendBulkInvoices(this.baseUrl, token, request, this.darajaHttp());
4157
+ }
4158
+ async cancelInvoice(request) {
4159
+ const token = await this.getToken();
4160
+ return cancelInvoice(this.baseUrl, token, request, this.darajaHttp());
4161
+ }
4162
+ async cancelBulkInvoices(request) {
4163
+ const token = await this.getToken();
4164
+ return cancelBulkInvoices(this.baseUrl, token, request, this.darajaHttp());
4165
+ }
4166
+ async reconcilePayment(request) {
4167
+ const token = await this.getToken();
4168
+ return reconcilePayment(this.baseUrl, token, request, this.darajaHttp());
4169
+ }
4170
+ clearTokenCache() {
4171
+ this.tokenManager.clearCache();
4172
+ }
4173
+ get environment() {
4174
+ return this.config.environment;
4175
+ }
4176
+ };
4177
+
4178
+ //#endregion
4179
+ //#region src/schemas/webhooks.ts
4180
+ const B2CResultWebhookSchema = z.object({ Result: z.object({
4181
+ ResultType: z.number(),
4182
+ ResultCode: z.number(),
4183
+ ResultDesc: z.string(),
4184
+ OriginatorConversationID: z.string().optional(),
4185
+ ConversationID: z.string().optional(),
4186
+ TransactionID: z.string().optional()
4187
+ }) }).passthrough();
4188
+ const C2BConfirmationWebhookSchema = z.object({
4189
+ TransID: z.string(),
4190
+ TransAmount: z.union([z.string(), z.number()]),
4191
+ MSISDN: z.string(),
4192
+ BusinessShortCode: z.string()
4193
+ }).passthrough();
4194
+
4195
+ //#endregion
4196
+ export { ACCOUNT_BALANCE_ERROR_CODES, AUTH_ERROR_CODES, AccountBalanceRequestSchema, AccountBalanceResponseSchema, AsyncApiResponseSchema, B2BBuyGoodsRequestSchema, B2BBuyGoodsResponseSchema, B2BExpressCheckoutRequestSchema, B2BExpressCheckoutResponseSchema, B2BPayBillRequestSchema, B2BPayBillResponseSchema, B2BResponseSchema, B2B_BUY_GOODS_ERROR_CODES, B2B_BUY_GOODS_RESULT_CODES, B2B_PAY_BILL_ERROR_CODES, B2B_PAY_BILL_RESULT_CODES, B2B_RESULT_CODES, B2CDisbursementRequestSchema, B2CDisbursementResponseSchema, B2CRequestSchema, B2CResponseSchema, B2CResultWebhookSchema, B2C_DISBURSEMENT_RESULT_CODES, B2C_ERROR_CODES, B2C_RESULT_CODES, BillManagerBulkInvoiceRequestSchema, BillManagerBulkInvoiceResponseSchema, BillManagerCancelBulkInvoiceRequestSchema, BillManagerCancelBulkInvoiceResponseSchema, BillManagerCancelInvoiceRequestSchema, BillManagerCancelInvoiceResponseSchema, BillManagerInvoiceItemSchema, BillManagerOptInRequestSchema, BillManagerOptInResponseSchema, BillManagerReconciliationRequestSchema, BillManagerReconciliationResponseSchema, BillManagerSingleInvoiceRequestSchema, BillManagerSingleInvoiceResponseSchema, BillManagerUpdateOptInRequestSchema, BillManagerUpdateOptInResponseSchema, C2BBaseResponseSchema, C2BConfirmationWebhookSchema, C2BRegisterUrlRequestSchema, C2BRegisterUrlResponseSchema, C2BSimulateRequestSchema, C2BSimulateResponseSchema, C2BValidationWebhookSchema, C2B_REGISTER_URL_ERROR_CODES, C2B_VALIDATION_RESULT_CODES, DARAJA_BASE_URLS, DEFAULT_QR_SIZE, DarajaErrorResponseSchema, DynamicQRRequestSchema, DynamicQRResponseSchema, EnvironmentSchema, IdempotencyManager, InMemoryIdempotencyStore, KRA_SHORTCODE, KesAmountSchema, MAX_QR_SIZE, MIN_AMOUNT, MIN_QR_SIZE, Mpesa, MsisdnSchema, NonEmptyStringSchema, PesafyError, QR_TRANSACTION_CODES, REVERSAL_COMMAND_ID, REVERSAL_ERROR_CODES, REVERSAL_RECEIVER_IDENTIFIER_TYPE, REVERSAL_RESULT_CODES, ReversalRequestSchema, ReversalResponseSchema, SAFARICOM_IPS, STK_PUSH_LIMITS, STK_RESULT_CODES, StkPushRequestSchema, StkPushResponseSchema, StkPushWebhookSchema, StkQueryRequestSchema, StkQueryResponseSchema, TAX_COMMAND_ID, TRANSACTION_STATUS_ERROR_CODES, TRANSACTION_STATUS_RESULT_CODES, TaxRemittanceRequestSchema, TaxRemittanceResponseSchema, TokenManager, TransactionStatusRequestSchema, TransactionStatusResponseSchema, TransactionTypeSchema, UrlSchema, acceptC2BValidation, acknowledgeC2BConfirmation, billManagerOptIn, cancelBulkInvoices, cancelInvoice, createError, encryptSecurityCredential, err, extractAmount, extractPhoneNumber, extractTransactionId, formatSafaricomPhone as formatPhoneNumber, formatSafaricomPhone, generateDynamicQR, generateIdempotencyKey, generateOriginatorConversationId, generateRequestRefId, getAccountBalanceCompletedTime, getAccountBalanceConversationId, getAccountBalanceOriginatorConversationId, getAccountBalanceParam, getAccountBalanceRawBalance, getAccountBalanceReferenceItem, getAccountBalanceTransactionId, getB2BAmount, getB2BBuyGoodsAmount, getB2BBuyGoodsBillReferenceNumber, getB2BBuyGoodsCompletedTime, getB2BBuyGoodsConversationId, getB2BBuyGoodsCurrency, getB2BBuyGoodsDebitAccountBalance, getB2BBuyGoodsDebitPartyAffectedBalance, getB2BBuyGoodsDebitPartyCharges, getB2BBuyGoodsInitiatorBalance, getB2BBuyGoodsOriginatorConversationId, getB2BBuyGoodsQueueTimeoutUrl, getB2BBuyGoodsReceiverName, getB2BBuyGoodsResultCode, getB2BBuyGoodsResultDesc, getB2BBuyGoodsResultParam, getB2BBuyGoodsTransactionId, getB2BConversationId, getB2BPayBillAmount, getB2BPayBillBillReferenceNumber, getB2BPayBillCompletedTime, getB2BPayBillConversationId, getB2BPayBillCurrency, getB2BPayBillDebitAccountBalance, getB2BPayBillDebitPartyAffectedBalance, getB2BPayBillDebitPartyCharges, getB2BPayBillInitiatorBalance, getB2BPayBillOriginatorConversationId, getB2BPayBillReceiverName, getB2BPayBillResultCode, getB2BPayBillResultDesc, getB2BPayBillResultParam, getB2BPayBillTransactionId, getB2BRequestId, getB2BTransactionId, getB2CAmount, getB2CConversationId, getB2CCurrency, getB2CDebitAccountBalance, getB2CDebitPartyCharges, getB2CDisbursementAmount, getB2CDisbursementCompletedTime, getB2CDisbursementConversationId, getB2CDisbursementOriginatorConversationId, getB2CDisbursementReceiptNumber, getB2CDisbursementReceiverName, getB2CDisbursementResultCode, getB2CDisbursementResultDesc, getB2CDisbursementResultParam, getB2CDisbursementTransactionId, getB2CDisbursementUtilityBalance, getB2CDisbursementWorkingBalance, getB2COriginatorConversationId, getB2CReceiverPublicName, getB2CResultDesc, getB2CResultParam, getB2CTransactionCompletedTime, getB2CTransactionId, getC2BAccountRef, getC2BAmount, getC2BCustomerName, getC2BTransactionId, getCallbackValue, getReversalAmount, getReversalCharge, getReversalCompletedTime, getReversalConversationId, getReversalCreditPartyPublicName, getReversalDebitAccountBalance, getReversalDebitPartyPublicName, getReversalOriginalTransactionId, getReversalOriginatorConversationId, getReversalResultCode, getReversalResultDesc, getReversalResultParam, getReversalTransactionId, getTaxAmount, getTaxCompletedTime, getTaxConversationId, getTaxOriginatorConversationId, getTaxReceiverName, getTaxResultCode, getTaxResultDesc, getTaxResultParam, getTaxTransactionId, getTimestamp, getTransactionStatusAmount, getTransactionStatusConversationId, getTransactionStatusCreditPartyName, getTransactionStatusDebitAccountBalance, getTransactionStatusDebitPartyName, getTransactionStatusOriginatorConversationId, getTransactionStatusReceiptNo, getTransactionStatusResultCode, getTransactionStatusResultDesc, getTransactionStatusResultParam, getTransactionStatusStatus, getTransactionStatusTransactionDate, getTransactionStatusTransactionId, handleWebhook, httpRequest, initiateB2BBuyGoods, initiateB2BExpressCheckout, initiateB2BPayBill, initiateB2CDisbursement, initiateB2CPayment, isAccountBalanceSuccess, isB2BBuyGoodsFailure, isB2BBuyGoodsResult, isB2BBuyGoodsSuccess, isB2BCheckoutCallback, isB2BCheckoutCancelled, isB2BCheckoutSuccess, isB2BPayBillFailure, isB2BPayBillResult, isB2BPayBillSuccess, isB2CDisbursementFailure, isB2CDisbursementRecipientRegistered, isB2CDisbursementResult, isB2CDisbursementSuccess, isB2CFailure, isB2CResult, isB2CSuccess, isBuyGoodsPayment, isC2BPayload, isKnownB2BBuyGoodsResultCode, isKnownB2BPayBillResultCode, isKnownB2CDisbursementResultCode, isKnownB2CResultCode, isKnownReversalResultCode, isKnownStkResultCode, isKnownTransactionStatusResultCode, isPaybillPayment, isPesafyError, isReversalFailure, isReversalResult, isReversalSuccess, isStkCallbackSuccess, isSuccessfulCallback, isTaxRemittanceFailure, isTaxRemittanceResult, isTaxRemittanceSuccess, isTransactionStatusFailure, isTransactionStatusResult, isTransactionStatusSuccess, ok, parseAccountBalance, parseStkPushWebhook, queryAccountBalance, queryTransactionStatus, reconcilePayment, registerC2BUrls, rejectC2BValidation, remitTax, requestReversal, retryWithBackoff, sendBulkInvoices, sendSingleInvoice, simulateC2B, toKesAmount, toMsisdn, toNonEmpty, toPaybill, toShortCode, toTill, updateOptIn, validateAmount, validateCpi, validateDynamicQRRequest, validateMerchantName, validateRefNo, validateSize, validateTrxCode, verifyWebhook, verifyWebhookHMAC, verifyWebhookIP };
4197
+ //# sourceMappingURL=index.js.map