= Функции, Процедури и Тригери =
[[html(faza4.sql)]]
Во оваа фаза беа имплементирани функции, процедури и тригери кои ја поддржуваат основната бизнис логика на системот за booking на артисти и бендови. Секоја компонента е одговорна за одредена операција — од пресметување статистики, преку управување со bookings, до автоматска заштита од грешки.
== Функции ==
Функциите се користат за пресметување и враќање на вредности кои се потребни на повеќе места во апликацијата. Секоја функција прима параметри, извршува логика врз базата и враќа резултат.
=== 1. fn_artist_average_rating ===
Функцијата {{{fn_artist_average_rating}}} го пресметува просечниот рејтинг на артист или бенд врз основа на сите reviews оставени за нивните завршени bookings.
Ги поврзува табелите {{{Review}}}, {{{Booking}}} и {{{Offer}}} за да го пронајде просекот на рејтинзите за конкретниот артист. Ако артистот сè уште нема reviews, функцијата враќа {{{0}}} наместо {{{NULL}}} преку употреба на {{{COALESCE}}}.
{{{
CREATE OR REPLACE FUNCTION fn_artist_average_rating(
p_bookable_id INT
)
RETURNS NUMERIC(3,2)
AS
$$
DECLARE
v_average_rating NUMERIC(3,2);
BEGIN
SELECT
ROUND(AVG(r.rating), 2)
INTO v_average_rating
FROM Review r
JOIN Booking bk
ON r.booking_id = bk.booking_id
JOIN Offer o
ON bk.offer_id = o.offer_id
WHERE o.bookable_id = p_bookable_id;
RETURN COALESCE(v_average_rating, 0);
END;
$$
LANGUAGE plpgsql;
}}}
=== 2. fn_artist_total_earnings ===
Функцијата {{{fn_artist_total_earnings}}} ја пресметува вкупната заработка на артист или бенд земајќи ги предвид само успешно завршените плаќања со статус {{{PAID}}}.
Ги поврзува табелите {{{Payment}}}, {{{Booking}}} и {{{Offer}}} и го сумира износот на сите потврдени плаќања. Ако артистот сè уште нема реализирани плаќања, функцијата враќа {{{0}}} наместо {{{NULL}}}.
{{{
CREATE OR REPLACE FUNCTION fn_artist_total_earnings(
p_bookable_id INT
)
RETURNS NUMERIC(10,2)
AS
$$
DECLARE
v_total NUMERIC(10,2);
BEGIN
SELECT
SUM(p.amount)
INTO v_total
FROM Payment p
JOIN Booking bk
ON p.booking_id = bk.booking_id
JOIN Offer o
ON bk.offer_id = o.offer_id
WHERE o.bookable_id = p_bookable_id
AND p.payment_status = 'PAID';
RETURN COALESCE(v_total, 0);
END;
$$
LANGUAGE plpgsql;
}}}
=== 3. fn_check_artist_availability ===
Функцијата {{{fn_check_artist_availability}}} проверува дали артист или бенд има слободен термин на одреден датум и враќа {{{TRUE}}} или {{{FALSE}}}.
Наместо {{{COUNT(*)}}} кој ги брои сите редици, функцијата користи {{{EXISTS}}} кој застанува веднаш при првиот пронајден слободен слот — со што е значително поефикасна.
{{{
CREATE OR REPLACE FUNCTION fn_check_artist_availability(
p_bookable_id INT,
p_date DATE
)
RETURNS BOOLEAN
AS
$$
DECLARE
v_exists BOOLEAN;
BEGIN
SELECT EXISTS(
SELECT 1
FROM AvailabilitySlot
WHERE bookable_id = p_bookable_id
AND DATE(start_datetime) = p_date
AND status = 'AVAILABLE'
)
INTO v_exists;
RETURN v_exists;
END;
$$
LANGUAGE plpgsql;
}}}
=== 4. fn_total_client_bookings ===
Функцијата {{{fn_total_client_bookings}}} го враќа вкупниот број на bookings направени од одреден клиент.
Ги поврзува табелите {{{BookingRequest}}}, {{{Offer}}} и {{{Booking}}} за да го пресмета бројот на сите bookings поврзани со клиентот, без разлика на нивниот статус.
{{{
CREATE OR REPLACE FUNCTION fn_total_client_bookings(
p_client_id INT
)
RETURNS INT
AS
$$
DECLARE
v_total INT;
BEGIN
SELECT COUNT(*)
INTO v_total
FROM BookingRequest br
JOIN Offer o
ON br.request_id = o.request_id
JOIN Booking bk
ON bk.offer_id = o.offer_id
WHERE br.client_id = p_client_id;
RETURN v_total;
END;
$$
LANGUAGE plpgsql;
}}}
== Процедури ==
Процедурите се користат за извршување на акции врз базата — за разлика од функциите, тие не враќаат вредност туку вршат промени. Секоја процедура содржи валидација пред да изврши каква било промена, со цел да се спречат невалидни состојби во базата.
=== 1. sp_create_booking_request ===
Процедурата {{{sp_create_booking_request}}} креира нов барање за booking од страна на клиент.
Пред да го изврши вметнувањето, процедурата врши три валидации:
* Проверува дали клиентот постои во табелата {{{ClientProfile}}}
* Проверува дали локацијата постои во табелата {{{Location}}}
* Проверува дали датумот на настанот е во иднина
Ако некоја од валидациите не помине, се фрла исклучок со јасна порака за грешка.
{{{
CREATE OR REPLACE PROCEDURE sp_create_booking_request(
p_client_id INT,
p_duration_id INT,
p_event_type VARCHAR,
p_event_date DATE,
p_location_id INT
)
LANGUAGE plpgsql
AS
$$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM ClientProfile WHERE client_id = p_client_id
) THEN
RAISE EXCEPTION 'Client with ID % does not exist!', p_client_id;
END IF;
IF NOT EXISTS (
SELECT 1 FROM Location WHERE location_id = p_location_id
) THEN
RAISE EXCEPTION 'Location with ID % does not exist!', p_location_id;
END IF;
IF p_event_date <= CURRENT_DATE THEN
RAISE EXCEPTION 'Event date must be in the future!';
END IF;
INSERT INTO BookingRequest(
client_id,
duration_id,
event_type,
event_date,
location_id
)
VALUES (
p_client_id,
p_duration_id,
p_event_type,
p_event_date,
p_location_id
);
END;
$$;
}}}
=== 2. sp_cancel_booking ===
Процедурата {{{sp_cancel_booking}}} го откажува постоечки booking со менување на неговиот статус во {{{CANCELLED}}}.
Пред да го изврши откажувањето, процедурата врши три проверки:
* Проверува дали booking со дадениот ID воопшто постои
* Проверува дали booking не е веќе откажан
* Проверува дали booking не е веќе завршен — завршените bookings не можат да се откажат
Без овие проверки, повикот со непостоечки ID би поминал тивко без никаква промена или грешка.
{{{
CREATE OR REPLACE PROCEDURE sp_cancel_booking(
p_booking_id INT
)
LANGUAGE plpgsql
AS
$$
DECLARE
v_current_status VARCHAR(30);
BEGIN
SELECT booking_status
INTO v_current_status
FROM Booking
WHERE booking_id = p_booking_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Booking with ID % does not exist!', p_booking_id;
END IF;
IF v_current_status = 'CANCELLED' THEN
RAISE EXCEPTION 'Booking % is already cancelled!', p_booking_id;
END IF;
IF v_current_status = 'COMPLETED' THEN
RAISE EXCEPTION 'Booking % is already completed and cannot be cancelled!', p_booking_id;
END IF;
UPDATE Booking
SET booking_status = 'CANCELLED'
WHERE booking_id = p_booking_id;
END;
$$;
}}}
=== 3. sp_create_payment ===
Процедурата {{{sp_create_payment}}} креира ново плаќање поврзано со одреден booking.
Клучна поправка во однос на оригиналната верзија е дека плаќањето се креира со статус {{{PENDING}}} наместо веднаш {{{PAID}}}. Ова одговара на реалниот тек — плаќањето прво се иницира, па дури потоа се потврдува.
Процедурата врши три валидации:
* Проверува дали booking постои
* Проверува дали износот е позитивен број
* Проверува дали веќе не постои плаќање за истиот booking
{{{
CREATE OR REPLACE PROCEDURE sp_create_payment(
p_booking_id INT,
p_amount NUMERIC(10,2)
)
LANGUAGE plpgsql
AS
$$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM Booking WHERE booking_id = p_booking_id
) THEN
RAISE EXCEPTION 'Booking with ID % does not exist!', p_booking_id;
END IF;
IF p_amount <= 0 THEN
RAISE EXCEPTION 'Payment amount must be greater than 0!';
END IF;
IF EXISTS (
SELECT 1 FROM Payment WHERE booking_id = p_booking_id
) THEN
RAISE EXCEPTION 'Payment for booking % already exists!', p_booking_id;
END IF;
INSERT INTO Payment(
booking_id,
amount,
payment_status
)
VALUES (
p_booking_id,
p_amount,
'PENDING'
);
END;
$$;
}}}
== Тригери ==
Тригерите се специјални функции кои се извршуваат автоматски при одредени настани во базата — без потреба апликацијата да ги повикува рачно. Во системот се имплементирани три тригери кои обезбедуваат конзистентност и интегритет на податоците.
=== 1. trg_prevent_double_booking ===
Тригерот {{{trg_prevent_double_booking}}} спречува двоен booking — ситуација каде ист артист или бенд би бил потврден за два различни настани на ист датум.
Се извршува автоматски '''пред''' секој нов запис во табелата {{{Booking}}} ({{{BEFORE INSERT}}}). Доколку за истиот артист веќе постои {{{CONFIRMED}}} booking на истиот датум, тригерот фрла исклучок и го спречува вметнувањето.
{{{
CREATE OR REPLACE FUNCTION fn_prevent_double_booking()
RETURNS TRIGGER
AS
$$
DECLARE
v_count INT;
v_bookable INT;
v_event_date DATE;
BEGIN
SELECT o.bookable_id, br.event_date
INTO v_bookable, v_event_date
FROM Offer o
JOIN BookingRequest br ON br.request_id = o.request_id
WHERE o.offer_id = NEW.offer_id;
SELECT COUNT(*)
INTO v_count
FROM Booking b
JOIN Offer o
ON b.offer_id = o.offer_id
JOIN BookingRequest br
ON br.request_id = o.request_id
WHERE o.bookable_id = v_bookable
AND br.event_date = v_event_date
AND b.booking_status = 'CONFIRMED';
IF v_count > 0 THEN
RAISE EXCEPTION 'Artist/Band with ID % is already booked on %!',
v_bookable, v_event_date;
END IF;
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER trg_prevent_double_booking
BEFORE INSERT ON Booking
FOR EACH ROW
EXECUTE FUNCTION fn_prevent_double_booking();
}}}
=== 2. trg_update_slot_status ===
Тригерот {{{trg_update_slot_status}}} автоматски го означува слободниот термин на артистот како {{{BOOKED}}} во моментот кога се креира нов booking.
Се извршува автоматски '''по''' секој нов запис во табелата {{{Booking}}} ({{{AFTER INSERT}}}). Го пронаоѓа соодветниот {{{AvailabilitySlot}}} според артистот и датумот на настанот и го менува неговиот статус — со тоа се спречува двојна резервација на ист термин.
{{{
CREATE OR REPLACE FUNCTION fn_update_slot_status()
RETURNS TRIGGER
AS
$$
DECLARE
v_bookable INT;
v_event_date DATE;
BEGIN
SELECT o.bookable_id, br.event_date
INTO v_bookable, v_event_date
FROM Offer o
JOIN BookingRequest br ON br.request_id = o.request_id
WHERE o.offer_id = NEW.offer_id;
UPDATE AvailabilitySlot
SET status = 'BOOKED'
WHERE bookable_id = v_bookable
AND DATE(start_datetime) = v_event_date
AND status = 'AVAILABLE';
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER trg_update_slot_status
AFTER INSERT ON Booking
FOR EACH ROW
EXECUTE FUNCTION fn_update_slot_status();
}}}
=== 3. trg_booking_status_history ===
Тригерот {{{trg_booking_status_history}}} автоматски го запишува секој пат кога статусот на booking се менува, формирајќи целосен историјат на промени.
Се извршува автоматски '''по''' секој {{{UPDATE}}} на табелата {{{Booking}}}, но само кога вредноста на {{{booking_status}}} навистина се променила — ова се постигнува со условот {{{WHEN (OLD.booking_status IS DISTINCT FROM NEW.booking_status)}}}. Со тоа се избегнува непотребно запишување при update-и кои не го менуваат статусот.
{{{
CREATE OR REPLACE FUNCTION fn_booking_status_history()
RETURNS TRIGGER
AS
$$
BEGIN
INSERT INTO BookingStatusHistory(
booking_id,
new_status,
changed_at
)
VALUES (
NEW.booking_id,
NEW.booking_status,
NOW()
);
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER trg_booking_status_history
AFTER UPDATE ON Booking
FOR EACH ROW
WHEN (OLD.booking_status IS DISTINCT FROM NEW.booking_status)
EXECUTE FUNCTION fn_booking_status_history();
}}}