import { postRemittance } from "@/lib/remittance-client"; export type RemittanceChannel = { id: string; code: string; requestUrl: string; icon: string; name: string; description: string; amountRange: string; processingTime: string; fee: string; groupName: string; groupOrder: number; channelType: number | null; bankValid: number; /** 支付金额倍率;与标价相乘。为 null 或非有限数时按 1 处理。 */ rate: number | null; }; export type BankChannelOption = { value: string; label: string; currency: string; /** 与外层通道 `rate` 同语义;为 null 或非有限数时乘价按 1。 */ rate: number | null; }; function toNumber(v: unknown): number | null { if (typeof v === "number" && Number.isFinite(v)) return v; if (typeof v === "string" && v.trim()) { const n = Number(v); if (Number.isFinite(n)) return n; } return null; } function pickString(record: Record, keys: string[]): string { for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) return value.trim(); } return ""; } function pickAmountRange(record: Record): string { const min = toNumber( record.minAmount ?? record.amountMin ?? record.lowerAmount ?? record.min, ); const max = toNumber( record.maxAmount ?? record.amountMax ?? record.upperAmount ?? record.max, ); const unit = pickString(record, ["currency", "currencyCode", "coin", "unit"]).toUpperCase(); if (min !== null && max !== null) { return `$${min} - $${max}${unit ? ` ${unit}` : ""}`; } const rangeText = pickString(record, ["amountRange", "limitRange", "amountDesc"]); return rangeText || "-"; } function pickFee(record: Record): string { const feeRate = toNumber(record.feeRate); if (feeRate !== null) { if (feeRate <= 1) return `${(feeRate * 100).toFixed(2).replace(/\.00$/, "")}%`; return `${feeRate.toFixed(2).replace(/\.00$/, "")}%`; } const fee = pickString(record, ["fee", "charge", "feeDesc"]); return fee || "0%"; } const CHANNEL_GROUP_NAME_MAP: Record = { 1: "国际转账支付", 2: "中国网银支付", 3: "数字货币", 4: "电子钱包", 5: "CWG 电子卡", }; const CHANNEL_GROUP_DISPLAY_ORDER: Record = { 3: 1, // 数字货币 2: 2, // 中国网银支付 1: 3, // 国际转账支付 4: 4, // 电子钱包 5: 5, // CWG 电子卡 }; function pickGroupId(record: Record): number | null { const id = toNumber( record.type ?? record.channelType ?? record.payTypeType ?? record.groupId ?? record.channelGroupId ?? record.channelClassify ?? record.channelCategory ?? record.categoryId ?? record.payTypeGroup ?? record.payTypeClassify, ); if (id === null) return null; if (id >= 1 && id <= 5) return id; return null; } function normalizeChannelList(raw: unknown): RemittanceChannel[] { if (!raw || typeof raw !== "object") return []; const container = raw as Record; const inner = container.data ?? container.list ?? container.rows ?? container.records ?? container.channels; const source = Array.isArray(inner) ? inner : []; const output: RemittanceChannel[] = []; for (const item of source) { if (!item || typeof item !== "object") continue; const row = item as Record; const id = String( row.id ?? row.channelId ?? row.payTypeId ?? row.code ?? row.channelCode ?? output.length + 1, ); const name = pickString(row, ["name", "channelName", "payType", "channelCode", "code"]) || "-"; const code = pickString(row, ["code", "channelCode", "payCode", "channelNo"]) || id; const requestUrl = pickString(row, ["requestUrl", "payUrl", "url"]) || "/xfgpay/pay"; const icon = pickString(row, ["icon", "logo", "img", "image", "iconUrl"]); const processingTime = pickString(row, ["processingTime", "processTime", "arrivalTime", "timeDesc"]) || "1 hours"; const groupId = pickGroupId(row); const fallbackGroupName = pickString(row, [ "groupName", "categoryName", "channelGroup", "payScene", "group", ]) || "支付通道"; const groupName = groupId ? CHANNEL_GROUP_NAME_MAP[groupId] : fallbackGroupName; const groupOrder = groupId ? (CHANNEL_GROUP_DISPLAY_ORDER[groupId] ?? 999) : 999; output.push({ id, code, requestUrl, icon, name, description: name || "-", amountRange: pickAmountRange(row), processingTime, fee: pickFee(row), groupName, groupOrder, channelType: toNumber(row.type ?? row.channelType ?? row.payTypeType), bankValid: toNumber(row.bankValid) ?? 0, rate: toNumber(row.rate), }); } return output; } /** 支付展示/提交金额 = 标价 × 通道 `rate`;`rate` 为 null 或非有限数时按 1。金额最多保留两位小数。 */ export function payAmountWithChannelRate(basePrice: number, rate: number | null): number { if (!Number.isFinite(basePrice) || basePrice < 0) return 0; const mult = rate != null && Number.isFinite(rate) ? rate : 1; const product = basePrice * mult; if (!Number.isFinite(product)) return 0; return Math.round(product * 100) / 100; } /** * 有二级「选择付款方式」且已选中时,用该项的 `rate`; * 否则用 `/remittance/channel/list` 外层通道的 `rate`。 */ export function resolvePayRateMultiplier(input: { bankSelectorVisible: boolean; selectedBankCode: string; bankOptions: BankChannelOption[]; channelRate: number | null; }): number | null { if (input.bankSelectorVisible && input.selectedBankCode.trim()) { const bank = input.bankOptions.find((b) => b.value === input.selectedBankCode); if (bank) return bank.rate; } return input.channelRate; } export async function fetchRemittanceChannels(): Promise { // 存款通道固定走 remittance 路径,避免误取到提款通道(/remit/channel/list)。 const raw = await postRemittance("/remittance/channel/list", {}); return normalizeChannelList(raw); } function normalizeBankChannelList(raw: unknown): BankChannelOption[] { if (!raw || typeof raw !== "object") return []; const container = raw as Record; const inner = container.data ?? container.list ?? container.rows ?? container.records ?? container.channels; const source = Array.isArray(inner) ? inner : []; const output: BankChannelOption[] = []; for (const item of source) { if (!item || typeof item !== "object") continue; const row = item as Record; const valueFromCodeKeys: unknown[] = [row.code, row.channelCode, row.payCode]; let value = ""; for (const v of valueFromCodeKeys) { if (typeof v === "number" && Number.isFinite(v)) { value = String(v); break; } if (typeof v === "string" && v.trim()) { value = v.trim(); break; } } if (!value) value = pickString(row, ["name", "currency", "enName"]); if (!value) continue; const label = pickString(row, ["name", "enName", "currency", "code"]) || value; const currency = pickString(row, ["currency", "code", "name"]) || "USDT"; output.push({ value, label, currency, rate: toNumber(row.rate) }); } return output; } export async function fetchBankChannelOptions(channelCode?: string): Promise { const body = channelCode ? { channelCode } : {}; const raw = await postRemittance("/channel/bank/list", body); return normalizeBankChannelList(raw); } /** * 银联 / 电汇等走 `/telegraphic/pay` 的通道:金额、银行等参数放在请求体,不拼在 `.../pay/...` 路径后面。 */ export function isTelegraphicStylePayRequestUrl(requestUrl: string): boolean { const n = `/${requestUrl.replace(/^\/+|\/+$/g, "")}`.toLowerCase(); return n.includes("telegraphic"); } export type PayGoodsDetail = { goodsId: string; goodsNum: number; }; export async function submitXfgPayOrder(input: { requestUrl: string; amount: number; bankCode?: string; code?: string; goodsDetails: PayGoodsDetail[]; payName: string; payPhone: string; }): Promise<{ raw: unknown; resultUrl: string | null }> { const normalizedRequestUrl = `/${input.requestUrl.replace(/^\/+|\/+$/g, "")}`; // 仍以后端下发 requestUrl 为准(动态);仅当明显是提款路径时兜底,避免误打提款申请。 const payRequestPath = /^\/withdraw(\/|$)/i.test(normalizedRequestUrl) ? "/xfgpay/pay" : normalizedRequestUrl; const amount = String(input.amount); const useBodyForPayParams = isTelegraphicStylePayRequestUrl(payRequestPath); const path = useBodyForPayParams ? payRequestPath : input.bankCode ? `${payRequestPath}/1/${encodeURIComponent(amount)}/${encodeURIComponent(input.bankCode)}/0` : `${payRequestPath}/1/${encodeURIComponent(amount)}/0`; const body = useBodyForPayParams ? { goodsDetails: input.goodsDetails, payName: input.payName, payPhone: input.payPhone, amount: input.amount, ...(input.code ? { code: input.code } : {}), ...(input.bankCode ? { bankCode: input.bankCode } : {}), } : { goodsDetails: input.goodsDetails, payName: input.payName, payPhone: input.payPhone, /** 与路径中的金额一致:标价×通道 rate 后的实付额;避免后端只读 body 时落到商品原价。 */ amount: input.amount, }; const data = await postRemittance(path, body); return { raw: data, resultUrl: pickResultUrl(data) }; } function pickResultUrl(raw: unknown): string | null { if (!raw || typeof raw !== "object") return null; const o = raw as Record; const candidates: unknown[] = [ o.result, o.url, o.payUrl, o.redirectUrl, ]; if (o.data && typeof o.data === "object" && o.data !== null) { const d = o.data as Record; candidates.push(d.result, d.url, d.payUrl, d.redirectUrl); } for (const item of candidates) { if (typeof item === "string" && item.trim()) return item.trim(); } return null; }