wiki:DatabaseProgramming

Функции

-- ============================================================
--  FUNCTION1: го зема возачот, дистанцата и попустот, ги наоѓа цените од неговото возило во базата и враќа финалната цена на вожњата.
-- ============================================================
CREATE OR REPLACE FUNCTION fn_calculate_ride_price(
    p_driver_id       INT,
    p_distance_km     NUMERIC,
    p_discount_pct    NUMERIC DEFAULT 0
)
RETURNS NUMERIC AS $$
DECLARE
    v_base_price    NUMERIC;
    v_price_per_km  NUMERIC;
    v_final_price   NUMERIC;
BEGIN
    -- Земи ги цените според типот на возилото на возачот
    -- Ако има 2 возила → го земаме поскапото (MAX)
    SELECT 
        MAX(pr.base_price),
        MAX(pr.price_per_km)
    INTO 
        v_base_price,
        v_price_per_km
    FROM Drivers_Vehicle_ownership dvo
    JOIN Vehicle_ownership vo ON dvo.Vehicle_ownership_id = vo.Vehicle_ownership_id
    JOIN Vehicle_types     vt ON vo.vehicle_type_id       = vt.vehicle_type_id
    JOIN Pricing_rules     pr ON vt.vehicle_type_id       = pr.vehicle_type_id
    WHERE dvo.driver_id = p_driver_id;

    -- Ако возачот нема возило → fallback на Economy цена
    IF v_base_price IS NULL THEN
        v_base_price   := 80.00;
        v_price_per_km := 40.00;
    END IF;

    -- Пресметај финална цена со попуст
    v_final_price := ROUND(
        (v_base_price + p_distance_km * v_price_per_km) 
        * (1.0 - p_discount_pct / 100.0),
        2
    );

    RETURN v_final_price;
END;
$$ LANGUAGE plpgsql;

                      
--(driver_id,distance_km,discount%)                               
-- Economy возач, 5km, без попуст → (80 + 5*40) * 1.0 = 280.00
SELECT fn_calculate_ride_price(1, 5, 0);

-- Luxury возач, 10km, 10% попуст → (100 + 10*60) * 0.9 = 630.00
SELECT fn_calculate_ride_price(7, 10, 10);



-- ============================================================
--  FUNCTION2: Наоѓа најблискиот слободен возач до pickup локацијата користејќи ги координатите (latitude/longitude). 
-- ============================================================

CREATE OR REPLACE FUNCTION fn_find_nearest_driver(
    p_pickup_location_id INT
)
RETURNS TABLE(
    driver_id           INT,
    driver_name         VARCHAR,
    driver_phone        VARCHAR,
    driver_rating       NUMERIC,
    vehicle_plate       VARCHAR,
    vehicle_class       VARCHAR,
    distance_meters     NUMERIC
) AS $$
DECLARE
    v_pickup_lat  NUMERIC;
    v_pickup_lng  NUMERIC;
BEGIN
    -- Земи ги координатите на pickup локацијата
    SELECT latitude, longitude
    INTO v_pickup_lat, v_pickup_lng
    FROM Locations
    WHERE location_id = p_pickup_location_id;

    IF v_pickup_lat IS NULL THEN
        RAISE EXCEPTION 'Локацијата со ID % не постои', p_pickup_location_id;
    END IF;

    RETURN QUERY
    SELECT
        d.driver_id,
        d.name                                          AS driver_name,
        d.phone                                         AS driver_phone,
        d.rating                                        AS driver_rating,
        vo.plate                                        AS vehicle_plate,
        vt.description                                  AS vehicle_class,
        -- формула за растојание во метри
        ROUND(
            6371000 * 2 * ASIN(SQRT(
                POWER(SIN(RADIANS(l.latitude  - v_pickup_lat) / 2), 2) +
                COS(RADIANS(v_pickup_lat)) *
                COS(RADIANS(l.latitude))  *
                POWER(SIN(RADIANS(l.longitude - v_pickup_lng) / 2), 2)
            ))
        ::NUMERIC, 2)                                   AS distance_meters
    FROM Active_drivers ad
    JOIN Drivers d        ON ad.driver_id             = d.driver_id
    JOIN Locations l      ON ad.current_location_id   = l.location_id
    JOIN Vehicle_ownership vo ON ad.vehicle_ownership_id = vo.vehicle_ownership_id
    JOIN Vehicle_types vt ON vo.vehicle_type_id       = vt.vehicle_type_id
    -- Само слободни и активни возачи
    WHERE ad.is_available = true
      AND d.status_id = (
          SELECT status_id FROM Status 
          WHERE status_tip = 'driver' AND status_text = 'active' LIMIT 1
      )
    ORDER BY distance_meters ASC
    LIMIT 1;
END;
$$ LANGUAGE plpgsql;

