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.
@@ -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
+ }