mycontext-cli 2.0.29 → 2.0.31
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/README.md +119 -25
- package/dist/agents/implementations/CodeGenSubAgent.d.ts +4 -0
- package/dist/agents/implementations/CodeGenSubAgent.d.ts.map +1 -1
- package/dist/agents/implementations/CodeGenSubAgent.js +108 -22
- package/dist/agents/implementations/CodeGenSubAgent.js.map +1 -1
- package/dist/agents/implementations/DesignPipelineAgent.d.ts.map +1 -1
- package/dist/agents/implementations/DesignPipelineAgent.js +21 -16
- package/dist/agents/implementations/DesignPipelineAgent.js.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/generate-components.d.ts +5 -0
- package/dist/commands/generate-components.d.ts.map +1 -1
- package/dist/commands/generate-components.js +138 -12
- package/dist/commands/generate-components.js.map +1 -1
- package/dist/commands/generate.d.ts +4 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +74 -56
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +237 -61
- package/dist/commands/init.js.map +1 -1
- package/dist/config/intent-dictionary.json +47 -4
- package/dist/config/model-versions.json +26 -0
- package/dist/package.json +6 -2
- package/dist/templates/instantdb/db.template.ts +14 -0
- package/dist/templates/instantdb/home-client.template.tsx +127 -0
- package/dist/templates/instantdb/page.template.tsx +5 -0
- package/dist/templates/instantdb/perms.template.ts +9 -0
- package/dist/templates/instantdb/schema.template.ts +28 -0
- package/dist/templates/playbooks/instantdb-integration.md +851 -0
- package/dist/templates/playbooks/mpesa-integration.md +652 -0
- package/dist/templates/pm-integration-config.json +20 -0
- package/dist/templates/ui-spec-examples.md +318 -0
- package/dist/templates/ui-spec-templates.json +244 -0
- package/dist/types/intent-dictionary.d.ts +61 -0
- package/dist/types/intent-dictionary.d.ts.map +1 -1
- package/dist/utils/envExampleGenerator.d.ts.map +1 -1
- package/dist/utils/envExampleGenerator.js +36 -27
- package/dist/utils/envExampleGenerator.js.map +1 -1
- package/dist/utils/hostedApiClient.d.ts +10 -3
- package/dist/utils/hostedApiClient.d.ts.map +1 -1
- package/dist/utils/hostedApiClient.js +144 -209
- package/dist/utils/hostedApiClient.js.map +1 -1
- package/dist/utils/hybridAIClient.d.ts.map +1 -1
- package/dist/utils/hybridAIClient.js +8 -26
- package/dist/utils/hybridAIClient.js.map +1 -1
- package/dist/utils/openRouterClient.d.ts.map +1 -1
- package/dist/utils/openRouterClient.js +4 -14
- package/dist/utils/openRouterClient.js.map +1 -1
- package/dist/utils/unifiedDesignContextLoader.d.ts.map +1 -1
- package/dist/utils/unifiedDesignContextLoader.js +25 -12
- package/dist/utils/unifiedDesignContextLoader.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: mpesa-integration
|
|
3
|
+
title: M-Pesa Integration Guide
|
|
4
|
+
description: Complete M-Pesa STK Push and C2B integration for Next.js applications
|
|
5
|
+
category: payment
|
|
6
|
+
tags: ["mpesa", "payment", "stk-push", "c2b", "daraja-api", "nextjs"]
|
|
7
|
+
author: MyContext
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
createdAt: "2024-01-01T00:00:00.000Z"
|
|
10
|
+
updatedAt: "2024-01-01T00:00:00.000Z"
|
|
11
|
+
difficulty: intermediate
|
|
12
|
+
estimatedTime: "2-4 hours"
|
|
13
|
+
prerequisites:
|
|
14
|
+
[
|
|
15
|
+
"Next.js",
|
|
16
|
+
"Safaricom Daraja API credentials",
|
|
17
|
+
"Database (Supabase/PostgreSQL)",
|
|
18
|
+
]
|
|
19
|
+
relatedPlaybooks: ["stripe-integration", "payment-webhooks"]
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# M-Pesa Integration Technical Guide
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
This guide provides a comprehensive M-Pesa integration supporting both **STK Push** (Prompt-to-Pay) and **C2B** (Customer-to-Business) payment methods for Next.js applications.
|
|
27
|
+
|
|
28
|
+
## Prerequisites
|
|
29
|
+
|
|
30
|
+
- Safaricom Daraja API credentials (Consumer Key and Secret)
|
|
31
|
+
- Next.js application
|
|
32
|
+
- Database (Supabase/PostgreSQL recommended)
|
|
33
|
+
- HTTPS domain for callbacks
|
|
34
|
+
|
|
35
|
+
## Environment Setup
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Required environment variables
|
|
39
|
+
MPESA_CONSUMER_KEY=your_consumer_key
|
|
40
|
+
MPESA_CONSUMER_SECRET=your_consumer_secret
|
|
41
|
+
MPESA_BUSINESS_SHORT_CODE=your_shortcode
|
|
42
|
+
MPESA_PASSKEY=your_passkey
|
|
43
|
+
MPESA_CALLBACK_URL=https://your-domain.com/api/payment-callback
|
|
44
|
+
MPESA_C2B_VALIDATION_URL=https://your-domain.com/api/payment/c2b/validation
|
|
45
|
+
MPESA_C2B_CONFIRMATION_URL=https://your-domain.com/api/payment/c2b/confirmation
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Database Schema
|
|
49
|
+
|
|
50
|
+
### M-Pesa Transactions Table
|
|
51
|
+
|
|
52
|
+
```sql
|
|
53
|
+
CREATE TABLE mpesa_transactions (
|
|
54
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
55
|
+
checkout_request_id VARCHAR(255),
|
|
56
|
+
merchant_request_id VARCHAR(255),
|
|
57
|
+
order_id UUID REFERENCES orders(id),
|
|
58
|
+
phone_number VARCHAR(20),
|
|
59
|
+
amount DECIMAL(10,2) NOT NULL,
|
|
60
|
+
actual_amount DECIMAL(10,2),
|
|
61
|
+
account_reference VARCHAR(255),
|
|
62
|
+
transaction_desc VARCHAR(255),
|
|
63
|
+
mpesa_receipt_number VARCHAR(255),
|
|
64
|
+
transaction_date VARCHAR(20),
|
|
65
|
+
status VARCHAR(50) DEFAULT 'pending',
|
|
66
|
+
result_code INTEGER,
|
|
67
|
+
result_desc VARCHAR(255),
|
|
68
|
+
error_message TEXT,
|
|
69
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
70
|
+
updated_at TIMESTAMP DEFAULT NOW()
|
|
71
|
+
);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### C2B Requests Table
|
|
75
|
+
|
|
76
|
+
```sql
|
|
77
|
+
CREATE TABLE mpesa_c2b_requests (
|
|
78
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
79
|
+
transaction_type VARCHAR(50),
|
|
80
|
+
trans_id VARCHAR(255) UNIQUE,
|
|
81
|
+
trans_time VARCHAR(20),
|
|
82
|
+
trans_amount DECIMAL(10,2),
|
|
83
|
+
business_short_code VARCHAR(20),
|
|
84
|
+
bill_ref_number VARCHAR(255),
|
|
85
|
+
invoice_number VARCHAR(255),
|
|
86
|
+
org_account_balance DECIMAL(10,2),
|
|
87
|
+
third_party_trans_id VARCHAR(255),
|
|
88
|
+
msisdn VARCHAR(20),
|
|
89
|
+
first_name VARCHAR(100),
|
|
90
|
+
middle_name VARCHAR(100),
|
|
91
|
+
last_name VARCHAR(100),
|
|
92
|
+
request_type VARCHAR(20),
|
|
93
|
+
status VARCHAR(50) DEFAULT 'pending',
|
|
94
|
+
reconciliation_status VARCHAR(50) DEFAULT 'unmatched',
|
|
95
|
+
matched_order_id UUID REFERENCES orders(id),
|
|
96
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
97
|
+
);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## STK Push Implementation
|
|
101
|
+
|
|
102
|
+
### 1. STK Push Initiation API
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// app/api/payment/stk/route.ts
|
|
106
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
107
|
+
|
|
108
|
+
export async function POST(request: NextRequest) {
|
|
109
|
+
try {
|
|
110
|
+
const { orderId, phoneNumber, amount, accountReference } =
|
|
111
|
+
await request.json();
|
|
112
|
+
|
|
113
|
+
// Get M-Pesa configuration
|
|
114
|
+
const mpesaConfig = {
|
|
115
|
+
consumerKey: process.env.MPESA_CONSUMER_KEY!,
|
|
116
|
+
consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
|
|
117
|
+
businessShortCode: process.env.MPESA_BUSINESS_SHORT_CODE!,
|
|
118
|
+
passkey: process.env.MPESA_PASSKEY!,
|
|
119
|
+
callbackUrl: process.env.MPESA_CALLBACK_URL!,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Generate access token
|
|
123
|
+
const accessToken = await generateAccessToken(
|
|
124
|
+
mpesaConfig.consumerKey,
|
|
125
|
+
mpesaConfig.consumerSecret
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Generate password
|
|
129
|
+
const timestamp = new Date()
|
|
130
|
+
.toISOString()
|
|
131
|
+
.replace(/[^0-9]/g, "")
|
|
132
|
+
.slice(0, -3);
|
|
133
|
+
const password = Buffer.from(
|
|
134
|
+
`${mpesaConfig.businessShortCode}${mpesaConfig.passkey}${timestamp}`
|
|
135
|
+
).toString("base64");
|
|
136
|
+
|
|
137
|
+
// STK Push payload
|
|
138
|
+
const stkPushPayload = {
|
|
139
|
+
BusinessShortCode: mpesaConfig.businessShortCode,
|
|
140
|
+
Password: password,
|
|
141
|
+
Timestamp: timestamp,
|
|
142
|
+
TransactionType: "CustomerPayBillOnline",
|
|
143
|
+
Amount: Math.round(amount),
|
|
144
|
+
PartyA: phoneNumber.replace("+", ""),
|
|
145
|
+
PartyB: mpesaConfig.businessShortCode,
|
|
146
|
+
PhoneNumber: phoneNumber.replace("+", ""),
|
|
147
|
+
CallBackURL: mpesaConfig.callbackUrl,
|
|
148
|
+
AccountReference: accountReference || `ORDER-${orderId}`,
|
|
149
|
+
TransactionDesc: "Payment",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Make API call to M-Pesa
|
|
153
|
+
const response = await fetch(
|
|
154
|
+
"https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest",
|
|
155
|
+
{
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
Authorization: `Bearer ${accessToken}`,
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify(stkPushPayload),
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const result = await response.json();
|
|
166
|
+
|
|
167
|
+
if (result.ResponseCode === "0") {
|
|
168
|
+
// Store transaction in database
|
|
169
|
+
await storeTransaction({
|
|
170
|
+
checkoutRequestId: result.CheckoutRequestID,
|
|
171
|
+
merchantRequestId: result.MerchantRequestID,
|
|
172
|
+
orderId,
|
|
173
|
+
phoneNumber,
|
|
174
|
+
amount,
|
|
175
|
+
accountReference,
|
|
176
|
+
status: "pending",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return NextResponse.json(result);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error("STK Push error:", error);
|
|
183
|
+
return NextResponse.json({ error: "STK Push failed" }, { status: 500 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function generateAccessToken(
|
|
188
|
+
consumerKey: string,
|
|
189
|
+
consumerSecret: string
|
|
190
|
+
): Promise<string> {
|
|
191
|
+
const response = await fetch(
|
|
192
|
+
"https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials",
|
|
193
|
+
{
|
|
194
|
+
method: "GET",
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Basic ${Buffer.from(
|
|
197
|
+
`${consumerKey}:${consumerSecret}`
|
|
198
|
+
).toString("base64")}`,
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const data = await response.json();
|
|
204
|
+
return data.access_token;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 2. STK Push Callback Processing
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// app/api/payment-callback/route.ts
|
|
212
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
213
|
+
|
|
214
|
+
export async function POST(request: NextRequest) {
|
|
215
|
+
try {
|
|
216
|
+
const body = await request.json();
|
|
217
|
+
|
|
218
|
+
// Handle different callback structures
|
|
219
|
+
let stkCallback;
|
|
220
|
+
if (body.Body && body.Body.stkCallback) {
|
|
221
|
+
stkCallback = body.Body.stkCallback;
|
|
222
|
+
} else if (body.stkCallback) {
|
|
223
|
+
stkCallback = body.stkCallback;
|
|
224
|
+
} else if (body.Body) {
|
|
225
|
+
stkCallback = body.Body;
|
|
226
|
+
} else {
|
|
227
|
+
stkCallback = body;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const { ResultCode, ResultDesc, CheckoutRequestID, CallbackMetadata } =
|
|
231
|
+
stkCallback;
|
|
232
|
+
|
|
233
|
+
if (ResultCode === 0) {
|
|
234
|
+
// Payment successful
|
|
235
|
+
const metadata = CallbackMetadata?.Item || [];
|
|
236
|
+
const getMetadataValue = (name: string) =>
|
|
237
|
+
metadata.find((item: any) => item.Name === name)?.Value;
|
|
238
|
+
|
|
239
|
+
const amount = getMetadataValue("Amount");
|
|
240
|
+
const mpesaReceiptNumber = getMetadataValue("MpesaReceiptNumber");
|
|
241
|
+
const transactionDate = getMetadataValue("TransactionDate");
|
|
242
|
+
const phoneNumber = getMetadataValue("PhoneNumber");
|
|
243
|
+
|
|
244
|
+
// Update transaction record
|
|
245
|
+
await updateTransaction(CheckoutRequestID, {
|
|
246
|
+
mpesa_receipt_number: mpesaReceiptNumber,
|
|
247
|
+
transaction_date: transactionDate,
|
|
248
|
+
actual_amount: amount,
|
|
249
|
+
status: "completed",
|
|
250
|
+
result_code: ResultCode,
|
|
251
|
+
result_desc: ResultDesc,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Update order status
|
|
255
|
+
await updateOrderStatus(CheckoutRequestID, {
|
|
256
|
+
payment_status: "paid",
|
|
257
|
+
mpesa_receipt_number: mpesaReceiptNumber,
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
// Payment failed
|
|
261
|
+
await updateTransaction(CheckoutRequestID, {
|
|
262
|
+
status: "failed",
|
|
263
|
+
result_code: ResultCode,
|
|
264
|
+
result_desc: ResultDesc,
|
|
265
|
+
error_message: ResultDesc,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return NextResponse.json({ ResultCode: 0, ResultDesc: "Success" });
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error("Callback processing error:", error);
|
|
272
|
+
return NextResponse.json(
|
|
273
|
+
{ ResultCode: 1, ResultDesc: "Error" },
|
|
274
|
+
{ status: 500 }
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## C2B Implementation
|
|
281
|
+
|
|
282
|
+
### 1. C2B Validation
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// app/api/payment/c2b/validation/route.ts
|
|
286
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
287
|
+
|
|
288
|
+
export async function POST(request: NextRequest) {
|
|
289
|
+
try {
|
|
290
|
+
const {
|
|
291
|
+
TransactionType,
|
|
292
|
+
TransID,
|
|
293
|
+
TransTime,
|
|
294
|
+
TransAmount,
|
|
295
|
+
BusinessShortCode,
|
|
296
|
+
BillRefNumber,
|
|
297
|
+
InvoiceNumber,
|
|
298
|
+
OrgAccountBalance,
|
|
299
|
+
ThirdPartyTransID,
|
|
300
|
+
MSISDN,
|
|
301
|
+
FirstName,
|
|
302
|
+
MiddleName,
|
|
303
|
+
LastName,
|
|
304
|
+
} = await request.json();
|
|
305
|
+
|
|
306
|
+
// Amount validation
|
|
307
|
+
const amount = parseFloat(TransAmount);
|
|
308
|
+
if (amount < 10) {
|
|
309
|
+
return NextResponse.json({
|
|
310
|
+
ResultCode: "C2B00012",
|
|
311
|
+
ResultDesc: "Amount below minimum threshold",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (amount > 100000) {
|
|
316
|
+
return NextResponse.json({
|
|
317
|
+
ResultCode: "C2B00013",
|
|
318
|
+
ResultDesc: "Amount above maximum threshold",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Business shortcode validation
|
|
323
|
+
const isValidShortcode = await validateBusinessShortcode(BusinessShortCode);
|
|
324
|
+
if (!isValidShortcode) {
|
|
325
|
+
return NextResponse.json({
|
|
326
|
+
ResultCode: "C2B00014",
|
|
327
|
+
ResultDesc: "Invalid business shortcode",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return NextResponse.json({
|
|
332
|
+
ResultCode: "0",
|
|
333
|
+
ResultDesc: "Accepted",
|
|
334
|
+
});
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error("C2B validation error:", error);
|
|
337
|
+
return NextResponse.json({
|
|
338
|
+
ResultCode: "C2B00015",
|
|
339
|
+
ResultDesc: "Validation error",
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### 2. C2B Confirmation
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// app/api/payment/c2b/confirmation/route.ts
|
|
349
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
350
|
+
|
|
351
|
+
export async function POST(request: NextRequest) {
|
|
352
|
+
try {
|
|
353
|
+
const {
|
|
354
|
+
TransactionType,
|
|
355
|
+
TransID,
|
|
356
|
+
TransTime,
|
|
357
|
+
TransAmount,
|
|
358
|
+
BusinessShortCode,
|
|
359
|
+
BillRefNumber,
|
|
360
|
+
InvoiceNumber,
|
|
361
|
+
OrgAccountBalance,
|
|
362
|
+
ThirdPartyTransID,
|
|
363
|
+
MSISDN,
|
|
364
|
+
FirstName,
|
|
365
|
+
MiddleName,
|
|
366
|
+
LastName,
|
|
367
|
+
} = await request.json();
|
|
368
|
+
|
|
369
|
+
// Check for duplicate processing
|
|
370
|
+
const existingRequest = await getC2BRequest(TransID);
|
|
371
|
+
if (existingRequest) {
|
|
372
|
+
return NextResponse.json({
|
|
373
|
+
ResultCode: "0",
|
|
374
|
+
ResultDesc: "Already processed",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Store C2B request
|
|
379
|
+
await storeC2BRequest({
|
|
380
|
+
transactionType: TransactionType,
|
|
381
|
+
transId: TransID,
|
|
382
|
+
transTime: TransTime,
|
|
383
|
+
transAmount: parseFloat(TransAmount),
|
|
384
|
+
businessShortCode: BusinessShortCode,
|
|
385
|
+
billRefNumber: BillRefNumber,
|
|
386
|
+
invoiceNumber: InvoiceNumber,
|
|
387
|
+
orgAccountBalance: parseFloat(OrgAccountBalance),
|
|
388
|
+
thirdPartyTransId: ThirdPartyTransID,
|
|
389
|
+
msisdn: MSISDN,
|
|
390
|
+
firstName: FirstName,
|
|
391
|
+
middleName: MiddleName,
|
|
392
|
+
lastName: LastName,
|
|
393
|
+
requestType: "confirmation",
|
|
394
|
+
status: "pending",
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Attempt to match with existing order
|
|
398
|
+
const matchedOrder = await matchPaymentWithOrder({
|
|
399
|
+
phoneNumber: MSISDN,
|
|
400
|
+
amount: parseFloat(TransAmount),
|
|
401
|
+
billRefNumber: BillRefNumber,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (matchedOrder) {
|
|
405
|
+
await updateC2BRequest(TransID, {
|
|
406
|
+
reconciliationStatus: "matched",
|
|
407
|
+
matchedOrderId: matchedOrder.id,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await updateOrderStatus(matchedOrder.id, {
|
|
411
|
+
payment_status: "paid",
|
|
412
|
+
mpesa_receipt_number: TransID,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return NextResponse.json({
|
|
417
|
+
ResultCode: "0",
|
|
418
|
+
ResultDesc: "Success",
|
|
419
|
+
});
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error("C2B confirmation error:", error);
|
|
422
|
+
return NextResponse.json({
|
|
423
|
+
ResultCode: "1",
|
|
424
|
+
ResultDesc: "Error",
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Frontend Integration
|
|
431
|
+
|
|
432
|
+
### Payment Component
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// components/PaymentForm.tsx
|
|
436
|
+
"use client";
|
|
437
|
+
|
|
438
|
+
import { useState } from "react";
|
|
439
|
+
import { Button } from "@/components/ui/button";
|
|
440
|
+
import { Input } from "@/components/ui/input";
|
|
441
|
+
import { Label } from "@/components/ui/label";
|
|
442
|
+
|
|
443
|
+
interface PaymentFormProps {
|
|
444
|
+
orderId: string;
|
|
445
|
+
amount: number;
|
|
446
|
+
onSuccess: (receiptNumber: string) => void;
|
|
447
|
+
onError: (error: string) => void;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function PaymentForm({
|
|
451
|
+
orderId,
|
|
452
|
+
amount,
|
|
453
|
+
onSuccess,
|
|
454
|
+
onError,
|
|
455
|
+
}: PaymentFormProps) {
|
|
456
|
+
const [phoneNumber, setPhoneNumber] = useState("");
|
|
457
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
458
|
+
|
|
459
|
+
const handleSTKPush = async () => {
|
|
460
|
+
if (!phoneNumber) {
|
|
461
|
+
onError("Phone number is required");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
setIsLoading(true);
|
|
466
|
+
try {
|
|
467
|
+
const response = await fetch("/api/payment/stk", {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers: { "Content-Type": "application/json" },
|
|
470
|
+
body: JSON.stringify({
|
|
471
|
+
orderId,
|
|
472
|
+
phoneNumber,
|
|
473
|
+
amount,
|
|
474
|
+
accountReference: `ORDER-${orderId}`,
|
|
475
|
+
}),
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const result = await response.json();
|
|
479
|
+
|
|
480
|
+
if (result.ResponseCode === "0") {
|
|
481
|
+
// Start polling for payment status
|
|
482
|
+
pollPaymentStatus(orderId);
|
|
483
|
+
} else {
|
|
484
|
+
onError(result.ResponseDescription || "Payment initiation failed");
|
|
485
|
+
}
|
|
486
|
+
} catch (error) {
|
|
487
|
+
onError("Network error occurred");
|
|
488
|
+
} finally {
|
|
489
|
+
setIsLoading(false);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const pollPaymentStatus = async (orderId: string) => {
|
|
494
|
+
const maxAttempts = 30; // 5 minutes with 10-second intervals
|
|
495
|
+
let attempts = 0;
|
|
496
|
+
|
|
497
|
+
const interval = setInterval(async () => {
|
|
498
|
+
attempts++;
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
const response = await fetch(`/api/payment/status?orderId=${orderId}`);
|
|
502
|
+
const status = await response.json();
|
|
503
|
+
|
|
504
|
+
if (status.payment_status === "paid") {
|
|
505
|
+
clearInterval(interval);
|
|
506
|
+
onSuccess(status.mpesa_receipt_number);
|
|
507
|
+
} else if (status.payment_status === "failed") {
|
|
508
|
+
clearInterval(interval);
|
|
509
|
+
onError("Payment failed");
|
|
510
|
+
} else if (attempts >= maxAttempts) {
|
|
511
|
+
clearInterval(interval);
|
|
512
|
+
onError("Payment timeout");
|
|
513
|
+
}
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if (attempts >= maxAttempts) {
|
|
516
|
+
clearInterval(interval);
|
|
517
|
+
onError("Status check failed");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}, 10000); // Check every 10 seconds
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
return (
|
|
524
|
+
<div className="space-y-4">
|
|
525
|
+
<div>
|
|
526
|
+
<Label htmlFor="phone">Phone Number</Label>
|
|
527
|
+
<Input
|
|
528
|
+
id="phone"
|
|
529
|
+
type="tel"
|
|
530
|
+
placeholder="+254712345678"
|
|
531
|
+
value={phoneNumber}
|
|
532
|
+
onChange={(e) => setPhoneNumber(e.target.value)}
|
|
533
|
+
/>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<div className="text-center">
|
|
537
|
+
<p className="text-sm text-gray-600 mb-2">
|
|
538
|
+
Amount: KES {amount.toLocaleString()}
|
|
539
|
+
</p>
|
|
540
|
+
<Button onClick={handleSTKPush} disabled={isLoading} className="w-full">
|
|
541
|
+
{isLoading ? "Processing..." : "Pay with M-Pesa"}
|
|
542
|
+
</Button>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Error Handling
|
|
550
|
+
|
|
551
|
+
### Common M-Pesa Error Codes
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
const errorMessages = {
|
|
555
|
+
1032: "Payment cancelled by user",
|
|
556
|
+
1037: "User timeout - no response",
|
|
557
|
+
1: "Insufficient balance",
|
|
558
|
+
2001: "Invalid PIN entered",
|
|
559
|
+
1019: "Transaction expired",
|
|
560
|
+
1001: "User busy - another transaction in progress",
|
|
561
|
+
1025: "Request processing error",
|
|
562
|
+
9999: "System error",
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
export function getErrorMessage(resultCode: number): string {
|
|
566
|
+
return errorMessages[resultCode] || "Unknown error occurred";
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## Security Best Practices
|
|
571
|
+
|
|
572
|
+
1. **Validate all inputs** - Sanitize phone numbers and amounts
|
|
573
|
+
2. **Use HTTPS only** - Secure all payment endpoints
|
|
574
|
+
3. **Implement rate limiting** - Prevent abuse of payment endpoints
|
|
575
|
+
4. **Log all transactions** - Maintain audit trail
|
|
576
|
+
5. **Validate callbacks** - Verify webhook authenticity
|
|
577
|
+
6. **Store sensitive data securely** - Use environment variables
|
|
578
|
+
|
|
579
|
+
## Testing
|
|
580
|
+
|
|
581
|
+
### Test STK Push
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
// Test STK Push with sandbox
|
|
585
|
+
const testSTKPush = async () => {
|
|
586
|
+
const response = await fetch("/api/payment/stk", {
|
|
587
|
+
method: "POST",
|
|
588
|
+
headers: { "Content-Type": "application/json" },
|
|
589
|
+
body: JSON.stringify({
|
|
590
|
+
orderId: "test-order-123",
|
|
591
|
+
phoneNumber: "+254708374149", // Test number
|
|
592
|
+
amount: 100,
|
|
593
|
+
accountReference: "TEST-123",
|
|
594
|
+
}),
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
return response.json();
|
|
598
|
+
};
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## Monitoring
|
|
602
|
+
|
|
603
|
+
### Key Metrics to Track
|
|
604
|
+
|
|
605
|
+
- Transaction success rate
|
|
606
|
+
- Average processing time
|
|
607
|
+
- Error rates by type
|
|
608
|
+
- Payment completion rates
|
|
609
|
+
- Callback processing time
|
|
610
|
+
|
|
611
|
+
### Database Queries
|
|
612
|
+
|
|
613
|
+
```sql
|
|
614
|
+
-- Success rate
|
|
615
|
+
SELECT
|
|
616
|
+
COUNT(*) as total,
|
|
617
|
+
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successful,
|
|
618
|
+
ROUND(SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as success_rate
|
|
619
|
+
FROM mpesa_transactions
|
|
620
|
+
WHERE created_at >= NOW() - INTERVAL '24 hours';
|
|
621
|
+
|
|
622
|
+
-- Error analysis
|
|
623
|
+
SELECT result_desc, COUNT(*) as count
|
|
624
|
+
FROM mpesa_transactions
|
|
625
|
+
WHERE status = 'failed'
|
|
626
|
+
AND created_at >= NOW() - INTERVAL '24 hours'
|
|
627
|
+
GROUP BY result_desc
|
|
628
|
+
ORDER BY count DESC;
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
## Troubleshooting
|
|
632
|
+
|
|
633
|
+
### Common Issues
|
|
634
|
+
|
|
635
|
+
1. **STK Push Timeout**
|
|
636
|
+
|
|
637
|
+
- Check network connectivity
|
|
638
|
+
- Verify callback URL accessibility
|
|
639
|
+
- Check M-Pesa service status
|
|
640
|
+
|
|
641
|
+
2. **C2B Payment Not Matching**
|
|
642
|
+
|
|
643
|
+
- Verify phone number format
|
|
644
|
+
- Check amount precision
|
|
645
|
+
- Review matching strategies
|
|
646
|
+
|
|
647
|
+
3. **Configuration Errors**
|
|
648
|
+
- Validate environment variables
|
|
649
|
+
- Check business shortcode
|
|
650
|
+
- Verify API credentials
|
|
651
|
+
|
|
652
|
+
This playbook provides a complete, production-ready M-Pesa integration that can be used as a reference for any Next.js application requiring mobile payment functionality.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pmEndpoint": "https://your-mycontext-pm-instance.com/api",
|
|
3
|
+
"webhookUrl": "https://your-mycontext-pm-instance.com/webhook",
|
|
4
|
+
"apiKey": "your-api-key-here",
|
|
5
|
+
"projectId": "your-project-id",
|
|
6
|
+
"syncInterval": 60,
|
|
7
|
+
"enableRealTimeSync": true,
|
|
8
|
+
"retryAttempts": 3,
|
|
9
|
+
"timeout": 30000,
|
|
10
|
+
"_comments": {
|
|
11
|
+
"pmEndpoint": "The base URL of your mycontext PM instance",
|
|
12
|
+
"webhookUrl": "URL where progress updates should be sent",
|
|
13
|
+
"apiKey": "Authentication key for PM API access",
|
|
14
|
+
"projectId": "The specific project ID to integrate with",
|
|
15
|
+
"syncInterval": "How often to sync (minutes)",
|
|
16
|
+
"enableRealTimeSync": "Enable automatic periodic syncing",
|
|
17
|
+
"retryAttempts": "Number of retry attempts for failed requests",
|
|
18
|
+
"timeout": "Request timeout in milliseconds"
|
|
19
|
+
}
|
|
20
|
+
}
|