-- Најди најблизок возач до локација 1
SELECT * FROM fn_find_nearest_driver(1);
-- Најди најблизок возач до локација 500
SELECT * FROM fn_find_nearest_driver(500);

Процедури

-- ============================================================
--  PROCEDURE 1: sp_request_ride
--  Креира нова вожња: наоѓа најблизок слободен возач,
--  пресметува цена, вметнува во Rides и праќа нотификација.
-- ============================================================
CREATE OR REPLACE PROCEDURE sp_request_ride(
    p_user_id            INT,
    p_pickup_location_id INT,
    p_dropoff_location_id INT,
    p_distance_km        NUMERIC,
    OUT p_ride_id        INT
)
LANGUAGE plpgsql AS $$
DECLARE
    v_driver_id          INT;
    v_vehicle_ownership_id INT;
    v_discount_pct       NUMERIC := 0;
    v_final_price        NUMERIC;
    v_status_requested   INT;
    v_notif_status       INT;
    v_message_id         INT;
BEGIN
    -- 1. Земи го статусот 'requested' за вожњата
    SELECT status_id INTO v_status_requested
    FROM Status
    WHERE status_tip = 'ride' AND status_text = 'requested'
    LIMIT 1;

    -- 2. Провери дали корисникот постои и е активен
    IF NOT EXISTS (
        SELECT 1 FROM Users u
        JOIN Status s ON u.status_id = s.status_id
        WHERE u.user_id = p_user_id AND s.status_text = 'active'
    ) THEN
        RAISE EXCEPTION 'Корисникот со ID % не постои или не е активен', p_user_id;
    END IF;

    -- 3. Провери дали локациите постојат
    IF NOT EXISTS (SELECT 1 FROM Locations WHERE location_id = p_pickup_location_id) THEN
        RAISE EXCEPTION 'Pickup локацијата со ID % не постои', p_pickup_location_id;
    END IF;

    IF NOT EXISTS (SELECT 1 FROM Locations WHERE location_id = p_dropoff_location_id) THEN
        RAISE EXCEPTION 'Dropoff локацијата со ID % не постои', p_dropoff_location_id;
    END IF;

    -- 4. Провери дали дистанцата е валидна
    IF p_distance_km <= 0 THEN
        RAISE EXCEPTION 'Дистанцата мора да биде поголема од 0';
    END IF;

    -- 5. Пресметај попуст: >= 9km → 10%, инаку 0%
    IF p_distance_km >= 9 THEN
        v_discount_pct := 10.0;
    END IF;

    -- 6. Најди најблизок слободен возач (ја повикуваме функцијата fn_find_nearest_driver)
    SELECT driver_id INTO v_driver_id
    FROM fn_find_nearest_driver(p_pickup_location_id);

    IF v_driver_id IS NULL THEN
        -- Ако нема слободен возач → порака 9 (Нема достапни возачи)
        SELECT message_id INTO v_message_id
        FROM Messages WHERE message_text LIKE 'Nemate dostapni%' LIMIT 1;

        SELECT status_id INTO v_notif_status
        FROM Status WHERE status_tip = 'ride' AND status_text = 'cancelled' LIMIT 1;

        -- Вметнуваме "dummy" нотификација без ride_id (не можеме бидејќи ride_id е NOT NULL)
        -- Затоа само фрламе exception
        RAISE EXCEPTION 'Нема достапни возачи во моментот. Обидете се повторно подоцна.';
    END IF;

    -- 7. Земи го vehicle_ownership_id на тој возач (примарното возило)
    SELECT Vehicle_ownership_id INTO v_vehicle_ownership_id
    FROM Drivers_Vehicle_ownership
    WHERE driver_id = v_driver_id
    LIMIT 1;

    -- 8. Пресметај финална цена (ја повикуваме функцијата fn_calculate_ride_price)
    v_final_price := fn_calculate_ride_price(v_driver_id, p_distance_km, v_discount_pct);

    -- 9. Вметни ја вожњата во Rides
    INSERT INTO Rides (
        user_id,
        driver_id,
        vehicle_ownership_id,
        pickup_location_id,
        dropoff_location_id,
        status_id,
        request_time,
        pickup_time,
        start_time,
        end_time,
        distance_km,
        final_price,
        discount_percentage
    ) VALUES (
        p_user_id,
        v_driver_id,
        v_vehicle_ownership_id,
        p_pickup_location_id,
        p_dropoff_location_id,
        v_status_requested,
        NOW(),
        NOW() + interval '5 minutes',   -- очекувано pickup
        NOW() + interval '8 minutes',   -- очекуван почеток
        NOW() + interval '20 minutes',  -- очекуван крај
        p_distance_km,
        v_final_price,
        v_discount_pct
    )
    RETURNING ride_id INTO p_ride_id;

    -- 10. Прати нотификација: "Барањето е успешно испратено"
    SELECT message_id INTO v_message_id
    FROM Messages WHERE message_text LIKE 'Vaseto baranje e uspesno%' LIMIT 1;

    SELECT status_id INTO v_notif_status
    FROM Status WHERE status_tip = 'ride' AND status_text = 'completed' LIMIT 1;

    INSERT INTO Notifications (message_id, sent_time, user_id, ride_id, status_id)
    VALUES (v_message_id, NOW(), p_user_id, p_ride_id, v_notif_status);

    RAISE NOTICE 'Вожњата е успешно креирана: ride_id=%, driver_id=%, цена=% MKD',
        p_ride_id, v_driver_id, v_final_price;

