Changes between Initial Version and Version 1 of DatabaseProgramming


Ignore:
Timestamp:
05/20/26 16:32:39 (5 days ago)
Author:
231055
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • DatabaseProgramming

    v1 v1  
     1== Функции ==
     2{{{#!sql
     3-- ============================================================
     4--  FUNCTION1: го зема возачот, дистанцата и попустот, ги наоѓа цените од неговото возило во базата и враќа финалната цена на вожњата.
     5-- ============================================================
     6CREATE OR REPLACE FUNCTION fn_calculate_ride_price(
     7    p_driver_id       INT,
     8    p_distance_km     NUMERIC,
     9    p_discount_pct    NUMERIC DEFAULT 0
     10)
     11RETURNS NUMERIC AS $$
     12DECLARE
     13    v_base_price    NUMERIC;
     14    v_price_per_km  NUMERIC;
     15    v_final_price   NUMERIC;
     16BEGIN
     17    -- Земи ги цените според типот на возилото на возачот
     18    -- Ако има 2 возила → го земаме поскапото (MAX)
     19    SELECT
     20        MAX(pr.base_price),
     21        MAX(pr.price_per_km)
     22    INTO
     23        v_base_price,
     24        v_price_per_km
     25    FROM Drivers_Vehicle_ownership dvo
     26    JOIN Vehicle_ownership vo ON dvo.Vehicle_ownership_id = vo.Vehicle_ownership_id
     27    JOIN Vehicle_types     vt ON vo.vehicle_type_id       = vt.vehicle_type_id
     28    JOIN Pricing_rules     pr ON vt.vehicle_type_id       = pr.vehicle_type_id
     29    WHERE dvo.driver_id = p_driver_id;
     30
     31    -- Ако возачот нема возило → fallback на Economy цена
     32    IF v_base_price IS NULL THEN
     33        v_base_price   := 80.00;
     34        v_price_per_km := 40.00;
     35    END IF;
     36
     37    -- Пресметај финална цена со попуст
     38    v_final_price := ROUND(
     39        (v_base_price + p_distance_km * v_price_per_km)
     40        * (1.0 - p_discount_pct / 100.0),
     41        2
     42    );
     43
     44    RETURN v_final_price;
     45END;
     46$$ LANGUAGE plpgsql;
     47
     48                     
     49--(driver_id,distance_km,discount%)                               
     50-- Economy возач, 5km, без попуст → (80 + 5*40) * 1.0 = 280.00
     51SELECT fn_calculate_ride_price(1, 5, 0);
     52
     53-- Luxury возач, 10km, 10% попуст → (100 + 10*60) * 0.9 = 630.00
     54SELECT fn_calculate_ride_price(7, 10, 10);
     55
     56
     57
     58-- ============================================================
     59--  FUNCTION2: Наоѓа најблискиот слободен возач до pickup локацијата користејќи ги координатите (latitude/longitude).
     60-- ============================================================
     61
     62CREATE OR REPLACE FUNCTION fn_find_nearest_driver(
     63    p_pickup_location_id INT
     64)
     65RETURNS TABLE(
     66    driver_id           INT,
     67    driver_name         VARCHAR,
     68    driver_phone        VARCHAR,
     69    driver_rating       NUMERIC,
     70    vehicle_plate       VARCHAR,
     71    vehicle_class       VARCHAR,
     72    distance_meters     NUMERIC
     73) AS $$
     74DECLARE
     75    v_pickup_lat  NUMERIC;
     76    v_pickup_lng  NUMERIC;
     77BEGIN
     78    -- Земи ги координатите на pickup локацијата
     79    SELECT latitude, longitude
     80    INTO v_pickup_lat, v_pickup_lng
     81    FROM Locations
     82    WHERE location_id = p_pickup_location_id;
     83
     84    IF v_pickup_lat IS NULL THEN
     85        RAISE EXCEPTION 'Локацијата со ID % не постои', p_pickup_location_id;
     86    END IF;
     87
     88    RETURN QUERY
     89    SELECT
     90        d.driver_id,
     91        d.name                                          AS driver_name,
     92        d.phone                                         AS driver_phone,
     93        d.rating                                        AS driver_rating,
     94        vo.plate                                        AS vehicle_plate,
     95        vt.description                                  AS vehicle_class,
     96        -- формула за растојание во метри
     97        ROUND(
     98            6371000 * 2 * ASIN(SQRT(
     99                POWER(SIN(RADIANS(l.latitude  - v_pickup_lat) / 2), 2) +
     100                COS(RADIANS(v_pickup_lat)) *
     101                COS(RADIANS(l.latitude))  *
     102                POWER(SIN(RADIANS(l.longitude - v_pickup_lng) / 2), 2)
     103            ))
     104        ::NUMERIC, 2)                                   AS distance_meters
     105    FROM Active_drivers ad
     106    JOIN Drivers d        ON ad.driver_id             = d.driver_id
     107    JOIN Locations l      ON ad.current_location_id   = l.location_id
     108    JOIN Vehicle_ownership vo ON ad.vehicle_ownership_id = vo.vehicle_ownership_id
     109    JOIN Vehicle_types vt ON vo.vehicle_type_id       = vt.vehicle_type_id
     110    -- Само слободни и активни возачи
     111    WHERE ad.is_available = true
     112      AND d.status_id = (
     113          SELECT status_id FROM Status
     114          WHERE status_tip = 'driver' AND status_text = 'active' LIMIT 1
     115      )
     116    ORDER BY distance_meters ASC
     117    LIMIT 1;
     118END;
     119$$ LANGUAGE plpgsql;
     120
     121-- Најди најблизок возач до локација 1
     122SELECT * FROM fn_find_nearest_driver(1);
     123-- Најди најблизок возач до локација 500
     124SELECT * FROM fn_find_nearest_driver(500);
     125}}}
     126
     127== Процедури ==
     128{{{#!sql
     129-- ============================================================
     130--  PROCEDURE 1: sp_request_ride
     131--  Креира нова вожња: наоѓа најблизок слободен возач,
     132--  пресметува цена, вметнува во Rides и праќа нотификација.
     133-- ============================================================
     134CREATE OR REPLACE PROCEDURE sp_request_ride(
     135    p_user_id            INT,
     136    p_pickup_location_id INT,
     137    p_dropoff_location_id INT,
     138    p_distance_km        NUMERIC,
     139    OUT p_ride_id        INT
     140)
     141LANGUAGE plpgsql AS $$
     142DECLARE
     143    v_driver_id          INT;
     144    v_vehicle_ownership_id INT;
     145    v_discount_pct       NUMERIC := 0;
     146    v_final_price        NUMERIC;
     147    v_status_requested   INT;
     148    v_notif_status       INT;
     149    v_message_id         INT;
     150BEGIN
     151    -- 1. Земи го статусот 'requested' за вожњата
     152    SELECT status_id INTO v_status_requested
     153    FROM Status
     154    WHERE status_tip = 'ride' AND status_text = 'requested'
     155    LIMIT 1;
     156
     157    -- 2. Провери дали корисникот постои и е активен
     158    IF NOT EXISTS (
     159        SELECT 1 FROM Users u
     160        JOIN Status s ON u.status_id = s.status_id
     161        WHERE u.user_id = p_user_id AND s.status_text = 'active'
     162    ) THEN
     163        RAISE EXCEPTION 'Корисникот со ID % не постои или не е активен', p_user_id;
     164    END IF;
     165
     166    -- 3. Провери дали локациите постојат
     167    IF NOT EXISTS (SELECT 1 FROM Locations WHERE location_id = p_pickup_location_id) THEN
     168        RAISE EXCEPTION 'Pickup локацијата со ID % не постои', p_pickup_location_id;
     169    END IF;
     170
     171    IF NOT EXISTS (SELECT 1 FROM Locations WHERE location_id = p_dropoff_location_id) THEN
     172        RAISE EXCEPTION 'Dropoff локацијата со ID % не постои', p_dropoff_location_id;
     173    END IF;
     174
     175    -- 4. Провери дали дистанцата е валидна
     176    IF p_distance_km <= 0 THEN
     177        RAISE EXCEPTION 'Дистанцата мора да биде поголема од 0';
     178    END IF;
     179
     180    -- 5. Пресметај попуст: >= 9km → 10%, инаку 0%
     181    IF p_distance_km >= 9 THEN
     182        v_discount_pct := 10.0;
     183    END IF;
     184
     185    -- 6. Најди најблизок слободен возач (ја повикуваме функцијата fn_find_nearest_driver)
     186    SELECT driver_id INTO v_driver_id
     187    FROM fn_find_nearest_driver(p_pickup_location_id);
     188
     189    IF v_driver_id IS NULL THEN
     190        -- Ако нема слободен возач → порака 9 (Нема достапни возачи)
     191        SELECT message_id INTO v_message_id
     192        FROM Messages WHERE message_text LIKE 'Nemate dostapni%' LIMIT 1;
     193
     194        SELECT status_id INTO v_notif_status
     195        FROM Status WHERE status_tip = 'ride' AND status_text = 'cancelled' LIMIT 1;
     196
     197        -- Вметнуваме "dummy" нотификација без ride_id (не можеме бидејќи ride_id е NOT NULL)
     198        -- Затоа само фрламе exception
     199        RAISE EXCEPTION 'Нема достапни возачи во моментот. Обидете се повторно подоцна.';
     200    END IF;
     201
     202    -- 7. Земи го vehicle_ownership_id на тој возач (примарното возило)
     203    SELECT Vehicle_ownership_id INTO v_vehicle_ownership_id
     204    FROM Drivers_Vehicle_ownership
     205    WHERE driver_id = v_driver_id
     206    LIMIT 1;
     207
     208    -- 8. Пресметај финална цена (ја повикуваме функцијата fn_calculate_ride_price)
     209    v_final_price := fn_calculate_ride_price(v_driver_id, p_distance_km, v_discount_pct);
     210
     211    -- 9. Вметни ја вожњата во Rides
     212    INSERT INTO Rides (
     213        user_id,
     214        driver_id,
     215        vehicle_ownership_id,
     216        pickup_location_id,
     217        dropoff_location_id,
     218        status_id,
     219        request_time,
     220        pickup_time,
     221        start_time,
     222        end_time,
     223        distance_km,
     224        final_price,
     225        discount_percentage
     226    ) VALUES (
     227        p_user_id,
     228        v_driver_id,
     229        v_vehicle_ownership_id,
     230        p_pickup_location_id,
     231        p_dropoff_location_id,
     232        v_status_requested,
     233        NOW(),
     234        NOW() + interval '5 minutes',   -- очекувано pickup
     235        NOW() + interval '8 minutes',   -- очекуван почеток
     236        NOW() + interval '20 minutes',  -- очекуван крај
     237        p_distance_km,
     238        v_final_price,
     239        v_discount_pct
     240    )
     241    RETURNING ride_id INTO p_ride_id;
     242
     243    -- 10. Прати нотификација: "Барањето е успешно испратено"
     244    SELECT message_id INTO v_message_id
     245    FROM Messages WHERE message_text LIKE 'Vaseto baranje e uspesno%' LIMIT 1;
     246
     247    SELECT status_id INTO v_notif_status
     248    FROM Status WHERE status_tip = 'ride' AND status_text = 'completed' LIMIT 1;
     249
     250    INSERT INTO Notifications (message_id, sent_time, user_id, ride_id, status_id)
     251    VALUES (v_message_id, NOW(), p_user_id, p_ride_id, v_notif_status);
     252
     253    RAISE NOTICE 'Вожњата е успешно креирана: ride_id=%, driver_id=%, цена=% MKD',
     254        p_ride_id, v_driver_id, v_final_price;
     255
     256END;
     257$$;
     258
     259-- Тест:
     260DO $$
     261DECLARE
     262    v_ride_id INT;
     263BEGIN
     264    CALL sp_request_ride(
     265        p_user_id             => 1000,
     266        p_pickup_location_id  => 1,
     267        p_dropoff_location_id => 500,
     268        p_distance_km         => 4.5,
     269        p_ride_id             => v_ride_id
     270    );
     271    RAISE NOTICE 'Новата вожња има ID: %', v_ride_id;
     272END $$;
     273
     274
     275
     276-- ============================================================
     277--  PROCEDURE 2: sp_complete_ride
     278--  Ја завршува вожњата: го менува статусот на 'completed',
     279--  вметнува плаќање и ажурира активноста на возачот.
     280-- ============================================================
     281CREATE OR REPLACE PROCEDURE sp_complete_ride(
     282    p_ride_id         INT,
     283    p_payment_method  VARCHAR  -- 'CASH' или 'CARD'
     284)
     285LANGUAGE plpgsql AS $$
     286DECLARE
     287    v_ride              Rides%ROWTYPE;
     288    v_status_completed  INT;
     289    v_status_in_prog    INT;
     290    v_status_accepted   INT;
     291    v_pay_method_id     INT;
     292    v_pay_status_id     INT;
     293    v_notif_status      INT;
     294    v_message_id        INT;
     295BEGIN
     296    -- 1. Земи ги податоците за вожњата со LOCK за да спречиме race condition
     297    SELECT * INTO v_ride
     298    FROM Rides
     299    WHERE ride_id = p_ride_id
     300    FOR UPDATE;
     301
     302    IF NOT FOUND THEN
     303        RAISE EXCEPTION 'Вожњата со ID % не постои', p_ride_id;
     304    END IF;
     305
     306    -- 2. Земи ги потребните статус ID-а
     307    SELECT status_id INTO v_status_completed
     308    FROM Status WHERE status_tip = 'ride' AND status_text = 'completed' LIMIT 1;
     309
     310    SELECT status_id INTO v_status_in_prog
     311    FROM Status WHERE status_tip = 'ride' AND status_text = 'in_progress' LIMIT 1;
     312
     313    SELECT status_id INTO v_status_accepted
     314    FROM Status WHERE status_tip = 'ride' AND status_text = 'accepted' LIMIT 1;
     315
     316    -- 3. Провери дека вожњата е во дозволена состојба за да се заврши
     317    IF v_ride.status_id NOT IN (v_status_in_prog, v_status_accepted) THEN
     318        RAISE EXCEPTION 'Вожњата со ID % не може да се заврши — тековен статус не е in_progress или accepted', p_ride_id;
     319    END IF;
     320
     321    -- 4. Провери дали методот на плаќање е валиден
     322    SELECT payment_method_id INTO v_pay_method_id
     323    FROM Payment_methods
     324    WHERE payment_tip = UPPER(p_payment_method);
     325
     326    IF v_pay_method_id IS NULL THEN
     327        RAISE EXCEPTION 'Невалиден метод на плаќање: %. Користете CASH или CARD', p_payment_method;
     328    END IF;
     329
     330    -- 5. Ажурирај го статусот на вожњата на 'completed' и end_time на NOW()
     331    --    (тригерот trg_notify_on_ride_status_change автоматски ќе прати нотификација)
     332    UPDATE Rides
     333    SET status_id = v_status_completed,
     334        end_time  = NOW()
     335    WHERE ride_id = p_ride_id;
     336
     337    -- 6. Вметни плаќање
     338    SELECT status_id INTO v_pay_status_id
     339    FROM Status WHERE status_tip = 'payment' AND status_text = 'completed' LIMIT 1;
     340
     341    INSERT INTO Payments (amount, payment_method_id, payment_time, ride_id, status_id)
     342    VALUES (v_ride.final_price, v_pay_method_id, NOW(), p_ride_id, v_pay_status_id);
     343
     344    -- 7. Ослободи го возачот → is_available = true
     345    UPDATE Active_drivers
     346    SET is_available = true
     347    WHERE driver_id = v_ride.driver_id;
     348
     349    RAISE NOTICE 'Вожњата % е завршена. Платено % MKD со %.',
     350        p_ride_id, v_ride.final_price, UPPER(p_payment_method);
     351
     352END;
     353$$;
     354
     355-- Тест:
     356-- Прво најди вожња со статус in_progress или accepted
     357SELECT ride_id, status_id FROM Rides
     358WHERE status_id IN (
     359    SELECT status_id FROM Status
     360    WHERE status_tip='ride' AND status_text IN ('in_progress','accepted')
     361)
     362LIMIT 3;
     363
     364-- Потоа заврши ја
     365CALL sp_complete_ride(p_ride_id => 3000021, p_payment_method => 'CARD');
     366
     367-- Провери резултат
     368SELECT ride_id, status_id, end_time FROM Rides WHERE ride_id = 3000021;
     369SELECT * FROM Payments WHERE ride_id = 3000021 ORDER BY payment_id DESC LIMIT 1;
     370}}}
     371
     372== Tригери ==
     373{{{#!sql
     374-- ============================================================
     375--  TRIGGER1: автоматски го ажурира rating на возачот во Drivers табелата секој пат кога некој ќе додаде нов рејтинг во Ratings табелата.
     376-- ============================================================
     377-- Прво правиме функција која тригерот ја повикува
     378CREATE OR REPLACE FUNCTION fn_update_driver_rating()
     379RETURNS TRIGGER AS $$
     380BEGIN
     381    UPDATE Drivers
     382    SET rating = (
     383        SELECT ROUND(AVG(score)::NUMERIC, 2)
     384        FROM Ratings
     385        WHERE driver_id = NEW.driver_id
     386    )
     387    WHERE driver_id = NEW.driver_id;
     388
     389    RETURN NEW;
     390END;
     391$$ LANGUAGE plpgsql;
     392
     393-- Потоа го правиме тригерот
     394CREATE OR REPLACE TRIGGER trg_update_driver_rating
     395AFTER INSERT ON Ratings
     396FOR EACH ROW
     397EXECUTE FUNCTION fn_update_driver_rating();
     398
     399-- Погледни го рејтингот пред (4.77)
     400SELECT driver_id, rating FROM Drivers WHERE driver_id = 40;
     401
     402-- Додади нов рејтинг
     403INSERT INTO Ratings (score, comment, created_at, ride_id, user_id, driver_id)
     404VALUES (5, 'Odlichen vozac!', NOW(), 6675760, 25761, 40);
     405
     406-- Погледни го рејтингот после → треба автоматски да се смени
     407SELECT driver_id, rating FROM Drivers WHERE driver_id = 40;
     408
     409-- ============================================================
     410--  TRIGGER2:автоматски вметнува нотификација во Notifications кога се менува статусот на вожњата.
     411-- ============================================================
     412CREATE OR REPLACE FUNCTION fn_notify_on_ride_status_change()
     413RETURNS TRIGGER AS $$
     414DECLARE
     415    v_message_id    INT;
     416    v_status_text   VARCHAR;
     417    v_notif_status  INT;
     418BEGIN
     419    -- Земи го текстот на новиот статус
     420    SELECT status_text INTO v_status_text
     421    FROM Status WHERE status_id = NEW.status_id;
     422
     423    -- Одреди која порака да се прати според новиот статус
     424    v_message_id := CASE v_status_text
     425        WHEN 'accepted'    THEN (SELECT message_id FROM Messages WHERE message_text LIKE 'Vozacot go prifati%'       LIMIT 1)
     426        WHEN 'in_progress' THEN (SELECT message_id FROM Messages WHERE message_text LIKE 'Vozacot pristigna%'        LIMIT 1)
     427        WHEN 'completed'   THEN (SELECT message_id FROM Messages WHERE message_text LIKE 'Plakjanjeto e uspesno%'    LIMIT 1)
     428        WHEN 'cancelled'   THEN (SELECT message_id FROM Messages WHERE message_text LIKE 'Vozenjeto e otkazano%'     LIMIT 1)
     429        ELSE NULL
     430    END;
     431
     432    -- Ако нема порака за овој статус → не прави ништо
     433    IF v_message_id IS NULL THEN
     434        RETURN NEW;
     435    END IF;
     436
     437    -- Статус на нотификацијата
     438    SELECT status_id INTO v_notif_status
     439    FROM Status WHERE status_tip = 'ride' AND status_text = 'completed' LIMIT 1;
     440
     441    -- Вметни нотификација
     442    INSERT INTO Notifications (message_id, sent_time, user_id, ride_id, status_id)
     443    VALUES (v_message_id, NOW(), NEW.user_id, NEW.ride_id, v_notif_status);
     444
     445    RETURN NEW;
     446END;
     447$$ LANGUAGE plpgsql;
     448
     449CREATE OR REPLACE TRIGGER trg_notify_on_ride_status_change
     450AFTER UPDATE OF status_id ON Rides
     451FOR EACH ROW
     452WHEN (OLD.status_id IS DISTINCT FROM NEW.status_id)
     453EXECUTE FUNCTION fn_notify_on_ride_status_change();
     454
     455SELECT * FROM Notifications WHERE ride_id = 12907878;
     456-- Смени статус на вожњата
     457UPDATE Rides SET status_id = (
     458    SELECT status_id FROM Status
     459    WHERE status_tip='ride' AND status_text='accepted' LIMIT 1
     460) WHERE ride_id = 12907878;
     461
     462-- Провери дали се додала нотификација автоматски
     463SELECT * FROM Notifications WHERE ride_id = 12907878;
     464
     465--===============================================
     466--za da najdeme nekoj ride ss cancelled status
     467--===============================================
     468SELECT * FROM Notifications;
     469-- Najdi ride koj ima status cancelled
     470SELECT ride_id, status_id FROM Rides
     471WHERE status_id = (
     472    SELECT status_id FROM Status
     473    WHERE status_tip='ride' AND status_text='cancelled' LIMIT 1
     474)
     475LIMIT 5;
     476}}}
     477
     478
     479