checkout-api.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { postRemittance } from "@/lib/remittance-client";
  2. export type RemittanceChannel = {
  3. id: string;
  4. code: string;
  5. requestUrl: string;
  6. icon: string;
  7. name: string;
  8. description: string;
  9. amountRange: string;
  10. processingTime: string;
  11. fee: string;
  12. groupName: string;
  13. groupOrder: number;
  14. channelType: number | null;
  15. bankValid: number;
  16. };
  17. export type BankChannelOption = {
  18. value: string;
  19. label: string;
  20. currency: string;
  21. };
  22. function toNumber(v: unknown): number | null {
  23. if (typeof v === "number" && Number.isFinite(v)) return v;
  24. if (typeof v === "string" && v.trim()) {
  25. const n = Number(v);
  26. if (Number.isFinite(n)) return n;
  27. }
  28. return null;
  29. }
  30. function pickString(record: Record<string, unknown>, keys: string[]): string {
  31. for (const key of keys) {
  32. const value = record[key];
  33. if (typeof value === "string" && value.trim()) return value.trim();
  34. }
  35. return "";
  36. }
  37. function pickAmountRange(record: Record<string, unknown>): string {
  38. const min = toNumber(
  39. record.minAmount ?? record.amountMin ?? record.lowerAmount ?? record.min,
  40. );
  41. const max = toNumber(
  42. record.maxAmount ?? record.amountMax ?? record.upperAmount ?? record.max,
  43. );
  44. const unit = pickString(record, ["currency", "currencyCode", "coin", "unit"]).toUpperCase();
  45. if (min !== null && max !== null) {
  46. return `$${min} - $${max}${unit ? ` ${unit}` : ""}`;
  47. }
  48. const rangeText = pickString(record, ["amountRange", "limitRange", "amountDesc"]);
  49. return rangeText || "-";
  50. }
  51. function pickFee(record: Record<string, unknown>): string {
  52. const feeRate = toNumber(record.feeRate ?? record.rate);
  53. if (feeRate !== null) {
  54. if (feeRate <= 1) return `${(feeRate * 100).toFixed(2).replace(/\.00$/, "")}%`;
  55. return `${feeRate.toFixed(2).replace(/\.00$/, "")}%`;
  56. }
  57. const fee = pickString(record, ["fee", "charge", "feeDesc"]);
  58. return fee || "0%";
  59. }
  60. const CHANNEL_GROUP_NAME_MAP: Record<number, string> = {
  61. 1: "国际转账支付",
  62. 2: "中国网银支付",
  63. 3: "数字货币",
  64. 4: "电子钱包",
  65. 5: "CWG 电子卡",
  66. };
  67. const CHANNEL_GROUP_DISPLAY_ORDER: Record<number, number> = {
  68. 3: 1, // 数字货币
  69. 2: 2, // 中国网银支付
  70. 1: 3, // 国际转账支付
  71. 4: 4, // 电子钱包
  72. 5: 5, // CWG 电子卡
  73. };
  74. function pickGroupId(record: Record<string, unknown>): number | null {
  75. const id = toNumber(
  76. record.type ??
  77. record.channelType ??
  78. record.payTypeType ??
  79. record.groupId ??
  80. record.channelGroupId ??
  81. record.channelClassify ??
  82. record.channelCategory ??
  83. record.categoryId ??
  84. record.payTypeGroup ??
  85. record.payTypeClassify,
  86. );
  87. if (id === null) return null;
  88. if (id >= 1 && id <= 5) return id;
  89. return null;
  90. }
  91. function normalizeChannelList(raw: unknown): RemittanceChannel[] {
  92. if (!raw || typeof raw !== "object") return [];
  93. const container = raw as Record<string, unknown>;
  94. const inner =
  95. container.data ??
  96. container.list ??
  97. container.rows ??
  98. container.records ??
  99. container.channels;
  100. const source = Array.isArray(inner) ? inner : [];
  101. const output: RemittanceChannel[] = [];
  102. for (const item of source) {
  103. if (!item || typeof item !== "object") continue;
  104. const row = item as Record<string, unknown>;
  105. const id = String(
  106. row.id ??
  107. row.channelId ??
  108. row.payTypeId ??
  109. row.code ??
  110. row.channelCode ??
  111. output.length + 1,
  112. );
  113. const name =
  114. pickString(row, ["name", "channelName", "payType", "channelCode", "code"]) || "-";
  115. const code = pickString(row, ["code", "channelCode", "payCode", "channelNo"]) || id;
  116. const requestUrl = pickString(row, ["requestUrl", "payUrl", "url"]) || "/xfgpay/pay";
  117. const icon = pickString(row, ["icon", "logo", "img", "image", "iconUrl"]);
  118. const processingTime =
  119. pickString(row, ["processingTime", "processTime", "arrivalTime", "timeDesc"]) || "1 hours";
  120. const groupId = pickGroupId(row);
  121. const fallbackGroupName =
  122. pickString(row, [
  123. "groupName",
  124. "categoryName",
  125. "channelGroup",
  126. "payScene",
  127. "group",
  128. ]) || "支付通道";
  129. const groupName = groupId ? CHANNEL_GROUP_NAME_MAP[groupId] : fallbackGroupName;
  130. const groupOrder = groupId ? (CHANNEL_GROUP_DISPLAY_ORDER[groupId] ?? 999) : 999;
  131. output.push({
  132. id,
  133. code,
  134. requestUrl,
  135. icon,
  136. name,
  137. description: name || "-",
  138. amountRange: pickAmountRange(row),
  139. processingTime,
  140. fee: pickFee(row),
  141. groupName,
  142. groupOrder,
  143. channelType: toNumber(row.type ?? row.channelType ?? row.payTypeType),
  144. bankValid: toNumber(row.bankValid) ?? 0,
  145. });
  146. }
  147. return output;
  148. }
  149. export async function fetchRemittanceChannels(): Promise<RemittanceChannel[]> {
  150. let raw: unknown;
  151. try {
  152. raw = await postRemittance<unknown>("/remit/channel/list", {});
  153. } catch {
  154. raw = await postRemittance<unknown>("/remittance/channel/list", {});
  155. }
  156. return normalizeChannelList(raw);
  157. }
  158. function normalizeBankChannelList(raw: unknown): BankChannelOption[] {
  159. if (!raw || typeof raw !== "object") return [];
  160. const container = raw as Record<string, unknown>;
  161. const inner =
  162. container.data ??
  163. container.list ??
  164. container.rows ??
  165. container.records ??
  166. container.channels;
  167. const source = Array.isArray(inner) ? inner : [];
  168. const output: BankChannelOption[] = [];
  169. for (const item of source) {
  170. if (!item || typeof item !== "object") continue;
  171. const row = item as Record<string, unknown>;
  172. const value = pickString(row, ["code", "name", "currency", "enName"]);
  173. if (!value) continue;
  174. const label = pickString(row, ["name", "enName", "currency", "code"]) || value;
  175. const currency = pickString(row, ["currency", "code", "name"]) || "USDT";
  176. output.push({ value, label, currency });
  177. }
  178. return output;
  179. }
  180. export async function fetchBankChannelOptions(channelCode?: string): Promise<BankChannelOption[]> {
  181. const body = channelCode ? { channelCode } : {};
  182. const raw = await postRemittance<unknown>("/channel/bank/list", body);
  183. return normalizeBankChannelList(raw);
  184. }
  185. export async function submitXfgPayOrder(input: {
  186. requestUrl: string;
  187. amount: number;
  188. bankCode?: string;
  189. goodIds: string[];
  190. payName: string;
  191. payPhone: string;
  192. }): Promise<{ raw: unknown; resultUrl: string | null }> {
  193. const normalizedRequestUrl = `/${input.requestUrl.replace(/^\/+|\/+$/g, "")}`;
  194. const amount = String(input.amount);
  195. const path = input.bankCode
  196. ? `${normalizedRequestUrl}/1/${encodeURIComponent(amount)}/${encodeURIComponent(input.bankCode)}/0`
  197. : `${normalizedRequestUrl}/1/${encodeURIComponent(amount)}/0`;
  198. const body = {
  199. goodIds: input.goodIds,
  200. payName: input.payName,
  201. payPhone: input.payPhone,
  202. };
  203. const data = await postRemittance<unknown>(path, body);
  204. return { raw: data, resultUrl: pickResultUrl(data) };
  205. }
  206. function pickResultUrl(raw: unknown): string | null {
  207. if (!raw || typeof raw !== "object") return null;
  208. const o = raw as Record<string, unknown>;
  209. const candidates: unknown[] = [
  210. o.result,
  211. o.url,
  212. o.payUrl,
  213. o.redirectUrl,
  214. ];
  215. if (o.data && typeof o.data === "object" && o.data !== null) {
  216. const d = o.data as Record<string, unknown>;
  217. candidates.push(d.result, d.url, d.payUrl, d.redirectUrl);
  218. }
  219. for (const item of candidates) {
  220. if (typeof item === "string" && item.trim()) return item.trim();
  221. }
  222. return null;
  223. }