@singularity-payments/nextjs 0.1.0-alpha.3 → 0.1.0-alpha.4
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.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +332 -48
- package/dist/index.mjs +332 -48
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -9,11 +9,13 @@ interface MpesaRouteHandlers {
|
|
|
9
9
|
stkCallback: RouteHandler;
|
|
10
10
|
c2bValidation: RouteHandler;
|
|
11
11
|
c2bConfirmation: RouteHandler;
|
|
12
|
+
b2cResult: RouteHandler;
|
|
13
|
+
b2cTimeout: RouteHandler;
|
|
12
14
|
catchAll: RouteHandler;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* Create Next.js route handlers for M-Pesa callbacks
|
|
18
|
+
* Create Next.js route handlers for M-Pesa callbacks and client-side API requests
|
|
17
19
|
*/
|
|
18
20
|
declare function createMpesaHandlers(client: MpesaClient): MpesaRouteHandlers;
|
|
19
21
|
|
package/dist/index.d.ts
CHANGED
|
@@ -9,11 +9,13 @@ interface MpesaRouteHandlers {
|
|
|
9
9
|
stkCallback: RouteHandler;
|
|
10
10
|
c2bValidation: RouteHandler;
|
|
11
11
|
c2bConfirmation: RouteHandler;
|
|
12
|
+
b2cResult: RouteHandler;
|
|
13
|
+
b2cTimeout: RouteHandler;
|
|
12
14
|
catchAll: RouteHandler;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* Create Next.js route handlers for M-Pesa callbacks
|
|
18
|
+
* Create Next.js route handlers for M-Pesa callbacks and client-side API requests
|
|
17
19
|
*/
|
|
18
20
|
declare function createMpesaHandlers(client: MpesaClient): MpesaRouteHandlers;
|
|
19
21
|
|
package/dist/index.js
CHANGED
|
@@ -99,18 +99,96 @@ function createMpesaHandlers(client) {
|
|
|
99
99
|
}
|
|
100
100
|
},
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
102
|
+
* B2C result handler
|
|
103
|
+
* Handles callbacks from M-Pesa after B2C requests
|
|
104
|
+
*/
|
|
105
|
+
b2cResult: {
|
|
106
|
+
POST: async (request) => {
|
|
107
|
+
try {
|
|
108
|
+
const body = await request.json();
|
|
109
|
+
const parsed = client.getCallbackHandler().parseB2CCallback(body);
|
|
110
|
+
console.log("B2C Result:", parsed);
|
|
111
|
+
return import_server.NextResponse.json(
|
|
112
|
+
{
|
|
113
|
+
ResultCode: 0,
|
|
114
|
+
ResultDesc: "Accepted"
|
|
115
|
+
},
|
|
116
|
+
{ status: 200 }
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error("B2C Result error:", error);
|
|
120
|
+
return import_server.NextResponse.json(
|
|
121
|
+
{
|
|
122
|
+
ResultCode: 1,
|
|
123
|
+
ResultDesc: "Processing failed"
|
|
124
|
+
},
|
|
125
|
+
{ status: 200 }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
/**
|
|
131
|
+
* B2C timeout handler
|
|
132
|
+
* Handles timeout notifications from M-Pesa for B2C requests
|
|
133
|
+
*/
|
|
134
|
+
b2cTimeout: {
|
|
135
|
+
POST: async (request) => {
|
|
136
|
+
try {
|
|
137
|
+
const body = await request.json();
|
|
138
|
+
console.log("B2C Timeout:", body);
|
|
139
|
+
return import_server.NextResponse.json(
|
|
140
|
+
{
|
|
141
|
+
ResultCode: 0,
|
|
142
|
+
ResultDesc: "Timeout received"
|
|
143
|
+
},
|
|
144
|
+
{ status: 200 }
|
|
145
|
+
);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("B2C Timeout error:", error);
|
|
148
|
+
return import_server.NextResponse.json(
|
|
149
|
+
{
|
|
150
|
+
ResultCode: 1,
|
|
151
|
+
ResultDesc: "Processing failed"
|
|
152
|
+
},
|
|
153
|
+
{ status: 200 }
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* Catch-all handler for all M-Pesa webhooks and client-side API requests
|
|
103
160
|
* Routes based on URL path segment
|
|
104
161
|
*
|
|
105
162
|
* Usage: app/api/mpesa/[...mpesa]/route.ts
|
|
106
163
|
* export const { POST } = mpesa.handlers.catchAll;
|
|
107
164
|
*
|
|
108
165
|
* Supported endpoints:
|
|
166
|
+
*
|
|
167
|
+
* WEBHOOKS (from M-Pesa):
|
|
109
168
|
* - /api/mpesa/callback or /api/mpesa/stk-callback - STK Push callbacks
|
|
110
169
|
* - /api/mpesa/validation or /api/mpesa/c2b-validation - C2B validation
|
|
111
170
|
* - /api/mpesa/confirmation or /api/mpesa/c2b-confirmation - C2B confirmation
|
|
171
|
+
* - /api/mpesa/b2c-result - B2C result callback
|
|
172
|
+
* - /api/mpesa/b2c-timeout - B2C timeout callback
|
|
173
|
+
* - /api/mpesa/b2b-result - B2B result callback
|
|
174
|
+
* - /api/mpesa/b2b-timeout - B2B timeout callback
|
|
175
|
+
* - /api/mpesa/balance-result - Account balance result
|
|
176
|
+
* - /api/mpesa/balance-timeout - Account balance timeout
|
|
177
|
+
* - /api/mpesa/reversal-result - Reversal result
|
|
178
|
+
* - /api/mpesa/reversal-timeout - Reversal timeout
|
|
179
|
+
* - /api/mpesa/status-result - Transaction status result
|
|
180
|
+
* - /api/mpesa/status-timeout - Transaction status timeout
|
|
181
|
+
*
|
|
182
|
+
* CLIENT APIs (from the frontend package, will recode react soon):
|
|
112
183
|
* - /api/mpesa/stk-push - Initiate STK Push request
|
|
113
184
|
* - /api/mpesa/stk-query - Query STK Push status
|
|
185
|
+
* - /api/mpesa/b2c - Initiate B2C payment
|
|
186
|
+
* - /api/mpesa/b2b - Initiate B2B payment
|
|
187
|
+
* - /api/mpesa/balance - Query account balance
|
|
188
|
+
* - /api/mpesa/transaction-status - Query transaction status
|
|
189
|
+
* - /api/mpesa/reversal - Reverse a transaction
|
|
190
|
+
* - /api/mpesa/register-c2b - Register C2B URLs
|
|
191
|
+
* - /api/mpesa/generate-qr - Generate dynamic QR code
|
|
114
192
|
*/
|
|
115
193
|
catchAll: {
|
|
116
194
|
POST: async (request) => {
|
|
@@ -118,60 +196,266 @@ function createMpesaHandlers(client) {
|
|
|
118
196
|
const segments = pathname.split("/").filter(Boolean);
|
|
119
197
|
const lastSegment = segments[segments.length - 1];
|
|
120
198
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
if (lastSegment === "callback" || lastSegment === "stk-callback") {
|
|
200
|
+
return handlers.stkCallback.POST(request);
|
|
201
|
+
}
|
|
202
|
+
if (lastSegment === "validation" || lastSegment === "c2b-validation") {
|
|
203
|
+
return handlers.c2bValidation.POST(request);
|
|
204
|
+
}
|
|
205
|
+
if (lastSegment === "confirmation" || lastSegment === "c2b-confirmation") {
|
|
206
|
+
return handlers.c2bConfirmation.POST(request);
|
|
207
|
+
}
|
|
208
|
+
if (lastSegment === "b2c-result") {
|
|
209
|
+
return handlers.b2cResult.POST(request);
|
|
210
|
+
}
|
|
211
|
+
if (lastSegment === "b2c-timeout") {
|
|
212
|
+
return handlers.b2cTimeout.POST(request);
|
|
213
|
+
}
|
|
214
|
+
if (lastSegment === "b2b-result") {
|
|
215
|
+
const body = await request.json();
|
|
216
|
+
const parsed = client.getCallbackHandler().parseB2BCallback(body);
|
|
217
|
+
console.log("B2B Result:", parsed);
|
|
218
|
+
return import_server.NextResponse.json({ ResultCode: 0, ResultDesc: "Accepted" });
|
|
219
|
+
}
|
|
220
|
+
if (lastSegment === "b2b-timeout") {
|
|
221
|
+
const body = await request.json();
|
|
222
|
+
console.log("B2B Timeout:", body);
|
|
223
|
+
return import_server.NextResponse.json({
|
|
224
|
+
ResultCode: 0,
|
|
225
|
+
ResultDesc: "Timeout received"
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (lastSegment === "balance-result") {
|
|
229
|
+
const body = await request.json();
|
|
230
|
+
const parsed = client.getCallbackHandler().parseAccountBalanceCallback(body);
|
|
231
|
+
console.log("Balance Result:", parsed);
|
|
232
|
+
return import_server.NextResponse.json({ ResultCode: 0, ResultDesc: "Accepted" });
|
|
233
|
+
}
|
|
234
|
+
if (lastSegment === "balance-timeout") {
|
|
235
|
+
const body = await request.json();
|
|
236
|
+
console.log("Balance Timeout:", body);
|
|
237
|
+
return import_server.NextResponse.json({
|
|
238
|
+
ResultCode: 0,
|
|
239
|
+
ResultDesc: "Timeout received"
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (lastSegment === "status-result") {
|
|
243
|
+
const body = await request.json();
|
|
244
|
+
const parsed = client.getCallbackHandler().parseTransactionStatusCallback(body);
|
|
245
|
+
console.log("Status Result:", parsed);
|
|
246
|
+
return import_server.NextResponse.json({ ResultCode: 0, ResultDesc: "Accepted" });
|
|
247
|
+
}
|
|
248
|
+
if (lastSegment === "status-timeout") {
|
|
249
|
+
const body = await request.json();
|
|
250
|
+
console.log("Status Timeout:", body);
|
|
251
|
+
return import_server.NextResponse.json({
|
|
252
|
+
ResultCode: 0,
|
|
253
|
+
ResultDesc: "Timeout received"
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (lastSegment === "reversal-result") {
|
|
257
|
+
const body = await request.json();
|
|
258
|
+
const parsed = client.getCallbackHandler().parseReversalCallback(body);
|
|
259
|
+
console.log("Reversal Result:", parsed);
|
|
260
|
+
return import_server.NextResponse.json({ ResultCode: 0, ResultDesc: "Accepted" });
|
|
261
|
+
}
|
|
262
|
+
if (lastSegment === "reversal-timeout") {
|
|
263
|
+
const body = await request.json();
|
|
264
|
+
console.log("Reversal Timeout:", body);
|
|
265
|
+
return import_server.NextResponse.json({
|
|
266
|
+
ResultCode: 0,
|
|
267
|
+
ResultDesc: "Timeout received"
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (lastSegment === "stk-push") {
|
|
271
|
+
const body = await request.json();
|
|
272
|
+
const {
|
|
273
|
+
amount,
|
|
274
|
+
phoneNumber,
|
|
275
|
+
accountReference,
|
|
276
|
+
transactionDesc,
|
|
277
|
+
callbackUrl
|
|
278
|
+
} = body;
|
|
279
|
+
if (!amount || !phoneNumber) {
|
|
280
|
+
return import_server.NextResponse.json(
|
|
281
|
+
{ error: "Amount and phone number are required" },
|
|
282
|
+
{ status: 400 }
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const response = await client.stkPush({
|
|
286
|
+
amount: Number(amount),
|
|
287
|
+
phoneNumber: String(phoneNumber),
|
|
288
|
+
accountReference: accountReference || "Payment",
|
|
289
|
+
transactionDesc: transactionDesc || "Payment",
|
|
290
|
+
callbackUrl
|
|
291
|
+
});
|
|
292
|
+
return import_server.NextResponse.json(response);
|
|
293
|
+
}
|
|
294
|
+
if (lastSegment === "stk-query") {
|
|
295
|
+
const body = await request.json();
|
|
296
|
+
const { CheckoutRequestID } = body;
|
|
297
|
+
if (!CheckoutRequestID) {
|
|
298
|
+
return import_server.NextResponse.json(
|
|
299
|
+
{ error: "CheckoutRequestID is required" },
|
|
300
|
+
{ status: 400 }
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const response = await client.stkQuery({ CheckoutRequestID });
|
|
304
|
+
return import_server.NextResponse.json(response);
|
|
305
|
+
}
|
|
306
|
+
if (lastSegment === "b2c") {
|
|
307
|
+
const body = await request.json();
|
|
308
|
+
const {
|
|
309
|
+
amount,
|
|
310
|
+
phoneNumber,
|
|
311
|
+
commandID,
|
|
312
|
+
remarks,
|
|
313
|
+
occasion,
|
|
314
|
+
resultUrl,
|
|
315
|
+
timeoutUrl
|
|
316
|
+
} = body;
|
|
317
|
+
if (!amount || !phoneNumber || !commandID) {
|
|
318
|
+
return import_server.NextResponse.json(
|
|
319
|
+
{ error: "Amount, phone number, and command ID are required" },
|
|
320
|
+
{ status: 400 }
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const response = await client.b2c({
|
|
324
|
+
amount: Number(amount),
|
|
325
|
+
phoneNumber: String(phoneNumber),
|
|
326
|
+
commandID,
|
|
327
|
+
remarks: remarks || "Payment",
|
|
328
|
+
occasion,
|
|
329
|
+
resultUrl,
|
|
330
|
+
timeoutUrl
|
|
331
|
+
});
|
|
332
|
+
return import_server.NextResponse.json(response);
|
|
333
|
+
}
|
|
334
|
+
if (lastSegment === "b2b") {
|
|
335
|
+
const body = await request.json();
|
|
336
|
+
const {
|
|
337
|
+
amount,
|
|
338
|
+
partyB,
|
|
339
|
+
commandID,
|
|
340
|
+
senderIdentifierType,
|
|
341
|
+
receiverIdentifierType,
|
|
342
|
+
accountReference,
|
|
343
|
+
remarks,
|
|
344
|
+
resultUrl,
|
|
345
|
+
timeoutUrl
|
|
346
|
+
} = body;
|
|
347
|
+
if (!amount || !partyB || !commandID || !accountReference) {
|
|
348
|
+
return import_server.NextResponse.json(
|
|
349
|
+
{
|
|
350
|
+
error: "Amount, partyB, commandID, and account reference are required"
|
|
351
|
+
},
|
|
352
|
+
{ status: 400 }
|
|
353
|
+
);
|
|
151
354
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
355
|
+
const response = await client.b2b({
|
|
356
|
+
amount: Number(amount),
|
|
357
|
+
partyB: String(partyB),
|
|
358
|
+
commandID,
|
|
359
|
+
senderIdentifierType,
|
|
360
|
+
receiverIdentifierType,
|
|
361
|
+
accountReference: String(accountReference),
|
|
362
|
+
remarks: remarks || "Payment",
|
|
363
|
+
resultUrl,
|
|
364
|
+
timeoutUrl
|
|
365
|
+
});
|
|
366
|
+
return import_server.NextResponse.json(response);
|
|
367
|
+
}
|
|
368
|
+
if (lastSegment === "balance") {
|
|
369
|
+
const body = await request.json();
|
|
370
|
+
const response = await client.accountBalance(
|
|
371
|
+
body
|
|
372
|
+
);
|
|
373
|
+
return import_server.NextResponse.json(response);
|
|
374
|
+
}
|
|
375
|
+
if (lastSegment === "transaction-status") {
|
|
376
|
+
const body = await request.json();
|
|
377
|
+
const { transactionID } = body;
|
|
378
|
+
if (!transactionID) {
|
|
379
|
+
return import_server.NextResponse.json(
|
|
380
|
+
{ error: "Transaction ID is required" },
|
|
381
|
+
{ status: 400 }
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const response = await client.transactionStatus(body);
|
|
385
|
+
return import_server.NextResponse.json(response);
|
|
386
|
+
}
|
|
387
|
+
if (lastSegment === "reversal") {
|
|
388
|
+
const body = await request.json();
|
|
389
|
+
const { transactionID, amount } = body;
|
|
390
|
+
if (!transactionID || !amount) {
|
|
391
|
+
return import_server.NextResponse.json(
|
|
392
|
+
{ error: "Transaction ID and amount are required" },
|
|
393
|
+
{ status: 400 }
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const response = await client.reversal(body);
|
|
397
|
+
return import_server.NextResponse.json(response);
|
|
398
|
+
}
|
|
399
|
+
if (lastSegment === "register-c2b") {
|
|
400
|
+
const body = await request.json();
|
|
401
|
+
const { shortCode, responseType, confirmationURL, validationURL } = body;
|
|
402
|
+
if (!confirmationURL || !validationURL) {
|
|
403
|
+
return import_server.NextResponse.json(
|
|
404
|
+
{ error: "Confirmation URL and validation URL are required" },
|
|
405
|
+
{ status: 400 }
|
|
406
|
+
);
|
|
164
407
|
}
|
|
165
|
-
|
|
166
|
-
|
|
408
|
+
const response = await client.registerC2BUrl({
|
|
409
|
+
shortCode,
|
|
410
|
+
responseType,
|
|
411
|
+
confirmationURL,
|
|
412
|
+
validationURL
|
|
413
|
+
});
|
|
414
|
+
return import_server.NextResponse.json(response);
|
|
415
|
+
}
|
|
416
|
+
if (lastSegment === "generate-qr") {
|
|
417
|
+
const body = await request.json();
|
|
418
|
+
const {
|
|
419
|
+
merchantName,
|
|
420
|
+
refNo,
|
|
421
|
+
amount,
|
|
422
|
+
transactionType,
|
|
423
|
+
creditPartyIdentifier,
|
|
424
|
+
size
|
|
425
|
+
} = body;
|
|
426
|
+
if (size != "500" && size != "300") {
|
|
427
|
+
return import_server.NextResponse.json(
|
|
428
|
+
{
|
|
429
|
+
error: "Size must be either 500 or 300"
|
|
430
|
+
},
|
|
431
|
+
{ status: 400 }
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (!merchantName || !refNo || !amount || !transactionType || !creditPartyIdentifier) {
|
|
167
435
|
return import_server.NextResponse.json(
|
|
168
436
|
{
|
|
169
|
-
|
|
170
|
-
ResultDesc: `Unknown endpoint: ${lastSegment}`
|
|
437
|
+
error: "Merchant name, reference number, amount, transaction type, and credit party identifier are required"
|
|
171
438
|
},
|
|
172
|
-
{ status:
|
|
439
|
+
{ status: 400 }
|
|
173
440
|
);
|
|
441
|
+
}
|
|
442
|
+
const response = await client.generateDynamicQR({
|
|
443
|
+
merchantName,
|
|
444
|
+
refNo,
|
|
445
|
+
amount: Number(amount),
|
|
446
|
+
transactionType,
|
|
447
|
+
creditPartyIdentifier,
|
|
448
|
+
size
|
|
449
|
+
});
|
|
450
|
+
return import_server.NextResponse.json(response);
|
|
174
451
|
}
|
|
452
|
+
return import_server.NextResponse.json(
|
|
453
|
+
{
|
|
454
|
+
ResultCode: 1,
|
|
455
|
+
ResultDesc: `Unknown endpoint: ${lastSegment}`
|
|
456
|
+
},
|
|
457
|
+
{ status: 404 }
|
|
458
|
+
);
|
|
175
459
|
} catch (error) {
|
|
176
460
|
console.error(`M-Pesa ${lastSegment} error:`, error);
|
|
177
461
|
return import_server.NextResponse.json(
|
package/dist/index.mjs
CHANGED
|
@@ -73,18 +73,96 @@ function createMpesaHandlers(client) {
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
76
|
+
* B2C result handler
|
|
77
|
+
* Handles callbacks from M-Pesa after B2C requests
|
|
78
|
+
*/
|
|
79
|
+
b2cResult: {
|
|
80
|
+
POST: async (request) => {
|
|
81
|
+
try {
|
|
82
|
+
const body = await request.json();
|
|
83
|
+
const parsed = client.getCallbackHandler().parseB2CCallback(body);
|
|
84
|
+
console.log("B2C Result:", parsed);
|
|
85
|
+
return NextResponse.json(
|
|
86
|
+
{
|
|
87
|
+
ResultCode: 0,
|
|
88
|
+
ResultDesc: "Accepted"
|
|
89
|
+
},
|
|
90
|
+
{ status: 200 }
|
|
91
|
+
);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error("B2C Result error:", error);
|
|
94
|
+
return NextResponse.json(
|
|
95
|
+
{
|
|
96
|
+
ResultCode: 1,
|
|
97
|
+
ResultDesc: "Processing failed"
|
|
98
|
+
},
|
|
99
|
+
{ status: 200 }
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
/**
|
|
105
|
+
* B2C timeout handler
|
|
106
|
+
* Handles timeout notifications from M-Pesa for B2C requests
|
|
107
|
+
*/
|
|
108
|
+
b2cTimeout: {
|
|
109
|
+
POST: async (request) => {
|
|
110
|
+
try {
|
|
111
|
+
const body = await request.json();
|
|
112
|
+
console.log("B2C Timeout:", body);
|
|
113
|
+
return NextResponse.json(
|
|
114
|
+
{
|
|
115
|
+
ResultCode: 0,
|
|
116
|
+
ResultDesc: "Timeout received"
|
|
117
|
+
},
|
|
118
|
+
{ status: 200 }
|
|
119
|
+
);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error("B2C Timeout error:", error);
|
|
122
|
+
return NextResponse.json(
|
|
123
|
+
{
|
|
124
|
+
ResultCode: 1,
|
|
125
|
+
ResultDesc: "Processing failed"
|
|
126
|
+
},
|
|
127
|
+
{ status: 200 }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
/**
|
|
133
|
+
* Catch-all handler for all M-Pesa webhooks and client-side API requests
|
|
77
134
|
* Routes based on URL path segment
|
|
78
135
|
*
|
|
79
136
|
* Usage: app/api/mpesa/[...mpesa]/route.ts
|
|
80
137
|
* export const { POST } = mpesa.handlers.catchAll;
|
|
81
138
|
*
|
|
82
139
|
* Supported endpoints:
|
|
140
|
+
*
|
|
141
|
+
* WEBHOOKS (from M-Pesa):
|
|
83
142
|
* - /api/mpesa/callback or /api/mpesa/stk-callback - STK Push callbacks
|
|
84
143
|
* - /api/mpesa/validation or /api/mpesa/c2b-validation - C2B validation
|
|
85
144
|
* - /api/mpesa/confirmation or /api/mpesa/c2b-confirmation - C2B confirmation
|
|
145
|
+
* - /api/mpesa/b2c-result - B2C result callback
|
|
146
|
+
* - /api/mpesa/b2c-timeout - B2C timeout callback
|
|
147
|
+
* - /api/mpesa/b2b-result - B2B result callback
|
|
148
|
+
* - /api/mpesa/b2b-timeout - B2B timeout callback
|
|
149
|
+
* - /api/mpesa/balance-result - Account balance result
|
|
150
|
+
* - /api/mpesa/balance-timeout - Account balance timeout
|
|
151
|
+
* - /api/mpesa/reversal-result - Reversal result
|
|
152
|
+
* - /api/mpesa/reversal-timeout - Reversal timeout
|
|
153
|
+
* - /api/mpesa/status-result - Transaction status result
|
|
154
|
+
* - /api/mpesa/status-timeout - Transaction status timeout
|
|
155
|
+
*
|
|
156
|
+
* CLIENT APIs (from the frontend package, will recode react soon):
|
|
86
157
|
* - /api/mpesa/stk-push - Initiate STK Push request
|
|
87
158
|
* - /api/mpesa/stk-query - Query STK Push status
|
|
159
|
+
* - /api/mpesa/b2c - Initiate B2C payment
|
|
160
|
+
* - /api/mpesa/b2b - Initiate B2B payment
|
|
161
|
+
* - /api/mpesa/balance - Query account balance
|
|
162
|
+
* - /api/mpesa/transaction-status - Query transaction status
|
|
163
|
+
* - /api/mpesa/reversal - Reverse a transaction
|
|
164
|
+
* - /api/mpesa/register-c2b - Register C2B URLs
|
|
165
|
+
* - /api/mpesa/generate-qr - Generate dynamic QR code
|
|
88
166
|
*/
|
|
89
167
|
catchAll: {
|
|
90
168
|
POST: async (request) => {
|
|
@@ -92,60 +170,266 @@ function createMpesaHandlers(client) {
|
|
|
92
170
|
const segments = pathname.split("/").filter(Boolean);
|
|
93
171
|
const lastSegment = segments[segments.length - 1];
|
|
94
172
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
173
|
+
if (lastSegment === "callback" || lastSegment === "stk-callback") {
|
|
174
|
+
return handlers.stkCallback.POST(request);
|
|
175
|
+
}
|
|
176
|
+
if (lastSegment === "validation" || lastSegment === "c2b-validation") {
|
|
177
|
+
return handlers.c2bValidation.POST(request);
|
|
178
|
+
}
|
|
179
|
+
if (lastSegment === "confirmation" || lastSegment === "c2b-confirmation") {
|
|
180
|
+
return handlers.c2bConfirmation.POST(request);
|
|
181
|
+
}
|
|
182
|
+
if (lastSegment === "b2c-result") {
|
|
183
|
+
return handlers.b2cResult.POST(request);
|
|
184
|
+
}
|
|
185
|
+
if (lastSegment === "b2c-timeout") {
|
|
186
|
+
return handlers.b2cTimeout.POST(request);
|
|
187
|
+
}
|
|
188
|
+
if (lastSegment === "b2b-result") {
|
|
189
|
+
const body = await request.json();
|
|
190
|
+
const parsed = client.getCallbackHandler().parseB2BCallback(body);
|
|
191
|
+
console.log("B2B Result:", parsed);
|
|
192
|
+
return NextResponse.json({ ResultCode: 0, ResultDesc: "Accepted" });
|
|
193
|
+
}
|
|
194
|
+
if (lastSegment === "b2b-timeout") {
|
|
195
|
+
const body = await request.json();
|
|
196
|
+
console.log("B2B Timeout:", body);
|
|
197
|
+
return NextResponse.json({
|
|
198
|
+
ResultCode: 0,
|
|
199
|
+
ResultDesc: "Timeout received"
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (lastSegment === "balance-result") {
|
|
203
|
+
const body = await request.json();
|
|
204
|
+
const parsed = client.getCallbackHandler().parseAccountBalanceCallback(body);
|
|
205
|
+
console.log("Balance Result:", parsed);
|
|
206
|
+
return NextResponse.json({ ResultCode: 0, ResultDesc: "Accepted" });
|
|
207
|
+
}
|
|
208
|
+
if (lastSegment === "balance-timeout") {
|
|
209
|
+
const body = await request.json();
|
|
210
|
+
console.log("Balance Timeout:", body);
|
|
211
|
+
return NextResponse.json({
|
|
212
|
+
ResultCode: 0,
|
|
213
|
+
ResultDesc: "Timeout received"
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (lastSegment === "status-result") {
|
|
217
|
+
const body = await request.json();
|
|
218
|
+
const parsed = client.getCallbackHandler().parseTransactionStatusCallback(body);
|
|
219
|
+
console.log("Status Result:", parsed);
|
|
220
|
+
return NextResponse.json({ ResultCode: 0, ResultDesc: "Accepted" });
|
|
221
|
+
}
|
|
222
|
+
if (lastSegment === "status-timeout") {
|
|
223
|
+
const body = await request.json();
|
|
224
|
+
console.log("Status Timeout:", body);
|
|
225
|
+
return NextResponse.json({
|
|
226
|
+
ResultCode: 0,
|
|
227
|
+
ResultDesc: "Timeout received"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (lastSegment === "reversal-result") {
|
|
231
|
+
const body = await request.json();
|
|
232
|
+
const parsed = client.getCallbackHandler().parseReversalCallback(body);
|
|
233
|
+
console.log("Reversal Result:", parsed);
|
|
234
|
+
return NextResponse.json({ ResultCode: 0, ResultDesc: "Accepted" });
|
|
235
|
+
}
|
|
236
|
+
if (lastSegment === "reversal-timeout") {
|
|
237
|
+
const body = await request.json();
|
|
238
|
+
console.log("Reversal Timeout:", body);
|
|
239
|
+
return NextResponse.json({
|
|
240
|
+
ResultCode: 0,
|
|
241
|
+
ResultDesc: "Timeout received"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (lastSegment === "stk-push") {
|
|
245
|
+
const body = await request.json();
|
|
246
|
+
const {
|
|
247
|
+
amount,
|
|
248
|
+
phoneNumber,
|
|
249
|
+
accountReference,
|
|
250
|
+
transactionDesc,
|
|
251
|
+
callbackUrl
|
|
252
|
+
} = body;
|
|
253
|
+
if (!amount || !phoneNumber) {
|
|
254
|
+
return NextResponse.json(
|
|
255
|
+
{ error: "Amount and phone number are required" },
|
|
256
|
+
{ status: 400 }
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const response = await client.stkPush({
|
|
260
|
+
amount: Number(amount),
|
|
261
|
+
phoneNumber: String(phoneNumber),
|
|
262
|
+
accountReference: accountReference || "Payment",
|
|
263
|
+
transactionDesc: transactionDesc || "Payment",
|
|
264
|
+
callbackUrl
|
|
265
|
+
});
|
|
266
|
+
return NextResponse.json(response);
|
|
267
|
+
}
|
|
268
|
+
if (lastSegment === "stk-query") {
|
|
269
|
+
const body = await request.json();
|
|
270
|
+
const { CheckoutRequestID } = body;
|
|
271
|
+
if (!CheckoutRequestID) {
|
|
272
|
+
return NextResponse.json(
|
|
273
|
+
{ error: "CheckoutRequestID is required" },
|
|
274
|
+
{ status: 400 }
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const response = await client.stkQuery({ CheckoutRequestID });
|
|
278
|
+
return NextResponse.json(response);
|
|
279
|
+
}
|
|
280
|
+
if (lastSegment === "b2c") {
|
|
281
|
+
const body = await request.json();
|
|
282
|
+
const {
|
|
283
|
+
amount,
|
|
284
|
+
phoneNumber,
|
|
285
|
+
commandID,
|
|
286
|
+
remarks,
|
|
287
|
+
occasion,
|
|
288
|
+
resultUrl,
|
|
289
|
+
timeoutUrl
|
|
290
|
+
} = body;
|
|
291
|
+
if (!amount || !phoneNumber || !commandID) {
|
|
292
|
+
return NextResponse.json(
|
|
293
|
+
{ error: "Amount, phone number, and command ID are required" },
|
|
294
|
+
{ status: 400 }
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const response = await client.b2c({
|
|
298
|
+
amount: Number(amount),
|
|
299
|
+
phoneNumber: String(phoneNumber),
|
|
300
|
+
commandID,
|
|
301
|
+
remarks: remarks || "Payment",
|
|
302
|
+
occasion,
|
|
303
|
+
resultUrl,
|
|
304
|
+
timeoutUrl
|
|
305
|
+
});
|
|
306
|
+
return NextResponse.json(response);
|
|
307
|
+
}
|
|
308
|
+
if (lastSegment === "b2b") {
|
|
309
|
+
const body = await request.json();
|
|
310
|
+
const {
|
|
311
|
+
amount,
|
|
312
|
+
partyB,
|
|
313
|
+
commandID,
|
|
314
|
+
senderIdentifierType,
|
|
315
|
+
receiverIdentifierType,
|
|
316
|
+
accountReference,
|
|
317
|
+
remarks,
|
|
318
|
+
resultUrl,
|
|
319
|
+
timeoutUrl
|
|
320
|
+
} = body;
|
|
321
|
+
if (!amount || !partyB || !commandID || !accountReference) {
|
|
322
|
+
return NextResponse.json(
|
|
323
|
+
{
|
|
324
|
+
error: "Amount, partyB, commandID, and account reference are required"
|
|
325
|
+
},
|
|
326
|
+
{ status: 400 }
|
|
327
|
+
);
|
|
125
328
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
329
|
+
const response = await client.b2b({
|
|
330
|
+
amount: Number(amount),
|
|
331
|
+
partyB: String(partyB),
|
|
332
|
+
commandID,
|
|
333
|
+
senderIdentifierType,
|
|
334
|
+
receiverIdentifierType,
|
|
335
|
+
accountReference: String(accountReference),
|
|
336
|
+
remarks: remarks || "Payment",
|
|
337
|
+
resultUrl,
|
|
338
|
+
timeoutUrl
|
|
339
|
+
});
|
|
340
|
+
return NextResponse.json(response);
|
|
341
|
+
}
|
|
342
|
+
if (lastSegment === "balance") {
|
|
343
|
+
const body = await request.json();
|
|
344
|
+
const response = await client.accountBalance(
|
|
345
|
+
body
|
|
346
|
+
);
|
|
347
|
+
return NextResponse.json(response);
|
|
348
|
+
}
|
|
349
|
+
if (lastSegment === "transaction-status") {
|
|
350
|
+
const body = await request.json();
|
|
351
|
+
const { transactionID } = body;
|
|
352
|
+
if (!transactionID) {
|
|
353
|
+
return NextResponse.json(
|
|
354
|
+
{ error: "Transaction ID is required" },
|
|
355
|
+
{ status: 400 }
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const response = await client.transactionStatus(body);
|
|
359
|
+
return NextResponse.json(response);
|
|
360
|
+
}
|
|
361
|
+
if (lastSegment === "reversal") {
|
|
362
|
+
const body = await request.json();
|
|
363
|
+
const { transactionID, amount } = body;
|
|
364
|
+
if (!transactionID || !amount) {
|
|
365
|
+
return NextResponse.json(
|
|
366
|
+
{ error: "Transaction ID and amount are required" },
|
|
367
|
+
{ status: 400 }
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const response = await client.reversal(body);
|
|
371
|
+
return NextResponse.json(response);
|
|
372
|
+
}
|
|
373
|
+
if (lastSegment === "register-c2b") {
|
|
374
|
+
const body = await request.json();
|
|
375
|
+
const { shortCode, responseType, confirmationURL, validationURL } = body;
|
|
376
|
+
if (!confirmationURL || !validationURL) {
|
|
377
|
+
return NextResponse.json(
|
|
378
|
+
{ error: "Confirmation URL and validation URL are required" },
|
|
379
|
+
{ status: 400 }
|
|
380
|
+
);
|
|
138
381
|
}
|
|
139
|
-
|
|
140
|
-
|
|
382
|
+
const response = await client.registerC2BUrl({
|
|
383
|
+
shortCode,
|
|
384
|
+
responseType,
|
|
385
|
+
confirmationURL,
|
|
386
|
+
validationURL
|
|
387
|
+
});
|
|
388
|
+
return NextResponse.json(response);
|
|
389
|
+
}
|
|
390
|
+
if (lastSegment === "generate-qr") {
|
|
391
|
+
const body = await request.json();
|
|
392
|
+
const {
|
|
393
|
+
merchantName,
|
|
394
|
+
refNo,
|
|
395
|
+
amount,
|
|
396
|
+
transactionType,
|
|
397
|
+
creditPartyIdentifier,
|
|
398
|
+
size
|
|
399
|
+
} = body;
|
|
400
|
+
if (size != "500" && size != "300") {
|
|
401
|
+
return NextResponse.json(
|
|
402
|
+
{
|
|
403
|
+
error: "Size must be either 500 or 300"
|
|
404
|
+
},
|
|
405
|
+
{ status: 400 }
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (!merchantName || !refNo || !amount || !transactionType || !creditPartyIdentifier) {
|
|
141
409
|
return NextResponse.json(
|
|
142
410
|
{
|
|
143
|
-
|
|
144
|
-
ResultDesc: `Unknown endpoint: ${lastSegment}`
|
|
411
|
+
error: "Merchant name, reference number, amount, transaction type, and credit party identifier are required"
|
|
145
412
|
},
|
|
146
|
-
{ status:
|
|
413
|
+
{ status: 400 }
|
|
147
414
|
);
|
|
415
|
+
}
|
|
416
|
+
const response = await client.generateDynamicQR({
|
|
417
|
+
merchantName,
|
|
418
|
+
refNo,
|
|
419
|
+
amount: Number(amount),
|
|
420
|
+
transactionType,
|
|
421
|
+
creditPartyIdentifier,
|
|
422
|
+
size
|
|
423
|
+
});
|
|
424
|
+
return NextResponse.json(response);
|
|
148
425
|
}
|
|
426
|
+
return NextResponse.json(
|
|
427
|
+
{
|
|
428
|
+
ResultCode: 1,
|
|
429
|
+
ResultDesc: `Unknown endpoint: ${lastSegment}`
|
|
430
|
+
},
|
|
431
|
+
{ status: 404 }
|
|
432
|
+
);
|
|
149
433
|
} catch (error) {
|
|
150
434
|
console.error(`M-Pesa ${lastSegment} error:`, error);
|
|
151
435
|
return NextResponse.json(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@singularity-payments/nextjs",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"next": ">=13.0.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@singularity-payments/core": "0.1.0-alpha.
|
|
34
|
+
"@singularity-payments/core": "0.1.0-alpha.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.0.3",
|