pesafy 0.4.1 → 0.4.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/dist/index.cjs +169 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -28
- package/dist/index.d.ts +264 -28
- package/dist/index.js +159 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var crypto = require('crypto');
|
|
3
|
+
var crypto$1 = require('crypto');
|
|
4
4
|
|
|
5
5
|
// Pesafy - Payment Gateway Library
|
|
6
6
|
// https://github.com/levos-snr/pesafy
|
|
@@ -45,11 +45,11 @@ function createError(options) {
|
|
|
45
45
|
function encryptSecurityCredential(initiatorPassword, certificatePem) {
|
|
46
46
|
try {
|
|
47
47
|
const passwordBuffer = Buffer.from(initiatorPassword, "utf-8");
|
|
48
|
-
const encrypted = crypto.publicEncrypt(
|
|
48
|
+
const encrypted = crypto$1.publicEncrypt(
|
|
49
49
|
{
|
|
50
50
|
key: certificatePem,
|
|
51
51
|
// RSA_PKCS1_PADDING = 1 (NOT RSA_PKCS1_OAEP_PADDING = 4)
|
|
52
|
-
padding: crypto.constants.RSA_PKCS1_PADDING
|
|
52
|
+
padding: crypto$1.constants.RSA_PKCS1_PADDING
|
|
53
53
|
},
|
|
54
54
|
passwordBuffer
|
|
55
55
|
);
|
|
@@ -217,6 +217,101 @@ var TokenManager = class {
|
|
|
217
217
|
}
|
|
218
218
|
};
|
|
219
219
|
|
|
220
|
+
// src/mpesa/b2b-express-checkout/initiate.ts
|
|
221
|
+
function generateRequestRefId() {
|
|
222
|
+
try {
|
|
223
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
224
|
+
return crypto.randomUUID();
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;
|
|
229
|
+
}
|
|
230
|
+
async function initiateB2BExpressCheckout(baseUrl, accessToken, request) {
|
|
231
|
+
if (!request.primaryShortCode?.trim()) {
|
|
232
|
+
throw createError({
|
|
233
|
+
code: "VALIDATION_ERROR",
|
|
234
|
+
message: "primaryShortCode is required \u2014 the merchant's till number (debit party)."
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (!request.receiverShortCode?.trim()) {
|
|
238
|
+
throw createError({
|
|
239
|
+
code: "VALIDATION_ERROR",
|
|
240
|
+
message: "receiverShortCode is required \u2014 the vendor's Paybill account (credit party)."
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const amount = Math.round(request.amount);
|
|
244
|
+
if (!Number.isFinite(amount) || amount < 1) {
|
|
245
|
+
throw createError({
|
|
246
|
+
code: "VALIDATION_ERROR",
|
|
247
|
+
message: `amount must be a whole number \u2265 1 (got ${request.amount} which rounds to ${amount}).`
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (!request.paymentRef?.trim()) {
|
|
251
|
+
throw createError({
|
|
252
|
+
code: "VALIDATION_ERROR",
|
|
253
|
+
message: "paymentRef is required \u2014 shown in the merchant's USSD prompt as the payment reference."
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (!request.callbackUrl?.trim()) {
|
|
257
|
+
throw createError({
|
|
258
|
+
code: "VALIDATION_ERROR",
|
|
259
|
+
message: "callbackUrl is required \u2014 Safaricom POSTs the transaction result here."
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (!request.partnerName?.trim()) {
|
|
263
|
+
throw createError({
|
|
264
|
+
code: "VALIDATION_ERROR",
|
|
265
|
+
message: "partnerName is required \u2014 your friendly name shown in the merchant's USSD prompt."
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const payload = {
|
|
269
|
+
primaryShortCode: String(request.primaryShortCode),
|
|
270
|
+
receiverShortCode: String(request.receiverShortCode),
|
|
271
|
+
amount: String(amount),
|
|
272
|
+
paymentRef: request.paymentRef,
|
|
273
|
+
callbackUrl: request.callbackUrl,
|
|
274
|
+
partnerName: request.partnerName,
|
|
275
|
+
RequestRefID: request.requestRefId ?? generateRequestRefId()
|
|
276
|
+
};
|
|
277
|
+
const { data } = await httpRequest(
|
|
278
|
+
`${baseUrl}/v1/ussdpush/get-msisdn`,
|
|
279
|
+
{
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
282
|
+
body: payload
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
return data;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/mpesa/b2b-express-checkout/webhooks.ts
|
|
289
|
+
function isB2BCheckoutSuccess(callback) {
|
|
290
|
+
return callback.resultCode === "0";
|
|
291
|
+
}
|
|
292
|
+
function isB2BCheckoutCancelled(callback) {
|
|
293
|
+
return callback.resultCode === "4001";
|
|
294
|
+
}
|
|
295
|
+
function isB2BCheckoutCallback(body) {
|
|
296
|
+
if (!body || typeof body !== "object") return false;
|
|
297
|
+
const b = body;
|
|
298
|
+
return typeof b["resultCode"] === "string" && typeof b["requestId"] === "string" && typeof b["amount"] === "string";
|
|
299
|
+
}
|
|
300
|
+
function getB2BTransactionId(callback) {
|
|
301
|
+
if (!isB2BCheckoutSuccess(callback)) return null;
|
|
302
|
+
return callback.transactionId ?? null;
|
|
303
|
+
}
|
|
304
|
+
function getB2BAmount(callback) {
|
|
305
|
+
return Number(callback.amount);
|
|
306
|
+
}
|
|
307
|
+
function getB2BRequestId(callback) {
|
|
308
|
+
return callback.requestId;
|
|
309
|
+
}
|
|
310
|
+
function getB2BConversationId(callback) {
|
|
311
|
+
if (!isB2BCheckoutSuccess(callback)) return null;
|
|
312
|
+
return callback.conversationID ?? null;
|
|
313
|
+
}
|
|
314
|
+
|
|
220
315
|
// src/mpesa/c2b/register-url.ts
|
|
221
316
|
var FORBIDDEN_URL_KEYWORDS = [
|
|
222
317
|
"mpesa",
|
|
@@ -732,7 +827,7 @@ var Mpesa = class {
|
|
|
732
827
|
}
|
|
733
828
|
return encryptSecurityCredential(this.config.initiatorPassword, cert);
|
|
734
829
|
}
|
|
735
|
-
// ── STK Push
|
|
830
|
+
// ── STK Push ───────────────────────────────────────────────────────────────
|
|
736
831
|
/**
|
|
737
832
|
* M-Pesa Express — sends a payment prompt to the customer's phone.
|
|
738
833
|
*
|
|
@@ -827,7 +922,7 @@ var Mpesa = class {
|
|
|
827
922
|
request
|
|
828
923
|
);
|
|
829
924
|
}
|
|
830
|
-
// ── Dynamic QR Code
|
|
925
|
+
// ── Dynamic QR Code ────────────────────────────────────────────────────────
|
|
831
926
|
/**
|
|
832
927
|
* Dynamic QR — generates an M-PESA QR code for LNM merchant payments.
|
|
833
928
|
*
|
|
@@ -839,19 +934,17 @@ var Mpesa = class {
|
|
|
839
934
|
* merchantName: "My Shop",
|
|
840
935
|
* refNo: "INV-001",
|
|
841
936
|
* amount: 500,
|
|
842
|
-
* trxCode: "BG",
|
|
937
|
+
* trxCode: "BG",
|
|
843
938
|
* cpi: "373132",
|
|
844
939
|
* size: 300,
|
|
845
940
|
* });
|
|
846
|
-
*
|
|
847
|
-
* // res.QRCode is a base64-encoded PNG — render in an <img> tag:
|
|
848
941
|
* // <img src={`data:image/png;base64,${res.QRCode}`} />
|
|
849
942
|
*/
|
|
850
943
|
async generateDynamicQR(request) {
|
|
851
944
|
const token = await this.getToken();
|
|
852
945
|
return generateDynamicQR(this.baseUrl, token, request);
|
|
853
946
|
}
|
|
854
|
-
// ── C2B Register URL
|
|
947
|
+
// ── C2B Register URL ───────────────────────────────────────────────────────
|
|
855
948
|
/**
|
|
856
949
|
* Registers your Confirmation and Validation URLs with M-PESA.
|
|
857
950
|
*
|
|
@@ -862,61 +955,45 @@ var Mpesa = class {
|
|
|
862
955
|
* Production: One-time call. To change URLs, delete them via Daraja Self
|
|
863
956
|
* Services → URL Management, then call this again.
|
|
864
957
|
*
|
|
865
|
-
* URL rules (Daraja docs — enforced by this library):
|
|
866
|
-
* ✓ Must be publicly accessible
|
|
867
|
-
* ✓ Production: HTTPS required
|
|
868
|
-
* ✗ Must NOT contain: M-PESA, Safaricom, exe, exec, cmd, sql, query
|
|
869
|
-
* ✗ Do NOT use ngrok, mockbin, requestbin in production
|
|
870
|
-
* ✓ responseType must be exactly "Completed" or "Cancelled" (sentence case)
|
|
871
|
-
*
|
|
872
|
-
* External Validation (optional):
|
|
873
|
-
* By default it is disabled. To enable, email apisupport@safaricom.co.ke.
|
|
874
|
-
* When enabled, Safaricom calls your validationUrl before processing payment.
|
|
875
|
-
* You must respond within ~8 seconds.
|
|
876
|
-
*
|
|
877
958
|
* @example
|
|
878
959
|
* await mpesa.registerC2BUrls({
|
|
879
960
|
* shortCode: "600984",
|
|
880
961
|
* responseType: "Completed",
|
|
881
962
|
* confirmationUrl: "https://yourdomain.com/mpesa/c2b/confirmation",
|
|
882
963
|
* validationUrl: "https://yourdomain.com/mpesa/c2b/validation",
|
|
883
|
-
* apiVersion: "v2",
|
|
964
|
+
* apiVersion: "v2",
|
|
884
965
|
* });
|
|
885
966
|
*/
|
|
886
967
|
async registerC2BUrls(request) {
|
|
887
968
|
const token = await this.getToken();
|
|
888
969
|
return registerC2BUrls(this.baseUrl, token, request);
|
|
889
970
|
}
|
|
890
|
-
// ── C2B Simulate (Sandbox ONLY)
|
|
971
|
+
// ── C2B Simulate (Sandbox ONLY) ────────────────────────────────────────────
|
|
891
972
|
/**
|
|
892
973
|
* Simulates a C2B customer payment. SANDBOX ONLY.
|
|
893
974
|
*
|
|
894
975
|
* In production, real customers initiate payments via M-PESA App, USSD,
|
|
895
976
|
* or SIM Toolkit — simulation is not available.
|
|
896
977
|
*
|
|
897
|
-
* The API version used here should match the version used when registering URLs.
|
|
898
|
-
*
|
|
899
978
|
* @example
|
|
900
979
|
* await mpesa.simulateC2B({
|
|
901
980
|
* shortCode: 600984,
|
|
902
981
|
* commandId: "CustomerPayBillOnline",
|
|
903
982
|
* amount: 10,
|
|
904
|
-
* msisdn: 254708374149,
|
|
905
|
-
* billRefNumber: "INV-001",
|
|
906
|
-
* apiVersion: "v2",
|
|
983
|
+
* msisdn: 254708374149,
|
|
984
|
+
* billRefNumber: "INV-001",
|
|
985
|
+
* apiVersion: "v2",
|
|
907
986
|
* });
|
|
908
987
|
*/
|
|
909
988
|
async simulateC2B(request) {
|
|
910
989
|
const token = await this.getToken();
|
|
911
990
|
return simulateC2B(this.baseUrl, token, request);
|
|
912
991
|
}
|
|
913
|
-
// ── Tax Remittance
|
|
992
|
+
// ── Tax Remittance ─────────────────────────────────────────────────────────
|
|
914
993
|
/**
|
|
915
994
|
* Tax Remittance — remits tax to Kenya Revenue Authority (KRA) via M-PESA.
|
|
916
995
|
*
|
|
917
|
-
* Requires:
|
|
918
|
-
* - initiatorName in config
|
|
919
|
-
* - initiatorPassword + certificate (or pre-computed securityCredential)
|
|
996
|
+
* Requires: initiatorName + certificate (or pre-computed securityCredential).
|
|
920
997
|
*
|
|
921
998
|
* This is ASYNCHRONOUS. The synchronous response only confirms receipt.
|
|
922
999
|
* Final details are POSTed to your resultUrl.
|
|
@@ -926,17 +1003,17 @@ var Mpesa = class {
|
|
|
926
1003
|
* - A Payment Registration Number (PRN) from KRA.
|
|
927
1004
|
* - Initiator with "Tax Remittance ORG API" role on M-PESA org portal.
|
|
928
1005
|
*
|
|
929
|
-
* Fixed values (set automatically — do NOT override
|
|
1006
|
+
* Fixed values (set automatically — do NOT override):
|
|
930
1007
|
* CommandID: "PayTaxToKRA"
|
|
931
1008
|
* SenderIdentifierType: "4"
|
|
932
1009
|
* RecieverIdentifierType: "4"
|
|
933
|
-
* PartyB: "572572"
|
|
1010
|
+
* PartyB: "572572" (KRA shortcode)
|
|
934
1011
|
*
|
|
935
1012
|
* @example
|
|
936
1013
|
* await mpesa.remitTax({
|
|
937
1014
|
* amount: 5000,
|
|
938
1015
|
* partyA: "888880",
|
|
939
|
-
* accountReference: "PRN1234XN",
|
|
1016
|
+
* accountReference: "PRN1234XN",
|
|
940
1017
|
* resultUrl: "https://yourdomain.com/mpesa/tax/result",
|
|
941
1018
|
* queueTimeOutUrl: "https://yourdomain.com/mpesa/tax/timeout",
|
|
942
1019
|
* remarks: "Monthly PAYE remittance",
|
|
@@ -956,6 +1033,55 @@ var Mpesa = class {
|
|
|
956
1033
|
]);
|
|
957
1034
|
return remitTax(this.baseUrl, token, securityCred, initiator, request);
|
|
958
1035
|
}
|
|
1036
|
+
// ── B2B Express Checkout ───────────────────────────────────────────────────
|
|
1037
|
+
/**
|
|
1038
|
+
* B2B Express Checkout — initiates a USSD Push to a merchant's till.
|
|
1039
|
+
*
|
|
1040
|
+
* Enables vendors to request payment from a fellow merchant by triggering
|
|
1041
|
+
* a USSD Push to the merchant's till number. The merchant is prompted to
|
|
1042
|
+
* enter their Operator ID and M-PESA PIN to authorise the payment.
|
|
1043
|
+
*
|
|
1044
|
+
* Requires: standard OAuth credentials only (consumerKey + consumerSecret).
|
|
1045
|
+
* NO initiatorName or SecurityCredential needed.
|
|
1046
|
+
*
|
|
1047
|
+
* Flow:
|
|
1048
|
+
* 1. You call b2bExpressCheckout() → Daraja sends USSD to merchant's till.
|
|
1049
|
+
* 2. Merchant enters Operator ID + PIN to confirm.
|
|
1050
|
+
* 3. M-PESA debits merchant (primaryShortCode), credits you (receiverShortCode).
|
|
1051
|
+
* 4. Daraja POSTs result to your callbackUrl.
|
|
1052
|
+
*
|
|
1053
|
+
* Synchronous response: confirms USSD was triggered (code "0").
|
|
1054
|
+
* Async callback: final success or cancellation result POSTed to callbackUrl.
|
|
1055
|
+
*
|
|
1056
|
+
* Prerequisites (from Daraja docs):
|
|
1057
|
+
* - Merchant's till (primaryShortCode) must have a Nominated Number
|
|
1058
|
+
* configured in M-PESA Web Portal (Organization Details).
|
|
1059
|
+
* - Merchant KYC must be valid.
|
|
1060
|
+
*
|
|
1061
|
+
* Error codes:
|
|
1062
|
+
* 4104 — Missing Nominated Number → configure in M-PESA Web Portal
|
|
1063
|
+
* 4102 — Merchant KYC Fail → provide valid KYC
|
|
1064
|
+
* 4201 — USSD Network Error → retry on stable network
|
|
1065
|
+
* 4203 — USSD Exception Error → retry on stable network
|
|
1066
|
+
*
|
|
1067
|
+
* @example
|
|
1068
|
+
* const res = await mpesa.b2bExpressCheckout({
|
|
1069
|
+
* primaryShortCode: "000001", // merchant's till (debit party)
|
|
1070
|
+
* receiverShortCode: "000002", // your Paybill (credit party)
|
|
1071
|
+
* amount: 5000,
|
|
1072
|
+
* paymentRef: "INV-001",
|
|
1073
|
+
* callbackUrl: "https://yourdomain.com/mpesa/b2b/callback",
|
|
1074
|
+
* partnerName: "My Vendor Co.",
|
|
1075
|
+
* });
|
|
1076
|
+
*
|
|
1077
|
+
* if (res.code === "0") {
|
|
1078
|
+
* console.log("USSD Push triggered:", res.status);
|
|
1079
|
+
* }
|
|
1080
|
+
*/
|
|
1081
|
+
async b2bExpressCheckout(request) {
|
|
1082
|
+
const token = await this.getToken();
|
|
1083
|
+
return initiateB2BExpressCheckout(this.baseUrl, token, request);
|
|
1084
|
+
}
|
|
959
1085
|
/** Force the cached OAuth token to be refreshed on the next API call */
|
|
960
1086
|
clearTokenCache() {
|
|
961
1087
|
this.tokenManager.clearCache();
|
|
@@ -1094,6 +1220,10 @@ exports.extractAmount = extractAmount;
|
|
|
1094
1220
|
exports.extractPhoneNumber = extractPhoneNumber;
|
|
1095
1221
|
exports.extractTransactionId = extractTransactionId;
|
|
1096
1222
|
exports.formatPhoneNumber = formatSafaricomPhone;
|
|
1223
|
+
exports.getB2BAmount = getB2BAmount;
|
|
1224
|
+
exports.getB2BConversationId = getB2BConversationId;
|
|
1225
|
+
exports.getB2BRequestId = getB2BRequestId;
|
|
1226
|
+
exports.getB2BTransactionId = getB2BTransactionId;
|
|
1097
1227
|
exports.getC2BAccountRef = getC2BAccountRef;
|
|
1098
1228
|
exports.getC2BAmount = getC2BAmount;
|
|
1099
1229
|
exports.getC2BCustomerName = getC2BCustomerName;
|
|
@@ -1101,6 +1231,10 @@ exports.getC2BTransactionId = getC2BTransactionId;
|
|
|
1101
1231
|
exports.getCallbackValue = getCallbackValue;
|
|
1102
1232
|
exports.getTimestamp = getTimestamp;
|
|
1103
1233
|
exports.handleWebhook = handleWebhook;
|
|
1234
|
+
exports.initiateB2BExpressCheckout = initiateB2BExpressCheckout;
|
|
1235
|
+
exports.isB2BCheckoutCallback = isB2BCheckoutCallback;
|
|
1236
|
+
exports.isB2BCheckoutCancelled = isB2BCheckoutCancelled;
|
|
1237
|
+
exports.isB2BCheckoutSuccess = isB2BCheckoutSuccess;
|
|
1104
1238
|
exports.isBuyGoodsPayment = isBuyGoodsPayment;
|
|
1105
1239
|
exports.isC2BPayload = isC2BPayload;
|
|
1106
1240
|
exports.isPaybillPayment = isPaybillPayment;
|