pesafy 0.4.2 → 0.4.3
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 +243 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +360 -61
- package/dist/index.d.ts +360 -61
- package/dist/index.js +228 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -312,6 +312,160 @@ function getB2BConversationId(callback) {
|
|
|
312
312
|
return callback.conversationID ?? null;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// src/mpesa/b2c/payment.ts
|
|
316
|
+
async function initiateB2CPayment(baseUrl, accessToken, securityCredential, initiatorName, request) {
|
|
317
|
+
if (!request.commandId) {
|
|
318
|
+
throw createError({
|
|
319
|
+
code: "VALIDATION_ERROR",
|
|
320
|
+
message: 'commandId is required: "BusinessPayToBulk" | "BusinessPayment" | "SalaryPayment" | "PromotionPayment"'
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const validCommandIds = [
|
|
324
|
+
"BusinessPayToBulk",
|
|
325
|
+
"BusinessPayment",
|
|
326
|
+
"SalaryPayment",
|
|
327
|
+
"PromotionPayment"
|
|
328
|
+
];
|
|
329
|
+
if (!validCommandIds.includes(request.commandId)) {
|
|
330
|
+
throw createError({
|
|
331
|
+
code: "VALIDATION_ERROR",
|
|
332
|
+
message: `commandId must be one of: ${validCommandIds.join(", ")}. Got: "${request.commandId}"`
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const amount = Math.round(request.amount);
|
|
336
|
+
if (!Number.isFinite(amount) || amount < 1) {
|
|
337
|
+
throw createError({
|
|
338
|
+
code: "VALIDATION_ERROR",
|
|
339
|
+
message: `amount must be a whole number \u2265 1 (got ${request.amount} which rounds to ${amount}).`
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (!request.partyA?.trim()) {
|
|
343
|
+
throw createError({
|
|
344
|
+
code: "VALIDATION_ERROR",
|
|
345
|
+
message: "partyA is required \u2014 your business shortcode from which money is deducted."
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (!request.partyB?.trim()) {
|
|
349
|
+
throw createError({
|
|
350
|
+
code: "VALIDATION_ERROR",
|
|
351
|
+
message: "partyB is required \u2014 the recipient shortcode (BusinessPayToBulk) or customer MSISDN (other commands)."
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (!request.accountReference?.trim()) {
|
|
355
|
+
throw createError({
|
|
356
|
+
code: "VALIDATION_ERROR",
|
|
357
|
+
message: "accountReference is required \u2014 a reference for this transaction."
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (!request.resultUrl?.trim()) {
|
|
361
|
+
throw createError({
|
|
362
|
+
code: "VALIDATION_ERROR",
|
|
363
|
+
message: "resultUrl is required \u2014 Safaricom POSTs the B2C result here."
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (!request.queueTimeOutUrl?.trim()) {
|
|
367
|
+
throw createError({
|
|
368
|
+
code: "VALIDATION_ERROR",
|
|
369
|
+
message: "queueTimeOutUrl is required \u2014 Safaricom calls this on request timeout."
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const payload = {
|
|
373
|
+
Initiator: initiatorName,
|
|
374
|
+
SecurityCredential: securityCredential,
|
|
375
|
+
CommandID: request.commandId,
|
|
376
|
+
SenderIdentifierType: request.senderIdentifierType ?? "4",
|
|
377
|
+
RecieverIdentifierType: request.receiverIdentifierType ?? "4",
|
|
378
|
+
Amount: String(amount),
|
|
379
|
+
PartyA: String(request.partyA),
|
|
380
|
+
PartyB: String(request.partyB),
|
|
381
|
+
AccountReference: request.accountReference,
|
|
382
|
+
Remarks: request.remarks ?? "B2C Payment",
|
|
383
|
+
QueueTimeOutURL: request.queueTimeOutUrl,
|
|
384
|
+
ResultURL: request.resultUrl
|
|
385
|
+
};
|
|
386
|
+
if (request.requester?.trim()) {
|
|
387
|
+
payload["Requester"] = String(request.requester);
|
|
388
|
+
}
|
|
389
|
+
const { data } = await httpRequest(
|
|
390
|
+
`${baseUrl}/mpesa/b2b/v1/paymentrequest`,
|
|
391
|
+
{
|
|
392
|
+
method: "POST",
|
|
393
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
394
|
+
body: payload
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
return data;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/mpesa/b2c/webhooks.ts
|
|
401
|
+
function isB2CResult(body) {
|
|
402
|
+
if (!body || typeof body !== "object") return false;
|
|
403
|
+
const b = body;
|
|
404
|
+
if (!b["Result"] || typeof b["Result"] !== "object") return false;
|
|
405
|
+
const result = b["Result"];
|
|
406
|
+
return typeof result["ResultCode"] === "number" && typeof result["ConversationID"] === "string";
|
|
407
|
+
}
|
|
408
|
+
function isB2CSuccess(result) {
|
|
409
|
+
return result.Result.ResultCode === 0;
|
|
410
|
+
}
|
|
411
|
+
function isB2CFailure(result) {
|
|
412
|
+
return result.Result.ResultCode !== 0;
|
|
413
|
+
}
|
|
414
|
+
function getB2CTransactionId(result) {
|
|
415
|
+
return result.Result.TransactionID ?? null;
|
|
416
|
+
}
|
|
417
|
+
function getB2CConversationId(result) {
|
|
418
|
+
return result.Result.ConversationID;
|
|
419
|
+
}
|
|
420
|
+
function getB2COriginatorConversationId(result) {
|
|
421
|
+
return result.Result.OriginatorConversationID;
|
|
422
|
+
}
|
|
423
|
+
function getB2CResultDesc(result) {
|
|
424
|
+
return result.Result.ResultDesc;
|
|
425
|
+
}
|
|
426
|
+
function getB2CAmount(result) {
|
|
427
|
+
const value = getB2CResultParam(result, "Amount");
|
|
428
|
+
if (value === void 0) return null;
|
|
429
|
+
return Number(value);
|
|
430
|
+
}
|
|
431
|
+
function getB2CTransactionCompletedTime(result) {
|
|
432
|
+
const value = getB2CResultParam(result, "TransCompletedTime");
|
|
433
|
+
if (value === void 0) return null;
|
|
434
|
+
return String(value);
|
|
435
|
+
}
|
|
436
|
+
function getB2CDebitPartyCharges(result) {
|
|
437
|
+
const value = getB2CResultParam(result, "DebitPartyCharges");
|
|
438
|
+
if (value === void 0 || value === "") return null;
|
|
439
|
+
return String(value);
|
|
440
|
+
}
|
|
441
|
+
function getB2CReceiverPublicName(result) {
|
|
442
|
+
const value = getB2CResultParam(result, "ReceiverPartyPublicName");
|
|
443
|
+
if (value === void 0) return null;
|
|
444
|
+
return String(value);
|
|
445
|
+
}
|
|
446
|
+
function getB2CCurrency(result) {
|
|
447
|
+
const value = getB2CResultParam(result, "Currency");
|
|
448
|
+
if (value === void 0) return "KES";
|
|
449
|
+
return String(value);
|
|
450
|
+
}
|
|
451
|
+
function getB2CDebitAccountBalance(result) {
|
|
452
|
+
const value = getB2CResultParam(result, "DebitAccountBalance");
|
|
453
|
+
if (value === void 0) return null;
|
|
454
|
+
return String(value);
|
|
455
|
+
}
|
|
456
|
+
function getB2CInitiatorAccountBalance(result) {
|
|
457
|
+
const value = getB2CResultParam(result, "InitiatorAccountCurrentBalance");
|
|
458
|
+
if (value === void 0) return null;
|
|
459
|
+
return String(value);
|
|
460
|
+
}
|
|
461
|
+
function getB2CResultParam(result, key) {
|
|
462
|
+
const params = result.Result.ResultParameters?.ResultParameter;
|
|
463
|
+
if (!params) return void 0;
|
|
464
|
+
const paramArray = Array.isArray(params) ? params : [params];
|
|
465
|
+
const item = paramArray.find((p) => p.Key === key);
|
|
466
|
+
return item?.Value;
|
|
467
|
+
}
|
|
468
|
+
|
|
315
469
|
// src/mpesa/c2b/register-url.ts
|
|
316
470
|
var FORBIDDEN_URL_KEYWORDS = [
|
|
317
471
|
"mpesa",
|
|
@@ -884,6 +1038,7 @@ var Mpesa = class {
|
|
|
884
1038
|
passKey
|
|
885
1039
|
});
|
|
886
1040
|
}
|
|
1041
|
+
// ── Transaction Status ─────────────────────────────────────────────────────
|
|
887
1042
|
/**
|
|
888
1043
|
* Transaction Status — queries the result of a completed M-Pesa transaction.
|
|
889
1044
|
*
|
|
@@ -926,9 +1081,6 @@ var Mpesa = class {
|
|
|
926
1081
|
/**
|
|
927
1082
|
* Dynamic QR — generates an M-PESA QR code for LNM merchant payments.
|
|
928
1083
|
*
|
|
929
|
-
* Customers scan the code with My Safaricom App or M-PESA app to
|
|
930
|
-
* capture the till/paybill number and amount, then authorize payment.
|
|
931
|
-
*
|
|
932
1084
|
* @example
|
|
933
1085
|
* const res = await mpesa.generateDynamicQR({
|
|
934
1086
|
* merchantName: "My Shop",
|
|
@@ -948,13 +1100,6 @@ var Mpesa = class {
|
|
|
948
1100
|
/**
|
|
949
1101
|
* Registers your Confirmation and Validation URLs with M-PESA.
|
|
950
1102
|
*
|
|
951
|
-
* Use v2 (default) for new integrations — callbacks include a masked MSISDN.
|
|
952
|
-
* Use v1 only if you need SHA256-hashed MSISDN in callbacks.
|
|
953
|
-
*
|
|
954
|
-
* Sandbox: URLs can be re-registered freely (overwriting existing ones).
|
|
955
|
-
* Production: One-time call. To change URLs, delete them via Daraja Self
|
|
956
|
-
* Services → URL Management, then call this again.
|
|
957
|
-
*
|
|
958
1103
|
* @example
|
|
959
1104
|
* await mpesa.registerC2BUrls({
|
|
960
1105
|
* shortCode: "600984",
|
|
@@ -972,9 +1117,6 @@ var Mpesa = class {
|
|
|
972
1117
|
/**
|
|
973
1118
|
* Simulates a C2B customer payment. SANDBOX ONLY.
|
|
974
1119
|
*
|
|
975
|
-
* In production, real customers initiate payments via M-PESA App, USSD,
|
|
976
|
-
* or SIM Toolkit — simulation is not available.
|
|
977
|
-
*
|
|
978
1120
|
* @example
|
|
979
1121
|
* await mpesa.simulateC2B({
|
|
980
1122
|
* shortCode: 600984,
|
|
@@ -995,20 +1137,6 @@ var Mpesa = class {
|
|
|
995
1137
|
*
|
|
996
1138
|
* Requires: initiatorName + certificate (or pre-computed securityCredential).
|
|
997
1139
|
*
|
|
998
|
-
* This is ASYNCHRONOUS. The synchronous response only confirms receipt.
|
|
999
|
-
* Final details are POSTed to your resultUrl.
|
|
1000
|
-
*
|
|
1001
|
-
* Prerequisites (from Daraja docs):
|
|
1002
|
-
* - Prior integration with KRA for tax declaration.
|
|
1003
|
-
* - A Payment Registration Number (PRN) from KRA.
|
|
1004
|
-
* - Initiator with "Tax Remittance ORG API" role on M-PESA org portal.
|
|
1005
|
-
*
|
|
1006
|
-
* Fixed values (set automatically — do NOT override):
|
|
1007
|
-
* CommandID: "PayTaxToKRA"
|
|
1008
|
-
* SenderIdentifierType: "4"
|
|
1009
|
-
* RecieverIdentifierType: "4"
|
|
1010
|
-
* PartyB: "572572" (KRA shortcode)
|
|
1011
|
-
*
|
|
1012
1140
|
* @example
|
|
1013
1141
|
* await mpesa.remitTax({
|
|
1014
1142
|
* amount: 5000,
|
|
@@ -1037,51 +1165,90 @@ var Mpesa = class {
|
|
|
1037
1165
|
/**
|
|
1038
1166
|
* B2B Express Checkout — initiates a USSD Push to a merchant's till.
|
|
1039
1167
|
*
|
|
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
1168
|
* @example
|
|
1068
1169
|
* const res = await mpesa.b2bExpressCheckout({
|
|
1069
|
-
* primaryShortCode: "000001",
|
|
1070
|
-
* receiverShortCode: "000002",
|
|
1170
|
+
* primaryShortCode: "000001",
|
|
1171
|
+
* receiverShortCode: "000002",
|
|
1071
1172
|
* amount: 5000,
|
|
1072
1173
|
* paymentRef: "INV-001",
|
|
1073
1174
|
* callbackUrl: "https://yourdomain.com/mpesa/b2b/callback",
|
|
1074
1175
|
* partnerName: "My Vendor Co.",
|
|
1075
1176
|
* });
|
|
1076
|
-
*
|
|
1077
|
-
* if (res.code === "0") {
|
|
1078
|
-
* console.log("USSD Push triggered:", res.status);
|
|
1079
|
-
* }
|
|
1080
1177
|
*/
|
|
1081
1178
|
async b2bExpressCheckout(request) {
|
|
1082
1179
|
const token = await this.getToken();
|
|
1083
1180
|
return initiateB2BExpressCheckout(this.baseUrl, token, request);
|
|
1084
1181
|
}
|
|
1182
|
+
// ── B2C Payment ────────────────────────────────────────────────────────────
|
|
1183
|
+
/**
|
|
1184
|
+
* B2C Payment — sends money from a business to customers, or loads funds
|
|
1185
|
+
* to a B2C shortcode for bulk disbursement.
|
|
1186
|
+
*
|
|
1187
|
+
* Requires: initiatorName + (initiatorPassword + certificate) OR securityCredential.
|
|
1188
|
+
* The initiator must have the appropriate role for the chosen CommandID.
|
|
1189
|
+
*
|
|
1190
|
+
* This is ASYNCHRONOUS. The synchronous response only confirms receipt.
|
|
1191
|
+
* Final details are POSTed to your resultUrl.
|
|
1192
|
+
*
|
|
1193
|
+
* CommandID options:
|
|
1194
|
+
* "BusinessPayToBulk" — Load funds to a B2C shortcode (Account Top Up)
|
|
1195
|
+
* "BusinessPayment" — Direct unsecured payment to a customer
|
|
1196
|
+
* "SalaryPayment" — Salary disbursement to a customer
|
|
1197
|
+
* "PromotionPayment" — Promotion/bonus payment to a customer
|
|
1198
|
+
*
|
|
1199
|
+
* Required M-PESA org portal roles:
|
|
1200
|
+
* BusinessPayToBulk → "Org Business Pay to Bulk API initiator"
|
|
1201
|
+
* BusinessPayment → "Org Business Payment API initiator"
|
|
1202
|
+
* SalaryPayment → "Org Salary Payment API initiator"
|
|
1203
|
+
* PromotionPayment → "Org Promotion Payment API initiator"
|
|
1204
|
+
*
|
|
1205
|
+
* @example
|
|
1206
|
+
* // B2C Account Top Up (load funds to B2C shortcode)
|
|
1207
|
+
* await mpesa.b2cPayment({
|
|
1208
|
+
* commandId: "BusinessPayToBulk",
|
|
1209
|
+
* amount: 10000,
|
|
1210
|
+
* partyA: "600979",
|
|
1211
|
+
* partyB: "600000",
|
|
1212
|
+
* accountReference: "BATCH-001",
|
|
1213
|
+
* resultUrl: "https://yourdomain.com/mpesa/b2c/result",
|
|
1214
|
+
* queueTimeOutUrl: "https://yourdomain.com/mpesa/b2c/timeout",
|
|
1215
|
+
* remarks: "Monthly salary batch load",
|
|
1216
|
+
* });
|
|
1217
|
+
*
|
|
1218
|
+
* @example
|
|
1219
|
+
* // Direct customer payment
|
|
1220
|
+
* await mpesa.b2cPayment({
|
|
1221
|
+
* commandId: "SalaryPayment",
|
|
1222
|
+
* amount: 5000,
|
|
1223
|
+
* partyA: "600979",
|
|
1224
|
+
* partyB: "254712345678",
|
|
1225
|
+
* accountReference: "SAL-JAN-2024",
|
|
1226
|
+
* requester: "254712345678",
|
|
1227
|
+
* resultUrl: "https://yourdomain.com/mpesa/b2c/result",
|
|
1228
|
+
* queueTimeOutUrl: "https://yourdomain.com/mpesa/b2c/timeout",
|
|
1229
|
+
* remarks: "January salary",
|
|
1230
|
+
* });
|
|
1231
|
+
*/
|
|
1232
|
+
async b2cPayment(request) {
|
|
1233
|
+
const initiator = this.config.initiatorName ?? "";
|
|
1234
|
+
if (!initiator) {
|
|
1235
|
+
throw new PesafyError({
|
|
1236
|
+
code: "VALIDATION_ERROR",
|
|
1237
|
+
message: "initiatorName is required for B2C Payment"
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
const [token, securityCred] = await Promise.all([
|
|
1241
|
+
this.getToken(),
|
|
1242
|
+
this.buildSecurityCredential()
|
|
1243
|
+
]);
|
|
1244
|
+
return initiateB2CPayment(
|
|
1245
|
+
this.baseUrl,
|
|
1246
|
+
token,
|
|
1247
|
+
securityCred,
|
|
1248
|
+
initiator,
|
|
1249
|
+
request
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1085
1252
|
/** Force the cached OAuth token to be refreshed on the next API call */
|
|
1086
1253
|
clearTokenCache() {
|
|
1087
1254
|
this.tokenManager.clearCache();
|
|
@@ -1224,6 +1391,18 @@ exports.getB2BAmount = getB2BAmount;
|
|
|
1224
1391
|
exports.getB2BConversationId = getB2BConversationId;
|
|
1225
1392
|
exports.getB2BRequestId = getB2BRequestId;
|
|
1226
1393
|
exports.getB2BTransactionId = getB2BTransactionId;
|
|
1394
|
+
exports.getB2CAmount = getB2CAmount;
|
|
1395
|
+
exports.getB2CConversationId = getB2CConversationId;
|
|
1396
|
+
exports.getB2CCurrency = getB2CCurrency;
|
|
1397
|
+
exports.getB2CDebitAccountBalance = getB2CDebitAccountBalance;
|
|
1398
|
+
exports.getB2CDebitPartyCharges = getB2CDebitPartyCharges;
|
|
1399
|
+
exports.getB2CInitiatorAccountBalance = getB2CInitiatorAccountBalance;
|
|
1400
|
+
exports.getB2COriginatorConversationId = getB2COriginatorConversationId;
|
|
1401
|
+
exports.getB2CReceiverPublicName = getB2CReceiverPublicName;
|
|
1402
|
+
exports.getB2CResultDesc = getB2CResultDesc;
|
|
1403
|
+
exports.getB2CResultParam = getB2CResultParam;
|
|
1404
|
+
exports.getB2CTransactionCompletedTime = getB2CTransactionCompletedTime;
|
|
1405
|
+
exports.getB2CTransactionId = getB2CTransactionId;
|
|
1227
1406
|
exports.getC2BAccountRef = getC2BAccountRef;
|
|
1228
1407
|
exports.getC2BAmount = getC2BAmount;
|
|
1229
1408
|
exports.getC2BCustomerName = getC2BCustomerName;
|
|
@@ -1232,9 +1411,13 @@ exports.getCallbackValue = getCallbackValue;
|
|
|
1232
1411
|
exports.getTimestamp = getTimestamp;
|
|
1233
1412
|
exports.handleWebhook = handleWebhook;
|
|
1234
1413
|
exports.initiateB2BExpressCheckout = initiateB2BExpressCheckout;
|
|
1414
|
+
exports.initiateB2CPayment = initiateB2CPayment;
|
|
1235
1415
|
exports.isB2BCheckoutCallback = isB2BCheckoutCallback;
|
|
1236
1416
|
exports.isB2BCheckoutCancelled = isB2BCheckoutCancelled;
|
|
1237
1417
|
exports.isB2BCheckoutSuccess = isB2BCheckoutSuccess;
|
|
1418
|
+
exports.isB2CFailure = isB2CFailure;
|
|
1419
|
+
exports.isB2CResult = isB2CResult;
|
|
1420
|
+
exports.isB2CSuccess = isB2CSuccess;
|
|
1238
1421
|
exports.isBuyGoodsPayment = isBuyGoodsPayment;
|
|
1239
1422
|
exports.isC2BPayload = isC2BPayload;
|
|
1240
1423
|
exports.isPaybillPayment = isPaybillPayment;
|