END;
$$;

-- Тест:
DO $$
DECLARE
    v_ride_id INT;
BEGIN
    CALL sp_request_ride(
        p_user_id             => 1000,
        p_pickup_location_id  => 1,
        p_dropoff_location_id => 500,
        p_distance_km         => 4.5,
        p_ride_id             => v_ride_id
    );
    RAISE NOTICE 'Новата вожња има ID: %', v_ride_id;
END $$;



-- ============================================================
--  PROCEDURE 2: sp_complete_ride
--  Ја завршува вожњата: го менува статусот на 'completed',
--  вметнува плаќање и ажурира активноста на возачот.
-- ============================================================
CREATE OR REPLACE PROCEDURE sp_complete_ride(
    p_ride_id         INT,
    p_payment_method  VARCHAR  -- 'CASH' или 'CARD'
)
LANGUAGE plpgsql AS $$
DECLARE
    v_ride              Rides%ROWTYPE;
    v_status_completed  INT;
    v_status_in_prog    INT;
    v_status_accepted   INT;
    v_pay_method_id     INT;
    v_pay_status_id     INT;
    v_notif_status      INT;
    v_message_id        INT;
BEGIN
    -- 1. Земи ги податоците за вожњата со LOCK за да спречиме race condition
    SELECT * INTO v_ride
    FROM Rides
    WHERE ride_id = p_ride_id
    FOR UPDATE;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Вожњата со ID % не постои', p_ride_id;
    END IF;

    -- 2. Земи ги потребните статус ID-а
    SELECT status_id INTO v_status_completed
    FROM Status WHERE status_tip = 'ride' AND status_text = 'completed' LIMIT 1;

    SELECT status_id INTO v_status_in_prog
    FROM Status WHERE status_tip = 'ride' AND status_text = 'in_progress' LIMIT 1;

    SELECT status_id INTO v_status_accepted
    FROM Status WHERE status_tip = 'ride' AND status_text = 'accepted' LIMIT 1;

    -- 3. Провери дека вожњата е во дозволена состојба за да се заврши
    IF v_ride.status_id NOT IN (v_status_in_prog, v_status_accepted) THEN
        RAISE EXCEPTION 'Вожњата со ID % не може да се заврши — тековен статус не е in_progress или accepted', p_ride_id;
    END IF;

    -- 4. Провери дали методот на плаќање е валиден
    SELECT payment_method_id INTO v_pay_method_id
    FROM Payment_methods
    WHERE payment_tip = UPPER(p_payment_method);

    IF v_pay_method_id IS NULL THEN
        RAISE EXCEPTION 'Невалиден метод на плаќање: %. Користете CASH или CARD', p_payment_method;
    END IF;

    -- 5. Ажурирај го статусот на вожњата на 'completed' и end_time на NOW()
    --    (тригерот trg_notify_on_ride_status_change автоматски ќе прати нотификација)
    UPDATE Rides
    SET status_id = v_status_completed,
        end_time  = NOW()
    WHERE ride_id = p_ride_id;

    -- 6. Вметни плаќање
    SELECT status_id INTO v_pay_status_id
    FROM Status WHERE status_tip = 'payment' AND status_text = 'completed' LIMIT 1;

    INSERT INTO Payments (amount, payment_method_id, payment_time, ride_id, status_id)
    VALUES (v_ride.final_price, v_pay_method_id, NOW(), p_ride_id, v_pay_status_id);

    -- 7. Ослободи го возачот → is_available = true
    UPDATE Active_drivers
    SET is_available = true
    WHERE driver_id = v_ride.driver_id;

    RAISE NOTICE 'Вожњата % е завршена. Платено % MKD со %.',
        p_ride_id, v_ride.final_price, UPPER(p_payment_method);

END;
$$;

-- Тест:
-- Прво најди вожња со статус in_progress или accepted
SELECT ride_id, status_id FROM Rides
WHERE status_id IN (
    SELECT status_id FROM Status
    WHERE status_tip='ride' AND status_text IN ('in_progress','accepted')
)
LIMIT 3;

