fixed matches fee calc
This commit is contained in:
+9
-5
@@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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). */
|
||||||
|
|||||||
Reference in New Issue
Block a user