fixed matches fee calc

This commit is contained in:
2026-05-12 16:32:05 +05:30
parent e5414a9920
commit fcf7654e78
5 changed files with 112 additions and 73 deletions
+9 -5
View File
@@ -18,7 +18,7 @@ import {
type MatchmakerLogSource, type MatchmakerLogSource,
} from "@/lib/matchmaker-log-source"; } from "@/lib/matchmaker-log-source";
import { readMatchmakerLogFile } from "@/lib/matchmaker-logs-server"; import { readMatchmakerLogFile } from "@/lib/matchmaker-logs-server";
import { fetchEntryHoldPerPlayerCoinsByMatchIds } from "@/lib/match-entry-hold-from-ledger"; import { fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds } from "@/lib/match-entry-hold-from-ledger";
import type { import type {
AdminMatchRow, AdminMatchRow,
DbMatch, DbMatch,
@@ -144,7 +144,8 @@ export default async function Home({
matchesError = matchesRes.error.message; matchesError = matchesRes.error.message;
} else { } else {
const rawMatches = (matchesRes.data ?? []) as DbMatch[]; const rawMatches = (matchesRes.data ?? []) as DbMatch[];
const holdByMatch = await fetchEntryHoldPerPlayerCoinsByMatchIds( const { holdPerPlayer, feePerPlayer } =
await fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds(
supabase, supabase,
rawMatches rawMatches
.map((m) => Number(m.id)) .map((m) => Number(m.id))
@@ -152,13 +153,16 @@ export default async function Home({
); );
matches = rawMatches.map((m) => { matches = rawMatches.map((m) => {
const idNum = Number(m.id); const idNum = Number(m.id);
const hold = Number.isFinite(idNum) const hold =
? holdByMatch.get(idNum) Number.isFinite(idNum) ? holdPerPlayer.get(idNum) : undefined;
: undefined; const fee =
Number.isFinite(idNum) ? feePerPlayer.get(idNum) : undefined;
return { return {
...m, ...m,
entryHoldPerPlayerCoins: entryHoldPerPlayerCoins:
hold !== undefined ? hold.toString() : null, hold !== undefined ? hold.toString() : null,
entryFeePerPlayerCoins:
fee !== undefined ? fee.toString() : null,
}; };
}); });
} }
+1 -6
View File
@@ -66,11 +66,6 @@ function formatTs(value: string | null): string {
} }
} }
function matchEntryFeeCoinString(v: DbMatch["entry_fee"]): string | null {
if (v == null) return null;
return txAmountToBigInt(v).toString();
}
function formatPrizeCcChip(v: DbMatch["prize_cc"]): string { function formatPrizeCcChip(v: DbMatch["prize_cc"]): string {
if (v == null) return "—"; if (v == null) return "—";
const b = txAmountToBigInt(v); const b = txAmountToBigInt(v);
@@ -819,7 +814,7 @@ export function AdminDashboard({
matchId={m.id} matchId={m.id}
statusLabel={statusLabel(m.status)} statusLabel={statusLabel(m.status)}
createdAtLabel={formatTs(m.created_at)} createdAtLabel={formatTs(m.created_at)}
entryFeeCoins={matchEntryFeeCoinString(m.entry_fee)} entryFeeCoins={m.entryFeePerPlayerCoins}
entryHoldPerPlayerCoins={m.entryHoldPerPlayerCoins} entryHoldPerPlayerCoins={m.entryHoldPerPlayerCoins}
prizeCcLabel={formatPrizeCcChip(m.prize_cc)} prizeCcLabel={formatPrizeCcChip(m.prize_cc)}
winnerId={m.winner_id} winnerId={m.winner_id}
+45 -37
View File
@@ -4,6 +4,40 @@ import Link from "next/link";
import { ClickableUserId } from "@/components/clickable-user-id"; import { ClickableUserId } from "@/components/clickable-user-id";
import { formatRcLabelFromCoinsBigInt } from "@/lib/coins-rc"; import { formatRcLabelFromCoinsBigInt } from "@/lib/coins-rc";
/** Ledger rows are per player; both players pay → show `1.0 RC x 2 = 2.0 RC`. */
function rcPerPlayerTimesTwoLine(coinStr: string | null): string {
if (coinStr == null || String(coinStr).trim() === "") return "—";
try {
const perPlayer = BigInt(String(coinStr).trim());
const bothPlayers = perPlayer * BigInt(2);
return `${formatRcLabelFromCoinsBigInt(perPlayer)} x 2 = ${formatRcLabelFromCoinsBigInt(bothPlayers)}`;
} catch {
return "—";
}
}
function entryCombinedPerPlayerTimesTwoLine(
feeCoins: string | null,
holdCoins: string | null,
): string {
if (
feeCoins == null ||
holdCoins == null ||
String(feeCoins).trim() === "" ||
String(holdCoins).trim() === ""
) {
return "—";
}
try {
const perPlayer =
BigInt(String(feeCoins).trim()) + BigInt(String(holdCoins).trim());
const bothPlayers = perPlayer * BigInt(2);
return `${formatRcLabelFromCoinsBigInt(perPlayer)} x 2 = ${formatRcLabelFromCoinsBigInt(bothPlayers)}`;
} catch {
return "—";
}
}
type PlayerSide = { type PlayerSide = {
id: number | null; id: number | null;
username: string | null; username: string | null;
@@ -16,7 +50,7 @@ type Props = {
matchId: number; matchId: number;
statusLabel: string; statusLabel: string;
createdAtLabel: string; createdAtLabel: string;
/** `matches.entry_fee` as coin string (bigint); RC shown in footer. */ /** Ledger `entry_fee` / player as coin string; null if unknown. */
entryFeeCoins: string | null; entryFeeCoins: string | null;
/** Ledger `entry_hold` / player as coin string; null if unknown. */ /** Ledger `entry_hold` / player as coin string; null if unknown. */
entryHoldPerPlayerCoins: string | null; entryHoldPerPlayerCoins: string | null;
@@ -28,35 +62,6 @@ type Props = {
winnerId: number | string | null; winnerId: number | string | null;
}; };
function rcLabelFromCoinString(s: string | null): string {
if (s == null || String(s).trim() === "") return "—";
try {
return formatRcLabelFromCoinsBigInt(BigInt(String(s).trim()));
} catch {
return "—";
}
}
function entryTotalRcLabel(
feeCoins: string | null,
holdCoins: string | null,
): string {
if (
feeCoins == null ||
holdCoins == null ||
String(feeCoins).trim() === "" ||
String(holdCoins).trim() === ""
) {
return "—";
}
try {
const sum = BigInt(String(feeCoins).trim()) + BigInt(String(holdCoins).trim());
return formatRcLabelFromCoinsBigInt(sum);
} catch {
return "—";
}
}
function idsMatch( function idsMatch(
a: number | string | null | undefined, a: number | string | null | undefined,
b: number | string | null | undefined, b: number | string | null | undefined,
@@ -185,9 +190,12 @@ export function MatchHistoryBattleCard({
right, right,
winnerId, winnerId,
}: Props) { }: Props) {
const entryRc = entryTotalRcLabel(entryFeeCoins, entryHoldPerPlayerCoins); const entryLine = entryCombinedPerPlayerTimesTwoLine(
const entryFeeRc = rcLabelFromCoinString(entryFeeCoins); entryFeeCoins,
const entryHoldRc = rcLabelFromCoinString(entryHoldPerPlayerCoins); entryHoldPerPlayerCoins,
);
const entryHoldLine = rcPerPlayerTimesTwoLine(entryHoldPerPlayerCoins);
const entryFeeLine = rcPerPlayerTimesTwoLine(entryFeeCoins);
const prizeCcDisplay = prizeCcLabel.trim() === "" ? "—" : prizeCcLabel; const prizeCcDisplay = prizeCcLabel.trim() === "" ? "—" : prizeCcLabel;
const hasWinner = const hasWinner =
winnerId != null && winnerId != null &&
@@ -238,16 +246,16 @@ export function MatchHistoryBattleCard({
Time {createdAtLabel} Time {createdAtLabel}
</span> </span>
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10"> <span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
Entry {entryRc} Entry {entryLine}
</span> </span>
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10"> <span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
Entry hold {entryHoldRc} Entry hold {entryHoldLine}
</span> </span>
<span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10"> <span className="rounded bg-zinc-900/10 px-2 py-1 dark:bg-zinc-100/10">
Prize {prizeCcDisplay} CC Prize {prizeCcDisplay} CC
</span> </span>
<span className="rounded-md border border-emerald-200/90 bg-emerald-50 px-2 py-1 text-[11px] font-medium text-emerald-600 dark:border-emerald-300/35 dark:bg-emerald-400/15 dark:text-emerald-200"> <span className="rounded-md border border-emerald-200/90 bg-emerald-50 px-2 py-1 text-[11px] font-medium text-emerald-600 tabular-nums dark:border-emerald-300/35 dark:bg-emerald-400/15 dark:text-emerald-200">
Entry fee {entryFeeRc} Entry fee {entryFeeLine}
</span> </span>
</div> </div>
<Link <Link
+50 -20
View File
@@ -6,16 +6,43 @@ function normRemark(r: string | null | undefined): string {
return (r ?? "").toLowerCase().trim(); return (r ?? "").toLowerCase().trim();
} }
type PerRemarkAgg = {
sums: Map<number, bigint>;
counts: Map<number, number>;
};
function bumpAgg(agg: PerRemarkAgg, matchId: number, amount: bigint): void {
agg.sums.set(matchId, (agg.sums.get(matchId) ?? BigInt(0)) + amount);
agg.counts.set(matchId, (agg.counts.get(matchId) ?? 0) + 1);
}
function avgPerMatch(agg: PerRemarkAgg): Map<number, bigint> {
const out = new Map<number, bigint>();
for (const [mid, sum] of agg.sums) {
const c = agg.counts.get(mid) ?? 0;
if (c > 0) out.set(mid, sum / BigInt(c));
}
return out;
}
export type MatchEntryHoldAndFeePerPlayerCoins = {
holdPerPlayer: Map<number, bigint>;
feePerPlayer: Map<number, bigint>;
};
/** /**
* For each match_id, average `entry_hold` debit amount (same per player when * For each match_id, average debit amount for `entry_hold` and `entry_fee`
* both rows exist; sum/count handles odd counts). * (per player when two rows exist; sum/count handles odd counts).
*/ */
export async function fetchEntryHoldPerPlayerCoinsByMatchIds( export async function fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds(
supabase: SupabaseClient, supabase: SupabaseClient,
matchIds: number[], matchIds: number[],
): Promise<Map<number, bigint>> { ): Promise<MatchEntryHoldAndFeePerPlayerCoins> {
const out = new Map<number, bigint>(); const empty: MatchEntryHoldAndFeePerPlayerCoins = {
if (matchIds.length === 0) return out; holdPerPlayer: new Map(),
feePerPlayer: new Map(),
};
if (matchIds.length === 0) return empty;
const { data, error } = await supabase const { data, error } = await supabase
.from("transactions") .from("transactions")
@@ -23,19 +50,24 @@ export async function fetchEntryHoldPerPlayerCoinsByMatchIds(
.in("match_id", matchIds); .in("match_id", matchIds);
if (error) { if (error) {
return out; return empty;
} }
const sums = new Map<number, bigint>(); const holdAgg: PerRemarkAgg = {
const counts = new Map<number, number>(); sums: new Map(),
counts: new Map(),
};
const feeAgg: PerRemarkAgg = {
sums: new Map(),
counts: new Map(),
};
for (const row of (data ?? []) as Pick< for (const row of (data ?? []) as Pick<
DbTransaction, DbTransaction,
"match_id" | "amount" | "remarks" "match_id" | "amount" | "remarks"
>[]) { >[]) {
if (normRemark(row.remarks) !== "entry_hold") { const rmk = normRemark(row.remarks);
continue; if (rmk !== "entry_hold" && rmk !== "entry_fee") continue;
}
const midRaw = row.match_id; const midRaw = row.match_id;
if (midRaw == null) continue; if (midRaw == null) continue;
const mid = const mid =
@@ -43,14 +75,12 @@ export async function fetchEntryHoldPerPlayerCoinsByMatchIds(
if (!Number.isFinite(mid)) continue; if (!Number.isFinite(mid)) continue;
const a = txAmountToBigInt(row.amount); const a = txAmountToBigInt(row.amount);
if (a === BigInt(0)) continue; if (a === BigInt(0)) continue;
sums.set(mid, (sums.get(mid) ?? BigInt(0)) + a); if (rmk === "entry_hold") bumpAgg(holdAgg, mid, a);
counts.set(mid, (counts.get(mid) ?? 0) + 1); else bumpAgg(feeAgg, mid, a);
} }
for (const [mid, sum] of sums) { return {
const c = counts.get(mid) ?? 0; holdPerPlayer: avgPerMatch(holdAgg),
if (c > 0) out.set(mid, sum / BigInt(c)); feePerPlayer: avgPerMatch(feeAgg),
} };
return out;
} }
+2
View File
@@ -29,6 +29,8 @@ export type DbMatch = {
export type AdminMatchRow = DbMatch & { export type AdminMatchRow = DbMatch & {
/** Per-player `entry_hold` debit in coins (stringified bigint); null if unknown. */ /** Per-player `entry_hold` debit in coins (stringified bigint); null if unknown. */
entryHoldPerPlayerCoins: string | null; entryHoldPerPlayerCoins: string | null;
/** Per-player `entry_fee` debit in coins from `transactions`; null if unknown. */
entryFeePerPlayerCoins: string | null;
}; };
/** Mirrors `public.settings` (key/value config rows). */ /** Mirrors `public.settings` (key/value config rows). */