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
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# 9pay-integrate
|
|
2
|
+
|
|
3
|
+
Complete 9Pay payment gateway integration for Node.js and Next.js. Generate payment URLs, verify callbacks, process IPN webhooks — all with a clean, framework-agnostic core.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm install 9pay-integrate
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### 1. Configure
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// lib/9pay-config.ts
|
|
15
|
+
import { configFromEnv } from "9pay-integrate";
|
|
16
|
+
|
|
17
|
+
export const ninepay = configFromEnv(process.env);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Required environment variables:
|
|
21
|
+
- `NINEPAY_MERCHANT_KEY`
|
|
22
|
+
- `NINEPAY_SECRET_KEY`
|
|
23
|
+
- `NINEPAY_CHECKSUM_KEY`
|
|
24
|
+
- `NINEPAY_RETURN_URL` (e.g. `https://yoursite.com/api/checkout/9pay/callback`)
|
|
25
|
+
|
|
26
|
+
Optional:
|
|
27
|
+
- `NINEPAY_BASE_URL` (default: `https://payment.9pay.vn`)
|
|
28
|
+
- `NINEPAY_CANCEL_URL` (default: same as RETURN_URL)
|
|
29
|
+
|
|
30
|
+
### 2. Implement Order Repository
|
|
31
|
+
|
|
32
|
+
The package needs a way to manage orders. Implement the `OrderRepository` interface for your data layer (Strapi, Prisma, Drizzle, MongoDB, REST API, etc.).
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// lib/order-repository.ts
|
|
36
|
+
import type { OrderRepository, OrderStatus } from "9pay-integrate";
|
|
37
|
+
|
|
38
|
+
export const repo: OrderRepository = {
|
|
39
|
+
async createOrder(params) {
|
|
40
|
+
// Create order in your database
|
|
41
|
+
// Return whatever your DB returns
|
|
42
|
+
},
|
|
43
|
+
async getOrderStatus(orderId: string): Promise<OrderStatus | null> {
|
|
44
|
+
// Query order status from your database
|
|
45
|
+
// Return "neworder" | "pending" | "completed" | "failed" | null
|
|
46
|
+
},
|
|
47
|
+
async updateOrderStatus(orderId: string, status: "completed" | "failed") {
|
|
48
|
+
// Update order status in your database
|
|
49
|
+
// Return the updated order or null
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Implement Notification Hooks (optional)
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// lib/notifications.ts
|
|
58
|
+
import type { NotificationContext } from "9pay-integrate";
|
|
59
|
+
|
|
60
|
+
export const notifications = {
|
|
61
|
+
async onSuccess(ctx: NotificationContext) {
|
|
62
|
+
// Send confirmation email
|
|
63
|
+
// Send Telegram/Slack notification
|
|
64
|
+
console.log(`Order ${ctx.orderId} completed: ${ctx.amount} ${ctx.currency}`);
|
|
65
|
+
},
|
|
66
|
+
async onFailure(ctx: NotificationContext) {
|
|
67
|
+
// Alert on payment failure
|
|
68
|
+
console.log(`Order ${ctx.orderId} failed`);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 4. Create Payment URL (Checkout POST)
|
|
74
|
+
|
|
75
|
+
In your checkout API route, create the order in your database, then generate the 9Pay redirect URL.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// app/api/checkout/route.ts
|
|
79
|
+
import { NextResponse } from "next/server";
|
|
80
|
+
import { createPaymentUrl } from "9pay-integrate";
|
|
81
|
+
import { ninepay } from "@/lib/9pay-config";
|
|
82
|
+
import { repo } from "@/lib/order-repository";
|
|
83
|
+
|
|
84
|
+
export async function POST(request: Request) {
|
|
85
|
+
const body = await request.json();
|
|
86
|
+
|
|
87
|
+
// 1. Validate checkout form data, calculate pricing, etc.
|
|
88
|
+
// 2. Create order in your database
|
|
89
|
+
const order = await repo.createOrder({
|
|
90
|
+
totalAmount: body.amount,
|
|
91
|
+
currency: body.currency,
|
|
92
|
+
paymentMethod: "9pay",
|
|
93
|
+
metadata: body,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// 3. Generate 9Pay payment URL
|
|
97
|
+
const paymentUrl = createPaymentUrl(ninepay, {
|
|
98
|
+
orderId: order.id, // or order.documentId, whatever your DB returns
|
|
99
|
+
amount: body.amount,
|
|
100
|
+
currency: body.currency,
|
|
101
|
+
description: "Order Payment",
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return NextResponse.json({ success: true, orderId: order.id, paymentUrl });
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 5. Wire Up Routes
|
|
109
|
+
|
|
110
|
+
#### Callback Route (GET)
|
|
111
|
+
|
|
112
|
+
Handles the browser redirect after payment on 9Pay's portal.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// app/api/checkout/9pay/callback/route.ts
|
|
116
|
+
import { NextResponse } from "next/server";
|
|
117
|
+
import { verifyNinePayCallback } from "9pay-integrate/next";
|
|
118
|
+
import { ninepay } from "@/lib/9pay-config";
|
|
119
|
+
|
|
120
|
+
export async function GET(request: Request) {
|
|
121
|
+
const result = verifyNinePayCallback(request, ninepay);
|
|
122
|
+
|
|
123
|
+
if (!result.success) {
|
|
124
|
+
return NextResponse.redirect(new URL("/payment/error", request.url));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return NextResponse.redirect(
|
|
128
|
+
new URL(`/payment/process?orderId=${result.orderId}`, request.url)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### IPN Webhook (POST)
|
|
134
|
+
|
|
135
|
+
Handles the server-to-server webhook from 9Pay. This is the source of truth for payment status.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// app/api/checkout/9pay/ipn/route.ts
|
|
139
|
+
import { handle9PayIpn } from "9pay-integrate/next";
|
|
140
|
+
import { ninepay } from "@/lib/9pay-config";
|
|
141
|
+
import { repo } from "@/lib/order-repository";
|
|
142
|
+
import { notifications } from "@/lib/notifications";
|
|
143
|
+
|
|
144
|
+
export async function POST(request: Request) {
|
|
145
|
+
return handle9PayIpn(request, {
|
|
146
|
+
config: ninepay,
|
|
147
|
+
orderRepository: repo,
|
|
148
|
+
notifications,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Payment Status (GET)
|
|
154
|
+
|
|
155
|
+
For frontend polling.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// app/api/payment/status/route.ts
|
|
159
|
+
import { handlePaymentStatus } from "9pay-integrate/next";
|
|
160
|
+
import { repo } from "@/lib/order-repository";
|
|
161
|
+
|
|
162
|
+
export async function GET(request: Request) {
|
|
163
|
+
return handlePaymentStatus(request, { orderRepository: repo });
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## API Reference
|
|
168
|
+
|
|
169
|
+
### Core (`9pay-integrate`)
|
|
170
|
+
|
|
171
|
+
| Export | Description |
|
|
172
|
+
|--------|-------------|
|
|
173
|
+
| `configFromEnv(env)` | Build config from any env-like object |
|
|
174
|
+
| `validateConfig(partial)` | Validate and normalize config |
|
|
175
|
+
| `createPaymentUrl(config, params)` | Generate signed 9Pay redirect URL |
|
|
176
|
+
| `verifyChecksum(result, checksum, key)` | Verify callback/IPN checksum |
|
|
177
|
+
| `verifyCallback(searchParams, config)` | Verify browser redirect callback |
|
|
178
|
+
| `processIpn(payload, options)` | Process IPN (adapter-agnostic) |
|
|
179
|
+
| `buildHttpQuery(data)` | Build sorted query string (advanced) |
|
|
180
|
+
| `buildSignature(baseUrl, time, query, key)` | Build HMAC signature (advanced) |
|
|
181
|
+
| `extractIpnFromFormData(formData)` | Extract IPN from FormData |
|
|
182
|
+
| `extractIpnFromUrlEncoded(body)` | Extract IPN from URL-encoded body |
|
|
183
|
+
| `NINEPAY_STATUS` | Status codes (SUCCESS = 5) |
|
|
184
|
+
| `SUCCESS_STATUS` | Constant for successful payment |
|
|
185
|
+
|
|
186
|
+
### Next.js Adapter (`9pay-integrate/next`)
|
|
187
|
+
|
|
188
|
+
| Export | Description |
|
|
189
|
+
|--------|-------------|
|
|
190
|
+
| `verifyNinePayCallback(request, config)` | Verify callback, return result object |
|
|
191
|
+
| `handle9PayIpn(request, options)` | Full IPN handler → NextResponse |
|
|
192
|
+
| `handlePaymentStatus(request, options)` | Status polling handler → NextResponse |
|
|
193
|
+
|
|
194
|
+
### Types
|
|
195
|
+
|
|
196
|
+
All types are exported from the main entry point. Key interfaces:
|
|
197
|
+
|
|
198
|
+
- `NinePayConfig` — configuration
|
|
199
|
+
- `OrderRepository` — implement for your data layer
|
|
200
|
+
- `NotificationHooks` — optional notification callbacks
|
|
201
|
+
- `IpnProcessResult` — IPN processing result
|
|
202
|
+
- `VerifyCallbackResult` — callback verification result
|
|
203
|
+
- `OrderStatus` — `"neworder" | "pending" | "completed" | "failed"`
|
|
204
|
+
|
|
205
|
+
## Architecture
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
┌─────────────────────────────────────────┐
|
|
209
|
+
│ 9Pay Portal │
|
|
210
|
+
│ payment.9pay.vn/portal │
|
|
211
|
+
└────────────┬────────────────┬───────────┘
|
|
212
|
+
│ │
|
|
213
|
+
Browser redirect Server-to-server
|
|
214
|
+
(callback GET) (IPN POST)
|
|
215
|
+
│ │
|
|
216
|
+
┌────────────▼────────────────▼───────────┐
|
|
217
|
+
│ 9pay-integrate/core │
|
|
218
|
+
│ ┌─────────┐ ┌───────────┐ ┌───────┐ │
|
|
219
|
+
│ │ signing │ │ verify │ │ ipn │ │
|
|
220
|
+
│ │ +payment │ │ callback │ │ proc │ │
|
|
221
|
+
│ └─────────┘ └───────────┘ └───────┘ │
|
|
222
|
+
│ (zero framework deps) │
|
|
223
|
+
└────────────────┬────────────────────────┘
|
|
224
|
+
│
|
|
225
|
+
┌────────────────▼────────────────────────┐
|
|
226
|
+
│ 9pay-integrate/next │
|
|
227
|
+
│ (thin glue: parse req → call core) │
|
|
228
|
+
└─────────────────────────────────────────┘
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Design Principles
|
|
232
|
+
|
|
233
|
+
1. **Core has zero framework dependencies** — works in Express, Fastify, Hono, Bun, Deno
|
|
234
|
+
2. **Inversion of Control** — package never hardcodes ORM/CMS/notification service
|
|
235
|
+
3. **Option B for callback** — returns data, consumer controls redirect
|
|
236
|
+
4. **Option C for IPN** — encapsulates all 6 steps, but consumer can bypass and call `processIpn()` directly from core
|
|
237
|
+
5. **Fail fast** — config validation at init time, not during live payment
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
34
|
+
NINEPAY_STATUS: () => NINEPAY_STATUS,
|
|
35
|
+
SUCCESS_STATUS: () => SUCCESS_STATUS,
|
|
36
|
+
buildHttpQuery: () => buildHttpQuery,
|
|
37
|
+
buildSignature: () => buildSignature,
|
|
38
|
+
configFromEnv: () => configFromEnv,
|
|
39
|
+
createPaymentUrl: () => createPaymentUrl,
|
|
40
|
+
extractIpnFromFormData: () => extractIpnFromFormData,
|
|
41
|
+
extractIpnFromUrlEncoded: () => extractIpnFromUrlEncoded,
|
|
42
|
+
processIpn: () => processIpn,
|
|
43
|
+
validateConfig: () => validateConfig,
|
|
44
|
+
verifyCallback: () => verifyCallback,
|
|
45
|
+
verifyChecksum: () => verifyChecksum
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(index_exports);
|
|
48
|
+
|
|
49
|
+
// src/core/constants.ts
|
|
50
|
+
var NINEPAY_STATUS = {
|
|
51
|
+
PENDING: 1,
|
|
52
|
+
PROCESSING: 2,
|
|
53
|
+
CANCELLED: 3,
|
|
54
|
+
REFUNDED: 4,
|
|
55
|
+
SUCCESS: 5,
|
|
56
|
+
FAILED: 6
|
|
57
|
+
};
|
|
58
|
+
var SUCCESS_STATUS = NINEPAY_STATUS.SUCCESS;
|
|
59
|
+
var DEFAULT_BASE_URL = "https://payment.9pay.vn";
|
|
60
|
+
|
|
61
|
+
// src/core/config.ts
|
|
62
|
+
function validateConfig(config) {
|
|
63
|
+
const errors = [];
|
|
64
|
+
if (!config.merchantKey) errors.push("merchantKey is required");
|
|
65
|
+
if (!config.secretKey) errors.push("secretKey is required");
|
|
66
|
+
if (!config.checksumKey) errors.push("checksumKey is required");
|
|
67
|
+
if (!config.returnUrl) errors.push("returnUrl is required");
|
|
68
|
+
if (errors.length > 0) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`9pay-integrate configuration error:
|
|
71
|
+
- ${errors.join("\n - ")}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
merchantKey: config.merchantKey,
|
|
76
|
+
secretKey: config.secretKey,
|
|
77
|
+
checksumKey: config.checksumKey,
|
|
78
|
+
baseUrl: config.baseUrl || DEFAULT_BASE_URL,
|
|
79
|
+
returnUrl: config.returnUrl,
|
|
80
|
+
cancelUrl: config.cancelUrl || config.returnUrl
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function configFromEnv(env) {
|
|
84
|
+
return validateConfig({
|
|
85
|
+
merchantKey: env.NINEPAY_MERCHANT_KEY,
|
|
86
|
+
secretKey: env.NINEPAY_SECRET_KEY,
|
|
87
|
+
checksumKey: env.NINEPAY_CHECKSUM_KEY,
|
|
88
|
+
baseUrl: env.NINEPAY_BASE_URL,
|
|
89
|
+
returnUrl: env.NINEPAY_RETURN_URL,
|
|
90
|
+
cancelUrl: env.NINEPAY_CANCEL_URL
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/core/signing.ts
|
|
95
|
+
var crypto = __toESM(require("crypto"), 1);
|
|
96
|
+
function buildHttpQuery(data) {
|
|
97
|
+
const params = new URLSearchParams();
|
|
98
|
+
const sortedKeys = Object.keys(data).sort();
|
|
99
|
+
for (const key of sortedKeys) {
|
|
100
|
+
if (data[key] !== void 0 && data[key] !== null) {
|
|
101
|
+
params.append(key, String(data[key]));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return params.toString();
|
|
105
|
+
}
|
|
106
|
+
function buildSignature(baseUrl, time, httpQuery, secretKey) {
|
|
107
|
+
const message = `POST
|
|
108
|
+
${baseUrl}/payments/create
|
|
109
|
+
${time}
|
|
110
|
+
${httpQuery}`;
|
|
111
|
+
return crypto.createHmac("sha256", secretKey).update(message).digest().toString("base64");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/core/payment-url.ts
|
|
115
|
+
function createPaymentUrl(config, params) {
|
|
116
|
+
const time = Math.floor(Date.now() / 1e3);
|
|
117
|
+
const parameters = {
|
|
118
|
+
merchantKey: config.merchantKey,
|
|
119
|
+
time,
|
|
120
|
+
invoice_no: params.orderId,
|
|
121
|
+
amount: params.amount,
|
|
122
|
+
currency: params.currency,
|
|
123
|
+
description: params.description,
|
|
124
|
+
return_url: config.returnUrl,
|
|
125
|
+
...config.cancelUrl ? { back_url: config.cancelUrl } : {},
|
|
126
|
+
...params.options
|
|
127
|
+
};
|
|
128
|
+
const httpQuery = buildHttpQuery(parameters);
|
|
129
|
+
const signature = buildSignature(
|
|
130
|
+
config.baseUrl || "https://payment.9pay.vn",
|
|
131
|
+
time,
|
|
132
|
+
httpQuery,
|
|
133
|
+
config.secretKey
|
|
134
|
+
);
|
|
135
|
+
const sortedParameters = {};
|
|
136
|
+
Object.keys(parameters).sort().forEach((key) => {
|
|
137
|
+
sortedParameters[key] = parameters[key];
|
|
138
|
+
});
|
|
139
|
+
const baseEncode = Buffer.from(JSON.stringify(sortedParameters)).toString(
|
|
140
|
+
"base64"
|
|
141
|
+
);
|
|
142
|
+
const redirectParams = new URLSearchParams({ baseEncode, signature });
|
|
143
|
+
return `${config.baseUrl || "https://payment.9pay.vn"}/portal?${redirectParams.toString()}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/core/verification.ts
|
|
147
|
+
var crypto2 = __toESM(require("crypto"), 1);
|
|
148
|
+
function verifyChecksum(result, checksum, checksumKey) {
|
|
149
|
+
if (!result || !checksum) {
|
|
150
|
+
return { valid: false, data: null, error: "Missing result or checksum" };
|
|
151
|
+
}
|
|
152
|
+
const expected = crypto2.createHash("sha256").update(result + checksumKey).digest("hex").toUpperCase();
|
|
153
|
+
if (expected !== checksum) {
|
|
154
|
+
return { valid: false, data: null, error: "Checksum mismatch" };
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const decoded = JSON.parse(
|
|
158
|
+
Buffer.from(result, "base64").toString("utf-8")
|
|
159
|
+
);
|
|
160
|
+
return { valid: true, data: decoded };
|
|
161
|
+
} catch {
|
|
162
|
+
return { valid: false, data: null, error: "Failed to decode result" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/core/verify-callback.ts
|
|
167
|
+
function verifyCallback(searchParams, config) {
|
|
168
|
+
const result = searchParams.get("result") || "";
|
|
169
|
+
const checksum = searchParams.get("checksum") || "";
|
|
170
|
+
const verification = verifyChecksum(result, checksum, config.checksumKey);
|
|
171
|
+
if (!verification.valid || !verification.data) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
rawParams: {},
|
|
175
|
+
error: verification.error || "Invalid callback payload"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const paymentInfo = verification.data;
|
|
179
|
+
const invoiceNo = paymentInfo.invoice_no;
|
|
180
|
+
const orderId = typeof invoiceNo === "string" && invoiceNo.trim().length > 0 ? invoiceNo : typeof invoiceNo === "number" ? String(invoiceNo) : null;
|
|
181
|
+
if (!orderId) {
|
|
182
|
+
return {
|
|
183
|
+
success: false,
|
|
184
|
+
rawParams: paymentInfo,
|
|
185
|
+
error: "Missing invoice_no in callback payload"
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
success: true,
|
|
190
|
+
orderId,
|
|
191
|
+
amount: Number(paymentInfo.amount ?? 0),
|
|
192
|
+
currency: String(paymentInfo.currency ?? ""),
|
|
193
|
+
status: Number(paymentInfo.status ?? 0),
|
|
194
|
+
rawParams: paymentInfo
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/core/ipn.ts
|
|
199
|
+
function extractIpnFromFormData(formData) {
|
|
200
|
+
return {
|
|
201
|
+
result: String(formData.get("result") || ""),
|
|
202
|
+
checksum: String(formData.get("checksum") || "")
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function extractIpnFromUrlEncoded(body) {
|
|
206
|
+
const params = new URLSearchParams(body);
|
|
207
|
+
return {
|
|
208
|
+
result: params.get("result") || "",
|
|
209
|
+
checksum: params.get("checksum") || ""
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/core/ipn-processor.ts
|
|
214
|
+
async function processIpn(payload, options) {
|
|
215
|
+
const { config, orderRepository, notifications } = options;
|
|
216
|
+
const verification = verifyChecksum(
|
|
217
|
+
payload.result,
|
|
218
|
+
payload.checksum,
|
|
219
|
+
config.checksumKey
|
|
220
|
+
);
|
|
221
|
+
if (!verification.valid || !verification.data) {
|
|
222
|
+
return {
|
|
223
|
+
success: false,
|
|
224
|
+
message: verification.error || "Invalid checksum",
|
|
225
|
+
statusCode: 403
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const paymentInfo = verification.data;
|
|
229
|
+
const invoiceNo = paymentInfo.invoice_no;
|
|
230
|
+
const orderId = typeof invoiceNo === "string" && invoiceNo.trim().length > 0 ? invoiceNo : typeof invoiceNo === "number" ? String(invoiceNo) : null;
|
|
231
|
+
if (!orderId) {
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
message: "Missing invoice_no in IPN payload",
|
|
235
|
+
statusCode: 400
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const isSuccess = Number(paymentInfo.status) === SUCCESS_STATUS;
|
|
239
|
+
const amount = Number(paymentInfo.amount ?? 0);
|
|
240
|
+
const currency = String(paymentInfo.currency ?? "");
|
|
241
|
+
if (isSuccess) {
|
|
242
|
+
try {
|
|
243
|
+
await orderRepository.updateOrderStatus(orderId, "completed");
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
message: `Failed to update order: ${error instanceof Error ? error.message : String(error)}`,
|
|
248
|
+
statusCode: 500,
|
|
249
|
+
orderId
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (notifications?.onSuccess) {
|
|
253
|
+
try {
|
|
254
|
+
await notifications.onSuccess({
|
|
255
|
+
orderId,
|
|
256
|
+
status: "completed",
|
|
257
|
+
amount,
|
|
258
|
+
currency,
|
|
259
|
+
paymentMethod: "9pay",
|
|
260
|
+
metadata: paymentInfo
|
|
261
|
+
});
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
message: "Order updated successfully",
|
|
268
|
+
statusCode: 200,
|
|
269
|
+
orderId
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
await orderRepository.updateOrderStatus(orderId, "failed");
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
message: `Failed to update order: ${error instanceof Error ? error.message : String(error)}`,
|
|
278
|
+
statusCode: 500,
|
|
279
|
+
orderId
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (notifications?.onFailure) {
|
|
283
|
+
try {
|
|
284
|
+
await notifications.onFailure({
|
|
285
|
+
orderId,
|
|
286
|
+
status: "failed",
|
|
287
|
+
amount,
|
|
288
|
+
currency,
|
|
289
|
+
paymentMethod: "9pay",
|
|
290
|
+
metadata: paymentInfo
|
|
291
|
+
});
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
success: false,
|
|
297
|
+
message: "Payment failed",
|
|
298
|
+
statusCode: 200,
|
|
299
|
+
orderId
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
303
|
+
0 && (module.exports = {
|
|
304
|
+
DEFAULT_BASE_URL,
|
|
305
|
+
NINEPAY_STATUS,
|
|
306
|
+
SUCCESS_STATUS,
|
|
307
|
+
buildHttpQuery,
|
|
308
|
+
buildSignature,
|
|
309
|
+
configFromEnv,
|
|
310
|
+
createPaymentUrl,
|
|
311
|
+
extractIpnFromFormData,
|
|
312
|
+
extractIpnFromUrlEncoded,
|
|
313
|
+
processIpn,
|
|
314
|
+
validateConfig,
|
|
315
|
+
verifyCallback,
|
|
316
|
+
verifyChecksum
|
|
317
|
+
});
|