Changes between Initial Version and Version 1 of DatabaseProgramming


Ignore:
Timestamp:
05/29/26 21:53:28 (3 weeks ago)
Author:
231003
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • DatabaseProgramming

    v1 v1  
     1= Програмирање на базата на податоци
     2
     3== Функции
     4
     5=== `get_customer_ticket_summary`
     6
     7Оваа функција враќа детален преглед за билетите на одреден корисник, прикажувајќи колку вкупно билети има купено, колку од нив се single или pass билети, како и вкупната сума што ја има потрошено преку успешно реализирани плаќања.
     8
     9{{{
     10
     11CREATE OR REPLACE FUNCTION get_customer_ticket_summary(p_customer_id BIGINT)
     12RETURNS TABLE (
     13    customer_id BIGINT,
     14    total_tickets BIGINT,
     15    single_tickets BIGINT,
     16    pass_tickets BIGINT,
     17    total_spent NUMERIC
     18)
     19LANGUAGE plpgsql
     20AS $$
     21BEGIN
     22    IF NOT EXISTS (
     23        SELECT 1
     24        FROM Customer c
     25        WHERE c.user_id = p_customer_id
     26    ) THEN
     27        RAISE EXCEPTION 'Customer with id % does not exist.', p_customer_id;
     28    END IF;
     29
     30    RETURN QUERY
     31    SELECT
     32        c.user_id AS customer_id,
     33        COUNT(DISTINCT t.ticket_id) AS total_tickets,
     34        COUNT(DISTINCT st.ticket_id) AS single_tickets,
     35        COUNT(DISTINCT pt.ticket_id) AS pass_tickets,
     36        COALESCE(
     37            SUM(
     38                CASE
     39                    WHEN p.status = 'Completed'::payment_status
     40                    THEN COALESCE(st.amount, pass.amount, 0)
     41                    ELSE 0
     42                END
     43            ),
     44            0
     45        )::NUMERIC AS total_spent
     46    FROM Customer c
     47    LEFT JOIN Ticket t
     48        ON c.user_id = t.user_id
     49    LEFT JOIN Single_ticket st
     50        ON t.ticket_id = st.ticket_id
     51    LEFT JOIN Pass_ticket pt
     52        ON t.ticket_id = pt.ticket_id
     53    LEFT JOIN Pass_type pass
     54        ON pt.type_id = pass.id
     55    LEFT JOIN Customer_Payment_Ticket cpt
     56        ON t.ticket_id = cpt.ticket_id
     57       AND c.user_id = cpt.user_id
     58    LEFT JOIN Payment p
     59        ON cpt.payment_id = p.payment_id
     60    WHERE c.user_id = p_customer_id
     61    GROUP BY c.user_id;
     62END;
     63$$;
     64
     65}}}
     66
     67=== `is_ticket_valid`
     68
     69Оваа функција служи за проверка дали даден билет е валиден за користење односно дали е платен и активен според неговиот тип (single или pass) и условите за важност.
     70
     71{{{
     72
     73CREATE OR REPLACE FUNCTION is_ticket_valid(
     74    p_ticket_id BIGINT,
     75    p_assignment_id BIGINT
     76)
     77RETURNS BOOLEAN
     78LANGUAGE plpgsql
     79AS $$
     80DECLARE
     81    v_is_valid BOOLEAN;
     82BEGIN
     83    IF NOT EXISTS (
     84        SELECT 1
     85        FROM Ticket t
     86        WHERE t.ticket_id = p_ticket_id
     87    ) THEN
     88        RAISE EXCEPTION 'Ticket with id % does not exist.', p_ticket_id;
     89    END IF;
     90
     91    SELECT
     92        EXISTS (
     93            SELECT 1
     94            FROM Single_ticket st
     95            JOIN Customer_Payment_Ticket cpt
     96                ON st.ticket_id = cpt.ticket_id
     97            JOIN Payment p
     98                ON cpt.payment_id = p.payment_id
     99            WHERE st.ticket_id = p_ticket_id
     100              AND st.assignment_id = p_assignment_id
     101              AND p.status = 'Completed'::payment_status
     102        )
     103        OR
     104        EXISTS (
     105            SELECT 1
     106            FROM Pass_ticket pt
     107            JOIN Pass_type pass
     108                ON pt.type_id = pass.id
     109            JOIN Customer_Payment_Ticket cpt
     110                ON pt.ticket_id = cpt.ticket_id
     111            JOIN Payment p
     112                ON cpt.payment_id = p.payment_id
     113            WHERE pt.ticket_id = p_ticket_id
     114              AND p.status = 'Completed'::payment_status
     115              AND CURRENT_DATE BETWEEN pass.valid_from AND pass.valid_until
     116        )
     117    INTO v_is_valid;
     118
     119    RETURN v_is_valid;
     120END;
     121$$;
     122
     123}}}
     124
     125=== `get_customer_monthly_spending`
     126
     127Оваа функција служи за пресметување на вкупната сума што одреден клиент ја има потрошено во даден месец за сите успешно завршени плаќања за билети (single или pass).
     128
     129{{{
     130
     131CREATE OR REPLACE FUNCTION get_customer_monthly_spending(
     132    p_customer_id BIGINT,
     133    p_month DATE
     134)
     135RETURNS NUMERIC
     136LANGUAGE plpgsql
     137AS $$
     138DECLARE
     139    v_month_start DATE;
     140    v_month_end DATE;
     141    v_total NUMERIC;
     142BEGIN
     143    IF NOT EXISTS (
     144        SELECT 1
     145        FROM Customer c
     146        WHERE c.user_id = p_customer_id
     147    ) THEN
     148        RAISE EXCEPTION 'Customer with id % does not exist.', p_customer_id;
     149    END IF;
     150
     151    v_month_start := date_trunc('month', p_month)::date;
     152    v_month_end := (v_month_start + INTERVAL '1 month')::date;
     153
     154    SELECT
     155        COALESCE(SUM(COALESCE(st.amount, pass.amount, 0)), 0)
     156    INTO v_total
     157    FROM Customer_Payment_Ticket cpt
     158    JOIN Payment p
     159        ON cpt.payment_id = p.payment_id
     160    JOIN Ticket t
     161        ON cpt.ticket_id = t.ticket_id
     162    LEFT JOIN Single_ticket st
     163        ON t.ticket_id = st.ticket_id
     164    LEFT JOIN Pass_ticket pt
     165        ON t.ticket_id = pt.ticket_id
     166    LEFT JOIN Pass_type pass
     167        ON pt.type_id = pass.id
     168    WHERE cpt.user_id = p_customer_id
     169      AND p.status = 'Completed'::payment_status
     170      AND p.payment_date >= v_month_start
     171      AND p.payment_date < v_month_end;
     172
     173    RETURN v_total;
     174END;
     175$$;
     176
     177}}}
     178
     179=== `get_driver_shift_count`
     180
     181Оваа функција служи за да го пресмета бројот на смени (line assignments) што одреден возач ги има извршено во даден временски интервал.
     182
     183{{{
     184
     185CREATE OR REPLACE FUNCTION get_driver_shift_count(
     186    p_driver_id BIGINT,
     187    p_date_from TIMESTAMP,
     188    p_date_to TIMESTAMP
     189)
     190RETURNS INTEGER
     191LANGUAGE plpgsql
     192AS $$
     193DECLARE
     194    v_count INTEGER;
     195BEGIN
     196    IF p_date_from >= p_date_to THEN
     197        RAISE EXCEPTION 'Start date must be before end date.';
     198    END IF;
     199
     200    IF NOT EXISTS (
     201        SELECT 1
     202        FROM Driver d
     203        WHERE d.user_id = p_driver_id
     204    ) THEN
     205        RAISE EXCEPTION 'Driver with id % does not exist.', p_driver_id;
     206    END IF;
     207
     208    SELECT COUNT(*)
     209    INTO v_count
     210    FROM Line_assignment la
     211    WHERE la.driver_id = p_driver_id
     212      AND la.start_time >= p_date_from
     213      AND la.start_time < p_date_to;
     214
     215    RETURN v_count;
     216END;
     217$$;
     218
     219}}}
     220
     221== Процедури
     222
     223=== `buy_single_ticket`
     224
     225Оваа процедура служи за креирање и купување на single билет за одреден клиент, при што автоматски се регистрира плаќање, се генерира билет поврзан со одредена линија и се поврзуваат сите потребни записи во системот за евиденција.
     226
     227{{{
     228
     229CREATE OR REPLACE PROCEDURE buy_single_ticket(
     230    p_customer_id BIGINT,
     231    p_assignment_id BIGINT,
     232    p_payment_type_id BIGINT
     233)
     234LANGUAGE plpgsql
     235AS $$
     236DECLARE
     237    v_payment_id BIGINT;
     238    v_ticket_id BIGINT;
     239BEGIN
     240    IF NOT EXISTS (
     241        SELECT 1
     242        FROM Customer c
     243        WHERE c.user_id = p_customer_id
     244    ) THEN
     245        RAISE EXCEPTION 'Customer with id % does not exist.', p_customer_id;
     246    END IF;
     247
     248    IF NOT EXISTS (
     249        SELECT 1
     250        FROM Line_assignment la
     251        WHERE la.assignment_id = p_assignment_id
     252    ) THEN
     253        RAISE EXCEPTION 'Line assignment with id % does not exist.', p_assignment_id;
     254    END IF;
     255
     256    IF NOT EXISTS (
     257        SELECT 1
     258        FROM Payment_type pt
     259        WHERE pt.type_id = p_payment_type_id
     260    ) THEN
     261        RAISE EXCEPTION 'Payment type with id % does not exist.', p_payment_type_id;
     262    END IF;
     263
     264    INSERT INTO Payment (type_id, status, payment_date, transaction_number)
     265    VALUES (
     266        p_payment_type_id,
     267        'Completed'::payment_status,
     268        CURRENT_DATE,
     269        'TRX-SINGLE-' || extract(epoch from clock_timestamp())::BIGINT || '-' || p_customer_id
     270    )
     271    RETURNING payment_id INTO v_payment_id;
     272
     273    INSERT INTO Ticket (user_id)
     274    VALUES (p_customer_id)
     275    RETURNING ticket_id INTO v_ticket_id;
     276
     277    INSERT INTO Single_ticket (ticket_id, amount, assignment_id)
     278    VALUES (v_ticket_id, 50, p_assignment_id);
     279
     280    INSERT INTO Customer_Payment_Ticket (payment_id, ticket_id, user_id)
     281    VALUES (v_payment_id, v_ticket_id, p_customer_id);
     282
     283    RAISE NOTICE 'Single ticket created. ticket_id=%, payment_id=%', v_ticket_id, v_payment_id;
     284END;
     285$$;
     286
     287}}}
     288
     289=== `buy_pass_ticket`
     290
     291Оваа процедура служи за купување и креирање на pass билет за клиент, при што се регистрира плаќање, се создава билет од избран тип на pass и се поврзуваат сите релевантни записи во системот за евиденција.
     292
     293{{{
     294
     295CREATE OR REPLACE PROCEDURE buy_pass_ticket(
     296    p_customer_id BIGINT,
     297    p_pass_type_id INTEGER,
     298    p_payment_type_id BIGINT
     299)
     300LANGUAGE plpgsql
     301AS $$
     302DECLARE
     303    v_payment_id BIGINT;
     304    v_ticket_id BIGINT;
     305BEGIN
     306    IF NOT EXISTS (
     307        SELECT 1
     308        FROM Customer c
     309        WHERE c.user_id = p_customer_id
     310    ) THEN
     311        RAISE EXCEPTION 'Customer with id % does not exist.', p_customer_id;
     312    END IF;
     313
     314    IF NOT EXISTS (
     315        SELECT 1
     316        FROM Pass_type pt
     317        WHERE pt.id = p_pass_type_id
     318    ) THEN
     319        RAISE EXCEPTION 'Pass type with id % does not exist.', p_pass_type_id;
     320    END IF;
     321
     322    IF NOT EXISTS (
     323        SELECT 1
     324        FROM Payment_type pt
     325        WHERE pt.type_id = p_payment_type_id
     326    ) THEN
     327        RAISE EXCEPTION 'Payment type with id % does not exist.', p_payment_type_id;
     328    END IF;
     329
     330    INSERT INTO Payment (type_id, status, payment_date, transaction_number)
     331    VALUES (
     332        p_payment_type_id,
     333        'Completed'::payment_status,
     334        CURRENT_DATE,
     335        'TRX-PASS-' || extract(epoch from clock_timestamp())::BIGINT || '-' || p_customer_id
     336    )
     337    RETURNING payment_id INTO v_payment_id;
     338
     339    INSERT INTO Ticket (user_id)
     340    VALUES (p_customer_id)
     341    RETURNING ticket_id INTO v_ticket_id;
     342
     343    INSERT INTO Pass_ticket (ticket_id, type_id)
     344    VALUES (v_ticket_id, p_pass_type_id);
     345
     346    INSERT INTO Customer_Payment_Ticket (payment_id, ticket_id, user_id)
     347    VALUES (v_payment_id, v_ticket_id, p_customer_id);
     348
     349    RAISE NOTICE 'Pass ticket created. ticket_id=%, payment_id=%', v_ticket_id, v_payment_id;
     350END;
     351$$;
     352
     353}}}
     354
     355=== `assign_driver_to_line`
     356
     357Оваа процедура служи за доделување на возач на конкретна линија и автобус во одреден временски период, при што прво проверува дали сите учесници и ресурси се активни и достапни и дали нема преклопување со други смени.
     358
     359{{{
     360
     361CREATE OR REPLACE PROCEDURE assign_driver_to_line(
     362    p_driver_id BIGINT,
     363    p_admin_id BIGINT,
     364    p_chassis_number VARCHAR,
     365    p_schedule_id INTEGER,
     366    p_start_time TIMESTAMP,
     367    p_end_time TIMESTAMP
     368)
     369LANGUAGE plpgsql
     370AS $$
     371DECLARE
     372    v_assignment_id BIGINT;
     373BEGIN
     374    IF p_start_time >= p_end_time THEN
     375        RAISE EXCEPTION 'Start time must be before end time.';
     376    END IF;
     377
     378    IF NOT EXISTS (
     379        SELECT 1
     380        FROM Driver d
     381        WHERE d.user_id = p_driver_id
     382          AND d.status = 'Active'::employee_status
     383    ) THEN
     384        RAISE EXCEPTION 'Driver % does not exist or is not active.', p_driver_id;
     385    END IF;
     386
     387    IF NOT EXISTS (
     388        SELECT 1
     389        FROM Admin a
     390        WHERE a.user_id = p_admin_id
     391          AND a.status = 'Active'::employee_status
     392    ) THEN
     393        RAISE EXCEPTION 'Admin % does not exist or is not active.', p_admin_id;
     394    END IF;
     395
     396    IF NOT EXISTS (
     397        SELECT 1
     398        FROM Bus_instance bi
     399        WHERE bi.chassis_number = p_chassis_number
     400          AND bi.status = 'Active'::bus_status
     401    ) THEN
     402        RAISE EXCEPTION 'Bus % does not exist or is not active.', p_chassis_number;
     403    END IF;
     404
     405    IF NOT EXISTS (
     406        SELECT 1
     407        FROM Schedule s
     408        WHERE s.schedule_id = p_schedule_id
     409    ) THEN
     410        RAISE EXCEPTION 'Schedule % does not exist.', p_schedule_id;
     411    END IF;
     412
     413    IF EXISTS (
     414        SELECT 1
     415        FROM Line_assignment la
     416        WHERE la.driver_id = p_driver_id
     417          AND p_start_time < la.end_time
     418          AND p_end_time > la.start_time
     419    ) THEN
     420        RAISE EXCEPTION 'Driver % already has an overlapping shift.', p_driver_id;
     421    END IF;
     422
     423    IF EXISTS (
     424        SELECT 1
     425        FROM Line_assignment la
     426        WHERE la.chassis_number = p_chassis_number
     427          AND p_start_time < la.end_time
     428          AND p_end_time > la.start_time
     429    ) THEN
     430        RAISE EXCEPTION 'Bus % is already assigned in this time interval.', p_chassis_number;
     431    END IF;
     432
     433    INSERT INTO Line_assignment
     434        (driver_id, admin_id, chassis_number, schedule_id, start_time, end_time)
     435    VALUES
     436        (p_driver_id, p_admin_id, p_chassis_number, p_schedule_id, p_start_time, p_end_time)
     437    RETURNING assignment_id INTO v_assignment_id;
     438
     439    RAISE NOTICE 'Line assignment created. assignment_id=%', v_assignment_id;
     440END;
     441$$;
     442
     443}}}
     444
     445== Тригери
     446
     447=== `prevent_driver_shift_overlap`
     448
     449Оваа функција со trigger служи за автоматска проверка и спречување на преклопување на работни смени за возач, односно не дозволува внес или измена на assignment ако временски се поклопува со друга веќе постоечка смена за истиот возач.
     450
     451{{{
     452
     453CREATE OR REPLACE FUNCTION prevent_driver_shift_overlap_fn()
     454RETURNS TRIGGER
     455LANGUAGE plpgsql
     456AS $$
     457BEGIN
     458    IF NEW.start_time >= NEW.end_time THEN
     459        RAISE EXCEPTION 'Start time must be before end time.';
     460    END IF;
     461
     462    IF EXISTS (
     463        SELECT 1
     464        FROM Line_assignment la
     465        WHERE la.driver_id = NEW.driver_id
     466          AND la.assignment_id <> COALESCE(NEW.assignment_id, -1)
     467          AND NEW.start_time < la.end_time
     468          AND NEW.end_time > la.start_time
     469    ) THEN
     470        RAISE EXCEPTION 'Driver % already has an overlapping shift.', NEW.driver_id;
     471    END IF;
     472
     473    RETURN NEW;
     474END;
     475$$;
     476
     477DROP TRIGGER IF EXISTS trg_prevent_driver_shift_overlap ON Line_assignment;
     478
     479CREATE TRIGGER trg_prevent_driver_shift_overlap
     480BEFORE INSERT OR UPDATE ON Line_assignment
     481FOR EACH ROW
     482EXECUTE FUNCTION prevent_driver_shift_overlap_fn();
     483
     484}}}
     485
     486=== `log_payment_status_change`
     487
     488Оваа функција со trigger служи за автоматско логирање на секоја промена на статусот на плаќање, така што секогаш кога ќе се смени статусот во табелата Payment, се зачувува запис со стар и нов статус во посебна лог табела.
     489
     490{{{
     491
     492CREATE TABLE IF NOT EXISTS Payment_status_log
     493(
     494    log_id BIGSERIAL PRIMARY KEY,
     495    payment_id BIGINT NOT NULL,
     496    old_status payment_status,
     497    new_status payment_status,
     498    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
     499    description TEXT,
     500    CONSTRAINT payment_status_log_payment_fk
     501        FOREIGN KEY (payment_id) REFERENCES Payment(payment_id)
     502        ON DELETE CASCADE
     503        ON UPDATE CASCADE
     504);
     505
     506CREATE OR REPLACE FUNCTION log_payment_status_change_fn()
     507RETURNS TRIGGER
     508LANGUAGE plpgsql
     509AS $$
     510BEGIN
     511    IF OLD.status IS DISTINCT FROM NEW.status THEN
     512        INSERT INTO Payment_status_log
     513            (payment_id, old_status, new_status, description)
     514        VALUES
     515            (
     516                NEW.payment_id,
     517                OLD.status,
     518                NEW.status,
     519                'Payment status changed from ' || OLD.status || ' to ' || NEW.status
     520            );
     521    END IF;
     522
     523    RETURN NEW;
     524END;
     525$$;
     526
     527DROP TRIGGER IF EXISTS trg_log_payment_status_change ON Payment;
     528
     529CREATE TRIGGER trg_log_payment_status_change
     530AFTER UPDATE OF status ON Payment
     531FOR EACH ROW
     532EXECUTE FUNCTION log_payment_status_change_fn();
     533
     534}}}