import type { TicketItemListRow } from "@/types/api/ticket-items"; export type TicketItemGroup = { key: string; order_no: string | null; order_status: string | null; draw_no: string | null; placed_at: string | null; currency_code: string | null; status: string; items: TicketItemListRow[]; total_bet_amount: number; actual_deduct_amount: number; win_amount: number; jackpot_win_amount: number; }; /** 分组键:有 order_no 则按订单;否则 draw_no + placed_at + currency + status 兜底 */ export function getTicketItemGroupKey(row: TicketItemListRow): string { const orderNo = (row.order_no ?? "").trim(); if (orderNo) { return `order:${orderNo}`; } const drawNo = row.draw_no ?? ""; const placedAt = row.placed_at ?? ""; const currency = row.currency_code ?? ""; const status = row.status ?? ""; return `fallback:${drawNo}|${placedAt}|${currency}|${status}`; } function resolveGroupStatus(items: TicketItemListRow[]): string { const orderStatus = items.map((row) => row.order_status).find((s) => s != null && s !== ""); if (orderStatus === "partial_failed" || orderStatus === "partial_pending_confirm") { return orderStatus; } const hasStatus = (status: string): boolean => items.some((row) => row.status === status); const hasAny = (statuses: string[]): boolean => statuses.some((status) => hasStatus(status)); const allMatch = (statuses: string[]): boolean => items.length > 0 && items.every((row) => statuses.includes(row.status)); const hasWinningItem = items.some( (row) => row.status === "settled_win" && row.win_amount + row.jackpot_win_amount > 0, ); if (hasStatus("failed") && hasAny(["pending_draw", "placed", "pending_confirm"])) { return "partial_failed"; } if (hasAny(["pending_confirm", "partial_pending_confirm"])) { return hasAny(["failed", "pending_draw", "placed", "pending_payout", "settled_win", "settled_lose"]) ? "partial_pending_confirm" : "pending_confirm"; } if (hasAny(["pending_draw", "placed"])) { return "pending_draw"; } if (hasStatus("pending_payout")) { return "pending_payout"; } if (hasWinningItem) { return "settled_win"; } if (allMatch(["failed"])) { return "failed"; } if (allMatch(["refunded"])) { return "refunded"; } if (allMatch(["settled_lose", "settled_win"])) { return "settled_lose"; } return items[0]?.status ?? "unknown"; } export function sortTicketGroupItems(items: TicketItemListRow[]): TicketItemListRow[] { return [...items].sort((a, b) => { const byPlay = a.play_code.localeCompare(b.play_code); if (byPlay !== 0) return byPlay; return (a.original_number ?? "").localeCompare(b.original_number ?? ""); }); } /** 保持 API 返回顺序下,按分组键首次出现顺序输出合并组 */ export function groupTicketItems(items: TicketItemListRow[]): TicketItemGroup[] { const map = new Map(); const order: string[] = []; for (const row of items) { const key = getTicketItemGroupKey(row); let group = map.get(key); if (!group) { const orderNo = (row.order_no ?? "").trim(); group = { key, order_no: orderNo || null, order_status: row.order_status ?? null, draw_no: row.draw_no ?? null, placed_at: row.placed_at ?? null, currency_code: row.currency_code ?? null, status: row.status, items: [], total_bet_amount: 0, actual_deduct_amount: 0, win_amount: 0, jackpot_win_amount: 0, }; map.set(key, group); order.push(key); } group.items.push(row); group.total_bet_amount += row.total_bet_amount; group.actual_deduct_amount += row.actual_deduct_amount; group.win_amount += row.win_amount; group.jackpot_win_amount += row.jackpot_win_amount; } return order.map((key) => { const group = map.get(key)!; const sortedItems = sortTicketGroupItems(group.items); return { ...group, items: sortedItems, status: resolveGroupStatus(sortedItems), order_status: sortedItems[0]?.order_status ?? group.order_status, }; }); } export function ticketDetailHref(ticketNo: string): string { return `/orders/${encodeURIComponent(ticketNo)}`; }