125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
export type PaymentOrder = {
|
||
id: string;
|
||
organizationId: string;
|
||
accountId: string;
|
||
amountFen: number;
|
||
currency: string;
|
||
status: "pending" | "paid" | "closed";
|
||
codeUrl?: string;
|
||
transactionId?: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
expiresAt: string;
|
||
paidAt?: string;
|
||
};
|
||
|
||
export type AdminPaymentOrder = PaymentOrder & {
|
||
organizationName: string;
|
||
accountName: string;
|
||
refundedFen: number;
|
||
refundingFen: number;
|
||
refundableFen: number;
|
||
};
|
||
|
||
export type PaymentRefund = {
|
||
id: string;
|
||
orderId: string;
|
||
organizationId: string;
|
||
operatorId: string;
|
||
operatorName?: string;
|
||
transactionId: string;
|
||
providerId?: string;
|
||
amountFen: number;
|
||
totalFen: number;
|
||
currency: string;
|
||
reason: string;
|
||
status: "pending" | "processing" | "success" | "closed" | "abnormal" | "rejected";
|
||
failureReason?: string;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
succeededAt?: string;
|
||
};
|
||
|
||
export type AdminPaymentOrdersPage = { orders: AdminPaymentOrder[]; hasMore: boolean; enabled: boolean };
|
||
|
||
export type PaymentOptions = {
|
||
enabled: boolean;
|
||
canRecharge: boolean;
|
||
presetAmountsFen: number[];
|
||
minAmountFen: number;
|
||
maxAmountFen: number;
|
||
};
|
||
|
||
// Work with decimal text so every accepted value maps to exactly one amount in fen.
|
||
export function parseYuanToFen(value: string, minimum: number, maximum: number): number | null {
|
||
const trimmed = value.trim();
|
||
if (!/^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/.test(trimmed)) return null;
|
||
const [yuan, fraction = ""] = trimmed.split(".");
|
||
const amount = Number(yuan) * 100 + Number(fraction.padEnd(2, "0"));
|
||
return Number.isSafeInteger(amount) && amount >= minimum && amount <= maximum ? amount : null;
|
||
}
|
||
|
||
export function formatYuan(fen: number): string {
|
||
return (fen / 100).toFixed(2);
|
||
}
|
||
|
||
async function paymentRequest<T>(url: string, init?: RequestInit): Promise<T> {
|
||
const response = await fetch(url, { cache: "no-store", ...init });
|
||
let payload: { error?: string } & Partial<T>;
|
||
try {
|
||
payload = await response.json();
|
||
} catch {
|
||
throw new Error(`支付服务返回了无效响应(HTTP ${response.status})`);
|
||
}
|
||
if (!response.ok) throw new Error(payload.error || `支付请求失败(HTTP ${response.status})`);
|
||
return payload as T;
|
||
}
|
||
|
||
export async function getPaymentOptions(signal?: AbortSignal): Promise<PaymentOptions> {
|
||
return paymentRequest<PaymentOptions>("/api/payments/options", { signal });
|
||
}
|
||
|
||
export async function listPaymentOrders(signal?: AbortSignal): Promise<PaymentOrder[]> {
|
||
const result = await paymentRequest<{ orders: PaymentOrder[] }>("/api/payments/orders", { signal });
|
||
return result.orders || [];
|
||
}
|
||
|
||
export async function createPaymentOrder(amountFen: number, requestKey: string): Promise<PaymentOrder> {
|
||
const result = await paymentRequest<{ order: PaymentOrder }>("/api/payments/orders", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ amountFen, requestKey })
|
||
});
|
||
return result.order;
|
||
}
|
||
|
||
export async function getPaymentOrder(id: string, signal?: AbortSignal): Promise<PaymentOrder> {
|
||
const result = await paymentRequest<{ order: PaymentOrder }>(`/api/payments/orders/${encodeURIComponent(id)}`, { signal });
|
||
return result.order;
|
||
}
|
||
|
||
export async function listAdminPaymentOrders(organizationId: string, offset: number, signal?: AbortSignal): Promise<AdminPaymentOrdersPage> {
|
||
const query = new URLSearchParams({ limit: "50", offset: String(offset) });
|
||
if (organizationId) query.set("organizationId", organizationId);
|
||
return paymentRequest<AdminPaymentOrdersPage>(`/api/payments/admin/orders?${query}`, { signal });
|
||
}
|
||
|
||
export async function listPaymentRefunds(orderId: string, signal?: AbortSignal): Promise<PaymentRefund[]> {
|
||
const result = await paymentRequest<{ refunds: PaymentRefund[] }>(`/api/payments/admin/orders/${encodeURIComponent(orderId)}/refunds`, { signal });
|
||
return result.refunds || [];
|
||
}
|
||
|
||
export async function createPaymentRefund(orderId: string, amountFen: number, reason: string, requestKey: string): Promise<PaymentRefund> {
|
||
const result = await paymentRequest<{ refund: PaymentRefund }>(`/api/payments/admin/orders/${encodeURIComponent(orderId)}/refunds`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ amountFen, reason, requestKey })
|
||
});
|
||
return result.refund;
|
||
}
|
||
|
||
export async function getPaymentRefund(id: string, signal?: AbortSignal): Promise<PaymentRefund> {
|
||
const result = await paymentRequest<{ refund: PaymentRefund }>(`/api/payments/admin/refunds/${encodeURIComponent(id)}`, { signal });
|
||
return result.refund;
|
||
}
|