Some checks failed
Deploy to Cloudflare Workers / deploy (push) Has been cancelled
303 lines
9.1 KiB
PL/PgSQL
303 lines
9.1 KiB
PL/PgSQL
-- Core business logic functions
|
|
|
|
CREATE OR REPLACE FUNCTION get_default_rules()
|
|
RETURNS JSONB AS $$
|
|
SELECT jsonb_build_object(
|
|
'points_win', 3,
|
|
'points_draw', 1,
|
|
'round_robin_format', 'double',
|
|
'transfer_policy', 'anytime',
|
|
'auto_qualify_count', 1,
|
|
'strict_availability', false,
|
|
'result_approval_required', true,
|
|
'cup_format', jsonb_build_object(
|
|
'legs', 'single',
|
|
'third_place_match', false,
|
|
'seeding', 'random'
|
|
)
|
|
);
|
|
$$ LANGUAGE sql IMMUTABLE;
|
|
|
|
-- Snapshot rules when activating competition
|
|
CREATE OR REPLACE FUNCTION activate_competition(p_competition_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_league_id UUID;
|
|
v_latest_rules JSONB;
|
|
v_version INT;
|
|
BEGIN
|
|
SELECT league_id INTO v_league_id FROM competitions WHERE id = p_competition_id;
|
|
|
|
SELECT rules, version INTO v_latest_rules, v_version
|
|
FROM league_rules
|
|
WHERE league_id = v_league_id
|
|
ORDER BY version DESC
|
|
LIMIT 1;
|
|
|
|
IF v_latest_rules IS NULL THEN
|
|
v_latest_rules := get_default_rules();
|
|
v_version := 1;
|
|
END IF;
|
|
|
|
UPDATE competitions
|
|
SET status = 'active',
|
|
rules_snapshot = v_latest_rules,
|
|
rule_set_version = v_version
|
|
WHERE id = p_competition_id AND status = 'draft';
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Generate double round-robin fixtures
|
|
CREATE OR REPLACE FUNCTION generate_league_fixtures(p_competition_id UUID)
|
|
RETURNS INT AS $$
|
|
DECLARE
|
|
team_ids UUID[];
|
|
n INT;
|
|
rounds INT;
|
|
r INT;
|
|
i INT;
|
|
j INT;
|
|
fixed UUID;
|
|
rotating UUID[];
|
|
match_count INT := 0;
|
|
leg INT;
|
|
BEGIN
|
|
SELECT ARRAY_AGG(id ORDER BY name) INTO team_ids
|
|
FROM teams WHERE competition_id = p_competition_id;
|
|
|
|
n := COALESCE(array_length(team_ids, 1), 0);
|
|
IF n < 2 THEN
|
|
RAISE EXCEPTION 'Need at least 2 teams';
|
|
END IF;
|
|
|
|
DELETE FROM matches WHERE competition_id = p_competition_id AND status = 'scheduled';
|
|
|
|
IF n % 2 = 1 THEN
|
|
team_ids := team_ids || NULL::UUID;
|
|
n := n + 1;
|
|
END IF;
|
|
|
|
rounds := n - 1;
|
|
rotating := team_ids[2:n];
|
|
|
|
FOR leg IN 1..2 LOOP
|
|
FOR r IN 1..rounds LOOP
|
|
IF team_ids[1] IS NOT NULL AND rotating[1] IS NOT NULL THEN
|
|
IF leg = 1 THEN
|
|
INSERT INTO matches (competition_id, home_team_id, away_team_id, round, matchday, leg, status)
|
|
VALUES (p_competition_id, team_ids[1], rotating[1], r, r + (leg-1)*rounds, leg, 'scheduled');
|
|
ELSE
|
|
INSERT INTO matches (competition_id, home_team_id, away_team_id, round, matchday, leg, status)
|
|
VALUES (p_competition_id, rotating[1], team_ids[1], r, r + (leg-1)*rounds, leg, 'scheduled');
|
|
END IF;
|
|
match_count := match_count + 1;
|
|
END IF;
|
|
|
|
FOR i IN 1..(array_length(rotating, 1) / 2) LOOP
|
|
j := array_length(rotating, 1) - i + 1;
|
|
IF rotating[i] IS NOT NULL AND rotating[j] IS NOT NULL AND i <> j THEN
|
|
IF leg = 1 THEN
|
|
INSERT INTO matches (competition_id, home_team_id, away_team_id, round, matchday, leg, status)
|
|
VALUES (p_competition_id, rotating[i], rotating[j], r, r + (leg-1)*rounds, leg, 'scheduled');
|
|
ELSE
|
|
INSERT INTO matches (competition_id, home_team_id, away_team_id, round, matchday, leg, status)
|
|
VALUES (p_competition_id, rotating[j], rotating[i], r, r + (leg-1)*rounds, leg, 'scheduled');
|
|
END IF;
|
|
match_count := match_count + 1;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
fixed := rotating[array_length(rotating, 1)];
|
|
rotating := ARRAY[fixed] || rotating[1:array_length(rotating,1)-1];
|
|
END LOOP;
|
|
END LOOP;
|
|
|
|
RETURN match_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Generate cup bracket (single elimination, first round)
|
|
CREATE OR REPLACE FUNCTION generate_cup_bracket(p_competition_id UUID)
|
|
RETURNS INT AS $$
|
|
DECLARE
|
|
team_ids UUID[];
|
|
n INT;
|
|
i INT;
|
|
match_count INT := 0;
|
|
BEGIN
|
|
SELECT ARRAY_AGG(id ORDER BY COALESCE(cup_seed, 999), name) INTO team_ids
|
|
FROM teams WHERE competition_id = p_competition_id;
|
|
|
|
n := COALESCE(array_length(team_ids, 1), 0);
|
|
IF n < 2 THEN RAISE EXCEPTION 'Need at least 2 teams'; END IF;
|
|
|
|
DELETE FROM matches WHERE competition_id = p_competition_id AND status = 'scheduled';
|
|
|
|
i := 1;
|
|
WHILE i < n LOOP
|
|
IF team_ids[i] IS NOT NULL AND team_ids[i+1] IS NOT NULL THEN
|
|
INSERT INTO matches (competition_id, home_team_id, away_team_id, bracket_round, bracket_slot, status)
|
|
VALUES (p_competition_id, team_ids[i], team_ids[i+1], 1, (i+1)/2, 'scheduled');
|
|
match_count := match_count + 1;
|
|
END IF;
|
|
i := i + 2;
|
|
END LOOP;
|
|
|
|
RETURN match_count;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Submit match result from team
|
|
CREATE OR REPLACE FUNCTION submit_match_result(
|
|
p_match_id UUID,
|
|
p_team_id UUID,
|
|
p_home_score INT,
|
|
p_away_score INT
|
|
)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_competition_id UUID;
|
|
v_sub_count INT;
|
|
v_home_sub INT;
|
|
v_away_sub INT;
|
|
v_sub1_home INT;
|
|
v_sub1_away INT;
|
|
v_sub2_home INT;
|
|
v_sub2_away INT;
|
|
BEGIN
|
|
SELECT competition_id INTO v_competition_id FROM matches WHERE id = p_match_id;
|
|
|
|
INSERT INTO match_result_submissions (match_id, team_id, home_score, away_score, submitted_by)
|
|
VALUES (p_match_id, p_team_id, p_home_score, p_away_score, auth.uid())
|
|
ON CONFLICT (match_id, team_id) DO UPDATE
|
|
SET home_score = EXCLUDED.home_score, away_score = EXCLUDED.away_score,
|
|
submitted_at = now(), submitted_by = auth.uid();
|
|
|
|
SELECT COUNT(*) INTO v_sub_count FROM match_result_submissions WHERE match_id = p_match_id;
|
|
|
|
IF v_sub_count < 2 THEN
|
|
UPDATE matches SET result_status = 'result_submitted', status = 'awaiting_results'
|
|
WHERE id = p_match_id;
|
|
RETURN;
|
|
END IF;
|
|
|
|
SELECT home_score, away_score INTO v_sub1_home, v_sub1_away
|
|
FROM match_result_submissions WHERE match_id = p_match_id
|
|
ORDER BY submitted_at LIMIT 1;
|
|
|
|
SELECT home_score, away_score INTO v_sub2_home, v_sub2_away
|
|
FROM match_result_submissions WHERE match_id = p_match_id
|
|
ORDER BY submitted_at DESC LIMIT 1;
|
|
|
|
IF v_sub1_home = v_sub2_home AND v_sub1_away = v_sub2_away THEN
|
|
UPDATE matches SET result_status = 'pending_approval', status = 'pending_approval'
|
|
WHERE id = p_match_id;
|
|
ELSE
|
|
UPDATE matches SET result_status = 'disputed', status = 'disputed'
|
|
WHERE id = p_match_id;
|
|
END IF;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- League manager approves result
|
|
CREATE OR REPLACE FUNCTION approve_match_result(p_match_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_sub RECORD;
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM matches m
|
|
WHERE m.id = p_match_id
|
|
AND is_competition_league_manager(m.competition_id)
|
|
) AND NOT EXISTS (
|
|
SELECT 1 FROM matches m JOIN competitions c ON c.id = m.competition_id
|
|
JOIN leagues l ON l.id = c.league_id
|
|
WHERE m.id = p_match_id AND l.created_by = auth.uid()
|
|
) THEN
|
|
RAISE EXCEPTION 'Not authorized';
|
|
END IF;
|
|
|
|
SELECT home_score, away_score INTO v_sub
|
|
FROM match_result_submissions WHERE match_id = p_match_id LIMIT 1;
|
|
|
|
UPDATE matches SET
|
|
home_score = v_sub.home_score,
|
|
away_score = v_sub.away_score,
|
|
status = 'completed',
|
|
result_status = 'completed',
|
|
approved_by = auth.uid(),
|
|
approved_at = now()
|
|
WHERE id = p_match_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- League manager sets result directly
|
|
CREATE OR REPLACE FUNCTION set_match_result_by_manager(
|
|
p_match_id UUID,
|
|
p_home_score INT,
|
|
p_away_score INT,
|
|
p_note TEXT DEFAULT NULL
|
|
)
|
|
RETURNS VOID AS $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM matches m WHERE m.id = p_match_id
|
|
AND is_competition_league_manager(m.competition_id)
|
|
) AND NOT EXISTS (
|
|
SELECT 1 FROM matches m JOIN competitions c ON c.id = m.competition_id
|
|
JOIN leagues l ON l.id = c.league_id
|
|
WHERE m.id = p_match_id AND l.created_by = auth.uid()
|
|
) THEN
|
|
RAISE EXCEPTION 'Not authorized';
|
|
END IF;
|
|
|
|
UPDATE matches SET
|
|
home_score = p_home_score,
|
|
away_score = p_away_score,
|
|
status = 'completed',
|
|
result_status = 'completed',
|
|
approved_by = auth.uid(),
|
|
approved_at = now(),
|
|
resolution_note = p_note
|
|
WHERE id = p_match_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Sign schedule
|
|
CREATE OR REPLACE FUNCTION sign_match_schedule(p_match_id UUID, p_team_id UUID)
|
|
RETURNS VOID AS $$
|
|
DECLARE
|
|
v_sig_count INT;
|
|
v_proposed TIMESTAMPTZ;
|
|
BEGIN
|
|
SELECT proposed_scheduled_at INTO v_proposed FROM matches WHERE id = p_match_id;
|
|
|
|
INSERT INTO match_signatures (match_id, team_id, user_id, signature_type)
|
|
VALUES (p_match_id, p_team_id, auth.uid(), 'schedule')
|
|
ON CONFLICT (match_id, team_id, signature_type) DO NOTHING;
|
|
|
|
SELECT COUNT(*) INTO v_sig_count FROM match_signatures
|
|
WHERE match_id = p_match_id AND signature_type = 'schedule';
|
|
|
|
IF v_sig_count >= 2 AND v_proposed IS NOT NULL THEN
|
|
UPDATE matches SET
|
|
status = 'schedule_confirmed',
|
|
scheduled_at = v_proposed,
|
|
venue = (SELECT home_stadium_name FROM teams t
|
|
JOIN matches m ON m.home_team_id = t.id WHERE m.id = p_match_id)
|
|
WHERE id = p_match_id;
|
|
END IF;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Propose schedule time
|
|
CREATE OR REPLACE FUNCTION propose_match_schedule(p_match_id UUID, p_scheduled_at TIMESTAMPTZ)
|
|
RETURNS VOID AS $$
|
|
BEGIN
|
|
UPDATE matches SET
|
|
proposed_scheduled_at = p_scheduled_at,
|
|
status = 'schedule_pending'
|
|
WHERE id = p_match_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|