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 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", // merchant's till (debit party)
1070
- * receiverShortCode: "000002", // your Paybill (credit party)
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;