pesafy 0.4.0 → 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 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", // Buy Goods (till number)
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", // default — recommended
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, // Daraja test MSISDN
905
- * billRefNumber: "INV-001", // account ref for Paybill; null for Till
906
- * apiVersion: "v2", // must match registered URL version
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 unless Safaricom changes them):
1006
+ * Fixed values (set automatically — do NOT override):
930
1007
  * CommandID: "PayTaxToKRA"
931
1008
  * SenderIdentifierType: "4"
932
1009
  * RecieverIdentifierType: "4"
933
- * PartyB: "572572" (KRA shortcode)
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", // PRN from KRA
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;