9pay-integrate 1.0.0
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 +241 -0
- package/dist/index.cjs +317 -0
- package/dist/index.d.cts +195 -0
- package/dist/index.d.ts +195 -0
- package/dist/index.js +268 -0
- package/dist/next/index.cjs +278 -0
- package/dist/next/index.d.cts +101 -0
- package/dist/next/index.d.ts +101 -0
- package/dist/next/index.js +239 -0
- package/package.json +65 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// src/next/handlers.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
|
|
4
|
+
// src/core/verification.ts
|
|
5
|
+
import * as crypto from "crypto";
|
|
6
|
+
function verifyChecksum(result, checksum, checksumKey) {
|
|
7
|
+
if (!result || !checksum) {
|
|
8
|
+
return { valid: false, data: null, error: "Missing result or checksum" };
|
|
9
|
+
}
|
|
10
|
+
const expected = crypto.createHash("sha256").update(result + checksumKey).digest("hex").toUpperCase();
|
|
11
|
+
if (expected !== checksum) {
|
|
12
|
+
return { valid: false, data: null, error: "Checksum mismatch" };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const decoded = JSON.parse(
|
|
16
|
+
Buffer.from(result, "base64").toString("utf-8")
|
|
17
|
+
);
|
|
18
|
+
return { valid: true, data: decoded };
|
|
19
|
+
} catch {
|
|
20
|
+
return { valid: false, data: null, error: "Failed to decode result" };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/core/verify-callback.ts
|
|
25
|
+
function verifyCallback(searchParams, config) {
|
|
26
|
+
const result = searchParams.get("result") || "";
|
|
27
|
+
const checksum = searchParams.get("checksum") || "";
|
|
28
|
+
const verification = verifyChecksum(result, checksum, config.checksumKey);
|
|
29
|
+
if (!verification.valid || !verification.data) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
rawParams: {},
|
|
33
|
+
error: verification.error || "Invalid callback payload"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const paymentInfo = verification.data;
|
|
37
|
+
const invoiceNo = paymentInfo.invoice_no;
|
|
38
|
+
const orderId = typeof invoiceNo === "string" && invoiceNo.trim().length > 0 ? invoiceNo : typeof invoiceNo === "number" ? String(invoiceNo) : null;
|
|
39
|
+
if (!orderId) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
rawParams: paymentInfo,
|
|
43
|
+
error: "Missing invoice_no in callback payload"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
orderId,
|
|
49
|
+
amount: Number(paymentInfo.amount ?? 0),
|
|
50
|
+
currency: String(paymentInfo.currency ?? ""),
|
|
51
|
+
status: Number(paymentInfo.status ?? 0),
|
|
52
|
+
rawParams: paymentInfo
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/core/constants.ts
|
|
57
|
+
var NINEPAY_STATUS = {
|
|
58
|
+
PENDING: 1,
|
|
59
|
+
PROCESSING: 2,
|
|
60
|
+
CANCELLED: 3,
|
|
61
|
+
REFUNDED: 4,
|
|
62
|
+
SUCCESS: 5,
|
|
63
|
+
FAILED: 6
|
|
64
|
+
};
|
|
65
|
+
var SUCCESS_STATUS = NINEPAY_STATUS.SUCCESS;
|
|
66
|
+
|
|
67
|
+
// src/core/ipn-processor.ts
|
|
68
|
+
async function processIpn(payload, options) {
|
|
69
|
+
const { config, orderRepository, notifications } = options;
|
|
70
|
+
const verification = verifyChecksum(
|
|
71
|
+
payload.result,
|
|
72
|
+
payload.checksum,
|
|
73
|
+
config.checksumKey
|
|
74
|
+
);
|
|
75
|
+
if (!verification.valid || !verification.data) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
message: verification.error || "Invalid checksum",
|
|
79
|
+
statusCode: 403
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const paymentInfo = verification.data;
|
|
83
|
+
const invoiceNo = paymentInfo.invoice_no;
|
|
84
|
+
const orderId = typeof invoiceNo === "string" && invoiceNo.trim().length > 0 ? invoiceNo : typeof invoiceNo === "number" ? String(invoiceNo) : null;
|
|
85
|
+
if (!orderId) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
message: "Missing invoice_no in IPN payload",
|
|
89
|
+
statusCode: 400
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const isSuccess = Number(paymentInfo.status) === SUCCESS_STATUS;
|
|
93
|
+
const amount = Number(paymentInfo.amount ?? 0);
|
|
94
|
+
const currency = String(paymentInfo.currency ?? "");
|
|
95
|
+
if (isSuccess) {
|
|
96
|
+
try {
|
|
97
|
+
await orderRepository.updateOrderStatus(orderId, "completed");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
message: `Failed to update order: ${error instanceof Error ? error.message : String(error)}`,
|
|
102
|
+
statusCode: 500,
|
|
103
|
+
orderId
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (notifications?.onSuccess) {
|
|
107
|
+
try {
|
|
108
|
+
await notifications.onSuccess({
|
|
109
|
+
orderId,
|
|
110
|
+
status: "completed",
|
|
111
|
+
amount,
|
|
112
|
+
currency,
|
|
113
|
+
paymentMethod: "9pay",
|
|
114
|
+
metadata: paymentInfo
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
message: "Order updated successfully",
|
|
122
|
+
statusCode: 200,
|
|
123
|
+
orderId
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await orderRepository.updateOrderStatus(orderId, "failed");
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
message: `Failed to update order: ${error instanceof Error ? error.message : String(error)}`,
|
|
132
|
+
statusCode: 500,
|
|
133
|
+
orderId
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (notifications?.onFailure) {
|
|
137
|
+
try {
|
|
138
|
+
await notifications.onFailure({
|
|
139
|
+
orderId,
|
|
140
|
+
status: "failed",
|
|
141
|
+
amount,
|
|
142
|
+
currency,
|
|
143
|
+
paymentMethod: "9pay",
|
|
144
|
+
metadata: paymentInfo
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
message: "Payment failed",
|
|
152
|
+
statusCode: 200,
|
|
153
|
+
orderId
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/core/ipn.ts
|
|
158
|
+
function extractIpnFromFormData(formData) {
|
|
159
|
+
return {
|
|
160
|
+
result: String(formData.get("result") || ""),
|
|
161
|
+
checksum: String(formData.get("checksum") || "")
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function extractIpnFromUrlEncoded(body) {
|
|
165
|
+
const params = new URLSearchParams(body);
|
|
166
|
+
return {
|
|
167
|
+
result: params.get("result") || "",
|
|
168
|
+
checksum: params.get("checksum") || ""
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/next/handlers.ts
|
|
173
|
+
function verifyNinePayCallback(request, config) {
|
|
174
|
+
const { searchParams } = new URL(request.url);
|
|
175
|
+
return verifyCallback(searchParams, config);
|
|
176
|
+
}
|
|
177
|
+
async function parseIpnRequest(request) {
|
|
178
|
+
const contentType = request.headers.get("content-type") || "";
|
|
179
|
+
if (contentType.includes("multipart/form-data")) {
|
|
180
|
+
const formData = await request.formData();
|
|
181
|
+
const extracted = extractIpnFromFormData(formData);
|
|
182
|
+
return extracted;
|
|
183
|
+
}
|
|
184
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
185
|
+
const body = await request.text();
|
|
186
|
+
const extracted = extractIpnFromUrlEncoded(body);
|
|
187
|
+
return extracted;
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`Unsupported IPN content-type: ${contentType || "unknown"}`);
|
|
190
|
+
}
|
|
191
|
+
async function handle9PayIpn(request, options) {
|
|
192
|
+
try {
|
|
193
|
+
const payload = await parseIpnRequest(request);
|
|
194
|
+
const result = await processIpn(payload, {
|
|
195
|
+
config: options.config,
|
|
196
|
+
orderRepository: options.orderRepository,
|
|
197
|
+
notifications: options.notifications
|
|
198
|
+
});
|
|
199
|
+
return NextResponse.json(
|
|
200
|
+
{ success: result.success, message: result.message },
|
|
201
|
+
{ status: result.statusCode }
|
|
202
|
+
);
|
|
203
|
+
} catch {
|
|
204
|
+
return NextResponse.json(
|
|
205
|
+
{ success: false, message: "Unsupported or invalid form payload" },
|
|
206
|
+
{ status: 400 }
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function handlePaymentStatus(request, options) {
|
|
211
|
+
const { searchParams } = new URL(request.url);
|
|
212
|
+
const orderId = searchParams.get("orderId");
|
|
213
|
+
if (!orderId) {
|
|
214
|
+
return NextResponse.json(
|
|
215
|
+
{ success: false, message: "Missing orderId" },
|
|
216
|
+
{ status: 400 }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const status = await options.orderRepository.getOrderStatus(orderId);
|
|
221
|
+
if (status === null) {
|
|
222
|
+
return NextResponse.json(
|
|
223
|
+
{ success: false, message: "Order not found" },
|
|
224
|
+
{ status: 404 }
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return NextResponse.json({ success: true, status });
|
|
228
|
+
} catch {
|
|
229
|
+
return NextResponse.json(
|
|
230
|
+
{ success: false, message: "Internal server error" },
|
|
231
|
+
{ status: 500 }
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export {
|
|
236
|
+
handle9PayIpn,
|
|
237
|
+
handlePaymentStatus,
|
|
238
|
+
verifyNinePayCallback
|
|
239
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "9pay-integrate",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Complete 9Pay payment gateway integration for Node.js and Next.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./next": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/next/index.d.ts",
|
|
23
|
+
"default": "./dist/next/index.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/next/index.d.cts",
|
|
27
|
+
"default": "./dist/next/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"next": ">=14.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"next": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"tsup": "^8.0.0",
|
|
48
|
+
"typescript": "^5.0.0",
|
|
49
|
+
"vitest": "^2.0.0",
|
|
50
|
+
"next": ">=14.0.0",
|
|
51
|
+
"@types/node": "^20.0.0"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist"
|
|
55
|
+
],
|
|
56
|
+
"keywords": [
|
|
57
|
+
"9pay",
|
|
58
|
+
"payment",
|
|
59
|
+
"payment-gateway",
|
|
60
|
+
"vietnam",
|
|
61
|
+
"nextjs",
|
|
62
|
+
"typescript"
|
|
63
|
+
],
|
|
64
|
+
"license": "MIT"
|
|
65
|
+
}
|