threesome fixed
This commit is contained in:
+347
-126
@@ -3,13 +3,60 @@ import * as crypto from 'crypto';
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { Room, Settings } from './types';
|
||||
import { createSupabase, supabaseConfigured } from './auth_bridge';
|
||||
import { logInfo, recordHistory } from './server_log';
|
||||
|
||||
/** Map Postgres RPC exception text to an HTTP-friendly error. */
|
||||
function mapEconomyRpcError(message: string): { status: number; error: string } {
|
||||
const m = message.toLowerCase();
|
||||
if (m.includes('insufficient_rc_red') || m.includes('insufficient_rc_blue')) {
|
||||
return { status: 402, error: 'One or both players have insufficient RC for the entry fee' };
|
||||
}
|
||||
if (m.includes('insufficient_rc')) {
|
||||
return { status: 402, error: 'Insufficient RC for entry fee' };
|
||||
}
|
||||
if (m.includes('joins_incomplete')) {
|
||||
return { status: 409, error: 'Both players must have joined before collecting entry fees' };
|
||||
}
|
||||
if (m.includes('players_incomplete')) {
|
||||
return { status: 409, error: 'Match is missing red or blue player' };
|
||||
}
|
||||
if (m.includes('entries_not_collected')) {
|
||||
return { status: 409, error: 'Entry fees not collected yet; both players must join first' };
|
||||
}
|
||||
if (m.includes('winner_already_set') || m.includes('match_already_settled')) {
|
||||
return { status: 409, error: 'Winner already set for this match' };
|
||||
}
|
||||
if (m.includes('winner_not_participant')) {
|
||||
return { status: 400, error: 'Winner must be red or blue on this match' };
|
||||
}
|
||||
if (m.includes('match_not_found')) {
|
||||
return { status: 404, error: 'Match not found' };
|
||||
}
|
||||
if (m.includes('escrow_balance_mismatch') || m.includes('escrow_not_zero') || m.includes('insufficient_escrow')) {
|
||||
return { status: 500, error: 'Match escrow balance is inconsistent' };
|
||||
}
|
||||
if (
|
||||
m.includes('could not find the function') ||
|
||||
m.includes('schema cache') ||
|
||||
(m.includes('function') && (m.includes('collect_match_entries') || m.includes('settle_match_reward')))
|
||||
) {
|
||||
return {
|
||||
status: 503,
|
||||
error: 'Economy RPCs not installed — apply schemas/rpc_collect_match_entries.sql and schemas/rpc_settle_match_reward.sql',
|
||||
};
|
||||
}
|
||||
return { status: 500, error: message };
|
||||
}
|
||||
|
||||
function rpcErrorMessage(err: { message?: string } | null): string {
|
||||
return err?.message?.trim() || 'Unknown RPC error';
|
||||
}
|
||||
|
||||
/** Fallback when `public.settings` has no `entry_fee` row or value is invalid. */
|
||||
export const MATCH_ENTRY_FEE_RC = 21;
|
||||
|
||||
const SETTINGS_KEY_ENTRY_FEE = 'entry_fee';
|
||||
const SETTINGS_KEY_BET_FEE = 'bet_fee';
|
||||
const SYSTEM_ACCOUNT_ID = 1;
|
||||
|
||||
/** Fallback when `public.settings` has no `bet_fee` row or value is invalid (percent applied to each player's entry). */
|
||||
export const DEFAULT_BET_FEE_PERCENT = 10;
|
||||
@@ -71,8 +118,8 @@ export type InsertMatchForNewRoomResult = {
|
||||
|
||||
/**
|
||||
* Winner's net RC = both entries minus per-player fees.
|
||||
* `fee_each = ceil(entry_fee * betFeePercent / 100)`, prize = `(entry_fee - fee_each) * 2` = `entry_fee * 2 - fee_each * 2`.
|
||||
* Matches ledger rows: each player pays `fee_each` to system (`entry_fee`) and holds `entry_fee - fee_each`.
|
||||
* `fee_each = ceil(entry_fee * betFeePercent / 100)`, prize = `(entry_fee - fee_each) * 2`.
|
||||
* Collect moves both entries into the match escrow; settle pays prize from escrow and house fee to system.
|
||||
*/
|
||||
export function computeRcPrizeFromEntryFee(entryFee: number, betFeePercent: number = DEFAULT_BET_FEE_PERCENT): number {
|
||||
const feeEach = Math.ceil((entryFee * betFeePercent) / 100);
|
||||
@@ -209,6 +256,41 @@ export async function getPlayerLast10MatchRecord(
|
||||
return { l10_wins, l10_losses };
|
||||
}
|
||||
|
||||
/** All-time completed match stats for a player (matches with a recorded winner). */
|
||||
export async function getPlayerMatchStats(
|
||||
settings: Settings,
|
||||
playerId: number
|
||||
): Promise<{ matches_played: number; wins: number; win_ratio: number }> {
|
||||
if (!supabaseConfigured(settings)) {
|
||||
return { matches_played: 0, wins: 0, win_ratio: 0 };
|
||||
}
|
||||
const supabase = createSupabase(settings);
|
||||
const { data, error } = await supabase
|
||||
.from('matches')
|
||||
.select('winner_id')
|
||||
.or(`user_red.eq.${playerId},user_blue.eq.${playerId}`)
|
||||
.not('winner_id', 'is', null);
|
||||
|
||||
if (error || !data) {
|
||||
console.error('matches: profile stats fetch failed', error?.message);
|
||||
return { matches_played: 0, wins: 0, win_ratio: 0 };
|
||||
}
|
||||
|
||||
let wins = 0;
|
||||
for (const row of data as { winner_id: unknown }[]) {
|
||||
if (coerceId(row.winner_id) === playerId) wins++;
|
||||
}
|
||||
const matches_played = data.length;
|
||||
const win_ratio = matches_played > 0 ? wins / matches_played : 0;
|
||||
return { matches_played, wins, win_ratio };
|
||||
}
|
||||
|
||||
function daysSinceRegistration(createdAt: string): number {
|
||||
const created = new Date(createdAt).getTime();
|
||||
if (Number.isNaN(created)) return 0;
|
||||
return Math.floor((Date.now() - created) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
function coerceNonNegativeInt(v: unknown): number | null {
|
||||
if (v == null) return null;
|
||||
if (typeof v === 'bigint') {
|
||||
@@ -227,99 +309,77 @@ function coerceNonNegativeInt(v: unknown): number | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Each player pays `entry_fee` RC; winner receives net RC after `betFeePercent` taken from **each** entry; each gets `prizeCc` CC.
|
||||
* Idempotent callers should set winner only once.
|
||||
* Move both entry fees into the per-match escrow when both players have joined.
|
||||
* Idempotent via `matches.entries_collected_at`.
|
||||
*/
|
||||
async function applyMatchWinnerEconomy(
|
||||
async function callCollectMatchEntries(
|
||||
supabase: SupabaseClient,
|
||||
matchId: number,
|
||||
userRed: number | null,
|
||||
userBlue: number | null,
|
||||
winnerId: number,
|
||||
entryFee: number,
|
||||
prizeCc: number,
|
||||
betFeePercent: number
|
||||
): Promise<string | null> {
|
||||
const ids = [...new Set([userRed, userBlue].filter((x): x is number => x != null && x >= 1))];
|
||||
if (ids.length === 0) return null;
|
||||
|
||||
const rcPrize = computeRcPrizeFromEntryFee(entryFee, betFeePercent);
|
||||
|
||||
const lookupIds = [...new Set([...ids, SYSTEM_ACCOUNT_ID])];
|
||||
const { data: rows, error: selErr } = await supabase.from('users').select('id, rc, cc').in('id', lookupIds);
|
||||
if (selErr) return selErr.message;
|
||||
if (!rows || rows.length !== lookupIds.length) return 'One or more users not found in users';
|
||||
|
||||
const byId = new Map<number, { rc: number; cc: number }>();
|
||||
for (const row of rows) {
|
||||
const uid = coerceId((row as { id: unknown }).id);
|
||||
if (uid == null) continue;
|
||||
byId.set(uid, {
|
||||
rc: Number((row as { rc: unknown }).rc ?? 0),
|
||||
cc: Number((row as { cc: unknown }).cc ?? 0),
|
||||
});
|
||||
matchId: number
|
||||
): Promise<{ ok: true; data: Record<string, unknown> } | { ok: false; status: number; error: string }> {
|
||||
const { data, error } = await supabase.rpc('collect_match_entries', { p_match_id: matchId });
|
||||
if (error) {
|
||||
return { ok: false, ...mapEconomyRpcError(rpcErrorMessage(error)) };
|
||||
}
|
||||
if (!byId.has(SYSTEM_ACCOUNT_ID)) return 'System account not found';
|
||||
|
||||
let totalEntryCollected = 0;
|
||||
|
||||
for (const uid of ids) {
|
||||
const base = byId.get(uid);
|
||||
if (!base) return `Player ${uid} not found in users`;
|
||||
const baseRc = base.rc;
|
||||
const baseCc = base.cc;
|
||||
let newRc = baseRc - entryFee;
|
||||
if (uid === winnerId) newRc += rcPrize;
|
||||
const newCc = baseCc + prizeCc;
|
||||
const { error: upErr } = await supabase.from('users').update({ rc: newRc, cc: newCc }).eq('id', uid);
|
||||
if (upErr) return upErr.message;
|
||||
totalEntryCollected += entryFee;
|
||||
}
|
||||
|
||||
const system = byId.get(SYSTEM_ACCOUNT_ID)!;
|
||||
const systemNewRc = system.rc + totalEntryCollected - rcPrize;
|
||||
const { error: systemUpErr } = await supabase
|
||||
.from('users')
|
||||
.update({ rc: systemNewRc })
|
||||
.eq('id', SYSTEM_ACCOUNT_ID);
|
||||
if (systemUpErr) return systemUpErr.message;
|
||||
|
||||
const perPlayerBetFee = Math.ceil((entryFee * betFeePercent) / 100);
|
||||
const perPlayerHold = entryFee - perPlayerBetFee;
|
||||
const txRows: Array<{ from: number; to: number; amount: number; remarks: string; match_id: number }> = [];
|
||||
for (const uid of ids) {
|
||||
txRows.push({
|
||||
from: uid,
|
||||
to: SYSTEM_ACCOUNT_ID,
|
||||
amount: perPlayerHold,
|
||||
remarks: 'entry_hold',
|
||||
match_id: matchId,
|
||||
});
|
||||
txRows.push({
|
||||
from: uid,
|
||||
to: SYSTEM_ACCOUNT_ID,
|
||||
amount: perPlayerBetFee,
|
||||
remarks: 'entry_fee',
|
||||
match_id: matchId,
|
||||
});
|
||||
}
|
||||
txRows.push({
|
||||
from: SYSTEM_ACCOUNT_ID,
|
||||
to: winnerId,
|
||||
amount: rcPrize,
|
||||
remarks: 'reward',
|
||||
match_id: matchId,
|
||||
});
|
||||
const { error: txErr } = await supabase.from('transactions').insert(txRows);
|
||||
if (txErr) return txErr.message;
|
||||
return null;
|
||||
const payload = (data && typeof data === 'object' ? data : {}) as Record<string, unknown>;
|
||||
return { ok: true, data: payload };
|
||||
}
|
||||
|
||||
/** Second player joining the room = blue. */
|
||||
/**
|
||||
* Pay prize + house fee only from match escrow; set winner_id + settled_at atomically.
|
||||
* Idempotent if already settled for the same winner.
|
||||
*/
|
||||
async function callSettleMatchReward(
|
||||
supabase: SupabaseClient,
|
||||
matchId: number,
|
||||
winnerId: number
|
||||
): Promise<{ ok: true; data: Record<string, unknown> } | { ok: false; status: number; error: string }> {
|
||||
const { data, error } = await supabase.rpc('settle_match_reward', {
|
||||
p_match_id: matchId,
|
||||
p_winner_id: winnerId,
|
||||
});
|
||||
if (error) {
|
||||
return { ok: false, ...mapEconomyRpcError(rpcErrorMessage(error)) };
|
||||
}
|
||||
const payload = (data && typeof data === 'object' ? data : {}) as Record<string, unknown>;
|
||||
return { ok: true, data: payload };
|
||||
}
|
||||
|
||||
/** Second player joining the room = blue. No-op if blue is already set. */
|
||||
export async function updateMatchUserBlue(settings: Settings, matchId: number, userBlueId: number): Promise<void> {
|
||||
if (!supabaseConfigured(settings)) return;
|
||||
const supabase = createSupabase(settings);
|
||||
const { error } = await supabase.from('matches').update({ user_blue: userBlueId }).eq('id', matchId);
|
||||
const { data: row, error: fetchErr } = await supabase
|
||||
.from('matches')
|
||||
.select('user_blue')
|
||||
.eq('id', matchId)
|
||||
.maybeSingle();
|
||||
if (fetchErr) {
|
||||
console.error('matches: update user_blue fetch failed', fetchErr.message);
|
||||
return;
|
||||
}
|
||||
if (!row) {
|
||||
console.error('matches: update user_blue match not found', matchId);
|
||||
return;
|
||||
}
|
||||
const existingBlue = coerceId((row as { user_blue: unknown }).user_blue);
|
||||
if (existingBlue != null) {
|
||||
if (existingBlue !== userBlueId) {
|
||||
logInfo('[update-user-blue] skip overwrite', {
|
||||
match_id: matchId,
|
||||
existing_user_blue: existingBlue,
|
||||
attempted_user_blue: userBlueId,
|
||||
});
|
||||
recordHistory(
|
||||
`Skip user_blue overwrite for match ${matchId}: already ${existingBlue}, not ${userBlueId}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const { error } = await supabase
|
||||
.from('matches')
|
||||
.update({ user_blue: userBlueId })
|
||||
.eq('id', matchId)
|
||||
.is('user_blue', null);
|
||||
if (error) {
|
||||
console.error('matches: update user_blue failed', error.message);
|
||||
}
|
||||
@@ -414,6 +474,47 @@ export function registerMatchRoutes(
|
||||
res.json({ ok: true, player_id: playerId, rc });
|
||||
});
|
||||
|
||||
/** Public: profile info for a player (`users.id` / matchmaking UserId). */
|
||||
app.get('/players/:playerId/profile', async (req: Request, res: Response) => {
|
||||
if (!supabaseConfigured(settings)) {
|
||||
res.status(503).json({ ok: false, error: 'Database not configured' });
|
||||
return;
|
||||
}
|
||||
const playerId = coerceId(req.params.playerId);
|
||||
if (playerId == null || playerId < 1) {
|
||||
res.status(400).json({ ok: false, error: 'Invalid player id' });
|
||||
return;
|
||||
}
|
||||
|
||||
const supabase = createSupabase(settings);
|
||||
const [userResult, stats] = await Promise.all([
|
||||
supabase.from('users').select('id, username, email, created_at').eq('id', playerId).maybeSingle(),
|
||||
getPlayerMatchStats(settings, playerId),
|
||||
]);
|
||||
|
||||
if (userResult.error) {
|
||||
res.status(500).json({ ok: false, error: userResult.error.message });
|
||||
return;
|
||||
}
|
||||
if (!userResult.data) {
|
||||
res.status(404).json({ ok: false, error: 'Player not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = userResult.data as { username: unknown; email: unknown; created_at: unknown };
|
||||
const createdAt = typeof user.created_at === 'string' ? user.created_at : String(user.created_at ?? '');
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
player_id: playerId,
|
||||
username: typeof user.username === 'string' ? user.username : '',
|
||||
email: typeof user.email === 'string' ? user.email : null,
|
||||
days_since_registration: daysSinceRegistration(createdAt),
|
||||
matches_played: stats.matches_played,
|
||||
win_ratio: stats.win_ratio,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Dedicated server only: open a room with two players and a custom RC entry fee.
|
||||
* Response body matches GET / on room success: `{ ok, entry_fee, for_red, for_blue }`.
|
||||
@@ -506,7 +607,12 @@ export function registerMatchRoutes(
|
||||
}
|
||||
|
||||
const supabase = createSupabase(settings);
|
||||
const { data, error } = await supabase.from('matches').update(patch).eq('id', matchId).select('id').maybeSingle();
|
||||
const { data, error } = await supabase
|
||||
.from('matches')
|
||||
.update(patch)
|
||||
.eq('id', matchId)
|
||||
.select('id, red_joined_at, blue_joined_at, entries_collected_at')
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
res.status(500).json({ ok: false, error: error.message });
|
||||
@@ -516,10 +622,67 @@ export function registerMatchRoutes(
|
||||
res.status(404).json({ ok: false, error: 'Match not found' });
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true, id: matchId });
|
||||
|
||||
const row = data as {
|
||||
red_joined_at: unknown;
|
||||
blue_joined_at: unknown;
|
||||
entries_collected_at: unknown;
|
||||
};
|
||||
const bothJoined = row.red_joined_at != null && row.blue_joined_at != null;
|
||||
let entriesCollected = row.entries_collected_at != null;
|
||||
|
||||
if (bothJoined && !entriesCollected) {
|
||||
logInfo('[collect-entries] start', { match_id: matchId });
|
||||
const collect = await callCollectMatchEntries(supabase, matchId);
|
||||
if (!collect.ok) {
|
||||
logInfo('[collect-entries] failed', {
|
||||
match_id: matchId,
|
||||
status: collect.status,
|
||||
error: collect.error,
|
||||
});
|
||||
recordHistory(`Fee collect failed for match ${matchId}: ${collect.error}`);
|
||||
res.status(collect.status).json({
|
||||
ok: false,
|
||||
error: collect.error,
|
||||
id: matchId,
|
||||
entries_collected: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const alreadyCollected = collect.data.already_collected === true;
|
||||
const escrowUserId = coerceId(collect.data.escrow_user_id);
|
||||
logInfo('[collect-entries] success', {
|
||||
match_id: matchId,
|
||||
already_collected: alreadyCollected,
|
||||
escrow_user_id: escrowUserId,
|
||||
entry_fee: collect.data.entry_fee,
|
||||
pot_rc: collect.data.pot_rc,
|
||||
fee_each: collect.data.fee_each,
|
||||
});
|
||||
recordHistory(
|
||||
alreadyCollected
|
||||
? `Fee collect already done for match ${matchId} (escrow ${escrowUserId ?? '—'})`
|
||||
: `Fee collect for match ${matchId}: entry_fee ${String(collect.data.entry_fee ?? '—')} pot ${String(collect.data.pot_rc ?? '—')} escrow ${escrowUserId ?? '—'}`
|
||||
);
|
||||
entriesCollected = true;
|
||||
res.json({
|
||||
ok: true,
|
||||
id: matchId,
|
||||
entries_collected: true,
|
||||
already_collected: alreadyCollected,
|
||||
escrow_user_id: escrowUserId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
id: matchId,
|
||||
entries_collected: entriesCollected,
|
||||
});
|
||||
});
|
||||
|
||||
/** Unity dedicated server: set `winner_id` from match sides (`user_red` / `user_blue`). */
|
||||
/** Unity dedicated server: set winner and settle prize from match escrow only. */
|
||||
app.patch('/internal/match/:matchId/winner', jsonParser, async (req: Request, res: Response) => {
|
||||
if (rejectUnlessDedicatedServer(req, res, settings)) return;
|
||||
|
||||
@@ -539,7 +702,7 @@ export function registerMatchRoutes(
|
||||
const supabase = createSupabase(settings);
|
||||
const { data: row, error: fetchErr } = await supabase
|
||||
.from('matches')
|
||||
.select('user_red, user_blue, entry_fee, prize_cc')
|
||||
.select('user_red, user_blue, entry_fee, prize_cc, entries_collected_at, settled_at, winner_id')
|
||||
.eq('id', matchId)
|
||||
.maybeSingle();
|
||||
|
||||
@@ -564,56 +727,114 @@ export function registerMatchRoutes(
|
||||
return;
|
||||
}
|
||||
|
||||
const betFeePercent = await getBetFeePercentFromSettings(settings);
|
||||
|
||||
const { data: updated, error: updErr } = await supabase
|
||||
.from('matches')
|
||||
.update({ winner_id: winnerId })
|
||||
.eq('id', matchId)
|
||||
.is('winner_id', null)
|
||||
.select('id')
|
||||
.maybeSingle();
|
||||
|
||||
if (updErr) {
|
||||
res.status(500).json({ ok: false, error: updErr.message });
|
||||
return;
|
||||
}
|
||||
if (!updated) {
|
||||
const { data: existing } = await supabase.from('matches').select('id').eq('id', matchId).maybeSingle();
|
||||
if (!existing) {
|
||||
res.status(404).json({ ok: false, error: 'Match not found' });
|
||||
const existingWinner = coerceId((row as { winner_id: unknown }).winner_id);
|
||||
if (existingWinner != null) {
|
||||
if (existingWinner === winnerId && (row as { settled_at: unknown }).settled_at != null) {
|
||||
const betFeePercent = await getBetFeePercentFromSettings(settings);
|
||||
const rcPrize = computeRcPrizeFromEntryFee(entryFee, betFeePercent);
|
||||
logInfo('[settle-reward] already settled', {
|
||||
match_id: matchId,
|
||||
winner: side,
|
||||
winner_id: winnerId,
|
||||
entry_fee_rc: entryFee,
|
||||
rc_prize: rcPrize,
|
||||
participant_cc: prizeCc,
|
||||
});
|
||||
recordHistory(`Reward already settled for match ${matchId} winner ${winnerId} (${side})`);
|
||||
res.json({
|
||||
ok: true,
|
||||
id: matchId,
|
||||
winner_id: winnerId,
|
||||
already_settled: true,
|
||||
economy: {
|
||||
entry_fee_rc: entryFee,
|
||||
rc_prize: rcPrize,
|
||||
participant_cc: prizeCc,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
logInfo('[settle-reward] winner already set', {
|
||||
match_id: matchId,
|
||||
requested_winner: side,
|
||||
requested_winner_id: winnerId,
|
||||
existing_winner_id: existingWinner,
|
||||
});
|
||||
recordHistory(
|
||||
`Reward settle rejected for match ${matchId}: winner already set (${existingWinner})`
|
||||
);
|
||||
res.status(409).json({ ok: false, error: 'Winner already set for this match' });
|
||||
return;
|
||||
}
|
||||
|
||||
const payErr = await applyMatchWinnerEconomy(
|
||||
supabase,
|
||||
matchId,
|
||||
userRed,
|
||||
userBlue,
|
||||
winnerId,
|
||||
entryFee,
|
||||
prizeCc,
|
||||
betFeePercent
|
||||
);
|
||||
if (payErr != null) {
|
||||
console.error('matches: economy after winner failed', matchId, payErr);
|
||||
await supabase.from('matches').update({ winner_id: null }).eq('id', matchId).eq('winner_id', winnerId);
|
||||
res.status(500).json({ ok: false, error: `Economy update failed: ${payErr}` });
|
||||
if ((row as { entries_collected_at: unknown }).entries_collected_at == null) {
|
||||
logInfo('[settle-reward] entries not collected', {
|
||||
match_id: matchId,
|
||||
winner: side,
|
||||
winner_id: winnerId,
|
||||
});
|
||||
recordHistory(`Reward settle rejected for match ${matchId}: entry fees not collected`);
|
||||
res.status(409).json({
|
||||
ok: false,
|
||||
error: 'Entry fees not collected yet; both players must join first',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const rcPrize = computeRcPrizeFromEntryFee(entryFee, betFeePercent);
|
||||
logInfo('[settle-reward] start', {
|
||||
match_id: matchId,
|
||||
winner: side,
|
||||
winner_id: winnerId,
|
||||
user_red: userRed,
|
||||
user_blue: userBlue,
|
||||
entry_fee_rc: entryFee,
|
||||
});
|
||||
const settle = await callSettleMatchReward(supabase, matchId, winnerId);
|
||||
if (!settle.ok) {
|
||||
logInfo('[settle-reward] failed', {
|
||||
match_id: matchId,
|
||||
winner: side,
|
||||
winner_id: winnerId,
|
||||
status: settle.status,
|
||||
error: settle.error,
|
||||
});
|
||||
recordHistory(`Reward settle failed for match ${matchId} winner ${winnerId}: ${settle.error}`);
|
||||
res.status(settle.status).json({ ok: false, error: settle.error });
|
||||
return;
|
||||
}
|
||||
|
||||
const betFeePercent = await getBetFeePercentFromSettings(settings);
|
||||
const rcPrizeFromRpc = coerceNonNegativeInt(settle.data.rc_prize);
|
||||
const participantCcFromRpc = coerceNonNegativeInt(settle.data.participant_cc);
|
||||
const rcPrize = rcPrizeFromRpc ?? computeRcPrizeFromEntryFee(entryFee, betFeePercent);
|
||||
const participantCc = participantCcFromRpc ?? prizeCc;
|
||||
const alreadySettled = settle.data.already_settled === true;
|
||||
|
||||
logInfo('[settle-reward] success', {
|
||||
match_id: matchId,
|
||||
winner: side,
|
||||
winner_id: winnerId,
|
||||
already_settled: alreadySettled,
|
||||
entry_fee_rc: entryFee,
|
||||
rc_prize: rcPrize,
|
||||
house_fee: settle.data.house_fee,
|
||||
participant_cc: participantCc,
|
||||
});
|
||||
recordHistory(
|
||||
alreadySettled
|
||||
? `Reward already settled for match ${matchId} winner ${winnerId} (${side})`
|
||||
: `Reward settle for match ${matchId}: winner ${winnerId} (${side}) prize ${rcPrize} house ${String(settle.data.house_fee ?? '—')} cc ${participantCc}`
|
||||
);
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
id: matchId,
|
||||
winner_id: winnerId,
|
||||
already_settled: alreadySettled,
|
||||
economy: {
|
||||
entry_fee_rc: entryFee,
|
||||
rc_prize: rcPrize,
|
||||
participant_cc: prizeCc,
|
||||
participant_cc: participantCc,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user