Changes between Initial Version and Version 1 of DatabaseProgramming


Ignore:
Timestamp:
06/03/26 02:42:41 (3 weeks ago)
Author:
231071
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • DatabaseProgramming

    v1 v1  
     1= Фаза 4 — Функции, Процедури, Тригери
     2
     3== Тригери
     4
     5=== trg_validate_ballot
     6
     7**Тип:** `BEFORE INSERT` на табела `ballot`
     8
     9Се активира пред секое внесување на гласачко ливче и ги проверува три услови: дали изборот е активен (`status=1`), дали гласачкото место е назначено на тој избор, и дали `entity_id` учествувал во изборот. Ја имплементира бизнис логиката за интегритет на гласање — спречува невалидни или фалсификувани ливчиња да влезат во системот.
     10
     11{{{
     12CREATE OR REPLACE FUNCTION public.fn_validate_ballot()
     13 RETURNS trigger
     14 LANGUAGE plpgsql
     15AS $function$
     16DECLARE
     17    v_status        SMALLINT;
     18    v_station_ok    BOOLEAN;
     19    v_entity_ok     BOOLEAN;
     20BEGIN
     21
     22    -- Check (a): election must be active
     23    SELECT status
     24    INTO   v_status
     25    FROM   election
     26    WHERE  election_id = NEW.election_id;
     27
     28    IF NOT FOUND THEN
     29        RAISE EXCEPTION
     30            'Election % does not exist', NEW.election_id;
     31    END IF;
     32
     33    IF v_status <> 1 THEN
     34        RAISE EXCEPTION
     35            'Election % is not active (status=%). Ballots cannot be cast.',
     36            NEW.election_id, v_status;
     37    END IF;
     38
     39    -- Check (b): station must be in this election
     40    SELECT EXISTS (
     41        SELECT 1
     42        FROM   station_election
     43        WHERE  station_id  = NEW.station_id
     44        AND    election_id = NEW.election_id
     45    ) INTO v_station_ok;
     46
     47    IF NOT v_station_ok THEN
     48        RAISE EXCEPTION
     49            'Polling station % is not assigned to election %.',
     50            NEW.station_id, NEW.election_id;
     51    END IF;
     52
     53    -- Check (c/d): entity validation for valid ballots
     54    IF NEW.is_valid = TRUE THEN
     55
     56        IF NEW.entity_id IS NULL AND NEW.candidate_id IS NULL THEN
     57            RAISE EXCEPTION
     58                'A valid ballot must have either an entity_id or candidate_id.';
     59        END IF;
     60
     61        IF NEW.entity_id IS NOT NULL THEN
     62            SELECT EXISTS (
     63                SELECT 1
     64                FROM   election_participant ep
     65                JOIN   electoral_district   ed
     66                       ON ed.district_id  = ep.district_id
     67                WHERE  ep.entity_id   = NEW.entity_id
     68                AND    ed.election_id  = NEW.election_id
     69            ) INTO v_entity_ok;
     70
     71            IF NOT v_entity_ok THEN
     72                RAISE EXCEPTION
     73                    'Entity % did not participate in election %.',
     74                    NEW.entity_id, NEW.election_id;
     75            END IF;
     76        END IF;
     77
     78    END IF;
     79
     80    RETURN NEW;
     81
     82END;
     83$function$;
     84
     85-- Trigger definition
     86CREATE TRIGGER trg_validate_ballot
     87BEFORE INSERT ON public.ballot
     88FOR EACH ROW EXECUTE FUNCTION fn_validate_ballot();
     89}}}
     90
     91----
     92
     93=== trg_after_insert_ballot_live_count
     94
     95**Тип:** `AFTER INSERT` на табела `ballot`
     96
     97Се активира по секое внесување на валидно ливче и автоматски го ажурира бројачот во `vote_result` преку UPSERT — ако записот постои го зголемува за 1, инаку создава нов. Ја имплементира логиката за live бројање на гласови во реално време, без потреба од рачна агрегација на резултати.
     98
     99{{{
     100CREATE OR REPLACE FUNCTION public.fn_trigger_live_vote_counter()
     101 RETURNS trigger
     102 LANGUAGE plpgsql
     103AS $function$
     104DECLARE
     105    v_result_id_exists BIGINT;
     106    v_max_id           BIGINT;
     107BEGIN
     108    IF NEW.is_valid = TRUE THEN
     109
     110        SELECT result_id INTO v_result_id_exists
     111        FROM public.vote_result
     112        WHERE election_id  = NEW.election_id
     113          AND station_id   = NEW.station_id
     114          AND entity_id    = NEW.entity_id
     115          AND (candidate_id = NEW.candidate_id
     116               OR (candidate_id IS NULL AND NEW.candidate_id IS NULL));
     117
     118        IF v_result_id_exists IS NOT NULL THEN
     119            UPDATE public.vote_result
     120            SET votes = votes + 1
     121            WHERE result_id = v_result_id_exists;
     122        ELSE
     123            SELECT COALESCE(MAX(result_id), 0) + 1
     124            INTO   v_max_id
     125            FROM   public.vote_result;
     126
     127            INSERT INTO public.vote_result
     128                (result_id, election_id, station_id, entity_id, candidate_id, votes)
     129            VALUES
     130                (v_max_id, NEW.election_id, NEW.station_id, NEW.entity_id, NEW.candidate_id, 1);
     131        END IF;
     132
     133    END IF;
     134
     135    RETURN NEW;
     136END;
     137$function$;
     138
     139-- Trigger definition
     140CREATE TRIGGER trg_after_insert_ballot_live_count
     141AFTER INSERT ON public.ballot
     142FOR EACH ROW EXECUTE FUNCTION fn_trigger_live_vote_counter();
     143}}}
     144
     145----
     146
     147=== trg_before_insert_voter_age
     148
     149**Тип:** `BEFORE INSERT` на табела `voter`
     150
     151Се активира пред регистрација на нов гласач и проверува дали лицето е полнолетно (≥18 години), фрлајќи детален исклучок со името на лицето ако не е. Директно ја имплементира законската забрана за гласање на малолетни лица на ниво на базата.
     152
     153{{{
     154CREATE OR REPLACE FUNCTION public.fn_check_voter_age_limit()
     155 RETURNS trigger
     156 LANGUAGE plpgsql
     157AS $function$
     158DECLARE
     159    v_birth_date DATE;
     160    v_full_name  TEXT;
     161BEGIN
     162    SELECT date_of_birth, (name || ' ' || surname)
     163    INTO   v_birth_date, v_full_name
     164    FROM   public.person
     165    WHERE  person_id = NEW.person_id;
     166
     167    IF NOT FOUND THEN
     168        RAISE EXCEPTION
     169            'Грешка: Лицето со ID % не постои во матичната евиденција (табела person).',
     170            NEW.person_id;
     171    END IF;
     172
     173    IF v_birth_date > CURRENT_DATE - INTERVAL '18 years' THEN
     174        RAISE EXCEPTION
     175            'Критична грешка: Лицето "%" (ID: %) има % години и е малолетно. '
     176            'Регистрацијата во базата на гласачи е ОДБИЕНА.',
     177            v_full_name,
     178            NEW.person_id,
     179            EXTRACT(YEAR FROM AGE(CURRENT_DATE, v_birth_date));
     180    END IF;
     181
     182    RETURN NEW;
     183END;
     184$function$;
     185
     186-- Trigger definition
     187CREATE TRIGGER trg_before_insert_voter_age
     188BEFORE INSERT ON public.voter
     189FOR EACH ROW EXECUTE FUNCTION fn_check_voter_age_limit();
     190}}}
     191
     192----
     193
     194=== trg_after_voter_change_sync_stats
     195
     196**Тип:** `AFTER INSERT OR DELETE` на табела `voter`
     197
     198Автоматски го ажурира `registered_voter` бројачот во `polling_station` при секоја промена на гласачкиот список — +1 при регистрација, -1 при бришење. Ја одржува конзистентноста на статистиките за регистрирани гласачи без рачно одржување.
     199
     200{{{
     201CREATE OR REPLACE FUNCTION public.fn_trigger_sync_registered_voters()
     202 RETURNS trigger
     203 LANGUAGE plpgsql
     204AS $function$
     205BEGIN
     206    IF TG_OP = 'INSERT' THEN
     207        UPDATE public.polling_station
     208        SET    registered_voter = COALESCE(registered_voter, 0) + 1
     209        WHERE  station_id = NEW.station_id;
     210
     211    ELSIF TG_OP = 'DELETE' THEN
     212        UPDATE public.polling_station
     213        SET    registered_voter = COALESCE(registered_voter, 0) - 1
     214        WHERE  station_id = OLD.station_id;
     215    END IF;
     216
     217    RETURN NULL;
     218END;
     219$function$;
     220
     221-- Trigger definition
     222CREATE TRIGGER trg_after_voter_change_sync_stats
     223AFTER INSERT OR DELETE ON public.voter
     224FOR EACH ROW EXECUTE FUNCTION fn_trigger_sync_registered_voters();
     225}}}
     226
     227----
     228
     229=== trg_prevent_duplicate_voter_checkin
     230
     231**Тип:** `BEFORE INSERT` на табела `voter_election`
     232
     233Проверува три услови: изборот е активен, гласачот не се пријавил веќе на тој избор, и check-in е на неговата матична станица. Ја имплементира основната изборна контрола против двојно гласање и гласање надвор од матичното место.
     234
     235{{{
     236CREATE OR REPLACE FUNCTION public.fn_prevent_duplicate_voter_checkin()
     237 RETURNS trigger
     238 LANGUAGE plpgsql
     239AS $function$
     240DECLARE
     241    v_registered_station BIGINT;
     242    v_election_status    SMALLINT;
     243    v_election_name      VARCHAR;
     244    v_voter_name         TEXT;
     245BEGIN
     246    -- (1) Изборот мора да биде активен
     247    SELECT e.status, e.name
     248    INTO   v_election_status, v_election_name
     249    FROM   election e
     250    WHERE  e.election_id = NEW.election_id;
     251
     252    IF NOT FOUND THEN
     253        RAISE EXCEPTION 'Election % does not exist.', NEW.election_id;
     254    END IF;
     255
     256    IF v_election_status <> 1 THEN
     257        RAISE EXCEPTION
     258            'Election "%" is not active. Voter check-in is not allowed.',
     259            v_election_name;
     260    END IF;
     261
     262    -- (2) Гласачот не смее двапати да се пријави
     263    IF EXISTS (
     264        SELECT 1
     265        FROM   voter_election ve
     266        WHERE  ve.voter_id    = NEW.voter_id
     267        AND    ve.election_id = NEW.election_id
     268    ) THEN
     269        SELECT (p.name || ' ' || p.surname)
     270        INTO   v_voter_name
     271        FROM   voter  v
     272        JOIN   person p ON p.person_id = v.person_id
     273        WHERE  v.voter_id = NEW.voter_id;
     274
     275        RAISE EXCEPTION
     276            'Voter "%" (id: %) has already checked in for election "%".',
     277            v_voter_name, NEW.voter_id, v_election_name;
     278    END IF;
     279
     280    -- (3) Check-in мора да биде на матичната станица
     281    SELECT v.station_id
     282    INTO   v_registered_station
     283    FROM   voter v
     284    WHERE  v.voter_id = NEW.voter_id;
     285
     286    IF NOT FOUND THEN
     287        RAISE EXCEPTION 'Voter % does not exist.', NEW.voter_id;
     288    END IF;
     289
     290    IF v_registered_station <> NEW.station_id THEN
     291        RAISE EXCEPTION
     292            'Voter % must check in at their registered station (%). '
     293            'Attempted check-in at station %.',
     294            NEW.voter_id, v_registered_station, NEW.station_id;
     295    END IF;
     296
     297    RETURN NEW;
     298END;
     299$function$;
     300
     301-- Trigger definition
     302CREATE TRIGGER trg_prevent_duplicate_voter_checkin
     303BEFORE INSERT ON public.voter_election
     304FOR EACH ROW EXECUTE FUNCTION fn_prevent_duplicate_voter_checkin();
     305}}}
     306
     307----
     308
     309== Функции
     310
     311=== fn_calculate_dhondt
     312
     313**Сигнатура:** `fn_calculate_dhondt(p_election_id bigint, p_district_id bigint)`
     314
     315**Враќа:** `TABLE(round, entity_id, entity_name, votes, divisor, quotient, seat_awarded)`
     316
     317Ја пресметува распределбата на мандати по D'Hondt метод за даден избор и изборна единица — генерира табела со сите квоциенти, дивизори, позиции и флаг дали е доделен мандат. Ја имплементира пропорционалната изборна математика која е законски задолжителна за парламентарни избори во Македонија и Србија, со вграден 5% праг на влез.
     318
     319{{{
     320CREATE OR REPLACE FUNCTION public.fn_calculate_dhondt(p_election_id bigint, p_district_id bigint)
     321 RETURNS TABLE(round bigint, entity_id bigint, entity_name character varying,
     322               votes bigint, divisor integer, quotient numeric, seat_awarded boolean)
     323 LANGUAGE plpgsql
     324AS $function$
     325DECLARE
     326    v_seats         INTEGER;
     327    v_method_id     BIGINT;
     328    v_election_name VARCHAR;
     329    v_region_name   VARCHAR;
     330BEGIN
     331
     332    SELECT e.winner_method_id, e.name
     333    INTO   v_method_id, v_election_name
     334    FROM   election e
     335    WHERE  e.election_id = p_election_id;
     336
     337    IF NOT FOUND THEN
     338        RAISE EXCEPTION 'Election % does not exist', p_election_id;
     339    END IF;
     340
     341    IF v_method_id <> 8 THEN
     342        RAISE EXCEPTION
     343            'Election "%" does not use D''Hondt method. '
     344            'This function only works for proportional elections.',
     345            v_election_name;
     346    END IF;
     347
     348    SELECT ed.seats_available, r.name
     349    INTO   v_seats, v_region_name
     350    FROM   electoral_district ed
     351    JOIN   region             r  ON r.region_id = ed.region_id
     352    WHERE  ed.district_id  = p_district_id
     353    AND    ed.election_id  = p_election_id;
     354
     355    IF NOT FOUND THEN
     356        RAISE EXCEPTION
     357            'District % does not exist for election %',
     358            p_district_id, p_election_id;
     359    END IF;
     360
     361    IF v_seats IS NULL OR v_seats = 0 THEN
     362        RAISE EXCEPTION 'District % has no seats defined', p_district_id;
     363    END IF;
     364
     365    RAISE NOTICE 'Calculating D''Hondt for: % — % (% seats)',
     366        v_election_name, v_region_name, v_seats;
     367
     368    RETURN QUERY
     369    WITH
     370    -- Чекор 1: гласови по ентитет во дистриктот
     371    raw_votes AS (
     372        SELECT
     373            COALESCE(pe2.entity_id, vr.entity_id) AS entity_id,
     374            SUM(vr.votes)                          AS district_votes
     375        FROM        vote_result      vr
     376        JOIN        polling_station  ps  ON ps.station_id   = vr.station_id
     377        JOIN        region           r2  ON r2.region_id    = ps.municipality_id
     378        JOIN        political_entity pe  ON pe.entity_id    = vr.entity_id
     379        LEFT JOIN   coalition_member cm  ON cm.party_id     = pe.party_id
     380        LEFT JOIN   party_coalition  pc  ON pc.coalition_id = cm.coalition_id
     381                                        AND pc.election_id  = vr.election_id
     382        LEFT JOIN   political_entity pe2 ON pe2.coalition_id = pc.coalition_id
     383        WHERE       vr.election_id  = p_election_id
     384        AND         vr.candidate_id IS NULL
     385        AND (
     386            r2.region_id        = (SELECT region_id FROM electoral_district
     387                                   WHERE district_id = p_district_id)
     388            OR
     389            r2.parent_region_id = (SELECT region_id FROM electoral_district
     390                                   WHERE district_id = p_district_id)
     391        )
     392        GROUP BY COALESCE(pe2.entity_id, vr.entity_id)
     393    ),
     394    -- Чекор 2: филтрирање на ентитети над 5% праг
     395    district_total AS (
     396        SELECT SUM(district_votes) AS total FROM raw_votes
     397    ),
     398    eligible AS (
     399        SELECT rv.entity_id, rv.district_votes
     400        FROM   raw_votes rv
     401        CROSS JOIN district_total dt
     402        WHERE  rv.district_votes * 100.0 / NULLIF(dt.total, 0) >= 5.0
     403    ),
     404    -- Чекор 3: генерирање на сите квоциенти
     405    quotients AS (
     406        SELECT
     407            e.entity_id,
     408            e.district_votes,
     409            d.divisor,
     410            ROUND(e.district_votes::NUMERIC / d.divisor, 4) AS quotient
     411        FROM       eligible e
     412        CROSS JOIN generate_series(1, v_seats) AS d(divisor)
     413    ),
     414    -- Чекор 4: рангирање на сите квоциенти
     415    ranked AS (
     416        SELECT
     417            q.*,
     418            RANK() OVER (ORDER BY q.quotient DESC) AS global_rank
     419        FROM quotients q
     420    )
     421    -- Чекор 5: враќање со флаг за доделен мандат
     422    SELECT
     423        r.global_rank                  AS round,
     424        r.entity_id,
     425        pe.name                        AS entity_name,
     426        r.district_votes               AS votes,
     427        r.divisor,
     428        r.quotient,
     429        (r.global_rank <= v_seats)     AS seat_awarded
     430    FROM   ranked           r
     431    JOIN   political_entity pe ON pe.entity_id = r.entity_id
     432    ORDER BY r.global_rank, r.entity_id;
     433
     434END;
     435$function$;
     436}}}
     437
     438----
     439
     440=== fn_get_candidate_list_for_entity
     441
     442**Сигнатура:** `fn_get_candidate_list_for_entity(p_entity_id bigint, p_election_id bigint)`
     443
     444**Враќа:** `TABLE(list_position, candidate_id, full_name, date_of_birth, gender, municipality, party_name, entity_name, election_name)`
     445
     446Ја враќа целосната листа на кандидати за даден политички ентитет на даден избор, подредена по позиција, со лични податоци, партиска припадност и општина. Ја имплементира логиката за јавна транспарентност — граѓаните и медиумите можат да видат кој е на каква позиција на изборната листа.
     447
     448{{{
     449CREATE OR REPLACE FUNCTION public.fn_get_candidate_list_for_entity(p_entity_id bigint, p_election_id bigint)
     450 RETURNS TABLE(list_position integer, candidate_id bigint, full_name text,
     451               date_of_birth date, gender character, municipality character varying,
     452               party_name character varying, entity_name character varying,
     453               election_name character varying)
     454 LANGUAGE plpgsql
     455AS $function$
     456DECLARE
     457    v_entity_name   VARCHAR;
     458    v_election_name VARCHAR;
     459    v_list_id       BIGINT;
     460BEGIN
     461    SELECT pe.name INTO v_entity_name
     462    FROM   political_entity pe
     463    WHERE  pe.entity_id = p_entity_id;
     464
     465    IF NOT FOUND THEN
     466        RAISE EXCEPTION 'Political entity % does not exist.', p_entity_id;
     467    END IF;
     468
     469    SELECT e.name INTO v_election_name
     470    FROM   election e
     471    WHERE  e.election_id = p_election_id;
     472
     473    IF NOT FOUND THEN
     474        RAISE EXCEPTION 'Election % does not exist.', p_election_id;
     475    END IF;
     476
     477    SELECT cl.list_id INTO v_list_id
     478    FROM   candidate_list cl
     479    WHERE  cl.entity_id   = p_entity_id
     480    AND    cl.election_id = p_election_id;
     481
     482    IF NOT FOUND THEN
     483        RAISE EXCEPTION
     484            'No candidate list found for entity "%" on election "%".',
     485            v_entity_name, v_election_name;
     486    END IF;
     487
     488    RAISE NOTICE 'Candidate list for "%" — Election: "%" (list_id: %)',
     489        v_entity_name, v_election_name, v_list_id;
     490
     491    RETURN QUERY
     492    SELECT
     493        cli.position                                AS list_position,
     494        c.candidate_id,
     495        (p.name || ' ' || p.surname)::TEXT          AS full_name,
     496        p.date_of_birth,
     497        p.gender,
     498        COALESCE(r.name, 'No municipality data')    AS municipality,
     499        COALESCE(
     500            (
     501                SELECT pp.name
     502                FROM   candidate_party cp
     503                JOIN   political_party  pp ON pp.party_id = cp.party_id
     504                WHERE  cp.candidate_id = c.candidate_id
     505                AND    cp.election_id  = p_election_id
     506                ORDER BY cp.candidate_party_id
     507                LIMIT  1
     508            ),
     509            v_entity_name
     510        )                                           AS party_name,
     511        v_entity_name                               AS entity_name,
     512        v_election_name                             AS election_name
     513    FROM        candidate_list_item cli
     514    JOIN        candidate           c   ON c.candidate_id = cli.candidate_id
     515    JOIN        person              p   ON p.person_id    = c.person_id
     516    LEFT JOIN   region              r   ON r.region_id    = p.municipality_id
     517    WHERE       cli.list_id = v_list_id
     518    ORDER BY    cli.position ASC;
     519END;
     520$function$;
     521}}}
     522
     523----
     524
     525=== fn_station_anomaly_score
     526
     527**Сигнатура:** `fn_station_anomaly_score(p_station_id bigint, p_election_id bigint)`
     528
     529**Враќа:** `TABLE(station_id, station_name, election_name, registered_voters, ballots_cast, valid_ballots, invalid_ballots, turnout_pct, invalid_pct, total_votes_vr, anomaly_score, flag_high_turnout, flag_impossible_turnout, flag_high_invalid, flag_no_ballots, flag_impossible_votes, recommendation)`
     530
     531Ја анализира статистиката на гласачко место и пресметува `anomaly_score` врз основа на 5 флагови: висока излезност (>95%), невозможна излезност (>100%), многу невалидни ливчиња (>10%), нула ливчиња, и гласови поголеми од регистрираните гласачи. Ја имплементира автоматската детекција на потенцијални изборни измами или грешки во внесот со конкретна препорака за акција.
     532
     533||= Anomaly Score =||= Препорака =||
     534|| 0 || OK — No anomalies detected. ||
     535|| 1–2 || MINOR — Monitor this station. ||
     536|| 3–5 || WARNING — Manual review recommended. ||
     537|| >5 || CRITICAL — Immediate investigation required. ||
     538
     539{{{
     540CREATE OR REPLACE FUNCTION public.fn_station_anomaly_score(p_station_id bigint, p_election_id bigint)
     541 RETURNS TABLE(station_id bigint, station_name character varying, election_name character varying,
     542               registered_voters integer, ballots_cast bigint, valid_ballots bigint,
     543               invalid_ballots bigint, turnout_pct numeric, invalid_pct numeric,
     544               total_votes_vr bigint, anomaly_score integer,
     545               flag_high_turnout boolean, flag_impossible_turnout boolean,
     546               flag_high_invalid boolean, flag_no_ballots boolean,
     547               flag_impossible_votes boolean, recommendation text)
     548 LANGUAGE plpgsql
     549AS $function$
     550DECLARE
     551    v_station_name    VARCHAR;
     552    v_election_name   VARCHAR;
     553    v_registered      INT;
     554    v_ballots         BIGINT;
     555    v_valid           BIGINT;
     556    v_invalid         BIGINT;
     557    v_votes_vr        BIGINT;
     558    v_turnout         NUMERIC;
     559    v_invalid_pct     NUMERIC;
     560    v_score           INT     := 0;
     561    v_flag_turnout    BOOLEAN := FALSE;
     562    v_flag_impossible BOOLEAN := FALSE;
     563    v_flag_invalid    BOOLEAN := FALSE;
     564    v_flag_no_ballot  BOOLEAN := FALSE;
     565    v_flag_imp_votes  BOOLEAN := FALSE;
     566    v_recommendation  TEXT;
     567BEGIN
     568    SELECT ps.name, ps.registered_voter
     569    INTO   v_station_name, v_registered
     570    FROM   polling_station ps
     571    WHERE  ps.station_id = p_station_id;
     572
     573    IF NOT FOUND THEN
     574        RAISE EXCEPTION 'Polling station % does not exist.', p_station_id;
     575    END IF;
     576
     577    SELECT e.name INTO v_election_name
     578    FROM   election e
     579    WHERE  e.election_id = p_election_id;
     580
     581    IF NOT FOUND THEN
     582        RAISE EXCEPTION 'Election % does not exist.', p_election_id;
     583    END IF;
     584
     585    IF NOT EXISTS (
     586        SELECT 1 FROM station_election se
     587        WHERE  se.station_id  = p_station_id
     588        AND    se.election_id = p_election_id
     589    ) THEN
     590        RAISE EXCEPTION 'Station % is not assigned to election %.',
     591            p_station_id, p_election_id;
     592    END IF;
     593
     594    SELECT
     595        COUNT(*),
     596        COUNT(*) FILTER (WHERE b.is_valid = TRUE),
     597        COUNT(*) FILTER (WHERE b.is_valid = FALSE)
     598    INTO v_ballots, v_valid, v_invalid
     599    FROM ballot b
     600    WHERE b.station_id  = p_station_id
     601    AND   b.election_id = p_election_id;
     602
     603    SELECT COALESCE(SUM(vr.votes), 0)
     604    INTO   v_votes_vr
     605    FROM   vote_result vr
     606    WHERE  vr.station_id  = p_station_id
     607    AND    vr.election_id = p_election_id;
     608
     609    v_turnout := ROUND(
     610        v_ballots::NUMERIC * 100.0 / NULLIF(COALESCE(v_registered, 0), 0), 2);
     611
     612    v_invalid_pct := ROUND(
     613        v_invalid::NUMERIC * 100.0 / NULLIF(v_ballots, 0), 2);
     614
     615    -- Flag 1: Излезност > 95%
     616    IF v_turnout > 95.0 THEN
     617        v_flag_turnout := TRUE;
     618        v_score        := v_score + 3;
     619    END IF;
     620
     621    -- Flag 2: Излезност > 100% (невозможно)
     622    IF v_turnout > 100.0 THEN
     623        v_flag_impossible := TRUE;
     624        v_score           := v_score + 10;
     625    END IF;
     626
     627    -- Flag 3: Невалидни > 10%
     628    IF v_invalid_pct > 10.0 THEN
     629        v_flag_invalid := TRUE;
     630        v_score        := v_score + 2;
     631    END IF;
     632
     633    -- Flag 4: Нула балоти
     634    IF v_ballots = 0 THEN
     635        v_flag_no_ballot := TRUE;
     636        v_score          := v_score + 2;
     637    END IF;
     638
     639    -- Flag 5: Гласови во vote_result > регистрирани гласачи
     640    IF v_votes_vr > COALESCE(v_registered, 0) THEN
     641        v_flag_imp_votes := TRUE;
     642        v_score          := v_score + 5;
     643    END IF;
     644
     645    v_recommendation := CASE
     646        WHEN v_score = 0             THEN 'OK — No anomalies detected.'
     647        WHEN v_score BETWEEN 1 AND 2 THEN 'MINOR — Monitor this station.'
     648        WHEN v_score BETWEEN 3 AND 5 THEN 'WARNING — Manual review recommended.'
     649        ELSE                              'CRITICAL — Immediate investigation required.'
     650    END;
     651
     652    RETURN QUERY
     653    SELECT
     654        p_station_id, v_station_name, v_election_name,
     655        v_registered, v_ballots, v_valid, v_invalid,
     656        COALESCE(v_turnout, 0), COALESCE(v_invalid_pct, 0),
     657        v_votes_vr, v_score,
     658        v_flag_turnout, v_flag_impossible, v_flag_invalid,
     659        v_flag_no_ballot, v_flag_imp_votes, v_recommendation;
     660END;
     661$function$;
     662}}}
     663
     664----
     665
     666== Процедури
     667
     668=== sp_close_election
     669
     670**Сигнатура:** `sp_close_election(IN p_election_id bigint)`
     671
     672Ги верифицира резултатите, го наоѓа победникот со процент на гласови, го затвора изборот (`status=0`) и логира детален извештај. Ја имплементира официјалната процедура за завршување на изборен циклус — само избори со внесени резултати можат да се затворат.
     673
     674{{{
     675CREATE OR REPLACE PROCEDURE public.sp_close_election(IN p_election_id bigint)
     676 LANGUAGE plpgsql
     677AS $procedure$
     678DECLARE
     679    v_election_name  VARCHAR;
     680    v_election_type  VARCHAR;
     681    v_status         SMALLINT;
     682    v_station_count  INT;
     683    v_total_votes    BIGINT;
     684    v_winner_name    VARCHAR;
     685    v_winner_votes   BIGINT;
     686    v_winner_pct     NUMERIC;
     687BEGIN
     688    -- (1) Провери дали изборот постои
     689    SELECT e.name, e.status, et.type_name
     690    INTO   v_election_name, v_status, v_election_type
     691    FROM   election e
     692    JOIN   election_type et ON et.election_type_id = e.election_type_id
     693    WHERE  e.election_id = p_election_id;
     694
     695    IF NOT FOUND THEN
     696        RAISE EXCEPTION 'Election % does not exist.', p_election_id;
     697    END IF;
     698
     699    -- (2) Провери дали е активен
     700    IF v_status = 0 THEN
     701        RAISE EXCEPTION 'Election "%" is already closed.', v_election_name;
     702    END IF;
     703
     704    -- (3) Провери дали постојат vote_result записи
     705    SELECT COUNT(DISTINCT vr.station_id), SUM(vr.votes)
     706    INTO   v_station_count, v_total_votes
     707    FROM   vote_result vr
     708    WHERE  vr.election_id  = p_election_id
     709    AND    vr.candidate_id IS NULL;
     710
     711    IF v_total_votes IS NULL OR v_total_votes = 0 THEN
     712        RAISE EXCEPTION
     713            'Election "%" has no vote results. Cannot close an election without results.',
     714            v_election_name;
     715    END IF;
     716
     717    -- Најди победник
     718    SELECT pe.name, SUM(vr.votes),
     719           ROUND(SUM(vr.votes)::NUMERIC * 100.0 / NULLIF(v_total_votes, 0), 2)
     720    INTO   v_winner_name, v_winner_votes, v_winner_pct
     721    FROM   vote_result vr
     722    JOIN   political_entity pe ON pe.entity_id = vr.entity_id
     723    WHERE  vr.election_id  = p_election_id
     724    AND    vr.candidate_id IS NULL
     725    GROUP BY pe.name
     726    ORDER BY SUM(vr.votes) DESC
     727    LIMIT 1;
     728
     729    -- Затвори го изборот
     730    UPDATE election SET status = 0 WHERE election_id = p_election_id;
     731
     732    RAISE NOTICE '════════════════════════════════════════';
     733    RAISE NOTICE 'Election closed: % (%)', v_election_name, v_election_type;
     734    RAISE NOTICE 'Stations reported : %', v_station_count;
     735    RAISE NOTICE 'Total votes        : %', v_total_votes;
     736    RAISE NOTICE 'Winner             : %', v_winner_name;
     737    RAISE NOTICE 'Winner votes       : %', v_winner_votes;
     738    RAISE NOTICE 'Winner share       : % pct', v_winner_pct;
     739    RAISE NOTICE '════════════════════════════════════════';
     740
     741END;
     742$procedure$;
     743}}}
     744
     745----
     746
     747=== sp_open_election
     748
     749**Сигнатура:** `sp_open_election(IN p_election_id bigint)`
     750
     751Го активира изборот (`status=1`) по верификација дека не постои веќе активен избор од ист тип во системот. Ја имплементира бизнис правилото дека не можат да се одржуваат два истовидни избори истовремено.
     752
     753{{{
     754CREATE OR REPLACE PROCEDURE public.sp_open_election(IN p_election_id bigint)
     755 LANGUAGE plpgsql
     756AS $procedure$
     757DECLARE
     758    v_election_name    VARCHAR;
     759    v_type_id          BIGINT;
     760    v_status           SMALLINT;
     761    v_same_type_active BOOLEAN;
     762BEGIN
     763    SELECT name, status, election_type_id
     764    INTO   v_election_name, v_status, v_type_id
     765    FROM   public.election
     766    WHERE  election_id = p_election_id;
     767
     768    IF NOT FOUND THEN
     769        RAISE EXCEPTION 'Грешка: Изборот со ID % не постои.', p_election_id;
     770    END IF;
     771
     772    IF v_status = 1 THEN
     773        RAISE EXCEPTION
     774            'Забелешка: Изборот "%" е веќе отворен и активен.', v_election_name;
     775    END IF;
     776
     777    SELECT EXISTS (
     778        SELECT 1 FROM public.election
     779        WHERE  status = 1 AND election_type_id = v_type_id
     780    ) INTO v_same_type_active;
     781
     782    IF v_same_type_active THEN
     783        RAISE EXCEPTION
     784            'Предупредување: Веќе постојат активни избори од овој тип во системот. '
     785            'Мора прво да ги затворите претходните од истата категорија.';
     786    END IF;
     787
     788    UPDATE public.election SET status = 1 WHERE election_id = p_election_id;
     789
     790    RAISE NOTICE '========================================';
     791    RAISE NOTICE 'Изборот "%" е УСПЕШНО ОТВОРЕН!', v_election_name;
     792    RAISE NOTICE 'Овозможено е истовремено гласање со други активни различни типови избори.';
     793    RAISE NOTICE '========================================';
     794
     795END;
     796$procedure$;
     797}}}
     798
     799----
     800
     801=== sp_register_newly_eligible_voters
     802
     803**Сигнатура:** `sp_register_newly_eligible_voters()`
     804
     805Ги регистрира сите лица кои наполниле 18 години а сè уште не се во гласачкиот список, и ги распоредува на најмалку оптоварената станица во batch-ови од 10,000. Ја автоматизира периодичната ажурирање на гласачкиот список која државните институции ја вршат пред секои избори.
     806
     807{{{
     808CREATE OR REPLACE PROCEDURE public.sp_register_newly_eligible_voters()
     809 LANGUAGE plpgsql
     810AS $procedure$
     811DECLARE
     812    v_count        INT := 0;
     813    v_max_voter_id BIGINT;
     814    v_station_id   BIGINT;
     815    v_region_id    BIGINT;
     816    rec            RECORD;
     817BEGIN
     818    SELECT COALESCE(MAX(voter_id), 0)
     819    INTO   v_max_voter_id
     820    FROM   voter;
     821
     822    -- Најди најмалку оптоварена станица
     823    SELECT ps.station_id, ps.municipality_id
     824    INTO   v_station_id, v_region_id
     825    FROM   polling_station ps
     826    LEFT JOIN voter v ON v.station_id = ps.station_id
     827    GROUP BY ps.station_id, ps.municipality_id, ps.registered_voter
     828    ORDER BY COUNT(v.voter_id) * 1.0 / NULLIF(ps.registered_voter, 1) ASC
     829    LIMIT 1;
     830
     831    IF v_station_id IS NULL THEN
     832        RAISE EXCEPTION 'No polling stations found in the system';
     833    END IF;
     834
     835    FOR rec IN
     836        SELECT p.person_id
     837        FROM   person p
     838        WHERE  p.date_of_birth <= CURRENT_DATE - INTERVAL '18 years'
     839        AND    p.person_id NOT IN (SELECT person_id FROM voter)
     840        ORDER BY p.person_id
     841    LOOP
     842        v_max_voter_id := v_max_voter_id + 1;
     843
     844        INSERT INTO voter (voter_id, person_id, region_id, station_id)
     845        VALUES (v_max_voter_id, rec.person_id, v_region_id, v_station_id)
     846        ON CONFLICT (person_id) DO NOTHING;
     847
     848        v_count := v_count + 1;
     849
     850        -- Commit во batch-ови од 10,000
     851        IF v_count % 10000 = 0 THEN
     852            RAISE NOTICE 'Registered % voters so far...', v_count;
     853        END IF;
     854
     855    END LOOP;
     856
     857    RAISE NOTICE 'Done. Total newly registered voters: %', v_count;
     858
     859END;
     860$procedure$;
     861}}}