feat: migrate payment provider to easy-pay, add order history and refund support
- Replace zpay with easy-pay payment provider (new lib/easy-pay/ module) - Add order history page for users (pay/orders) - Add GET /api/orders/my endpoint to list user's own orders - Add GET /api/users/[id] endpoint for sub2api user lookup - Add order status tracking module (lib/order/status.ts) - Update config to support easy-pay credentials (merchant ID, key, gateway) - Update PaymentForm and PaymentQRCode components for easy-pay flow - Update pay page and admin page with new order management UI - Update order service to support easy-pay, cancellation, and refund
This commit is contained in:
102
src/lib/sub2api/client.ts
Normal file
102
src/lib/sub2api/client.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { getEnv } from '@/lib/config';
|
||||
import type { Sub2ApiUser, Sub2ApiRedeemCode } from './types';
|
||||
|
||||
function getHeaders(idempotencyKey?: string): Record<string, string> {
|
||||
const env = getEnv();
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': env.SUB2API_ADMIN_API_KEY,
|
||||
};
|
||||
if (idempotencyKey) {
|
||||
headers['Idempotency-Key'] = idempotencyKey;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function getCurrentUserByToken(token: string): Promise<Sub2ApiUser> {
|
||||
const env = getEnv();
|
||||
const response = await fetch(`${env.SUB2API_BASE_URL}/api/v1/auth/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get current user: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.data as Sub2ApiUser;
|
||||
}
|
||||
|
||||
export async function getUser(userId: number): Promise<Sub2ApiUser> {
|
||||
const env = getEnv();
|
||||
const response = await fetch(`${env.SUB2API_BASE_URL}/api/v1/admin/users/${userId}`, {
|
||||
headers: getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) throw new Error('USER_NOT_FOUND');
|
||||
throw new Error(`Failed to get user: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.data as Sub2ApiUser;
|
||||
}
|
||||
|
||||
export async function createAndRedeem(
|
||||
code: string,
|
||||
value: number,
|
||||
userId: number,
|
||||
notes: string,
|
||||
): Promise<Sub2ApiRedeemCode> {
|
||||
const env = getEnv();
|
||||
const response = await fetch(
|
||||
`${env.SUB2API_BASE_URL}/api/v1/admin/redeem-codes/create-and-redeem`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: getHeaders(`sub2apipay:recharge:${code}`),
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
type: 'balance',
|
||||
value,
|
||||
user_id: userId,
|
||||
notes,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(`Recharge failed (${response.status}): ${JSON.stringify(errorData)}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.redeem_code as Sub2ApiRedeemCode;
|
||||
}
|
||||
|
||||
export async function subtractBalance(
|
||||
userId: number,
|
||||
amount: number,
|
||||
notes: string,
|
||||
idempotencyKey: string,
|
||||
): Promise<void> {
|
||||
const env = getEnv();
|
||||
const response = await fetch(
|
||||
`${env.SUB2API_BASE_URL}/api/v1/admin/users/${userId}/balance`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: getHeaders(idempotencyKey),
|
||||
body: JSON.stringify({
|
||||
operation: 'subtract',
|
||||
amount,
|
||||
notes,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(`Subtract balance failed (${response.status}): ${JSON.stringify(errorData)}`);
|
||||
}
|
||||
}
|
||||
23
src/lib/sub2api/types.ts
Normal file
23
src/lib/sub2api/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface Sub2ApiUser {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
status: string; // "active", "banned", etc.
|
||||
balance: number;
|
||||
}
|
||||
|
||||
export interface Sub2ApiRedeemCode {
|
||||
id: number;
|
||||
code: string;
|
||||
type: string;
|
||||
value: number;
|
||||
status: string;
|
||||
used_by: number;
|
||||
used_at: string;
|
||||
}
|
||||
|
||||
export interface Sub2ApiResponse<T> {
|
||||
code: number;
|
||||
data?: T;
|
||||
message?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user