pesafy 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -16
- package/LICENSE +21 -0
- package/dist/{express/index.d.cts → adapters/express.d.ts} +23 -138
- package/dist/adapters/express.js +1 -0
- package/dist/adapters/fastify.d.ts +21 -0
- package/dist/adapters/fastify.js +1 -0
- package/dist/adapters/hono.d.ts +27 -0
- package/dist/adapters/hono.js +1 -0
- package/dist/adapters/{nextjs.d.cts → nextjs.d.ts} +3 -30
- package/dist/adapters/nextjs.js +1 -0
- package/dist/{cli/index.mjs → cli.mjs} +10 -9
- package/dist/encryption.mjs +22 -0
- package/dist/{cli/errors-DL4bkMZV.mjs → errors.mjs} +2 -1
- package/dist/{index.d.mts → index.d.ts} +37 -64
- package/dist/index.js +1 -0
- package/dist/{cli/phone-5wwAaQ_8.mjs → phone.mjs} +4 -2
- package/dist/signature-verifier.js +1 -0
- package/dist/{adapters/fastify.d.cts → types.d.ts} +2 -22
- package/dist/{cli/utils-BzEKV3nJ.mjs → utils.mjs} +4 -2
- package/dist/webhook-handler.js +1 -0
- package/package.json +119 -49
- package/dist/adapters/fastify.cjs +0 -1607
- package/dist/adapters/fastify.cjs.map +0 -1
- package/dist/adapters/fastify.d.mts +0 -49
- package/dist/adapters/fastify.mjs +0 -1606
- package/dist/adapters/fastify.mjs.map +0 -1
- package/dist/adapters/hono.cjs +0 -1651
- package/dist/adapters/hono.cjs.map +0 -1
- package/dist/adapters/hono.d.cts +0 -55
- package/dist/adapters/hono.d.mts +0 -55
- package/dist/adapters/hono.mjs +0 -1650
- package/dist/adapters/hono.mjs.map +0 -1
- package/dist/adapters/nextjs.cjs +0 -1655
- package/dist/adapters/nextjs.cjs.map +0 -1
- package/dist/adapters/nextjs.d.mts +0 -79
- package/dist/adapters/nextjs.mjs +0 -1651
- package/dist/adapters/nextjs.mjs.map +0 -1
- package/dist/cli/encryption-BA-_xrIW.mjs +0 -45
- package/dist/cli/encryption-CkSveeYj.cjs +0 -45
- package/dist/cli/errors-Bscvlb7X.cjs +0 -45
- package/dist/cli/index.cjs +0 -559
- package/dist/cli/phone-BD4QmEyl.cjs +0 -21
- package/dist/cli/utils-Dg9Gv_D3.cjs +0 -31
- package/dist/components/react/index.d.mts +0 -1
- package/dist/components/react/index.mjs +0 -1
- package/dist/express/index.cjs +0 -2201
- package/dist/express/index.cjs.map +0 -1
- package/dist/express/index.d.mts +0 -1322
- package/dist/express/index.mjs +0 -2199
- package/dist/express/index.mjs.map +0 -1
- package/dist/index.cjs +0 -2124
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -1907
- package/dist/index.mjs +0 -2050
- package/dist/index.mjs.map +0 -1
- /package/dist/{components/react/index.d.cts → react/index.d.ts} +0 -0
- /package/dist/{components/react/index.cjs → react/index.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# pesafy
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- migrate from cjs/esm to only esm
|
|
8
|
+
|
|
9
|
+
## 0.5.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- reduce size increase speed
|
|
14
|
+
|
|
3
15
|
## 0.5.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -34,14 +46,20 @@
|
|
|
34
46
|
|
|
35
47
|
### Patch Changes
|
|
36
48
|
|
|
37
|
-
- C2B URL registration panel with per-shortcode tracking (Paybill + Buy Goods),
|
|
49
|
+
- C2B URL registration panel with per-shortcode tracking (Paybill + Buy Goods),
|
|
50
|
+
dual registration support, and session-persisted status badges"
|
|
38
51
|
|
|
39
52
|
## 0.3.12
|
|
40
53
|
|
|
41
54
|
### Patch Changes
|
|
42
55
|
|
|
43
|
-
- 047c30f: The payload was always sending BillRefNumber: request.billRefNumber
|
|
44
|
-
|
|
56
|
+
- 047c30f: The payload was always sending BillRefNumber: request.billRefNumber
|
|
57
|
+
?? "". For CustomerBuyGoodsOnline, Daraja treats any BillRefNumber field —
|
|
58
|
+
even "" — as an invalid AccountReference and rejects the request (400 or 503).
|
|
59
|
+
The fix conditionally includes the field only for Paybill
|
|
60
|
+
- fix error handling and URLs only registered for 600977, never for 600000. The
|
|
61
|
+
log shows registerC2BUrls shortCode=600977 but then simulateC2B
|
|
62
|
+
shortCode=600000
|
|
45
63
|
|
|
46
64
|
## 0.3.11
|
|
47
65
|
|
|
@@ -53,7 +71,17 @@
|
|
|
53
71
|
|
|
54
72
|
### Patch Changes
|
|
55
73
|
|
|
56
|
-
- Added Dynamic QR Code support to the pesafy SDK as a new mpesa/dynamic-qr
|
|
74
|
+
- Added Dynamic QR Code support to the pesafy SDK as a new mpesa/dynamic-qr
|
|
75
|
+
module. This introduces three new files — types.ts, generate.ts, and index.ts
|
|
76
|
+
— under src/mpesa/dynamic-qr/, implementing the Safaricom Daraja POST
|
|
77
|
+
/mpesa/qrcode/v1/generate endpoint. The Mpesa class gains a new
|
|
78
|
+
generateDynamicQR() method that handles token retrieval and delegates to the
|
|
79
|
+
internal generateDynamicQR function, which validates all inputs (merchant
|
|
80
|
+
name, reference, amount, transaction code, CPI, and size) before dispatching
|
|
81
|
+
the request via the existing httpRequest utility. Three new public types —
|
|
82
|
+
DynamicQRRequest, DynamicQRResponse, and QRTransactionCode — are exported from
|
|
83
|
+
the root src/index.ts. No breaking changes were introduced; all existing APIs
|
|
84
|
+
remain untouched.
|
|
57
85
|
|
|
58
86
|
## 0.3.9
|
|
59
87
|
|
|
@@ -65,7 +93,15 @@
|
|
|
65
93
|
|
|
66
94
|
### Patch Changes
|
|
67
95
|
|
|
68
|
-
- Fixed four blocking TypeScript compilation errors before prepublish by
|
|
96
|
+
- Fixed four blocking TypeScript compilation errors before prepublish by
|
|
97
|
+
extending the `ErrorCode` union to include `API_ERROR`, `NETWORK_ERROR`, and
|
|
98
|
+
`TIMEOUT`, which resolved type assignment issues in the HTTP layer.
|
|
99
|
+
Consolidated a duplicate `PesafyError` class (previously split between
|
|
100
|
+
`index.ts` and `types.ts`) into a single authoritative implementation that
|
|
101
|
+
preserves all fields, including `requestId`, `toJSON()`, and stack trace
|
|
102
|
+
handling. Also corrected `x-forwarded-for` IP extraction in the Express
|
|
103
|
+
webhook handler using safe optional chaining to prevent potential undefined
|
|
104
|
+
access from the `split()` result.
|
|
69
105
|
|
|
70
106
|
## 0.3.7
|
|
71
107
|
|
|
@@ -89,7 +125,9 @@
|
|
|
89
125
|
|
|
90
126
|
### Patch Changes
|
|
91
127
|
|
|
92
|
-
- f983f64: The /v2/simulate path is the most critical bug — it will cause
|
|
128
|
+
- f983f64: The /v2/simulate path is the most critical bug — it will cause
|
|
129
|
+
persistent 500.003.1001 errors in sandbox just like the /v2/registerurl issue
|
|
130
|
+
you already fixed.
|
|
93
131
|
|
|
94
132
|
## 0.3.3
|
|
95
133
|
|
|
@@ -107,21 +145,67 @@
|
|
|
107
145
|
|
|
108
146
|
### Patch Changes
|
|
109
147
|
|
|
110
|
-
- **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B` where
|
|
148
|
+
- **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B` where
|
|
149
|
+
`CommandID` was hardcoded to `CustomerPayBillOnline`, making
|
|
150
|
+
`CustomerBuyGoodsOnline` (Till) payments impossible. Corrected `BillRefNumber`
|
|
151
|
+
logic so the field is omitted entirely for BuyGoods requests rather than
|
|
152
|
+
defaulting to the string `"default"`, which caused a `400` from Daraja. Fixed
|
|
153
|
+
`Msisdn` being sent as a quoted string instead of a JSON number as the spec
|
|
154
|
+
requires. Removed the phantom `ConversationID` field from both
|
|
155
|
+
`C2BSimulateResponse` and `C2BRegisterUrlResponse` — Daraja never returns this
|
|
156
|
+
field for C2B, only `OriginatorCoversationID`. Extracted phone formatting into
|
|
157
|
+
a shared `src/utils/phone.ts` module with two intentionally separate
|
|
158
|
+
formatters: `formatSafaricomPhone` (strict, Safaricom/Airtel only, for STK
|
|
159
|
+
Push) and `formatKenyanMsisdn` (permissive, all Kenyan networks, for C2B and
|
|
160
|
+
B2C), eliminating duplicated logic across modules. Added the `C2BCommandId`
|
|
161
|
+
union type and full callback payload types — `C2BValidationPayload`,
|
|
162
|
+
`C2BConfirmationPayload`, and `C2BCallbackPayload` — to enable type-safe
|
|
163
|
+
webhook handlers.
|
|
111
164
|
- fbfa311: fix url patch and add elemt accountref
|
|
112
|
-
- aa9c176: **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B`
|
|
165
|
+
- aa9c176: **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B`
|
|
166
|
+
where `CommandID` was hardcoded to `CustomerPayBillOnline`, making
|
|
167
|
+
`CustomerBuyGoodsOnline` (Till) payments impossible. Corrected `BillRefNumber`
|
|
168
|
+
logic so the field is omitted entirely for BuyGoods requests rather than
|
|
169
|
+
defaulting to the string `"default"`, which caused a `400` from Daraja. Fixed
|
|
170
|
+
`Msisdn` being sent as a quoted string instead of a JSON number as the spec
|
|
171
|
+
requires. Removed the phantom `ConversationID` field from both
|
|
172
|
+
`C2BSimulateResponse` and `C2BRegisterUrlResponse` — Daraja never returns this
|
|
173
|
+
field for C2B, only `OriginatorCoversationID`. Extracted phone formatting into
|
|
174
|
+
a shared `src/utils/phone.ts` module with two intentionally separate
|
|
175
|
+
formatters: `formatSafaricomPhone` (strict, Safaricom/Airtel only, for STK
|
|
176
|
+
Push) and `formatKenyanMsisdn` (permissive, all Kenyan networks, for C2B and
|
|
177
|
+
B2C), eliminating duplicated logic across modules. Added the `C2BCommandId`
|
|
178
|
+
union type and full callback payload types — `C2BValidationPayload`,
|
|
179
|
+
`C2BConfirmationPayload`, and `C2BCallbackPayload` — to enable type-safe
|
|
180
|
+
webhook handlers.
|
|
113
181
|
|
|
114
182
|
## 0.3.0
|
|
115
183
|
|
|
116
184
|
### Minor Changes
|
|
117
185
|
|
|
118
|
-
- fe95c2b: The C2B M-Pesa implementation in the pesafy package has been
|
|
186
|
+
- fe95c2b: The C2B M-Pesa implementation in the pesafy package has been
|
|
187
|
+
comprehensively overhauled for full Daraja C2B v2 compliance, including a
|
|
188
|
+
complete rewrite of types.ts with all 13 callback fields (C2BCallbackPayload),
|
|
189
|
+
validation responses, rejection codes, and command/response unions; fixes to
|
|
190
|
+
register-url.ts ensuring ValidationURL is always sent (defaulting to
|
|
191
|
+
confirmationUrl); enhanced simulate.ts supporting both CustomerPayBillOnline
|
|
192
|
+
and CustomerBuyGoodsOnline with proper BillRefNumber handling, amount
|
|
193
|
+
validation, and Msisdn casting; new c2b index exports; root index public
|
|
194
|
+
exports; and improved http client error reporting. Dashboard updates add
|
|
195
|
+
c2bRegisterUrls and c2bSimulate actions in mpesaActions.ts, fix http.ts
|
|
196
|
+
webhook handlers with 6 missing fields (TransactionType, TransTime, etc.),
|
|
197
|
+
upsert logic via createTransaction, a new getByShortCode query in
|
|
198
|
+
businesses.ts, and a complete C2B tab in PaymentsPage.tsx featuring URL
|
|
199
|
+
registration, simulation forms, and unified transactions—addressing prior gaps
|
|
200
|
+
like hardcoded commands, missing UI, incomplete types, and non-upserting
|
|
201
|
+
handlers.
|
|
119
202
|
|
|
120
203
|
## 0.2.4
|
|
121
204
|
|
|
122
205
|
### Patch Changes
|
|
123
206
|
|
|
124
|
-
- 4a535ef: Request failed with status 500 but swallows the actual Daraja error
|
|
207
|
+
- 4a535ef: Request failed with status 500 but swallows the actual Daraja error
|
|
208
|
+
body — it parses it into data but never includes it in the error message.
|
|
125
209
|
|
|
126
210
|
## 0.2.3
|
|
127
211
|
|
|
@@ -146,14 +230,14 @@
|
|
|
146
230
|
### Minor Changes
|
|
147
231
|
|
|
148
232
|
- Add M-Pesa Express (STK Push) support
|
|
149
|
-
- Add `processStkPush` to initiate STK Push payment prompts on a
|
|
150
|
-
|
|
151
|
-
- Add `queryStkPush` to check the status of an STK Push transaction
|
|
152
|
-
|
|
233
|
+
- Add `processStkPush` to initiate STK Push payment prompts on a customer's
|
|
234
|
+
phone via POST /mpesa/stkpush/v1/processrequest
|
|
235
|
+
- Add `queryStkPush` to check the status of an STK Push transaction via POST
|
|
236
|
+
/mpesa/stkpushquery/v1/query
|
|
153
237
|
- Add `StkPushRequest`, `StkPushResponse`, `StkQueryRequest`,
|
|
154
238
|
`StkQueryResponse` and `TransactionType` types
|
|
155
|
-
- Add `formatPhoneNumber` utility to normalize Kenyan phone numbers
|
|
156
|
-
|
|
239
|
+
- Add `formatPhoneNumber` utility to normalize Kenyan phone numbers to 254
|
|
240
|
+
format
|
|
157
241
|
- Add `getStkPushPassword` to generate Base64(Shortcode+Passkey+Timestamp)
|
|
158
242
|
- Add `getTimestamp` helper for Daraja-formatted timestamps
|
|
159
243
|
- Export all STK Push types and utilities from the package root
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lewis Odero
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { n as MpesaConfig, t as Environment } from "../types.js";
|
|
1
2
|
import { Router } from "express";
|
|
2
3
|
|
|
3
4
|
//#region src/mpesa/stk-push/types.d.ts
|
|
@@ -13,7 +14,7 @@ import { Router } from "express";
|
|
|
13
14
|
* CustomerPayBillOnline → Paybill numbers (PartyB = shortcode)
|
|
14
15
|
* CustomerBuyGoodsOnline → Till numbers (PartyB = till number)
|
|
15
16
|
*/
|
|
16
|
-
type TransactionType =
|
|
17
|
+
type TransactionType = 'CustomerPayBillOnline' | 'CustomerBuyGoodsOnline';
|
|
17
18
|
interface StkPushRequest {
|
|
18
19
|
/** Transaction amount (minimum KES 1, must round to a whole number ≥ 1) */
|
|
19
20
|
amount: number;
|
|
@@ -138,7 +139,7 @@ interface TransactionStatusRequest {
|
|
|
138
139
|
* "4" = Organisation ShortCode (Paybill / B2C) ← most common
|
|
139
140
|
* Daraja field: IdentifierType
|
|
140
141
|
*/
|
|
141
|
-
identifierType:
|
|
142
|
+
identifierType: '1' | '2' | '4';
|
|
142
143
|
/**
|
|
143
144
|
* URL where Safaricom POSTs the final result.
|
|
144
145
|
* Must be publicly accessible.
|
|
@@ -168,34 +169,6 @@ interface TransactionStatusResponse {
|
|
|
168
169
|
OriginatorConversationID?: string;
|
|
169
170
|
}
|
|
170
171
|
//#endregion
|
|
171
|
-
//#region src/mpesa/types.d.ts
|
|
172
|
-
/**
|
|
173
|
-
* Core M-Pesa / Daraja configuration types
|
|
174
|
-
*/
|
|
175
|
-
type Environment = "sandbox" | "production";
|
|
176
|
-
interface MpesaConfig {
|
|
177
|
-
consumerKey: string;
|
|
178
|
-
consumerSecret: string;
|
|
179
|
-
environment: Environment;
|
|
180
|
-
lipaNaMpesaShortCode?: string;
|
|
181
|
-
lipaNaMpesaPassKey?: string;
|
|
182
|
-
initiatorName?: string;
|
|
183
|
-
initiatorPassword?: string;
|
|
184
|
-
certificatePath?: string;
|
|
185
|
-
certificatePem?: string;
|
|
186
|
-
/**
|
|
187
|
-
* Pre-computed base64 SecurityCredential — skips RSA encryption.
|
|
188
|
-
* Use when you encrypt at startup outside the library.
|
|
189
|
-
*/
|
|
190
|
-
securityCredential?: string;
|
|
191
|
-
/** Override default retry count (4) for all API calls */
|
|
192
|
-
retries?: number;
|
|
193
|
-
/** Override default base retry delay in ms (2000) */
|
|
194
|
-
retryDelay?: number;
|
|
195
|
-
/** Override default per-request timeout in ms (30000) */
|
|
196
|
-
timeout?: number;
|
|
197
|
-
}
|
|
198
|
-
//#endregion
|
|
199
172
|
//#region src/utils/errors/index.d.ts
|
|
200
173
|
/**
|
|
201
174
|
* Pesafy error types — single source of truth.
|
|
@@ -288,7 +261,7 @@ interface AccountBalanceRequest {
|
|
|
288
261
|
* "4" = Organisation ShortCode (most common — Paybill/B2C)
|
|
289
262
|
* Daraja field: IdentifierType
|
|
290
263
|
*/
|
|
291
|
-
identifierType:
|
|
264
|
+
identifierType: '1' | '2' | '4';
|
|
292
265
|
/**
|
|
293
266
|
* URL where Safaricom POSTs the balance result.
|
|
294
267
|
* Must be publicly accessible. HTTPS required in production.
|
|
@@ -447,7 +420,7 @@ type B2BExpressCheckoutCallback = B2BExpressCheckoutCallbackSuccess | B2BExpress
|
|
|
447
420
|
* PromotionPayment — Payment of promotions/bonuses
|
|
448
421
|
* BusinessPayToBulk — Load funds to a B2C shortcode for bulk disbursement
|
|
449
422
|
*/
|
|
450
|
-
type B2CCommandID =
|
|
423
|
+
type B2CCommandID = 'BusinessPayment' | 'SalaryPayment' | 'PromotionPayment' | 'BusinessPayToBulk';
|
|
451
424
|
interface B2CRequest {
|
|
452
425
|
/**
|
|
453
426
|
* The type of transaction. Use "BusinessPayToBulk" for account top-up.
|
|
@@ -485,14 +458,14 @@ interface B2CRequest {
|
|
|
485
458
|
* Daraja field: SenderIdentifierType
|
|
486
459
|
* Default: "4"
|
|
487
460
|
*/
|
|
488
|
-
senderIdentifierType?:
|
|
461
|
+
senderIdentifierType?: '4';
|
|
489
462
|
/**
|
|
490
463
|
* Type of the receiver (PartyB) identifier.
|
|
491
464
|
* For this API, only "4" (Organisation ShortCode) is allowed.
|
|
492
465
|
* Daraja field: RecieverIdentifierType
|
|
493
466
|
* Default: "4"
|
|
494
467
|
*/
|
|
495
|
-
receiverIdentifierType?:
|
|
468
|
+
receiverIdentifierType?: '4';
|
|
496
469
|
/**
|
|
497
470
|
* A reference for this transaction (e.g. invoice number, batch reference).
|
|
498
471
|
* Daraja field: AccountReference
|
|
@@ -540,7 +513,7 @@ interface B2CResponse {
|
|
|
540
513
|
* - Any unknown future key Daraja may return is still accepted.
|
|
541
514
|
* - The `no-redundant-type-constituents` ESLint rule is not triggered.
|
|
542
515
|
*/
|
|
543
|
-
type B2CResultParameterKey =
|
|
516
|
+
type B2CResultParameterKey = 'DebitAccountBalance' | 'Amount' | 'DebitPartyAffectedAccountBalance' | 'TransCompletedTime' | 'DebitPartyCharges' | 'ReceiverPartyPublicName' | 'Currency' | 'InitiatorAccountCurrentBalance' | 'B2CRecipientIsRegisteredCustomer' | 'B2CChargesPaidAccountAvailableFunds' | 'B2CWorkingAccountAvailableFunds' | 'B2CUtilityAccountAvailableFunds' | (string & {});
|
|
544
517
|
interface B2CResultParameter {
|
|
545
518
|
Key: B2CResultParameterKey;
|
|
546
519
|
Value: string | number;
|
|
@@ -592,7 +565,7 @@ interface BillManagerOptInRequest {
|
|
|
592
565
|
/** Your logo URL (public HTTPS link) */
|
|
593
566
|
officialContact: string;
|
|
594
567
|
/** Sender name shown on push notifications */
|
|
595
|
-
sendReminders:
|
|
568
|
+
sendReminders: '1' | '0';
|
|
596
569
|
/** Logo URL */
|
|
597
570
|
logo?: string;
|
|
598
571
|
/** Callback URL for payment confirmations */
|
|
@@ -660,7 +633,7 @@ interface BillManagerCancelInvoiceResponse {
|
|
|
660
633
|
* Ref: Customer To Business (C2B) — Daraja Developer Portal
|
|
661
634
|
*/
|
|
662
635
|
/** C2B API version. v2 is recommended; v1 sends SHA256-hashed MSISDN. */
|
|
663
|
-
type C2BApiVersion =
|
|
636
|
+
type C2BApiVersion = 'v1' | 'v2';
|
|
664
637
|
/**
|
|
665
638
|
* What M-PESA should do if your Validation URL is unreachable or times out.
|
|
666
639
|
* "Completed" — M-PESA automatically completes the transaction.
|
|
@@ -669,7 +642,7 @@ type C2BApiVersion = "v1" | "v2";
|
|
|
669
642
|
* NOTE: Must be exactly "Completed" or "Cancelled" (sentence case, no typos).
|
|
670
643
|
* Daraja docs: "the words Cancelled/Completed must be in sentence case and well-spelled."
|
|
671
644
|
*/
|
|
672
|
-
type C2BResponseType =
|
|
645
|
+
type C2BResponseType = 'Completed' | 'Cancelled';
|
|
673
646
|
interface C2BRegisterUrlRequest {
|
|
674
647
|
/**
|
|
675
648
|
* Your M-PESA Paybill or Till shortcode.
|
|
@@ -716,7 +689,7 @@ interface C2BRegisterUrlResponse {
|
|
|
716
689
|
*
|
|
717
690
|
* NOTE: Simulation is ONLY supported in Sandbox, NOT in production.
|
|
718
691
|
*/
|
|
719
|
-
type C2BCommandID =
|
|
692
|
+
type C2BCommandID = 'CustomerPayBillOnline' | 'CustomerBuyGoodsOnline';
|
|
720
693
|
interface C2BSimulateRequest {
|
|
721
694
|
/**
|
|
722
695
|
* Your M-PESA Paybill or Till shortcode.
|
|
@@ -808,7 +781,7 @@ interface C2BValidationPayload {
|
|
|
808
781
|
* "C2B00015" — Invalid Short code
|
|
809
782
|
* "C2B00016" — Other Error
|
|
810
783
|
*/
|
|
811
|
-
type C2BValidationResultCode =
|
|
784
|
+
type C2BValidationResultCode = '0' | 'C2B00011' | 'C2B00012' | 'C2B00013' | 'C2B00014' | 'C2B00015' | 'C2B00016';
|
|
812
785
|
interface C2BValidationResponse {
|
|
813
786
|
/**
|
|
814
787
|
* "0" = Accept the transaction.
|
|
@@ -822,7 +795,7 @@ interface C2BValidationResponse {
|
|
|
822
795
|
* "Accepted" when ResultCode is "0".
|
|
823
796
|
* "Rejected" when ResultCode is a non-zero error code.
|
|
824
797
|
*/
|
|
825
|
-
ResultDesc:
|
|
798
|
+
ResultDesc: 'Accepted' | 'Rejected';
|
|
826
799
|
/**
|
|
827
800
|
* Optional. If set, this value is echoed back in the Confirmation callback
|
|
828
801
|
* as ThirdPartyTransID. Useful for correlating validation → confirmation.
|
|
@@ -883,7 +856,7 @@ interface C2BConfirmationPayload {
|
|
|
883
856
|
* SM: Send Money (Mobile number)
|
|
884
857
|
* SB: Sent to Business (Business number CPI in MSISDN format)
|
|
885
858
|
*/
|
|
886
|
-
type QRTransactionCode =
|
|
859
|
+
type QRTransactionCode = 'BG' | 'WA' | 'PB' | 'SM' | 'SB';
|
|
887
860
|
interface DynamicQRRequest {
|
|
888
861
|
/**
|
|
889
862
|
* Name of the Company / M-Pesa Merchant Name.
|
|
@@ -975,7 +948,7 @@ interface ReversalRequest {
|
|
|
975
948
|
* "4" = Organisation ShortCode
|
|
976
949
|
* Daraja field: RecieverIdentifierType
|
|
977
950
|
*/
|
|
978
|
-
receiverIdentifierType:
|
|
951
|
+
receiverIdentifierType: '1' | '2' | '4';
|
|
979
952
|
/**
|
|
980
953
|
* URL where Safaricom POSTs the reversal result.
|
|
981
954
|
* Daraja field: ResultURL
|
|
@@ -1077,7 +1050,7 @@ interface TaxRemittanceResponse {
|
|
|
1077
1050
|
* - Any unknown future key Daraja may return is still accepted.
|
|
1078
1051
|
* - The `no-redundant-type-constituents` ESLint rule is not triggered.
|
|
1079
1052
|
*/
|
|
1080
|
-
type TaxRemittanceResultParameterKey =
|
|
1053
|
+
type TaxRemittanceResultParameterKey = 'DebitAccountBalance' | 'Amount' | 'DebitPartyAffectedAccountBalance' | 'TransCompletedTime' | 'DebitPartyCharges' | 'ReceiverPartyPublicName' | 'Currency' | 'InitiatorAccountCurrentBalance' | (string & {});
|
|
1081
1054
|
interface TaxRemittanceResultParameter {
|
|
1082
1055
|
Key: TaxRemittanceResultParameterKey;
|
|
1083
1056
|
Value: string | number;
|
|
@@ -1118,9 +1091,9 @@ declare class Mpesa {
|
|
|
1118
1091
|
* Like stkPush() but returns Result<T> instead of throwing.
|
|
1119
1092
|
* Ideal for application-level code that prefers not to use try/catch.
|
|
1120
1093
|
*/
|
|
1121
|
-
stkPushSafe(request: Omit<StkPushRequest,
|
|
1122
|
-
stkPush(request: Omit<StkPushRequest,
|
|
1123
|
-
stkQuery(request: Omit<StkQueryRequest,
|
|
1094
|
+
stkPushSafe(request: Omit<StkPushRequest, 'shortCode' | 'passKey'>): Promise<Result<Awaited<ReturnType<typeof this.stkPush>>>>;
|
|
1095
|
+
stkPush(request: Omit<StkPushRequest, 'shortCode' | 'passKey'>): Promise<StkPushResponse>;
|
|
1096
|
+
stkQuery(request: Omit<StkQueryRequest, 'shortCode' | 'passKey'>): Promise<StkQueryResponse>;
|
|
1124
1097
|
transactionStatus(request: TransactionStatusRequest): Promise<TransactionStatusResponse>;
|
|
1125
1098
|
/**
|
|
1126
1099
|
* Queries the balance of your M-PESA shortcode.
|
|
@@ -1203,25 +1176,14 @@ declare class Mpesa {
|
|
|
1203
1176
|
//#endregion
|
|
1204
1177
|
//#region src/express/index.d.ts
|
|
1205
1178
|
interface MpesaExpressConfig extends MpesaConfig {
|
|
1206
|
-
/**
|
|
1207
|
-
* Full public URL Safaricom will POST STK Push callbacks to.
|
|
1208
|
-
* @example "https://yourdomain.com/api/mpesa/express/callback"
|
|
1209
|
-
*/
|
|
1210
1179
|
callbackUrl: string;
|
|
1211
|
-
/**
|
|
1212
|
-
* Full public URL Safaricom will POST Transaction Status results to.
|
|
1213
|
-
* Required when using transactionStatus routes.
|
|
1214
|
-
*/
|
|
1215
1180
|
resultUrl?: string;
|
|
1216
|
-
/**
|
|
1217
|
-
* Full public URL Safaricom calls on queue timeout.
|
|
1218
|
-
*/
|
|
1219
1181
|
queueTimeOutUrl?: string;
|
|
1220
1182
|
c2bShortCode?: string;
|
|
1221
1183
|
c2bConfirmationUrl?: string;
|
|
1222
1184
|
c2bValidationUrl?: string;
|
|
1223
|
-
c2bResponseType?:
|
|
1224
|
-
c2bApiVersion?:
|
|
1185
|
+
c2bResponseType?: 'Completed' | 'Cancelled';
|
|
1186
|
+
c2bApiVersion?: 'v1' | 'v2';
|
|
1225
1187
|
onC2BValidation?: (payload: C2BValidationPayload) => Promise<C2BValidationResponse> | C2BValidationResponse;
|
|
1226
1188
|
onC2BConfirmation?: (payload: C2BConfirmationPayload) => Promise<void> | void;
|
|
1227
1189
|
taxPartyA?: string;
|
|
@@ -1231,92 +1193,15 @@ interface MpesaExpressConfig extends MpesaConfig {
|
|
|
1231
1193
|
b2bReceiverShortCode?: string;
|
|
1232
1194
|
b2bCallbackUrl?: string;
|
|
1233
1195
|
onB2BCheckoutCallback?: (callback: B2BExpressCheckoutCallback) => Promise<void> | void;
|
|
1234
|
-
/**
|
|
1235
|
-
* Your business shortcode from which B2C money is deducted.
|
|
1236
|
-
* Used as the default partyA for B2C payments when not in the request body.
|
|
1237
|
-
*/
|
|
1238
1196
|
b2cPartyA?: string;
|
|
1239
|
-
/**
|
|
1240
|
-
* Full public URL Safaricom POSTs B2C results to.
|
|
1241
|
-
* Required when using B2C routes.
|
|
1242
|
-
* @example "https://yourdomain.com/api/mpesa/b2c/result"
|
|
1243
|
-
*/
|
|
1244
1197
|
b2cResultUrl?: string;
|
|
1245
|
-
/**
|
|
1246
|
-
* Full public URL Safaricom calls on B2C queue timeout.
|
|
1247
|
-
* Required when using B2C routes.
|
|
1248
|
-
* @example "https://yourdomain.com/api/mpesa/b2c/timeout"
|
|
1249
|
-
*/
|
|
1250
1198
|
b2cQueueTimeOutUrl?: string;
|
|
1251
|
-
/**
|
|
1252
|
-
* Optional hook called when a B2C result arrives at the result URL.
|
|
1253
|
-
* Called for BOTH successful and failed results.
|
|
1254
|
-
*
|
|
1255
|
-
* Fire-and-forget — 200 response to Safaricom is sent immediately.
|
|
1256
|
-
* Errors in this hook are logged but do NOT affect the response.
|
|
1257
|
-
*
|
|
1258
|
-
* @example
|
|
1259
|
-
* onB2CResult: async (result) => {
|
|
1260
|
-
* if (isB2CSuccess(result)) {
|
|
1261
|
-
* await db.disbursements.markCompleted({
|
|
1262
|
-
* transactionId: getB2CTransactionId(result),
|
|
1263
|
-
* amount: getB2CAmount(result),
|
|
1264
|
-
* });
|
|
1265
|
-
* }
|
|
1266
|
-
* }
|
|
1267
|
-
*/
|
|
1268
1199
|
onB2CResult?: (result: B2CResult) => Promise<void> | void;
|
|
1269
|
-
/**
|
|
1270
|
-
* Skip Safaricom IP verification on callback routes.
|
|
1271
|
-
* ONLY set true in local development — never in production.
|
|
1272
|
-
*/
|
|
1273
1200
|
skipIPCheck?: boolean;
|
|
1274
1201
|
}
|
|
1275
1202
|
declare function createMpesaExpressClient(config: MpesaExpressConfig): {
|
|
1276
1203
|
mpesa: Mpesa;
|
|
1277
1204
|
};
|
|
1278
|
-
/**
|
|
1279
|
-
* Attaches all M-Pesa routes to the given Express Router.
|
|
1280
|
-
*
|
|
1281
|
-
* @example
|
|
1282
|
-
* import express from "express";
|
|
1283
|
-
* import {
|
|
1284
|
-
* createMpesaExpressRouter,
|
|
1285
|
-
* acceptC2BValidation,
|
|
1286
|
-
* rejectC2BValidation,
|
|
1287
|
-
* isB2CSuccess,
|
|
1288
|
-
* getB2CTransactionId,
|
|
1289
|
-
* getB2CAmount,
|
|
1290
|
-
* } from "pesafy/express";
|
|
1291
|
-
*
|
|
1292
|
-
* const router = express.Router();
|
|
1293
|
-
* createMpesaExpressRouter(router, {
|
|
1294
|
-
* consumerKey: process.env.MPESA_CONSUMER_KEY!,
|
|
1295
|
-
* consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
|
|
1296
|
-
* environment: "sandbox",
|
|
1297
|
-
* lipaNaMpesaShortCode: "174379",
|
|
1298
|
-
* lipaNaMpesaPassKey: "bfb279...",
|
|
1299
|
-
* callbackUrl: "https://yourdomain.com/mpesa/express/callback",
|
|
1300
|
-
* initiatorName: "testapi",
|
|
1301
|
-
* initiatorPassword: "Safaricom123!",
|
|
1302
|
-
* certificatePath: "./SandboxCertificate.cer",
|
|
1303
|
-
* // B2C
|
|
1304
|
-
* b2cPartyA: "600979",
|
|
1305
|
-
* b2cResultUrl: "https://yourdomain.com/mpesa/b2c/result",
|
|
1306
|
-
* b2cQueueTimeOutUrl: "https://yourdomain.com/mpesa/b2c/timeout",
|
|
1307
|
-
* onB2CResult: async (result) => {
|
|
1308
|
-
* if (isB2CSuccess(result)) {
|
|
1309
|
-
* await db.disbursements.markCompleted({
|
|
1310
|
-
* transactionId: getB2CTransactionId(result),
|
|
1311
|
-
* amount: getB2CAmount(result),
|
|
1312
|
-
* });
|
|
1313
|
-
* }
|
|
1314
|
-
* },
|
|
1315
|
-
* skipIPCheck: true, // local dev only
|
|
1316
|
-
* });
|
|
1317
|
-
* app.use("/api", router);
|
|
1318
|
-
*/
|
|
1319
1205
|
declare function createMpesaExpressRouter(router: Router, config: MpesaExpressConfig): Router;
|
|
1320
1206
|
//#endregion
|
|
1321
|
-
export { MpesaExpressConfig, createMpesaExpressClient, createMpesaExpressRouter };
|
|
1322
|
-
//# sourceMappingURL=index.d.cts.map
|
|
1207
|
+
export { MpesaExpressConfig, createMpesaExpressClient, createMpesaExpressRouter };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as e,n as t,r as n}from"../signature-verifier.js";import{a as r,i,n as a,r as o,t as s}from"../webhook-handler.js";function c(e){return e.resultCode===`0`}function l(e){return e.resultCode===`4001`}function u(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 d(e){return c(e)?e.transactionId??null:null}function f(e){return Number(e.amount)}function p(e){return c(e)?e.conversationID??null:null}function m(e){if(!e||typeof e!=`object`)return!1;let t=e;if(!t.Result||typeof t.Result!=`object`)return!1;let n=t.Result;return typeof n.ResultCode==`number`&&typeof n.ConversationID==`string`}function h(e){return e.Result.ResultCode===0}function g(e){return e.Result.TransactionID??null}function _(e){return e.Result.ConversationID}function v(e){return e.Result.OriginatorConversationID}function y(e){let t=b(e,`Amount`);return t===void 0?null:Number(t)}function b(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function x(e){return{ResultCode:`0`,ResultDesc:`Accepted`,...e?{ThirdPartyTransID:e}:{}}}function S(t){if(!t.consumerKey||!t.consumerSecret)throw new e({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required`});if(!t.lipaNaMpesaShortCode||!t.lipaNaMpesaPassKey)throw new e({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push`});if(!t.callbackUrl)throw new e({code:`VALIDATION_ERROR`,message:`callbackUrl is required for STK Push callbacks`});return{mpesa:new n(t)}}function C(t,n){if(n instanceof e){let e=n.statusCode??400;t.status(e).json({error:n.code,message:n.message,statusCode:e});return}t.status(500).json({error:`REQUEST_FAILED`,message:`An unexpected error occurred while processing the M-Pesa request`})}function w(e){return e.headers[`x-forwarded-for`]?.split(`,`)[0]?.trim()??e.ip??``}function T(n,b){let{mpesa:T}=S(b);return n.post(`/mpesa/express/stk-push`,async(t,n,r)=>{try{let r=t.body;if(!r||typeof r.amount!=`number`||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});let i=await T.stkPush({amount:r.amount,phoneNumber:r.phoneNumber,callbackUrl:b.callbackUrl,accountReference:r.accountReference??`PESAFY-${Date.now().toString(36).toUpperCase()}`,transactionDesc:r.transactionDesc??`Payment`,...r.transactionType===void 0?{}:{transactionType:r.transactionType},...r.partyB===void 0?{}:{partyB:r.partyB}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/express/stk-query`,async(t,n,r)=>{try{let r=t.body;if(!r?.checkoutRequestId)throw new e({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});let i=await T.stkQuery({checkoutRequestId:r.checkoutRequestId});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/express/callback`,(e,t)=>{let n=w(e),c=i(e.body,{requestIP:n,...b.skipIPCheck===void 0?{}:{skipIPCheck:b.skipIPCheck}});if(!c.success)return console.error(`[pesafy] STK Push webhook rejected:`,c.error),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let l=c.data;return r(l)?console.info(`[pesafy] STK Push success:`,{receiptNumber:o(l),amount:s(l),phone:a(l)}):console.warn(`[pesafy] STK Push failed:`,{resultCode:l.Body.stkCallback.ResultCode,resultDesc:l.Body.stkCallback.ResultDesc}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/transaction-status/query`,async(t,n,r)=>{try{if(!b.resultUrl||!b.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl must be set in config to use transaction status routes`});let r=t.body;if(!r?.transactionId)throw new e({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!r.partyA)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required`});if(!r.identifierType)throw new e({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" | "2" | "4"`});let i=await T.transactionStatus({transactionId:r.transactionId,partyA:r.partyA,identifierType:r.identifierType,resultUrl:b.resultUrl,queueTimeOutUrl:b.queueTimeOutUrl,...r.remarks===void 0?{}:{remarks:r.remarks},...r.occasion===void 0?{}:{occasion:r.occasion}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/transaction-status/result`,(e,t)=>{let n=e.body?.Result;n&&(n.ResultCode===0?console.info(`[pesafy] Transaction Status result (success):`,{transactionId:n.TransactionID,conversationId:n.ConversationID,resultDesc:n.ResultDesc}):console.warn(`[pesafy] Transaction Status result (failed):`,{resultCode:n.ResultCode,resultDesc:n.ResultDesc,transactionId:n.TransactionID})),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/c2b/register-url`,async(t,n,r)=>{try{let r=t.body,i=r.shortCode??b.c2bShortCode,a=r.confirmationUrl??b.c2bConfirmationUrl,o=r.validationUrl??b.c2bValidationUrl,s=r.responseType??b.c2bResponseType??`Completed`,c=r.apiVersion??b.c2bApiVersion??`v2`;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!a)throw new e({code:`VALIDATION_ERROR`,message:`confirmationUrl is required`});if(!o)throw new e({code:`VALIDATION_ERROR`,message:`validationUrl is required`});let l=await T.registerC2BUrls({shortCode:i,responseType:s,confirmationUrl:a,validationUrl:o,apiVersion:c});n.status(200).json(l)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/c2b/simulate`,async(t,n,r)=>{try{let r=t.body;if(!r?.commandId)throw new e({code:`VALIDATION_ERROR`,message:`commandId is required: "CustomerPayBillOnline" | "CustomerBuyGoodsOnline"`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.msisdn)throw new e({code:`VALIDATION_ERROR`,message:`msisdn is required`});let i=await T.simulateC2B({shortCode:r.shortCode??b.c2bShortCode??``,commandId:r.commandId,amount:r.amount,msisdn:r.msisdn,apiVersion:b.c2bApiVersion??`v2`,...r.billRefNumber===void 0?{}:{billRefNumber:r.billRefNumber}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/c2b/validation`,async(e,n)=>{if(!b.skipIPCheck){let r=w(e);if(!t(r))return console.error(`[pesafy] C2B validation rejected — IP not in Safaricom whitelist:`,r),n.status(200).json({ResultCode:`0`,ResultDesc:`Accepted`})}let r=e.body;try{let e;return e=b.onC2BValidation?await b.onC2BValidation(r):x(),console.info(`[pesafy] C2B validation response:`,{transactionId:r.TransID,amount:r.TransAmount,billRef:r.BillRefNumber,resultCode:e.ResultCode}),n.status(200).json(e)}catch(e){return console.error(`[pesafy] C2B validation hook threw an error:`,e),n.status(200).json({ResultCode:`0`,ResultDesc:`Accepted`})}}),n.post(`/mpesa/c2b/confirmation`,(e,t)=>{let n=e.body;console.info(`[pesafy] C2B confirmation received:`,{transactionId:n.TransID,amount:n.TransAmount,shortCode:n.BusinessShortCode,billRef:n.BillRefNumber,transactionType:n.TransactionType,transTime:n.TransTime,balance:n.OrgAccountBalance}),b.onC2BConfirmation&&Promise.resolve(b.onC2BConfirmation(n)).catch(e=>{console.error(`[pesafy] C2B confirmation hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Success`})}),n.post(`/mpesa/tax/remit`,async(t,n,r)=>{try{if(!b.taxResultUrl||!b.taxQueueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`taxResultUrl and taxQueueTimeOutUrl must be set in config to use tax remittance routes`});let r=t.body;if(!r||typeof r.amount!=`number`||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.accountReference)throw new e({code:`VALIDATION_ERROR`,message:`accountReference is required — the KRA PRN`});let i=r.partyA??b.taxPartyA??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required — set taxPartyA in config or provide in request body`});let a=await T.remitTax({amount:r.amount,partyA:i,accountReference:r.accountReference,resultUrl:b.taxResultUrl,queueTimeOutUrl:b.taxQueueTimeOutUrl,...r.remarks===void 0?{}:{remarks:r.remarks}});n.status(200).json(a)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/tax/result`,(e,t)=>{let n=e.body,r=n?.Result;r&&(r.ResultCode===0?console.info(`[pesafy] Tax Remittance result (success):`,{transactionId:r.TransactionID,conversationId:r.ConversationID,resultDesc:r.ResultDesc}):console.warn(`[pesafy] Tax Remittance result (failed):`,{resultCode:r.ResultCode,resultDesc:r.ResultDesc,transactionId:r.TransactionID})),b.onTaxRemittanceResult&&n&&Promise.resolve(b.onTaxRemittanceResult(n)).catch(e=>{console.error(`[pesafy] Tax Remittance result hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/b2b/checkout`,async(t,n,r)=>{try{let r=t.body;if(!r?.primaryShortCode)throw new e({code:`VALIDATION_ERROR`,message:`primaryShortCode is required`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.paymentRef)throw new e({code:`VALIDATION_ERROR`,message:`paymentRef is required`});if(!r.partnerName)throw new e({code:`VALIDATION_ERROR`,message:`partnerName is required`});let i=r.receiverShortCode??b.b2bReceiverShortCode??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — set b2bReceiverShortCode in config or provide in request body`});let a=r.callbackUrl??b.b2bCallbackUrl??``;if(!a)throw new e({code:`VALIDATION_ERROR`,message:`callbackUrl is required — set b2bCallbackUrl in config or provide in request body`});let o=await T.b2bExpressCheckout({primaryShortCode:r.primaryShortCode,receiverShortCode:i,amount:r.amount,paymentRef:r.paymentRef,callbackUrl:a,partnerName:r.partnerName,...r.requestRefId===void 0?{}:{requestRefId:r.requestRefId}});n.status(200).json(o)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/b2b/callback`,(e,t)=>{let n=e.body;if(!u(n))return console.error(`[pesafy] B2B callback received unrecognised payload:`,JSON.stringify(n).slice(0,200)),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let r=n;return c(r)?console.info(`[pesafy] B2B Express Checkout success:`,{transactionId:d(r),conversationId:p(r),amount:f(r),requestId:r.requestId,status:r.status}):l(r)?console.warn(`[pesafy] B2B Express Checkout cancelled by merchant:`,{resultCode:r.resultCode,resultDesc:r.resultDesc,requestId:r.requestId,amount:f(r)}):console.warn(`[pesafy] B2B Express Checkout unknown result:`,{resultCode:r.resultCode,resultDesc:r.resultDesc}),b.onB2BCheckoutCallback&&Promise.resolve(b.onB2BCheckoutCallback(r)).catch(e=>{console.error(`[pesafy] B2B callback hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/b2c/payment`,async(t,n,r)=>{try{if(!b.b2cResultUrl||!b.b2cQueueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`b2cResultUrl and b2cQueueTimeOutUrl must be set in config to use B2C routes`});let r=t.body;if(!r?.commandId)throw new e({code:`VALIDATION_ERROR`,message:`commandId is required: "BusinessPayToBulk" | "BusinessPayment" | "SalaryPayment" | "PromotionPayment"`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.partyB)throw new e({code:`VALIDATION_ERROR`,message:`partyB is required — the recipient shortcode (BusinessPayToBulk) or customer MSISDN`});if(!r.accountReference)throw new e({code:`VALIDATION_ERROR`,message:`accountReference is required`});let i=r.partyA??b.b2cPartyA??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required — set b2cPartyA in config or provide in request body`});let a=await T.b2cPayment({commandId:r.commandId,amount:r.amount,partyA:i,partyB:r.partyB,accountReference:r.accountReference,resultUrl:r.resultUrl??b.b2cResultUrl,queueTimeOutUrl:r.queueTimeOutUrl??b.b2cQueueTimeOutUrl,...r.requester===void 0?{}:{requester:r.requester},...r.remarks===void 0?{}:{remarks:r.remarks}});n.status(200).json(a)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/b2c/result`,(e,t)=>{let n=e.body;if(!m(n))return console.error(`[pesafy] B2C result received unrecognised payload:`,JSON.stringify(n).slice(0,200)),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let r=n;return h(r)?console.info(`[pesafy] B2C payment result (success):`,{transactionId:g(r),conversationId:_(r),originatorConversationId:v(r),amount:y(r),resultDesc:r.Result.ResultDesc}):console.warn(`[pesafy] B2C payment result (failed):`,{resultCode:r.Result.ResultCode,resultDesc:r.Result.ResultDesc,transactionId:r.Result.TransactionID,conversationId:_(r),originatorConversationId:v(r)}),b.onB2CResult&&Promise.resolve(b.onB2CResult(r)).catch(e=>{console.error(`[pesafy] B2C result hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n}export{S as createMpesaExpressClient,T as createMpesaExpressRouter};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { n as MpesaConfig } from "../types.js";
|
|
2
|
+
import { FastifyInstance } from "fastify";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/fastify.d.ts
|
|
5
|
+
interface MpesaFastifyConfig extends MpesaConfig {
|
|
6
|
+
callbackUrl: string;
|
|
7
|
+
resultUrl?: string;
|
|
8
|
+
queueTimeOutUrl?: string;
|
|
9
|
+
skipIPCheck?: boolean;
|
|
10
|
+
onStkSuccess?: (data: {
|
|
11
|
+
receiptNumber: string | null;
|
|
12
|
+
amount: number | null;
|
|
13
|
+
phone: string | null;
|
|
14
|
+
}) => Promise<void> | void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Registers all M-PESA Fastify routes.
|
|
18
|
+
*/
|
|
19
|
+
declare function registerMpesaRoutes(app: FastifyInstance, config: MpesaFastifyConfig): Promise<void>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { MpesaFastifyConfig, registerMpesaRoutes };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as e,n as t,r as n}from"../signature-verifier.js";function r(e){let t=e.headers[`x-forwarded-for`];return typeof t==`string`?t.split(`,`)[0]?.trim()??``:e.ip??``}function i(t,n){n instanceof e?t.status(n.statusCode??400).send({error:n.code,message:n.message}):t.status(500).send({error:`INTERNAL_ERROR`})}async function a(a,o){let s=new n(o);a.post(`/mpesa/stk-push`,async(t,n)=>{try{let n=t.body;if(!n.amount||n.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!n.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});return await s.stkPush({amount:n.amount,phoneNumber:n.phoneNumber,callbackUrl:o.callbackUrl,accountReference:n.accountReference??`REF-${Date.now().toString(36).toUpperCase()}`,transactionDesc:n.transactionDesc??`Payment`,...n.partyB===void 0?{}:{partyB:n.partyB}})}catch(e){i(n,e);return}}),a.post(`/mpesa/stk-query`,async(t,n)=>{try{let{checkoutRequestId:n}=t.body;if(!n)throw new e({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});return await s.stkQuery({checkoutRequestId:n})}catch(e){i(n,e);return}}),a.post(`/mpesa/callback`,async(e,n)=>{if(!o.skipIPCheck){let n=r(e);n&&!t(n)&&e.log.warn({ip:n},`[pesafy/fastify] Callback from unknown IP`)}let i=e.body?.Body?.stkCallback;if(i&&i.ResultCode===0){let t=i.CallbackMetadata?.Item??[],n=e=>t.find(t=>t.Name===e)?.Value??null;o.onStkSuccess&&Promise.resolve(o.onStkSuccess({receiptNumber:n(`MpesaReceiptNumber`),amount:n(`Amount`),phone:n(`PhoneNumber`)})).catch(e.log.error)}return{ResultCode:0,ResultDesc:`Accepted`}}),a.post(`/mpesa/balance`,async(t,n)=>{try{if(!o.resultUrl||!o.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl must be set in config`});return await s.accountBalance({...t.body,resultUrl:o.resultUrl,queueTimeOutUrl:o.queueTimeOutUrl})}catch(e){i(n,e);return}}),a.post(`/mpesa/balance/result`,e=>(e.log.info(e.body,`[pesafy/fastify] Balance result`),{ResultCode:0,ResultDesc:`Accepted`})),a.post(`/mpesa/reversal`,async(t,n)=>{try{if(!o.resultUrl||!o.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl must be set in config`});return await s.reverseTransaction({...t.body,resultUrl:o.resultUrl,queueTimeOutUrl:o.queueTimeOutUrl})}catch(e){i(n,e);return}}),a.post(`/mpesa/reversal/result`,e=>(e.log.info(e.body,`[pesafy/fastify] Reversal result`),{ResultCode:0,ResultDesc:`Accepted`}))}export{a as registerMpesaRoutes};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { n as MpesaConfig } from "../types.js";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/hono.d.ts
|
|
5
|
+
interface MpesaHonoConfig extends MpesaConfig {
|
|
6
|
+
callbackUrl: string;
|
|
7
|
+
resultUrl?: string;
|
|
8
|
+
queueTimeOutUrl?: string;
|
|
9
|
+
skipIPCheck?: boolean;
|
|
10
|
+
onStkSuccess?: (data: {
|
|
11
|
+
receiptNumber: string | null;
|
|
12
|
+
amount: number | null;
|
|
13
|
+
phone: string | null;
|
|
14
|
+
}) => Promise<void> | void;
|
|
15
|
+
onStkFailure?: (data: {
|
|
16
|
+
resultCode: number;
|
|
17
|
+
resultDesc: string;
|
|
18
|
+
}) => Promise<void> | void;
|
|
19
|
+
onAccountBalanceResult?: (body: unknown) => Promise<void> | void;
|
|
20
|
+
onReversalResult?: (body: unknown) => Promise<void> | void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Mounts M-PESA routes onto a Hono app instance.
|
|
24
|
+
*/
|
|
25
|
+
declare function createMpesaHonoRouter(app: Hono, config: MpesaHonoConfig): void;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { type MpesaConfig, MpesaHonoConfig, createMpesaHonoRouter };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as e,n as t,r as n}from"../signature-verifier.js";import{a as r,n as i,r as a,t as o}from"../webhook-handler.js";function s(e){return e.req.header(`x-forwarded-for`)?.split(`,`)[0]?.trim()??e.req.header(`cf-connecting-ip`)??e.req.header(`x-real-ip`)??``}function c(t,n){if(n instanceof e){let e=n.statusCode??400;return t.json({error:n.code,message:n.message},e)}return t.json({error:`INTERNAL_ERROR`,message:`An unexpected error occurred`},500)}function l(l,u){let d=new n(u);l.post(`/mpesa/express/stk-push`,async t=>{try{let n=await t.req.json();if(!n.amount||n.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!n.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});let r=await d.stkPush({amount:n.amount,phoneNumber:n.phoneNumber,callbackUrl:u.callbackUrl,accountReference:n.accountReference??`REF-${Date.now().toString(36).toUpperCase()}`,transactionDesc:n.transactionDesc??`Payment`,...n.partyB===void 0?{}:{partyB:n.partyB}});return t.json(r)}catch(e){return c(t,e)}}),l.post(`/mpesa/express/stk-query`,async t=>{try{let{checkoutRequestId:n}=await t.req.json();if(!n)throw new e({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});return t.json(await d.stkQuery({checkoutRequestId:n}))}catch(e){return c(t,e)}}),l.post(`/mpesa/express/callback`,async e=>{if(!u.skipIPCheck){let n=s(e);n&&!t(n)&&console.warn(`[pesafy/hono] STK callback from unknown IP:`,n)}let n=await e.req.json(),c=n?.Body?.stkCallback;if(!c)return e.json({ResultCode:0,ResultDesc:`Accepted`});if(r(n)){let e={receiptNumber:a(n),amount:o(n),phone:i(n)};console.info(`[pesafy/hono] STK success:`,e),u.onStkSuccess&&Promise.resolve(u.onStkSuccess(e)).catch(console.error)}else{let e={resultCode:c.ResultCode,resultDesc:c.ResultDesc};console.warn(`[pesafy/hono] STK failed:`,e),u.onStkFailure&&Promise.resolve(u.onStkFailure(e)).catch(console.error)}return e.json({ResultCode:0,ResultDesc:`Accepted`})}),l.post(`/mpesa/balance/query`,async t=>{try{if(!u.resultUrl||!u.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl are required in config for balance queries`});let n=await t.req.json();return t.json(await d.accountBalance({...n,resultUrl:u.resultUrl,queueTimeOutUrl:u.queueTimeOutUrl}))}catch(e){return c(t,e)}}),l.post(`/mpesa/balance/result`,async e=>{let t=await e.req.json();return u.onAccountBalanceResult&&Promise.resolve(u.onAccountBalanceResult(t)).catch(console.error),console.info(`[pesafy/hono] Account balance result:`,JSON.stringify(t).slice(0,300)),e.json({ResultCode:0,ResultDesc:`Accepted`})}),l.post(`/mpesa/reversal/request`,async t=>{try{if(!u.resultUrl||!u.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl are required in config for reversals`});let n={...await t.req.json(),resultUrl:u.resultUrl,queueTimeOutUrl:u.queueTimeOutUrl};return t.json(await d.reverseTransaction(n))}catch(e){return c(t,e)}}),l.post(`/mpesa/reversal/result`,async e=>{let t=await e.req.json();return u.onReversalResult&&Promise.resolve(u.onReversalResult(t)).catch(console.error),console.info(`[pesafy/hono] Reversal result:`,JSON.stringify(t).slice(0,300)),e.json({ResultCode:0,ResultDesc:`Accepted`})})}export{l as createMpesaHonoRouter};
|