pesafy 0.3.10 → 0.3.12

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
@@ -193,6 +193,174 @@ var TokenManager = class {
193
193
  }
194
194
  };
195
195
 
196
+ // src/mpesa/c2b/register-url.ts
197
+ var FORBIDDEN_URL_KEYWORDS = [
198
+ "mpesa",
199
+ "safaricom",
200
+ ".exe",
201
+ ".exec",
202
+ "cmd",
203
+ "sql",
204
+ "query"
205
+ ];
206
+ function validateCallbackUrl(url, fieldName) {
207
+ if (!url || !url.trim()) {
208
+ throw createError({
209
+ code: "VALIDATION_ERROR",
210
+ message: `${fieldName} is required`
211
+ });
212
+ }
213
+ const lower = url.toLowerCase();
214
+ for (const keyword of FORBIDDEN_URL_KEYWORDS) {
215
+ if (lower.includes(keyword)) {
216
+ throw createError({
217
+ code: "VALIDATION_ERROR",
218
+ message: `${fieldName} must not contain the keyword "${keyword}". Daraja rejects URLs containing: M-PESA, Safaricom, exe, exec, cmd, sql, query.`
219
+ });
220
+ }
221
+ }
222
+ }
223
+ async function registerC2BUrls(baseUrl, accessToken, request) {
224
+ if (!request.shortCode) {
225
+ throw createError({
226
+ code: "VALIDATION_ERROR",
227
+ message: "shortCode is required"
228
+ });
229
+ }
230
+ if (!request.responseType) {
231
+ throw createError({
232
+ code: "VALIDATION_ERROR",
233
+ message: 'responseType is required: "Completed" or "Cancelled" (sentence case, exactly as spelled)'
234
+ });
235
+ }
236
+ if (request.responseType !== "Completed" && request.responseType !== "Cancelled") {
237
+ throw createError({
238
+ code: "VALIDATION_ERROR",
239
+ message: `responseType must be exactly "Completed" or "Cancelled" (sentence case). Got: "${request.responseType}"`
240
+ });
241
+ }
242
+ validateCallbackUrl(request.confirmationUrl, "confirmationUrl");
243
+ validateCallbackUrl(request.validationUrl, "validationUrl");
244
+ const version = request.apiVersion ?? "v2";
245
+ const payload = {
246
+ ShortCode: String(request.shortCode),
247
+ ResponseType: request.responseType,
248
+ ConfirmationURL: request.confirmationUrl,
249
+ ValidationURL: request.validationUrl
250
+ };
251
+ const { data } = await httpRequest(
252
+ `${baseUrl}/mpesa/c2b/${version}/registerurl`,
253
+ {
254
+ method: "POST",
255
+ headers: { Authorization: `Bearer ${accessToken}` },
256
+ body: payload
257
+ }
258
+ );
259
+ return data;
260
+ }
261
+
262
+ // src/mpesa/c2b/simulate.ts
263
+ async function simulateC2B(baseUrl, accessToken, request) {
264
+ if (!baseUrl.includes("sandbox")) {
265
+ throw createError({
266
+ code: "VALIDATION_ERROR",
267
+ message: "C2B simulate is only available in the Sandbox environment. Production M-PESA payments must be initiated by the customer via M-PESA App, USSD, or SIM Toolkit."
268
+ });
269
+ }
270
+ if (!request.shortCode) {
271
+ throw createError({
272
+ code: "VALIDATION_ERROR",
273
+ message: "shortCode is required"
274
+ });
275
+ }
276
+ if (!request.commandId) {
277
+ throw createError({
278
+ code: "VALIDATION_ERROR",
279
+ message: 'commandId is required: "CustomerPayBillOnline" | "CustomerBuyGoodsOnline"'
280
+ });
281
+ }
282
+ if (request.commandId !== "CustomerPayBillOnline" && request.commandId !== "CustomerBuyGoodsOnline") {
283
+ throw createError({
284
+ code: "VALIDATION_ERROR",
285
+ message: `commandId must be "CustomerPayBillOnline" or "CustomerBuyGoodsOnline". Got: "${request.commandId}"`
286
+ });
287
+ }
288
+ const amount = Math.round(request.amount);
289
+ if (!Number.isFinite(amount) || amount < 1) {
290
+ throw createError({
291
+ code: "VALIDATION_ERROR",
292
+ message: `amount must be a whole number \u2265 1 (got ${request.amount})`
293
+ });
294
+ }
295
+ if (!request.msisdn) {
296
+ throw createError({
297
+ code: "VALIDATION_ERROR",
298
+ message: "msisdn is required. Use the test phone number from the Daraja simulator."
299
+ });
300
+ }
301
+ const version = request.apiVersion ?? "v2";
302
+ const isBuyGoods = request.commandId === "CustomerBuyGoodsOnline";
303
+ const payload = {
304
+ ShortCode: Number(request.shortCode),
305
+ CommandID: request.commandId,
306
+ Amount: amount,
307
+ Msisdn: Number(request.msisdn)
308
+ };
309
+ if (!isBuyGoods) {
310
+ payload.BillRefNumber = request.billRefNumber ?? "";
311
+ }
312
+ const { data } = await httpRequest(
313
+ `${baseUrl}/mpesa/c2b/${version}/simulate`,
314
+ {
315
+ method: "POST",
316
+ headers: { Authorization: `Bearer ${accessToken}` },
317
+ body: payload
318
+ }
319
+ );
320
+ return data;
321
+ }
322
+
323
+ // src/mpesa/c2b/webhooks.ts
324
+ function isC2BPayload(body) {
325
+ if (!body || typeof body !== "object") return false;
326
+ const b = body;
327
+ return typeof b["TransID"] === "string" && typeof b["BusinessShortCode"] === "string" && typeof b["TransAmount"] === "string";
328
+ }
329
+ function acceptC2BValidation(thirdPartyTransID) {
330
+ return {
331
+ ResultCode: "0",
332
+ ResultDesc: "Accepted",
333
+ ...thirdPartyTransID ? { ThirdPartyTransID: thirdPartyTransID } : {}
334
+ };
335
+ }
336
+ function rejectC2BValidation(resultCode = "C2B00016", resultDesc = "Rejected") {
337
+ return {
338
+ ResultCode: resultCode,
339
+ ResultDesc: resultDesc
340
+ };
341
+ }
342
+ function acknowledgeC2BConfirmation() {
343
+ return { ResultCode: 0, ResultDesc: "Success" };
344
+ }
345
+ function getC2BAmount(payload) {
346
+ return Number(payload.TransAmount);
347
+ }
348
+ function getC2BTransactionId(payload) {
349
+ return payload.TransID;
350
+ }
351
+ function getC2BAccountRef(payload) {
352
+ return payload.BillRefNumber;
353
+ }
354
+ function getC2BCustomerName(payload) {
355
+ return [payload.FirstName, payload.MiddleName, payload.LastName].filter(Boolean).join(" ").trim();
356
+ }
357
+ function isPaybillPayment(payload) {
358
+ return payload.TransactionType === "Pay Bill" || payload.TransactionType === "CustomerPayBillOnline";
359
+ }
360
+ function isBuyGoodsPayment(payload) {
361
+ return payload.TransactionType === "Buy Goods" || payload.TransactionType === "CustomerBuyGoodsOnline";
362
+ }
363
+
196
364
  // src/mpesa/dynamic-qr/generate.ts
