Changes between Version 17 and Version 18 of DatabaseProgramming


Ignore:
Timestamp:
05/09/26 20:55:16 (2 weeks ago)
Author:
231027
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • DatabaseProgramming

    v17 v18  
    33== Функции
    44
    5 === `fn_calculate_price`
    65
    7 Оваа функција ја пресметува крајната цена на билетот во реално време преку проверка на сите активни периоди за попуст или поскапување. Таа овозможува системот да биде флексибилен со цените без да се менуваат оригиналните записи во табелата со билети.
    8 
    9 {{{
    10 
    11 CREATE OR REPLACE FUNCTION fn_calculate_price(p_ticket_id BIGINT) RETURNS FLOAT4 AS $$
    12 DECLARE v_base FLOAT4; v_perc INT; v_inc BOOLEAN;
    13 BEGIN
    14     SELECT base_price INTO v_base FROM "Ticket" WHERE ticket_id = p_ticket_id;
    15     SELECT price_change_percent, increase_decrease INTO v_perc, v_inc FROM "Event_Period"
    16     WHERE event_happening_id = (SELECT event_happening_id FROM "Ticket" WHERE ticket_id = p_ticket_id)
    17     AND CURRENT_DATE BETWEEN start_date AND end_date LIMIT 1;
    18     IF v_perc IS NOT NULL THEN
    19         IF v_inc THEN v_base := v_base + (v_base * v_perc / 100.0);
    20         ELSE v_base := v_base - (v_base * v_perc / 100.0); END IF;
    21     END IF;
    22     RETURN v_base;
    23 END; $$ LANGUAGE plpgsql;
    24 
    25 }}}
    26 
    27 === `fn_check_resource_availability`
    28 
    29 Оваа функција служи како централен систем за детекција на конфликти помеѓу термините на настаните. Проверува дали одредена локација или изведувач се веќе зафатени во бараниот временски интервал, спречувајќи дуплирање на резервациите.
    30 
    31 {{{
    32 
    33 CREATE OR REPLACE FUNCTION fn_check_resource_availability(p_type TEXT, p_id BIGINT, p_start TIMESTAMP, p_dur INT) RETURNS BOOLEAN AS $$
    34 BEGIN
    35     IF p_type = 'VENUE' THEN
    36         RETURN NOT EXISTS (SELECT 1 FROM "Event_Happening" WHERE venue_id = p_id AND (p_start, (p_start + (p_dur || ' minutes')::interval)) OVERLAPS (event_time, (event_time + (duration || ' minutes')::interval)));
    37     ELSIF p_type = 'PERFORMER' THEN
    38         RETURN NOT EXISTS (SELECT 1 FROM "Event_Happening_Performer" ehp JOIN "Event_Happening" eh ON ehp.event_happening_id = eh.event_happening_id
    39         WHERE ehp.performer_id = p_id AND (p_start, (p_start + (p_dur || ' minutes')::interval)) OVERLAPS (eh.event_time, (eh.event_time + (eh.duration || ' minutes')::interval)));
    40     END IF;
    41     RETURN TRUE;
    42 END; $$ LANGUAGE plpgsql;
    43 
    44 }}}
    45 
    46 === `fn_validate_hierarchy_capacity`
    47 
    48 Оваа функција врши строга контрола врз хиерархиската структура на објектот (Venue -> Section -> Seat). Гарантира дека вкупниот број на седишта во секциите не ја надминува границата на салата, ниту пак бројот на реални седишта го надминува капацитетот на секцијата.
    49 
    50 {{{
    51 
    52 CREATE OR REPLACE FUNCTION fn_validate_hierarchy_capacity(p_parent_id BIGINT, p_new_amount INT, p_type TEXT) RETURNS BOOLEAN AS $$
    53 DECLARE v_limit INT; v_current INT;
    54 BEGIN
    55     IF p_type = 'VENUE_LIMIT' THEN
    56         SELECT number_of_seats INTO v_limit FROM "Venue" WHERE venue_id = p_parent_id;
    57         SELECT COALESCE(SUM(number_of_seats), 0) INTO v_current FROM "Section" WHERE venue_id = p_parent_id;
    58     ELSIF p_type = 'SECTION_LIMIT' THEN
    59         SELECT number_of_seats INTO v_limit FROM "Section" WHERE section_id = p_parent_id;
    60         SELECT COUNT(*) INTO v_current FROM "Seat" WHERE section_id = p_parent_id;
    61     END IF;
    62     RETURN (v_current + p_new_amount) <= v_limit;
    63 END; $$ LANGUAGE plpgsql;
    64 
    65 }}}
    666
    677== Процедури
    688
    69 === `proc_execute_purchase`
    709
    71 Оваа процедура го менаџира целиот процес на продажба на билет во една сигурна трансакција. Ја пресметува точната цена, го евидентира купувачот и автоматски го заклучува билетот за да спречи негова повторна продажба.
    72 
    73 {{{
    74 
    75 CREATE OR REPLACE PROCEDURE proc_execute_purchase(p_uid BIGINT, p_tid BIGINT, p_qr VARCHAR) AS $$
    76 BEGIN
    77     IF (SELECT is_available FROM "Ticket" WHERE ticket_id = p_tid) = FALSE THEN RAISE EXCEPTION 'Ticket is already sold'; END IF;
    78     INSERT INTO "Ticket_Purchase" (ticket_id, user_id, qr_code, purchase_amount)
    79     VALUES (p_tid, p_uid, p_qr, fn_calculate_price(p_tid));
    80     UPDATE "Ticket" SET is_available = FALSE WHERE ticket_id = p_tid;
    81 END; $$ LANGUAGE plpgsql;
    82 
    83 }}}
    84 
    85 === `proc_execute_refund`
    86 
    87 Ове е автоматизирана процедура за рефундирање која пресметува 85% поврат на средствата за корисникот. По успешното запишување на рефундацијата, таа веднаш го враќа билетот во продажба како „слободен“.
    88 
    89 {{{
    90 
    91 CREATE OR REPLACE PROCEDURE proc_execute_refund(p_purchase_id BIGINT) AS $$
    92 DECLARE v_paid FLOAT4; v_tid BIGINT;
    93 BEGIN
    94     SELECT purchase_amount, ticket_id INTO v_paid, v_tid FROM "Ticket_Purchase" WHERE purchase_id = p_purchase_id;
    95     INSERT INTO "Ticket_Refund" (purchase_id, refund_amount, refund_time) VALUES (p_purchase_id, v_paid * 0.85, CURRENT_TIMESTAMP);
    96     UPDATE "Ticket" SET is_available = TRUE WHERE ticket_id = v_tid;
    97 END; $$ LANGUAGE plpgsql;
    98 
    99 }}}
    100 
    101 === `proc_create_full_event`
    102 
    103 Оваа процедура овозможува креирање на комплетен настан заедно со неговиот термин и доделен изведувач со една команда. Процедурата ја користи логиката за проверка на достапност за да гарантира дека новиот настан не се поклопува со веќе постоечките.
    104 
    105 {{{
    106 
    107 CREATE OR REPLACE PROCEDURE proc_create_full_event(p_name VARCHAR, p_age INT, p_ven_id BIGINT, p_perf_id BIGINT, p_time TIMESTAMP, p_dur INT) AS $$
    108 DECLARE v_eid BIGINT; v_hid BIGINT;
    109 BEGIN
    110     IF NOT fn_check_resource_availability('VENUE', p_ven_id, p_time, p_dur) THEN
    111         RAISE EXCEPTION 'Venue is already booked for this time';
    112     END IF;
    113 
    114     INSERT INTO "Event" (name, description, min_age)
    115     VALUES (p_name, 'No description provided', p_age)
    116     RETURNING event_id INTO v_eid;
    117 
    118     INSERT INTO "Event_Happening" (event_id, venue_id, event_time, duration)
    119     VALUES (v_eid, p_ven_id, p_time, p_dur)
    120     RETURNING event_happening_id INTO v_hid;
    121 
    122     INSERT INTO "Event_Happening_Performer" (event_happening_id, performer_id)
    123     VALUES (v_hid, p_perf_id);
    124 END; $$ LANGUAGE plpgsql;
    125 
    126 }}}
    12710
    12811== Тригери
    12912
    130 === `trg_buy_age` и `func_age_gate`
    131 
    132 Овој тригер се активира при секој обид за купување билет за да ја заштити старосната граница на настанот. Спречува малолетни лица да купуваат билети за настани за кои не ја исполнуваат минималната возраст пропишана од организаторот.
    133 
    134 {{{
    135 
    136 CREATE OR REPLACE FUNCTION func_age_gate() RETURNS TRIGGER AS $$
    137 BEGIN
    138     IF (SELECT EXTRACT(YEAR FROM AGE(date_of_birth)) FROM "User" WHERE user_id = NEW.user_id) <
    139        (SELECT e.min_age FROM "Event" e JOIN "Event_Happening" eh ON e.event_id = eh.event_id JOIN "Ticket" t ON eh.event_happening_id = t.event_happening_id WHERE t.ticket_id = NEW.ticket_id)
    140     THEN RAISE EXCEPTION 'Access Denied: User age is below the minimum required for this event'; END IF;
    141     RETURN NEW;
    142 END; $$ LANGUAGE plpgsql;
    143 CREATE TRIGGER trg_buy_age BEFORE INSERT ON "Ticket_Purchase" FOR EACH ROW EXECUTE FUNCTION func_age_gate();
    144 
    145 }}}
    146 
    147 === `trg_rate_check` и `func_rating_logic`
    148 
    149 Овој тригер обезбедува кредибилитет на рејтингот на настаните преку строга контрола на корисниците. Дозволува оставање на оценка само доколку корисникот има купено билет и истиот не е рефундиран.
    150 
    151 {{{
    152 
    153 CREATE OR REPLACE FUNCTION func_rating_logic() RETURNS TRIGGER AS $$
    154 BEGIN
    155     IF NOT EXISTS (SELECT 1 FROM "Ticket_Purchase" tp JOIN "Ticket" t ON tp.ticket_id = t.ticket_id
    156        WHERE tp.user_id = NEW.user_id AND t.event_happening_id = NEW.event_happening_id
    157        AND NOT EXISTS (SELECT 1 FROM "Ticket_Refund" tr WHERE tr.purchase_id = tp.purchase_id))
    158     THEN RAISE EXCEPTION 'You must have a valid ticket and have attended the event to rate it'; END IF;
    159     RETURN NEW;
    160 END; $$ LANGUAGE plpgsql;
    161 CREATE TRIGGER trg_rate_check BEFORE INSERT ON "Event_Happening_Rating" FOR EACH ROW EXECUTE FUNCTION func_rating_logic();
    162 
    163 }}}
    164 
    165 === `trg_period_check` и `func_period_val`
    166 
    167 Овој тригер спречува логички грешки при дефинирањето на промотивните периоди. Оневозможува поставување на датуми за попуст кои траат откако самиот настан веќе завршил.
    168 
    169 {{{
    170 
    171 CREATE OR REPLACE FUNCTION func_period_val() RETURNS TRIGGER AS $$
    172 BEGIN
    173     IF NEW.end_date > (SELECT event_time::DATE FROM "Event_Happening" WHERE event_happening_id = NEW.event_happening_id)
    174     THEN RAISE EXCEPTION 'Invalid Period: End date cannot be after the event occurrence date'; END IF;
    175     RETURN NEW;
    176 END; $$ LANGUAGE plpgsql;
    177 CREATE TRIGGER trg_period_check BEFORE INSERT OR UPDATE ON "Event_Period" FOR EACH ROW EXECUTE FUNCTION func_period_val();
    178 
    179 }}}
    180 
    181 === `trg_venue_sync` и `func_sync_cap`
    182 
    183 Овој тригер ја одржува синхронизацијата на капацитетите преку повикување на хиерархиската функција. Ги блокира обидите за создавање секции кои би го надминале вкупниот број на седишта планиран за целиот објект.
    184 
    185 {{{
    186 
    187 CREATE OR REPLACE FUNCTION func_sync_cap() RETURNS TRIGGER AS $$
    188 BEGIN
    189     IF NOT fn_validate_hierarchy_capacity(NEW.venue_id, NEW.number_of_seats, 'VENUE_LIMIT')
    190     THEN RAISE EXCEPTION 'Capacity Error: New section exceeds total venue seat limit'; END IF;
    191     RETURN NEW;
    192 END; $$ LANGUAGE plpgsql;
    193 CREATE TRIGGER trg_venue_sync BEFORE INSERT OR UPDATE ON "Section" FOR EACH ROW EXECUTE FUNCTION func_sync_cap();
    194 
    195 }}}
    196 
    197 === `trg_ticket_gate` и `func_ticket_cap`
    198 
    199 Ова е тригер кој претставува бариера која спречува „overbooking“ на настаните. Оневозможува генерирање на повеќе билети од вкупниот физички капацитет на салата за секој поединечен термин.
    200 
    201 {{{
    202 
    203 CREATE OR REPLACE FUNCTION func_ticket_cap() RETURNS TRIGGER AS $$
    204 BEGIN
    205     IF (SELECT COUNT(*) FROM "Ticket" WHERE event_happening_id = NEW.event_happening_id) >=
    206        (SELECT number_of_seats FROM "Venue" v JOIN "Event_Happening" eh ON v.venue_id = eh.venue_id WHERE eh.event_happening_id = NEW.event_happening_id)
    207     THEN RAISE EXCEPTION 'Sold Out: Number of tickets cannot exceed the venue seat capacity'; END IF;
    208     RETURN NEW;
    209 END; $$ LANGUAGE plpgsql;
    210 CREATE TRIGGER trg_ticket_gate BEFORE INSERT ON "Ticket" FOR EACH ROW EXECUTE FUNCTION func_ticket_cap();
    211 
    212 }}}
    213 
    214 == Тестирање на `proc_execute_purchase`, `fn_calculate_price`, `proc_execute_refund`, `trg_buy_age`, `trg_ticket_gate`, `func_ticket_cap`
    215 
    216 {{{
    217 
    218 -- make purchase
    219 CALL proc_execute_purchase(1, 100, 'QR-TEST-CODE-001');
    220 
    221 -- check if ticket is not available (is_available should be false)
    222 SELECT is_available FROM "Ticket" WHERE ticket_id = 100;
    223 
    224 -- check if price is calculated correctly in Ticket_Purchase
    225 SELECT * FROM "Ticket_Purchase" WHERE ticket_id = 100;
    226 
    227 -- make refund to the purchase (purchase_id from before)
    228 CALL proc_execute_refund(16000001);
    229 
    230 -- check if the ticket is available (is_available should be true)
    231 SELECT is_available FROM "Ticket" WHERE ticket_id = 100;
    232 
    233 -- check if there is a corresponding record in Ticket_Refund
    234 SELECT * FROM "Ticket_Refund" WHERE purchase_id = 16000001;
    235 
    236 }}}
    237 
    238 == Тестирање на `trg_buy_age`, `func_age_gate`, `proc_execute_purchase`
    239 
    240 {{{
    241 
    242 -- get the user_id of a user who is under 18
    243 SELECT user_id FROM "User" WHERE date_of_birth > '2010-01-01' LIMIT 1;  -- 1081
    244 
    245 -- get the event_id of an event whose min_age is 18
    246 SELECT event_id FROM "Event" WHERE min_age = 18 LIMIT 1;    -- 5
    247 
    248 -- try to make the user under 18 buy a ticket for a happening of the event whose min entrance age is 18
    249 CALL proc_execute_purchase(1081, 5, 'QR-FAIL');
    250 
    251 }}}
    252 
    253 == Тестирање на `fn_check_resource_availability`, `proc_create_full_event`
    254 
    255 {{{
    256 
    257 -- reset system counter
    258 SELECT setval(pg_get_serial_sequence('"Event"', 'event_id'), COALESCE(MAX(event_id), 1)) FROM "Event";
    259 
    260 -- -- create an event happening from 20:00 to 22:00 in the venue with venue_id 1
    261 CALL proc_create_full_event('Test Concert 1', 12, 1, 1, '2026-05-20 20:00:00', 120);
    262 
    263 -- -- create another event happening from 21:00 to 23:00 in the same venue
    264 CALL proc_create_full_event('Test Concert 2', 12, 1, 2, '2026-05-20 21:00:00', 120);
    265 
    266 }}}
    267 
    268 == Тестирање на `trg_venue_sync`, `func_sync_cap`, `fn_validate_hierarchy_capacity`
    269 
    270 {{{
    271 
    272 -- select a venue (we need the number of seats)
    273 SELECT * FROM "Venue" LIMIT 1;  -- 1793
    274 
    275 -- try to insert a new seat in a section in that venue
    276 INSERT INTO "Section" (venue_id, name, number_of_seats) VALUES (1, 'TSC', 1);
    277 
    278 }}}
    279 
    280 == Тестирање на `trg_rate_check`, `func_rating_logic`
    281 
    282 {{{
    283 
    284 INSERT INTO "Event_Happening_Rating" (user_id, event_happening_id, rating, comment)
    285 VALUES (1, 1, 5, 'Test rating');
    286 
    287 }}}
    288 
    289 == Тестирање на `trg_period_check`, `func_period_val`
    290 
    291 {{{
    292 
    293 -- get the time of an event happening
    294 SELECT event_time FROM "Event_Happening" WHERE event_happening_id = 1;  -- 2022-04-29 03:46:32.296118
    295 
    296 -- try to add an event period that is after the event happening
    297 INSERT INTO "Event_Period" (event_happening_id, start_date, end_date, price_change_percent, increase_decrease)
    298 VALUES (1, '2026-05-10', '2026-05-21', 10, TRUE);
    299 
    300 }}}