-- Потоа заврши ја
CALL sp_complete_ride(p_ride_id => 3000021, p_payment_method => 'CARD');

-- Провери резултат
SELECT ride_id, status_id, end_time FROM Rides WHERE ride_id = 3000021;
SELECT * FROM Payments WHERE ride_id = 3000021 ORDER BY payment_id DESC LIMIT 1;

Tригери

-- ============================================================
--  TRIGGER1: автоматски го ажурира rating на возачот во Drivers табелата секој пат кога некој ќе додаде нов рејтинг во Ratings табелата.
-- ============================================================
-- Прво правиме функција која тригерот ја повикува
CREATE OR REPLACE FUNCTION fn_update_driver_rating()
RETURNS TRIGGER AS $$
BEGIN
    UPDATE Drivers
    SET rating = (
        SELECT ROUND(AVG(score)::NUMERIC, 2)
        FROM Ratings
        WHERE driver_id = NEW.driver_id
    )
    WHERE driver_id = NEW.driver_id;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Потоа го правиме тригерот
CREATE OR REPLACE TRIGGER trg_update_driver_rating
AFTER INSERT ON Ratings
FOR EACH ROW
EXECUTE FUNCTION fn_update_driver_rating();

-- Погледни го рејтингот пред (4.77)
SELECT driver_id, rating FROM Drivers WHERE driver_id = 40;

-- Додади нов рејтинг
INSERT INTO Ratings (score, comment, created_at, ride_id, user_id, driver_id)
VALUES (5, 'Odlichen vozac!', NOW(), 6675760, 25761, 40);

-- Погледни го рејтингот после → треба автоматски да се смени
SELECT driver_id, rating FROM Drivers WHERE driver_id = 40;

-- ============================================================
--  TRIGGER2:автоматски вметнува нотификација во Notifications кога се менува статусот на вожњата.
-- ============================================================
CREATE OR REPLACE FUNCTION fn_notify_on_ride_status_change()
RETURNS TRIGGER AS $$
DECLARE
    v_message_id    INT;
    v_status_text   VARCHAR;
    v_notif_status  INT;
BEGIN
    -- Земи го текстот на новиот статус
    SELECT status_text INTO v_status_text
    FROM Status WHERE status_id = NEW.status_id;

    -- Одреди која порака да се прати според новиот статус
    v_message_id := CASE v_status_text
        WHEN 'accepted'    THEN (SELECT message_id FROM Messages WHERE message_text LIKE 'Vozacot go prifati%'       LIMIT 1)
        WHEN 'in_progress' THEN (SELECT message_id FROM Messages WHERE message_text LIKE 'Vozacot pristigna%'        LIMIT 1)
        WHEN 'completed'   THEN (SELECT message_id FROM Messages WHERE message_text LIKE 'Plakjanjeto e uspesno%'    LIMIT 1)
        WHEN 'cancelled'   THEN (SELECT message_id FROM Messages WHERE message_text LIKE 'Vozenjeto e otkazano%'     LIMIT 1)
        ELSE NULL
    END;

    -- Ако нема порака за овој статус → не прави ништо
    IF v_message_id IS NULL THEN
        RETURN NEW;
    END IF;

    -- Статус на нотификацијата
    SELECT status_id INTO v_notif_status
    FROM Status WHERE status_tip = 'ride' AND status_text = 'completed' LIMIT 1;

    -- Вметни нотификација
    INSERT INTO Notifications (message_id, sent_time, user_id, ride_id, status_id)
    VALUES (v_message_id, NOW(), NEW.user_id, NEW.ride_id, v_notif_status);

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER trg_notify_on_ride_status_change
AFTER UPDATE OF status_id ON Rides
FOR EACH ROW
WHEN (OLD.status_id IS DISTINCT FROM NEW.status_id)
EXECUTE FUNCTION fn_notify_on_ride_status_change();

SELECT * FROM Notifications WHERE ride_id = 12907878;
-- Смени статус на вожњата
UPDATE Rides SET status_id = (
    SELECT status_id FROM Status 
    WHERE status_tip='ride' AND status_text='accepted' LIMIT 1
) WHERE ride_id = 12907878;

-- Провери дали се додала нотификација автоматски
SELECT * FROM Notifications WHERE ride_id = 12907878;

--===============================================
--za da najdeme nekoj ride ss cancelled status
--===============================================
SELECT * FROM Notifications;
-- Najdi ride koj ima status cancelled
SELECT ride_id, status_id FROM Rides 
WHERE status_id = (
    SELECT status_id FROM Status 
    WHERE status_tip='ride' AND status_text='cancelled' LIMIT 1
)
LIMIT 5;
Last modified 5 days ago Last modified on 05/20/26 16:32:39
Note: See TracWiki for help on using the wiki.