197
365
  async function generateDynamicQR(baseUrl, accessToken, request) {
198
366
  if (!request.merchantName?.trim()) {
@@ -598,6 +766,65 @@ var Mpesa = class {
598
766
  const token = await this.getToken();
599
767
  return generateDynamicQR(this.baseUrl, token, request);
600
768
  }
769
+ // ── C2B Register URL ──────────────────────────────────────────────────────
770
+ /**
771
+ * Registers your Confirmation and Validation URLs with M-PESA.
772
+ *
773
+ * Use v2 (default) for new integrations — callbacks include a masked MSISDN.
774
+ * Use v1 only if you need SHA256-hashed MSISDN in callbacks.
775
+ *
776
+ * Sandbox: URLs can be re-registered freely (overwriting existing ones).
777
+ * Production: One-time call. To change URLs, delete them via Daraja Self
778
+ * Services → URL Management, then call this again.
779
+ *
780
+ * URL rules (Daraja docs — enforced by this library):
781
+ * ✓ Must be publicly accessible
782
+ * ✓ Production: HTTPS required
783
+ * ✗ Must NOT contain: M-PESA, Safaricom, exe, exec, cmd, sql, query
784
+ * ✗ Do NOT use ngrok, mockbin, requestbin in production
785
+ * ✓ responseType must be exactly "Completed" or "Cancelled" (sentence case)
786
+ *
787
+ * External Validation (optional):
788
+ * By default it is disabled. To enable, email apisupport@safaricom.co.ke.
789
+ * When enabled, Safaricom calls your validationUrl before processing payment.
790
+ * You must respond within ~8 seconds.
791
+ *
792
+ * @example
793
+ * await mpesa.registerC2BUrls({
794
+ * shortCode: "600984",
795
+ * responseType: "Completed",
796
+ * confirmationUrl: "https://yourdomain.com/mpesa/c2b/confirmation",
797
+ * validationUrl: "https://yourdomain.com/mpesa/c2b/validation",
798
+ * apiVersion: "v2", // default — recommended
799
+ * });
800
+ */
801
+ async registerC2BUrls(request) {
802
+ const token = await this.getToken();
803
+ return registerC2BUrls(this.baseUrl, token, request);
804
+ }
805
+ // ── C2B Simulate (Sandbox ONLY) ───────────────────────────────────────────
806
+ /**
807
+ * Simulates a C2B customer payment. SANDBOX ONLY.
808
+ *
809
+ * In production, real customers initiate payments via M-PESA App, USSD,
810
+ * or SIM Toolkit — simulation is not available.
811
+ *
812
+ * The API version used here should match the version used when registering URLs.
813
+ *
814
+ * @example
815
+ * await mpesa.simulateC2B({
816
+ * shortCode: 600984,
817
+ * commandId: "CustomerPayBillOnline",
818
+ * amount: 10,
819
+ * msisdn: 254708374149, // Daraja test MSISDN
820
+ * billRefNumber: "INV-001", // account ref for Paybill; null for Till
821
+ * apiVersion: "v2", // must match registered URL version
822
+ * });
823
+ */
824
+ async simulateC2B(request) {
825
+ const token = await this.getToken();
826
+ return simulateC2B(this.baseUrl, token, request);
827
+ }
601
828
  /** Force the cached OAuth token to be refreshed on the next API call */
602
829
  clearTokenCache() {
603
830
  this.tokenManager.clearCache();
@@ -726,19 +953,31 @@ exports.DARAJA_BASE_URLS = DARAJA_BASE_URLS;
726
953
  exports.Mpesa = Mpesa;
727
954
  exports.PesafyError = PesafyError;
728
955
  exports.SAFARICOM_IPS = SAFARICOM_IPS;
956
+ exports.acceptC2BValidation = acceptC2BValidation;
957
+ exports.acknowledgeC2BConfirmation = acknowledgeC2BConfirmation;
729
958
  exports.createError = createError;
730
959
  exports.encryptSecurityCredential = encryptSecurityCredential;
731
960
  exports.extractAmount = extractAmount;
732
961
  exports.extractPhoneNumber = extractPhoneNumber;
733
962
  exports.extractTransactionId = extractTransactionId;
734
963
  exports.formatPhoneNumber = formatSafaricomPhone;
964
+ exports.getC2BAccountRef = getC2BAccountRef;
965
+ exports.getC2BAmount = getC2BAmount;
966
+ exports.getC2BCustomerName = getC2BCustomerName;
967
+ exports.getC2BTransactionId = getC2BTransactionId;
735
968
  exports.getCallbackValue = getCallbackValue;
736
969
  exports.getTimestamp = getTimestamp;
737
970
  exports.handleWebhook = handleWebhook;
971
+ exports.isBuyGoodsPayment = isBuyGoodsPayment;
972
+ exports.isC2BPayload = isC2BPayload;
973
+ exports.isPaybillPayment = isPaybillPayment;
738
974
  exports.isStkCallbackSuccess = isStkCallbackSuccess;
739
975
  exports.isSuccessfulCallback = isSuccessfulCallback;
740
976
  exports.parseStkPushWebhook = parseStkPushWebhook;
977
+ exports.registerC2BUrls = registerC2BUrls;
978
+ exports.rejectC2BValidation = rejectC2BValidation;
741
979
  exports.retryWithBackoff = retryWithBackoff;
980
+ exports.simulateC2B = simulateC2B;
742
981
  exports.verifyWebhookIP = verifyWebhookIP;
743
982
  //# sourceMappingURL=index.cjs.map
744
983
  //# sourceMappingURL=index.cjs.map