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,195 @@
1
+ /**
2
+ * Core type definitions for 9Pay integration.
3
+ * Framework-agnostic — no Next.js, React, or Express dependencies.
4
+ */
5
+ interface NinePayConfig {
6
+ merchantKey: string;
7
+ secretKey: string;
8
+ checksumKey: string;
9
+ baseUrl?: string;
10
+ returnUrl: string;
11
+ cancelUrl?: string;
12
+ }
13
+ interface NinePayPaymentOptions {
14
+ /** 0: All, 1: Domestic, 2: International */
15
+ card_origin_allow?: 0 | 1 | 2;
16
+ /** Comma-separated: "VISA,MASTER,JCB,AMEX" */
17
+ card_brand_allow?: string;
18
+ /** Comma-separated BIN codes */
19
+ bin_allow?: string;
20
+ /** Card type filter */
21
+ card_type_allow?: string;
22
+ lang?: "en" | "vi";
23
+ method?: string;
24
+ }
25
+ interface CreatePaymentParams {
26
+ orderId: string | number;
27
+ amount: number;
28
+ currency: string;
29
+ description: string;
30
+ options?: NinePayPaymentOptions;
31
+ }
32
+ interface VerifyCallbackResult {
33
+ success: boolean;
34
+ orderId?: string;
35
+ amount?: number;
36
+ currency?: string;
37
+ status?: number;
38
+ /** Raw decoded payload for consumer access */
39
+ rawParams: Record<string, unknown>;
40
+ error?: string;
41
+ }
42
+ interface VerificationResult<T = Record<string, unknown>> {
43
+ valid: boolean;
44
+ data: T | null;
45
+ error?: string;
46
+ }
47
+ type OrderStatus = "neworder" | "pending" | "completed" | "failed";
48
+ interface CreateOrderParams {
49
+ orderId?: string;
50
+ totalAmount: number;
51
+ currency: string;
52
+ paymentMethod: string;
53
+ metadata?: Record<string, unknown>;
54
+ }
55
+ interface OrderRepository<TOrder = unknown> {
56
+ createOrder(params: CreateOrderParams): Promise<TOrder>;
57
+ getOrderStatus(orderId: string): Promise<OrderStatus | null>;
58
+ updateOrderStatus(orderId: string, status: "completed" | "failed"): Promise<TOrder | null>;
59
+ }
60
+ interface NotificationContext {
61
+ orderId: string;
62
+ status: "completed" | "failed";
63
+ amount: number;
64
+ currency: string;
65
+ paymentMethod: string;
66
+ metadata?: Record<string, unknown>;
67
+ }
68
+ type NotificationHandler = (ctx: NotificationContext) => Promise<void> | void;
69
+ interface NotificationHooks {
70
+ onSuccess?: NotificationHandler;
71
+ onFailure?: NotificationHandler;
72
+ }
73
+ interface IpnRawPayload {
74
+ result: string;
75
+ checksum: string;
76
+ }
77
+ interface IpnProcessResult {
78
+ success: boolean;
79
+ message: string;
80
+ statusCode: number;
81
+ orderId?: string;
82
+ }
83
+ interface IpnProcessorOptions {
84
+ config: NinePayConfig;
85
+ orderRepository: OrderRepository;
86
+ notifications?: NotificationHooks;
87
+ }
88
+ interface PaymentStatusResponse {
89
+ success: boolean;
90
+ status: OrderStatus | null;
91
+ message?: string;
92
+ }
93
+
94
+ /**
95
+ * Validate and normalize a partial NinePayConfig.
96
+ * Throws if required fields are missing — fail fast at init time.
97
+ */
98
+ declare function validateConfig(config: Partial<NinePayConfig>): NinePayConfig;
99
+ /**
100
+ * Build NinePayConfig from any environment-like object.
101
+ * Accepts Record<string, string | undefined> so it works with
102
+ * process.env, Vercel env, or custom config loaders.
103
+ */
104
+ declare function configFromEnv(env: Record<string, string | undefined>): NinePayConfig;
105
+
106
+ /**
107
+ * Create a signed 9Pay payment redirect URL.
108
+ *
109
+ * The user is redirected to this URL to complete payment on 9Pay's portal.
110
+ * After payment, 9Pay redirects the browser to config.returnUrl (callback)
111
+ * and sends a server-to-server POST to config.returnUrl (IPN).
112
+ */
113
+ declare function createPaymentUrl(config: NinePayConfig, params: CreatePaymentParams): string;
114
+
115
+ /**
116
+ * Build sorted HTTP query string from key-value pairs.
117
+ * Keys are sorted alphabetically — required by 9Pay for valid signatures.
118
+ */
119
+ declare function buildHttpQuery(data: Record<string, string | number>): string;
120
+ /**
121
+ * Build HMAC-SHA256 signature for 9Pay payment creation request.
122
+ * Message format: POST\n{baseUrl}/payments/create\n{time}\n{httpQuery}
123
+ * Returns Base64-encoded signature.
124
+ */
125
+ declare function buildSignature(baseUrl: string, time: number, httpQuery: string, secretKey: string): string;
126
+
127
+ /**
128
+ * Verify 9Pay callback/IPN checksum.
129
+ *
130
+ * 9Pay computes: SHA256(result + checksumKey).toUpperCase()
131
+ * We recompute and compare. If valid, decode result from base64 → JSON.
132
+ *
133
+ * @param result Base64-encoded JSON payload from 9Pay
134
+ * @param checksum SHA256 hex hash from 9Pay
135
+ * @param checksumKey Shared secret key
136
+ */
137
+ declare function verifyChecksum(result: string, checksum: string, checksumKey: string): VerificationResult;
138
+
139
+ /**
140
+ * Verify 9Pay browser redirect callback.
141
+ *
142
+ * Takes URL search params (from GET /api/checkout/9pay/callback),
143
+ * verifies the checksum, extracts orderId and payment info.
144
+ *
145
+ * Returns an object — consumer decides what to redirect to.
146
+ * This is Option B: package does NOT redirect, only returns data.
147
+ */
148
+ declare function verifyCallback(searchParams: URLSearchParams, config: NinePayConfig): VerifyCallbackResult;
149
+
150
+ /**
151
+ * Extract IPN payload from FormData.
152
+ * 9Pay sends POST with result and checksum fields.
153
+ */
154
+ declare function extractIpnFromFormData(formData: FormData): IpnRawPayload;
155
+ /**
156
+ * Extract IPN payload from URL-encoded string body.
157
+ * 9Pay may send application/x-www-form-urlencoded instead of FormData.
158
+ */
159
+ declare function extractIpnFromUrlEncoded(body: string): IpnRawPayload;
160
+
161
+ /**
162
+ * Process a 9Pay IPN (Instant Payment Notification) — adapter-agnostic.
163
+ *
164
+ * This is the core IPN logic, completely independent of Next.js, Express,
165
+ * or any framework. It:
166
+ * 1. Verifies the checksum
167
+ * 2. Decodes the payment info
168
+ * 3. Extracts the orderId (invoice_no)
169
+ * 4. Checks payment status (5 = success)
170
+ * 5. Calls orderRepository.updateOrderStatus()
171
+ * 6. Calls notification hooks (onSuccess / onFailure)
172
+ *
173
+ * The Next.js adapter (or Express, Hono, etc.) is responsible for:
174
+ * - Parsing the incoming request into IpnRawPayload
175
+ * - Returning the appropriate HTTP response
176
+ */
177
+ declare function processIpn(payload: IpnRawPayload, options: IpnProcessorOptions): Promise<IpnProcessResult>;
178
+
179
+ /**
180
+ * 9Pay status codes and defaults.
181
+ */
182
+ declare const NINEPAY_STATUS: {
183
+ readonly PENDING: 1;
184
+ readonly PROCESSING: 2;
185
+ readonly CANCELLED: 3;
186
+ readonly REFUNDED: 4;
187
+ readonly SUCCESS: 5;
188
+ readonly FAILED: 6;
189
+ };
190
+ /** Status code indicating successful payment */
191
+ declare const SUCCESS_STATUS: 5;
192
+ /** Default 9Pay base URL */
193
+ declare const DEFAULT_BASE_URL = "https://payment.9pay.vn";
194
+
195
+ export { type CreateOrderParams, type CreatePaymentParams, DEFAULT_BASE_URL, type IpnProcessResult, type IpnProcessorOptions, type IpnRawPayload, NINEPAY_STATUS, type NinePayConfig, type NinePayPaymentOptions, type NotificationContext, type NotificationHandler, type NotificationHooks, type OrderRepository, type OrderStatus, type PaymentStatusResponse, SUCCESS_STATUS, type VerificationResult, type VerifyCallbackResult, buildHttpQuery, buildSignature, configFromEnv, createPaymentUrl, extractIpnFromFormData, extractIpnFromUrlEncoded, processIpn, validateConfig, verifyCallback, verifyChecksum };
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Core type definitions for 9Pay integration.
3
+ * Framework-agnostic — no Next.js, React, or Express dependencies.
4
+ */
5
+ interface NinePayConfig {
6
+ merchantKey: string;
7
+ secretKey: string;
8
+ checksumKey: string;
9
+ baseUrl?: string;
10
+ returnUrl: string;
11
+ cancelUrl?: string;
12
+ }
13
+ interface NinePayPaymentOptions {
14
+ /** 0: All, 1: Domestic, 2: International */
15
+ card_origin_allow?: 0 | 1 | 2;
16
+ /** Comma-separated: "VISA,MASTER,JCB,AMEX" */
17
+ card_brand_allow?: string;
18
+ /** Comma-separated BIN codes */
19
+ bin_allow?: string;
20
+ /** Card type filter */
21
+ card_type_allow?: string;
22
+ lang?: "en" | "vi";
23
+ method?: string;
24
+ }
25
+ interface CreatePaymentParams {
26
+ orderId: string | number;
27
+ amount: number;
28
+ currency: string;
29
+ description: string;
30
+ options?: NinePayPaymentOptions;
31
+ }
32
+ interface VerifyCallbackResult {
33
+ success: boolean;
34
+ orderId?: string;
35
+ amount?: number;
36
+ currency?: string;
37
+ status?: number;
38
+ /** Raw decoded payload for consumer access */
39
+ rawParams: Record<string, unknown>;
40
+ error?: string;
41
+ }
42
+ interface VerificationResult<T = Record<string, unknown>> {
43
+ valid: boolean;
44
+ data: T | null;
45
+ error?: string;
46
+ }
47
+ type OrderStatus = "neworder" | "pending" | "completed" | "failed";
48
+ interface CreateOrderParams {
49
+ orderId?: string;
50
+ totalAmount: number;
51
+ currency: string;
52
+ paymentMethod: string;
53
+ metadata?: Record<string, unknown>;
54
+ }
55
+ interface OrderRepository<TOrder = unknown> {
56
+ createOrder(params: CreateOrderParams): Promise<TOrder>;
57
+ getOrderStatus(orderId: string): Promise<OrderStatus | null>;
58
+ updateOrderStatus(orderId: string, status: "completed" | "failed"): Promise<TOrder | null>;
59
+ }
60
+ interface NotificationContext {
61
+ orderId: string;
62
+ status: "completed" | "failed";
63
+ amount: number;
64
+ currency: string;
65
+ paymentMethod: string;
66
+ metadata?: Record<string, unknown>;
67
+ }
68
+ type NotificationHandler = (ctx: NotificationContext) => Promise<void> | void;
69
+ interface NotificationHooks {
70
+ onSuccess?: NotificationHandler;
71
+ onFailure?: NotificationHandler;
72
+ }
73
+ interface IpnRawPayload {
74
+ result: string;
75
+ checksum: string;
76
+ }
77
+ interface IpnProcessResult {
78
+ success: boolean;
79
+ message: string;
80
+ statusCode: number;
81
+ orderId?: string;
82
+ }
83
+ interface IpnProcessorOptions {
84
+ config: NinePayConfig;
85
+ orderRepository: OrderRepository;
86
+ notifications?: NotificationHooks;
87
+ }
88
+ interface PaymentStatusResponse {
89
+ success: boolean;
90
+ status: OrderStatus | null;
91
+ message?: string;
92
+ }
93
+
94
+ /**
95
+ * Validate and normalize a partial NinePayConfig.
96
+ * Throws if required fields are missing — fail fast at init time.
97
+ */
98
+ declare function validateConfig(config: Partial<NinePayConfig>): NinePayConfig;
99
+ /**
100
+ * Build NinePayConfig from any environment-like object.
101
+ * Accepts Record<string, string | undefined> so it works with
102
+ * process.env, Vercel env, or custom config loaders.
103
+ */
104
+ declare function configFromEnv(env: Record<string, string | undefined>): NinePayConfig;
105
+
106
+ /**
107
+ * Create a signed 9Pay payment redirect URL.
108
+ *
109
+ * The user is redirected to this URL to complete payment on 9Pay's portal.
110
+ * After payment, 9Pay redirects the browser to config.returnUrl (callback)
111
+ * and sends a server-to-server POST to config.returnUrl (IPN).
112
+ */
113
+ declare function createPaymentUrl(config: NinePayConfig, params: CreatePaymentParams): string;
114
+
115
+ /**
116
+ * Build sorted HTTP query string from key-value pairs.
117
+ * Keys are sorted alphabetically — required by 9Pay for valid signatures.
118
+ */
119
+ declare function buildHttpQuery(data: Record<string, string | number>): string;
120
+ /**
121
+ * Build HMAC-SHA256 signature for 9Pay payment creation request.
122
+ * Message format: POST\n{baseUrl}/payments/create\n{time}\n{httpQuery}
123
+ * Returns Base64-encoded signature.
124
+ */
125
+ declare function buildSignature(baseUrl: string, time: number, httpQuery: string, secretKey: string): string;
126
+
127
+ /**
128
+ * Verify 9Pay callback/IPN checksum.
129
+ *
130
+ * 9Pay computes: SHA256(result + checksumKey).toUpperCase()
131
+ * We recompute and compare. If valid, decode result from base64 → JSON.
132
+ *
133
+ * @param result Base64-encoded JSON payload from 9Pay
134
+ * @param checksum SHA256 hex hash from 9Pay
135
+ * @param checksumKey Shared secret key
136
+ */
137
+ declare function verifyChecksum(result: string, checksum: string, checksumKey: string): VerificationResult;
138
+
139
+ /**
140
+ * Verify 9Pay browser redirect callback.
141
+ *
142
+ * Takes URL search params (from GET /api/checkout/9pay/callback),
143
+ * verifies the checksum, extracts orderId and payment info.
144
+ *
145
+ * Returns an object — consumer decides what to redirect to.
146
+ * This is Option B: package does NOT redirect, only returns data.
147
+ */
148
+ declare function verifyCallback(searchParams: URLSearchParams, config: NinePayConfig): VerifyCallbackResult;
149
+
150
+ /**
151
+ * Extract IPN payload from FormData.
152
+ * 9Pay sends POST with result and checksum fields.
153
+ */
154
+ declare function extractIpnFromFormData(formData: FormData): IpnRawPayload;
155
+ /**
156
+ * Extract IPN payload from URL-encoded string body.
157
+ * 9Pay may send application/x-www-form-urlencoded instead of FormData.
158
+ */
159
+ declare function extractIpnFromUrlEncoded(body: string): IpnRawPayload;
160
+
161
+ /**
162
+ * Process a 9Pay IPN (Instant Payment Notification) — adapter-agnostic.
163
+ *
164
+ * This is the core IPN logic, completely independent of Next.js, Express,
165
+ * or any framework. It:
166
+ * 1. Verifies the checksum
167
+ * 2. Decodes the payment info
168
+ * 3. Extracts the orderId (invoice_no)
169
+ * 4. Checks payment status (5 = success)
170
+ * 5. Calls orderRepository.updateOrderStatus()
171
+ * 6. Calls notification hooks (onSuccess / onFailure)
172
+ *
173
+ * The Next.js adapter (or Express, Hono, etc.) is responsible for:
174
+ * - Parsing the incoming request into IpnRawPayload
175
+ * - Returning the appropriate HTTP response
176
+ */
177
+ declare function processIpn(payload: IpnRawPayload, options: IpnProcessorOptions): Promise<IpnProcessResult>;
178
+
179
+ /**
180
+ * 9Pay status codes and defaults.
181
+ */
182
+ declare const NINEPAY_STATUS: {
183
+ readonly PENDING: 1;
184
+ readonly PROCESSING: 2;
185
+ readonly CANCELLED: 3;
186
+ readonly REFUNDED: 4;
187
+ readonly SUCCESS: 5;
188
+ readonly FAILED: 6;
189
+ };
190
+ /** Status code indicating successful payment */
191
+ declare const SUCCESS_STATUS: 5;
192
+ /** Default 9Pay base URL */
193
+ declare const DEFAULT_BASE_URL = "https://payment.9pay.vn";
194
+
195
+ export { type CreateOrderParams, type CreatePaymentParams, DEFAULT_BASE_URL, type IpnProcessResult, type IpnProcessorOptions, type IpnRawPayload, NINEPAY_STATUS, type NinePayConfig, type NinePayPaymentOptions, type NotificationContext, type NotificationHandler, type NotificationHooks, type OrderRepository, type OrderStatus, type PaymentStatusResponse, SUCCESS_STATUS, type VerificationResult, type VerifyCallbackResult, buildHttpQuery, buildSignature, configFromEnv, createPaymentUrl, extractIpnFromFormData, extractIpnFromUrlEncoded, processIpn, validateConfig, verifyCallback, verifyChecksum };
package/dist/index.js ADDED
@@ -0,0 +1,268 @@
1
+ // src/core/constants.ts
2
+ var NINEPAY_STATUS = {
3
+ PENDING: 1,
4
+ PROCESSING: 2,
5
+ CANCELLED: 3,
6
+ REFUNDED: 4,
7
+ SUCCESS: 5,
8
+ FAILED: 6
9
+ };
10
+ var SUCCESS_STATUS = NINEPAY_STATUS.SUCCESS;
11
+ var DEFAULT_BASE_URL = "https://payment.9pay.vn";
12
+
13
+ // src/core/config.ts
14
+ function validateConfig(config) {
15
+ const errors = [];
16
+ if (!config.merchantKey) errors.push("merchantKey is required");
17
+ if (!config.secretKey) errors.push("secretKey is required");
18
+ if (!config.checksumKey) errors.push("checksumKey is required");
19
+ if (!config.returnUrl) errors.push("returnUrl is required");
20
+ if (errors.length > 0) {
21
+ throw new Error(
22
+ `9pay-integrate configuration error:
23
+ - ${errors.join("\n - ")}`
24
+ );
25
+ }
26
+ return {
27
+ merchantKey: config.merchantKey,
28
+ secretKey: config.secretKey,
29
+ checksumKey: config.checksumKey,
30
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
31
+ returnUrl: config.returnUrl,
32
+ cancelUrl: config.cancelUrl || config.returnUrl
33
+ };
34
+ }
35
+ function configFromEnv(env) {
36
+ return validateConfig({
37
+ merchantKey: env.NINEPAY_MERCHANT_KEY,
38
+ secretKey: env.NINEPAY_SECRET_KEY,
39
+ checksumKey: env.NINEPAY_CHECKSUM_KEY,
40
+ baseUrl: env.NINEPAY_BASE_URL,
41
+ returnUrl: env.NINEPAY_RETURN_URL,
42
+ cancelUrl: env.NINEPAY_CANCEL_URL
43
+ });
44
+ }
45
+
46
+ // src/core/signing.ts
47
+ import * as crypto from "crypto";
48
+ function buildHttpQuery(data) {
49
+ const params = new URLSearchParams();
50
+ const sortedKeys = Object.keys(data).sort();
51
+ for (const key of sortedKeys) {
52
+ if (data[key] !== void 0 && data[key] !== null) {
53
+ params.append(key, String(data[key]));
54
+ }
55
+ }
56
+ return params.toString();
57
+ }
58
+ function buildSignature(baseUrl, time, httpQuery, secretKey) {
59
+ const message = `POST
60
+ ${baseUrl}/payments/create
61
+ ${time}
62
+ ${httpQuery}`;
63
+ return crypto.createHmac("sha256", secretKey).update(message).digest().toString("base64");
64
+ }
65
+
66
+ // src/core/payment-url.ts
67
+ function createPaymentUrl(config, params) {
68
+ const time = Math.floor(Date.now() / 1e3);
69
+ const parameters = {
70
+ merchantKey: config.merchantKey,
71
+ time,
72
+ invoice_no: params.orderId,
73
+ amount: params.amount,
74
+ currency: params.currency,
75
+ description: params.description,
76
+ return_url: config.returnUrl,
77
+ ...config.cancelUrl ? { back_url: config.cancelUrl } : {},
78
+ ...params.options
79
+ };
80
+ const httpQuery = buildHttpQuery(parameters);
81
+ const signature = buildSignature(
82
+ config.baseUrl || "https://payment.9pay.vn",
83
+ time,
84
+ httpQuery,
85
+ config.secretKey
86
+ );
87
+ const sortedParameters = {};
88
+ Object.keys(parameters).sort().forEach((key) => {
89
+ sortedParameters[key] = parameters[key];
90
+ });
91
+ const baseEncode = Buffer.from(JSON.stringify(sortedParameters)).toString(
92
+ "base64"
93
+ );
94
+ const redirectParams = new URLSearchParams({ baseEncode, signature });
95
+ return `${config.baseUrl || "https://payment.9pay.vn"}/portal?${redirectParams.toString()}`;
96
+ }
97
+
98
+ // src/core/verification.ts
99
+ import * as crypto2 from "crypto";
100
+ function verifyChecksum(result, checksum, checksumKey) {
101
+ if (!result || !checksum) {
102
+ return { valid: false, data: null, error: "Missing result or checksum" };
103
+ }
104
+ const expected = crypto2.createHash("sha256").update(result + checksumKey).digest("hex").toUpperCase();
105
+ if (expected !== checksum) {
106
+ return { valid: false, data: null, error: "Checksum mismatch" };
107
+ }
108
+ try {
109
+ const decoded = JSON.parse(
110
+ Buffer.from(result, "base64").toString("utf-8")
111
+ );
112
+ return { valid: true, data: decoded };
113
+ } catch {
114
+ return { valid: false, data: null, error: "Failed to decode result" };
115
+ }
116
+ }
117
+
118
+ // src/core/verify-callback.ts
119
+ function verifyCallback(searchParams, config) {
120
+ const result = searchParams.get("result") || "";
121
+ const checksum = searchParams.get("checksum") || "";
122
+ const verification = verifyChecksum(result, checksum, config.checksumKey);
123
+ if (!verification.valid || !verification.data) {
124
+ return {
125
+ success: false,
126
+ rawParams: {},
127
+ error: verification.error || "Invalid callback payload"
128
+ };
129
+ }
130
+ const paymentInfo = verification.data;
131
+ const invoiceNo = paymentInfo.invoice_no;
132
+ const orderId = typeof invoiceNo === "string" && invoiceNo.trim().length > 0 ? invoiceNo : typeof invoiceNo === "number" ? String(invoiceNo) : null;
133
+ if (!orderId) {
134
+ return {
135
+ success: false,
136
+ rawParams: paymentInfo,
137
+ error: "Missing invoice_no in callback payload"
138
+ };
139
+ }
140
+ return {
141
+ success: true,
142
+ orderId,
143
+ amount: Number(paymentInfo.amount ?? 0),
144
+ currency: String(paymentInfo.currency ?? ""),
145
+ status: Number(paymentInfo.status ?? 0),
146
+ rawParams: paymentInfo
147
+ };
148
+ }
149
+
150
+ // src/core/ipn.ts
151
+ function extractIpnFromFormData(formData) {
152
+ return {
153
+ result: String(formData.get("result") || ""),
154
+ checksum: String(formData.get("checksum") || "")
155
+ };
156
+ }
157
+ function extractIpnFromUrlEncoded(body) {
158
+ const params = new URLSearchParams(body);
159
+ return {
160
+ result: params.get("result") || "",
161
+ checksum: params.get("checksum") || ""
162
+ };
163
+ }
164
+
165
+ // src/core/ipn-processor.ts
166
+ async function processIpn(payload, options) {
167
+ const { config, orderRepository, notifications } = options;
168
+ const verification = verifyChecksum(
169
+ payload.result,
170
+ payload.checksum,
171
+ config.checksumKey
172
+ );
173
+ if (!verification.valid || !verification.data) {
174
+ return {
175
+ success: false,
176
+ message: verification.error || "Invalid checksum",
177
+ statusCode: 403
178
+ };
179
+ }
180
+ const paymentInfo = verification.data;
181
+ const invoiceNo = paymentInfo.invoice_no;
182
+ const orderId = typeof invoiceNo === "string" && invoiceNo.trim().length > 0 ? invoiceNo : typeof invoiceNo === "number" ? String(invoiceNo) : null;
183
+ if (!orderId) {
184
+ return {
185
+ success: false,
186
+ message: "Missing invoice_no in IPN payload",
187
+ statusCode: 400
188
+ };
189
+ }
190
+ const isSuccess = Number(paymentInfo.status) === SUCCESS_STATUS;
191
+ const amount = Number(paymentInfo.amount ?? 0);
192
+ const currency = String(paymentInfo.currency ?? "");
193
+ if (isSuccess) {
194
+ try {
195
+ await orderRepository.updateOrderStatus(orderId, "completed");
196
+ } catch (error) {
197
+ return {
198
+ success: false,
199
+ message: `Failed to update order: ${error instanceof Error ? error.message : String(error)}`,
200
+ statusCode: 500,
201
+ orderId
202
+ };
203
+ }
204
+ if (notifications?.onSuccess) {
205
+ try {
206
+ await notifications.onSuccess({
207
+ orderId,
208
+ status: "completed",
209
+ amount,
210
+ currency,
211
+ paymentMethod: "9pay",
212
+ metadata: paymentInfo
213
+ });
214
+ } catch {
215
+ }
216
+ }
217
+ return {
218
+ success: true,
219
+ message: "Order updated successfully",
220
+ statusCode: 200,
221
+ orderId
222
+ };
223
+ }
224
+ try {
225
+ await orderRepository.updateOrderStatus(orderId, "failed");
226
+ } catch (error) {
227
+ return {
228
+ success: false,
229
+ message: `Failed to update order: ${error instanceof Error ? error.message : String(error)}`,
230
+ statusCode: 500,
231
+ orderId
232
+ };
233
+ }
234
+ if (notifications?.onFailure) {
235
+ try {
236
+ await notifications.onFailure({
237
+ orderId,
238
+ status: "failed",
239
+ amount,
240
+ currency,
241
+ paymentMethod: "9pay",
242
+ metadata: paymentInfo
243
+ });
244
+ } catch {
245
+ }
246
+ }
247
+ return {
248
+ success: false,
249
+ message: "Payment failed",
250
+ statusCode: 200,
251
+ orderId
252
+ };
253
+ }
254
+ export {
255
+ DEFAULT_BASE_URL,
256
+ NINEPAY_STATUS,
257
+ SUCCESS_STATUS,
258
+ buildHttpQuery,
259
+ buildSignature,
260
+ configFromEnv,
261
+ createPaymentUrl,
262
+ extractIpnFromFormData,
263
+ extractIpnFromUrlEncoded,
264
+ processIpn,
265
+ validateConfig,
266
+ verifyCallback,
267
+ verifyChecksum
268
+ };