| | 1 | = Фаза 4 — Функции, Процедури, Тригери |
| | 2 | |
| | 3 | == Тригери |
| | 4 | |
| | 5 | === trg_validate_ballot |
| | 6 | |
| | 7 | **Тип:** `BEFORE INSERT` на табела `ballot` |
| | 8 | |
| | 9 | Се активира пред секое внесување на гласачко ливче и ги проверува три услови: дали изборот е активен (`status=1`), дали гласачкото место е назначено на тој избор, и дали `entity_id` учествувал во изборот. Ја имплементира бизнис логиката за интегритет на гласање — спречува невалидни или фалсификувани ливчиња да влезат во системот. |
| | 10 | |
| | 11 | {{{ |
| | 12 | CREATE OR REPLACE FUNCTION public.fn_validate_ballot() |
| | 13 | RETURNS trigger |
| | 14 | LANGUAGE plpgsql |
| | 15 | AS $function$ |
| | 16 | DECLARE |
| | 17 | v_status SMALLINT; |
| | 18 | v_station_ok BOOLEAN; |
| | 19 | v_entity_ok BOOLEAN; |
| | 20 | BEGIN |
| | 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 | |
| | 82 | END; |
| | 83 | $function$; |
| | 84 | |
| | 85 | -- Trigger definition |
| | 86 | CREATE TRIGGER trg_validate_ballot |
| | 87 | BEFORE INSERT ON public.ballot |
| | 88 | FOR 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 | {{{ |
| | 100 | CREATE OR REPLACE FUNCTION public.fn_trigger_live_vote_counter() |
| | 101 | RETURNS trigger |
| | 102 | LANGUAGE plpgsql |
| | 103 | AS $function$ |
| | 104 | DECLARE |
| | 105 | v_result_id_exists BIGINT; |
| | 106 | v_max_id BIGINT; |
| | 107 | BEGIN |
| | 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; |
| | 136 | END; |
| | 137 | $function$; |
| | 138 | |
| | 139 | -- Trigger definition |
| | 140 | CREATE TRIGGER trg_after_insert_ballot_live_count |
| | 141 | AFTER INSERT ON public.ballot |
| | 142 | FOR 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 | {{{ |
| | 154 | CREATE OR REPLACE FUNCTION public.fn_check_voter_age_limit() |
| | 155 | RETURNS trigger |
| | 156 | LANGUAGE plpgsql |
| | 157 | AS $function$ |
| | 158 | DECLARE |
| | 159 | v_birth_date DATE; |
| | 160 | v_full_name TEXT; |
| | 161 | BEGIN |
| | 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; |
| | 183 | END; |
| | 184 | $function$; |
| | 185 | |
| | 186 | -- Trigger definition |
| | 187 | CREATE TRIGGER trg_before_insert_voter_age |
| | 188 | BEFORE INSERT ON public.voter |
| | 189 | FOR 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 | {{{ |
| | 201 | CREATE OR REPLACE FUNCTION public.fn_trigger_sync_registered_voters() |
| | 202 | RETURNS trigger |
| | 203 | LANGUAGE plpgsql |
| | 204 | AS $function$ |
| | 205 | BEGIN |
| | 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; |
| | 218 | END; |
| | 219 | $function$; |
| | 220 | |
| | 221 | -- Trigger definition |
| | 222 | CREATE TRIGGER trg_after_voter_change_sync_stats |
| | 223 | AFTER INSERT OR DELETE ON public.voter |
| | 224 | FOR 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 | {{{ |
| | 236 | CREATE OR REPLACE FUNCTION public.fn_prevent_duplicate_voter_checkin() |
| | 237 | RETURNS trigger |
| | 238 | LANGUAGE plpgsql |
| | 239 | AS $function$ |
| | 240 | DECLARE |
| | 241 | v_registered_station BIGINT; |
| | 242 | v_election_status SMALLINT; |
| | 243 | v_election_name VARCHAR; |
| | 244 | v_voter_name TEXT; |
| | 245 | BEGIN |
| | 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; |
| | 298 | END; |
| | 299 | $function$; |
| | 300 | |
| | 301 | -- Trigger definition |
| | 302 | CREATE TRIGGER trg_prevent_duplicate_voter_checkin |
| | 303 | BEFORE INSERT ON public.voter_election |
| | 304 | FOR 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 | {{{ |
| | 320 | CREATE 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 |
| | 324 | AS $function$ |
| | 325 | DECLARE |
| | 326 | v_seats INTEGER; |
| | 327 | v_method_id BIGINT; |
| | 328 | v_election_name VARCHAR; |
| | 329 | v_region_name VARCHAR; |
| | 330 | BEGIN |
| | 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 | |
| | 434 | END; |
| | 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 | {{{ |
| | 449 | CREATE 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 |
| | 455 | AS $function$ |
| | 456 | DECLARE |
| | 457 | v_entity_name VARCHAR; |
| | 458 | v_election_name VARCHAR; |
| | 459 | v_list_id BIGINT; |
| | 460 | BEGIN |
| | 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; |
| | 519 | END; |
| | 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 | {{{ |
| | 540 | CREATE 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 |
| | 549 | AS $function$ |
| | 550 | DECLARE |
| | 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; |
| | 567 | BEGIN |
| | 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; |
| | 660 | END; |
| | 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 | {{{ |
| | 675 | CREATE OR REPLACE PROCEDURE public.sp_close_election(IN p_election_id bigint) |
| | 676 | LANGUAGE plpgsql |
| | 677 | AS $procedure$ |
| | 678 | DECLARE |
| | 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; |
| | 687 | BEGIN |
| | 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 | |
| | 741 | END; |
| | 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 | {{{ |
| | 754 | CREATE OR REPLACE PROCEDURE public.sp_open_election(IN p_election_id bigint) |
| | 755 | LANGUAGE plpgsql |
| | 756 | AS $procedure$ |
| | 757 | DECLARE |
| | 758 | v_election_name VARCHAR; |
| | 759 | v_type_id BIGINT; |
| | 760 | v_status SMALLINT; |
| | 761 | v_same_type_active BOOLEAN; |
| | 762 | BEGIN |
| | 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 | |
| | 795 | END; |
| | 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 | {{{ |
| | 808 | CREATE OR REPLACE PROCEDURE public.sp_register_newly_eligible_voters() |
| | 809 | LANGUAGE plpgsql |
| | 810 | AS $procedure$ |
| | 811 | DECLARE |
| | 812 | v_count INT := 0; |
| | 813 | v_max_voter_id BIGINT; |
| | 814 | v_station_id BIGINT; |
| | 815 | v_region_id BIGINT; |
| | 816 | rec RECORD; |
| | 817 | BEGIN |
| | 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 | |
| | 859 | END; |
| | 860 | $procedure$; |
| | 861 | }}} |