| | 1 | = Напредни бази на податоци = |
| | 2 | = Фаза 5 — Функции, процедури и тригери = |
| | 3 | == Проект: DriveNet == |
| | 4 | |
| | 5 | Александар Милошевски 231138 |
| | 6 | Исидора Кузмановска 231052 |
| | 7 | Андон Михајлов 231016 |
| | 8 | |
| | 9 | Во оваа фаза ги имплементираме функциите, процедурите и тригерите кои ја покриваат основната бизнис логика на апликацијата DriveNet. Секоја имплементација е поврзана со реален сценарио од апликацијата. |
| | 10 | |
| | 11 | ---- |
| | 12 | |
| | 13 | = ФУНКЦИИ = |
| | 14 | |
| | 15 | === 1. calculate_booking_fare === |
| | 16 | |
| | 17 | ==== Опис: |
| | 18 | |
| | 19 | Ја пресметува вкупната цена за одредена резервација врз основа на сегментите кои патникот ги поминува и бројот на патници на секој сегмент. Цената се дели рамномерно меѓу сите патници на секој сегмент — доколку на еден сегмент патуваат 3 патници, секој плаќа 1/3 од цената на тој сегмент. |
| | 20 | |
| | 21 | ==== Сигнатура: |
| | 22 | {{{ |
| | 23 | calculate_booking_fare(p_booking_id INT) RETURNS DECIMAL(10,2) |
| | 24 | }}} |
| | 25 | |
| | 26 | ==== Влезни параметри: |
| | 27 | |
| | 28 | || **Параметар** || **Тип** || **Опис** || |
| | 29 | || `p_booking_id` || INT || ID на резервацијата || |
| | 30 | |
| | 31 | ==== Логика: |
| | 32 | |
| | 33 | 1. Го наоѓа `ride_id` и `price_per_km` преку `bookings JOIN rides` |
| | 34 | 2. За секој сегмент во `passenger_segments` каде `is_present = TRUE` ја пресметува цената: `(distance_km × price_per_km) / passengers_on_segment` |
| | 35 | 3. Ги сумира сите сегменти и го враќа вкупниот износ |
| | 36 | |
| | 37 | ==== Исклучоци: |
| | 38 | |
| | 39 | * `RAISE EXCEPTION` — доколку `booking_id` не постои |
| | 40 | |
| | 41 | ==== Имплементација: |
| | 42 | {{{ |
| | 43 | CREATE OR REPLACE FUNCTION calculate_booking_fare(p_booking_id INT) |
| | 44 | RETURNS DECIMAL(10,2) |
| | 45 | LANGUAGE plpgsql |
| | 46 | AS $$ |
| | 47 | DECLARE |
| | 48 | v_total DECIMAL(10,2) := 0; |
| | 49 | v_ride_id INT; |
| | 50 | v_price_per_km DECIMAL(8,2); |
| | 51 | rec RECORD; |
| | 52 | BEGIN |
| | 53 | SELECT b.ride_id, r.price_per_km |
| | 54 | INTO v_ride_id, v_price_per_km |
| | 55 | FROM bookings b |
| | 56 | JOIN rides r ON r.id = b.ride_id |
| | 57 | WHERE b.id = p_booking_id; |
| | 58 | |
| | 59 | IF NOT FOUND THEN |
| | 60 | RAISE EXCEPTION 'Booking % not found', p_booking_id; |
| | 61 | END IF; |
| | 62 | |
| | 63 | FOR rec IN |
| | 64 | SELECT rs.distance_km, ps.passengers_on_segment |
| | 65 | FROM passenger_segments ps |
| | 66 | JOIN route_segments rs ON rs.id = ps.segment_id |
| | 67 | WHERE ps.booking_id = p_booking_id |
| | 68 | AND ps.is_present = TRUE |
| | 69 | LOOP |
| | 70 | v_total := v_total + ROUND( |
| | 71 | (rec.distance_km * v_price_per_km) / rec.passengers_on_segment, |
| | 72 | 2 |
| | 73 | ); |
| | 74 | END LOOP; |
| | 75 | |
| | 76 | RETURN v_total; |
| | 77 | END; |
| | 78 | $$; |
| | 79 | }}} |
| | 80 | |
| | 81 | ==== Пример за употреба: |
| | 82 | {{{ |
| | 83 | SELECT calculate_booking_fare(1); |
| | 84 | }}} |
| | 85 | |
| | 86 | ---- |
| | 87 | |
| | 88 | === 2. is_driver_available === |
| | 89 | |
| | 90 | ==== Опис: |
| | 91 | |
| | 92 | Проверува дали возачот веќе има закажано возење кое временски се преклопува со бараниот термин. Се користи пред креирање на ново возење за да се спречат конфликти во распоредот. Буферот е ±3 часа околу бараното време на поаѓање. |
| | 93 | |
| | 94 | ==== Сигнатура: |
| | 95 | {{{ |
| | 96 | is_driver_available(p_driver_id INT, p_departure TIMESTAMP) RETURNS BOOLEAN |
| | 97 | }}} |
| | 98 | |
| | 99 | ==== Влезни параметри: |
| | 100 | |
| | 101 | || **Параметар** || **Тип** || **Опис** || |
| | 102 | || `p_driver_id` || INT || ID на возачот || |
| | 103 | || `p_departure` || TIMESTAMP || Бараното време на поаѓање || |
| | 104 | |
| | 105 | ==== Логика: |
| | 106 | |
| | 107 | 1. Брои колку возења со статус `scheduled` или `in_progress` има возачот во прозорецот `[departure - 3h, departure + 3h]` |
| | 108 | 2. Доколку бројот е 0 враќа `TRUE` (слободен), инаку враќа `FALSE` (зафатен) |
| | 109 | |
| | 110 | ==== Имплементација: |
| | 111 | {{{ |
| | 112 | CREATE OR REPLACE FUNCTION is_driver_available( |
| | 113 | p_driver_id INT, |
| | 114 | p_departure TIMESTAMP |
| | 115 | ) |
| | 116 | RETURNS BOOLEAN |
| | 117 | LANGUAGE plpgsql |
| | 118 | AS $$ |
| | 119 | DECLARE |
| | 120 | v_count INT; |
| | 121 | BEGIN |
| | 122 | SELECT COUNT(*) |
| | 123 | INTO v_count |
| | 124 | FROM rides |
| | 125 | WHERE driver_id = p_driver_id |
| | 126 | AND status IN ('scheduled', 'in_progress') |
| | 127 | AND departure_time BETWEEN (p_departure - INTERVAL '3 hours') |
| | 128 | AND (p_departure + INTERVAL '3 hours'); |
| | 129 | |
| | 130 | RETURN v_count = 0; |
| | 131 | END; |
| | 132 | $$; |
| | 133 | }}} |
| | 134 | |
| | 135 | ==== Пример за употреба: |
| | 136 | {{{ |
| | 137 | -- Провери дали возачот е слободен на 15.06.2025 во 10:00 |
| | 138 | SELECT is_driver_available(1, '2025-06-15 10:00:00'); |
| | 139 | }}} |
| | 140 | |
| | 141 | ---- |
| | 142 | |
| | 143 | === 3. get_available_seats === |
| | 144 | |
| | 145 | ==== Опис: |
| | 146 | |
| | 147 | Го враќа тековниот број на слободни места за одредено возење. Се користи пред резервација за да се потврди дека има место за нов патник. |
| | 148 | |
| | 149 | ==== Сигнатура: |
| | 150 | {{{ |
| | 151 | get_available_seats(p_ride_id INT) RETURNS INT |
| | 152 | }}} |
| | 153 | |
| | 154 | ==== Влезни параметри: |
| | 155 | |
| | 156 | || **Параметар** || **Тип** || **Опис** || |
| | 157 | || `p_ride_id` || INT || ID на возењето || |
| | 158 | |
| | 159 | ==== Логика: |
| | 160 | |
| | 161 | 1. Го чита полето `seats_available` директно од табелата `rides` |
| | 162 | 2. Доколку возењето не постои фрла исклучок |
| | 163 | |
| | 164 | ==== Исклучоци: |
| | 165 | |
| | 166 | * `RAISE EXCEPTION` — доколку `ride_id` не постои |
| | 167 | |
| | 168 | ==== Имплементација: |
| | 169 | {{{ |
| | 170 | CREATE OR REPLACE FUNCTION get_available_seats(p_ride_id INT) |
| | 171 | RETURNS INT |
| | 172 | LANGUAGE plpgsql |
| | 173 | AS $$ |
| | 174 | DECLARE |
| | 175 | v_seats INT; |
| | 176 | BEGIN |
| | 177 | SELECT seats_available |
| | 178 | INTO v_seats |
| | 179 | FROM rides |
| | 180 | WHERE id = p_ride_id; |
| | 181 | |
| | 182 | IF NOT FOUND THEN |
| | 183 | RAISE EXCEPTION 'Ride % not found', p_ride_id; |
| | 184 | END IF; |
| | 185 | |
| | 186 | RETURN v_seats; |
| | 187 | END; |
| | 188 | $$; |
| | 189 | }}} |
| | 190 | |
| | 191 | ==== Пример за употреба: |
| | 192 | {{{ |
| | 193 | SELECT get_available_seats(1); |
| | 194 | }}} |
| | 195 | |
| | 196 | ---- |
| | 197 | |
| | 198 | = ПРОЦЕДУРИ = |
| | 199 | |
| | 200 | === 4. book_ride === |
| | 201 | |
| | 202 | ==== Опис: |
| | 203 | |
| | 204 | Го извршува целосниот процес на резервација на возење. Ги опфаќа сите потребни проверки и ажурирања во рамки на една трансакција: верификација на статусот и местата, проверка за дупликат резервација, креирање на резервацијата и намалување на слободните места. |
| | 205 | |
| | 206 | ==== Сигнатура: |
| | 207 | {{{ |
| | 208 | book_ride(p_passenger_id INT, p_ride_id INT, |
| | 209 | p_pickup_stop_id INT, p_dropoff_stop_id INT) |
| | 210 | }}} |
| | 211 | |
| | 212 | ==== Влезни параметри: |
| | 213 | |
| | 214 | || **Параметар** || **Тип** || **Опис** || |
| | 215 | || `p_passenger_id` || INT || ID на патникот || |
| | 216 | || `p_ride_id` || INT || ID на возењето || |
| | 217 | || `p_pickup_stop_id` || INT || ID на постојката за качување || |
| | 218 | || `p_dropoff_stop_id` || INT || ID на постојката за слегување || |
| | 219 | |
| | 220 | ==== Логика: |
| | 221 | |
| | 222 | 1. Ги заклучува редовите со `FOR UPDATE` за да се спречат состојби на трка |
| | 223 | 2. Проверува дали возењето постои и дали е со статус `scheduled` |
| | 224 | 3. Проверува дали `seats_available > 0` |
| | 225 | 4. Проверува дали патникот веќе има резервација за ова возење |
| | 226 | 5. Вметнува нов ред во `bookings` со статус `confirmed` |
| | 227 | 6. Го намалува `seats_available` за 1 |
| | 228 | 7. Вметнува запис во `booking_status_history` |
| | 229 | |
| | 230 | ==== Исклучоци: |
| | 231 | |
| | 232 | * `RAISE EXCEPTION` — возењето не постои |
| | 233 | * `RAISE EXCEPTION` — возењето не е со статус `scheduled` |
| | 234 | * `RAISE EXCEPTION` — нема слободни места |
| | 235 | * `RAISE EXCEPTION` — патникот веќе има резервација за ова возење |
| | 236 | |
| | 237 | ==== Имплементација: |
| | 238 | {{{ |
| | 239 | CREATE OR REPLACE PROCEDURE book_ride( |
| | 240 | p_passenger_id INT, |
| | 241 | p_ride_id INT, |
| | 242 | p_pickup_stop_id INT, |
| | 243 | p_dropoff_stop_id INT |
| | 244 | ) |
| | 245 | LANGUAGE plpgsql |
| | 246 | AS $$ |
| | 247 | DECLARE |
| | 248 | v_booking_id INT; |
| | 249 | v_seats INT; |
| | 250 | v_ride_status VARCHAR(20); |
| | 251 | BEGIN |
| | 252 | SELECT status, seats_available |
| | 253 | INTO v_ride_status, v_seats |
| | 254 | FROM rides |
| | 255 | WHERE id = p_ride_id |
| | 256 | FOR UPDATE; |
| | 257 | |
| | 258 | IF NOT FOUND THEN |
| | 259 | RAISE EXCEPTION 'Ride % not found', p_ride_id; |
| | 260 | END IF; |
| | 261 | |
| | 262 | IF v_ride_status != 'scheduled' THEN |
| | 263 | RAISE EXCEPTION 'Ride % is not available for booking (status: %)', |
| | 264 | p_ride_id, v_ride_status; |
| | 265 | END IF; |
| | 266 | |
| | 267 | IF v_seats <= 0 THEN |
| | 268 | RAISE EXCEPTION 'No available seats for ride %', p_ride_id; |
| | 269 | END IF; |
| | 270 | |
| | 271 | IF EXISTS ( |
| | 272 | SELECT 1 FROM bookings |
| | 273 | WHERE ride_id = p_ride_id |
| | 274 | AND passenger_id = p_passenger_id |
| | 275 | ) THEN |
| | 276 | RAISE EXCEPTION 'Passenger % already has a booking for ride %', |
| | 277 | p_passenger_id, p_ride_id; |
| | 278 | END IF; |
| | 279 | |
| | 280 | INSERT INTO bookings ( |
| | 281 | ride_id, passenger_id, |
| | 282 | pickup_stop_id, dropoff_stop_id, |
| | 283 | status, created_at |
| | 284 | ) |
| | 285 | VALUES ( |
| | 286 | p_ride_id, p_passenger_id, |
| | 287 | p_pickup_stop_id, p_dropoff_stop_id, |
| | 288 | 'confirmed', NOW() |
| | 289 | ) |
| | 290 | RETURNING id INTO v_booking_id; |
| | 291 | |
| | 292 | UPDATE rides |
| | 293 | SET seats_available = seats_available - 1 |
| | 294 | WHERE id = p_ride_id; |
| | 295 | |
| | 296 | INSERT INTO booking_status_history (booking_id, status, changed_at) |
| | 297 | VALUES (v_booking_id, 'confirmed', NOW()); |
| | 298 | |
| | 299 | RAISE NOTICE 'Booking % created for passenger % on ride %', |
| | 300 | v_booking_id, p_passenger_id, p_ride_id; |
| | 301 | END; |
| | 302 | $$; |
| | 303 | }}} |
| | 304 | |
| | 305 | ==== Пример за употреба: |
| | 306 | {{{ |
| | 307 | CALL book_ride(1, 1, 1, 4); |
| | 308 | }}} |
| | 309 | |
| | 310 | ---- |
| | 311 | |
| | 312 | === 5. cancel_booking === |
| | 313 | |
| | 314 | ==== Опис: |
| | 315 | |
| | 316 | Го извршува целосниот процес на откажување на резервација. Проверува дали откажувањето е дозволено (статус и временска рамка), го менува статусот, враќа едно место во возењето и ја запишува причината за откажување во историјата. |
| | 317 | |
| | 318 | ==== Сигнатура: |
| | 319 | {{{ |
| | 320 | cancel_booking(p_booking_id INT, p_reason VARCHAR(300) DEFAULT NULL) |
| | 321 | }}} |
| | 322 | |
| | 323 | ==== Влезни параметри: |
| | 324 | |
| | 325 | || **Параметар** || **Тип** || **Опис** || |
| | 326 | || `p_booking_id` || INT || ID на резервацијата || |
| | 327 | || `p_reason` || VARCHAR(300) || Причина за откажување (опционално) || |
| | 328 | |
| | 329 | ==== Логика: |
| | 330 | |
| | 331 | 1. Ги зема деталите за резервацијата и поврзаното возење со `FOR UPDATE` |
| | 332 | 2. Проверува дали статусот е `pending` или `confirmed` |
| | 333 | 3. Проверува дали до поаѓањето има повеќе од 1 час |
| | 334 | 4. Го менува статусот на `cancelled` |
| | 335 | 5. Го зголемува `seats_available` за 1 |
| | 336 | 6. Вметнува запис во `booking_status_history` со причината |
| | 337 | |
| | 338 | ==== Исклучоци: |
| | 339 | |
| | 340 | * `RAISE EXCEPTION` — резервацијата не постои |
| | 341 | * `RAISE EXCEPTION` — статусот не дозволува откажување |
| | 342 | * `RAISE EXCEPTION` — помалку од 1 час до поаѓање |
| | 343 | |
| | 344 | ==== Имплементација: |
| | 345 | {{{ |
| | 346 | CREATE OR REPLACE PROCEDURE cancel_booking( |
| | 347 | p_booking_id INT, |
| | 348 | p_reason VARCHAR(300) DEFAULT NULL |
| | 349 | ) |
| | 350 | LANGUAGE plpgsql |
| | 351 | AS $$ |
| | 352 | DECLARE |
| | 353 | v_ride_id INT; |
| | 354 | v_status VARCHAR(20); |
| | 355 | v_departure TIMESTAMP; |
| | 356 | BEGIN |
| | 357 | SELECT b.ride_id, b.status, r.departure_time |
| | 358 | INTO v_ride_id, v_status, v_departure |
| | 359 | FROM bookings b |
| | 360 | JOIN rides r ON r.id = b.ride_id |
| | 361 | WHERE b.id = p_booking_id |
| | 362 | FOR UPDATE; |
| | 363 | |
| | 364 | IF NOT FOUND THEN |
| | 365 | RAISE EXCEPTION 'Booking % not found', p_booking_id; |
| | 366 | END IF; |
| | 367 | |
| | 368 | IF v_status NOT IN ('pending', 'confirmed') THEN |
| | 369 | RAISE EXCEPTION 'Cannot cancel booking % with status %', |
| | 370 | p_booking_id, v_status; |
| | 371 | END IF; |
| | 372 | |
| | 373 | IF v_departure <= NOW() + INTERVAL '1 hour' THEN |
| | 374 | RAISE EXCEPTION 'Cannot cancel booking % — less than 1 hour before departure', |
| | 375 | p_booking_id; |
| | 376 | END IF; |
| | 377 | |
| | 378 | UPDATE bookings |
| | 379 | SET status = 'cancelled' |
| | 380 | WHERE id = p_booking_id; |
| | 381 | |
| | 382 | UPDATE rides |
| | 383 | SET seats_available = seats_available + 1 |
| | 384 | WHERE id = v_ride_id; |
| | 385 | |
| | 386 | INSERT INTO booking_status_history (booking_id, status, changed_at, reason) |
| | 387 | VALUES (p_booking_id, 'cancelled', NOW(), p_reason); |
| | 388 | |
| | 389 | RAISE NOTICE 'Booking % cancelled successfully', p_booking_id; |
| | 390 | END; |
| | 391 | $$; |
| | 392 | }}} |
| | 393 | |
| | 394 | ==== Пример за употреба: |
| | 395 | {{{ |
| | 396 | CALL cancel_booking(1, 'Патникот не може да патува'); |
| | 397 | }}} |
| | 398 | |
| | 399 | ---- |
| | 400 | |
| | 401 | === 6. complete_ride === |
| | 402 | |
| | 403 | ==== Опис: |
| | 404 | |
| | 405 | Го завршува возењето и автоматски ги завршува сите активни резервации. За секоја резервација се пресметува и зачувува финалната тарифа преку функцијата `calculate_booking_fare`. |
| | 406 | |
| | 407 | ==== Сигнатура: |
| | 408 | {{{ |
| | 409 | complete_ride(p_ride_id INT) |
| | 410 | }}} |
| | 411 | |
| | 412 | ==== Влезни параметри: |
| | 413 | |
| | 414 | || **Параметар** || **Тип** || **Опис** || |
| | 415 | || `p_ride_id` || INT || ID на возењето || |
| | 416 | |
| | 417 | ==== Логика: |
| | 418 | |
| | 419 | 1. Проверува дали возењето постои и дали е со статус `in_progress` |
| | 420 | 2. Го менува статусот на возењето во `completed` |
| | 421 | 3. За секоја резервација со статус `confirmed` или `picked_up`: |
| | 422 | * Го менува статусот во `completed` |
| | 423 | * Го поставува `dropoff_confirmed_at = NOW()` |
| | 424 | * Ја пресметува финалната тарифа преку `calculate_booking_fare` |
| | 425 | * Ја зачувува тарифата во `booking_final_fare` (INSERT или UPDATE) |
| | 426 | * Вметнува запис во `booking_status_history` |
| | 427 | |
| | 428 | ==== Исклучоци: |
| | 429 | |
| | 430 | * `RAISE EXCEPTION` — возењето не постои |
| | 431 | * `RAISE EXCEPTION` — возењето не е со статус `in_progress` |
| | 432 | |
| | 433 | ==== Имплементација: |
| | 434 | {{{ |
| | 435 | CREATE OR REPLACE PROCEDURE complete_ride(p_ride_id INT) |
| | 436 | LANGUAGE plpgsql |
| | 437 | AS $$ |
| | 438 | DECLARE |
| | 439 | v_status VARCHAR(20); |
| | 440 | rec RECORD; |
| | 441 | v_fare DECIMAL(10,2); |
| | 442 | BEGIN |
| | 443 | SELECT status INTO v_status |
| | 444 | FROM rides |
| | 445 | WHERE id = p_ride_id |
| | 446 | FOR UPDATE; |
| | 447 | |
| | 448 | IF NOT FOUND THEN |
| | 449 | RAISE EXCEPTION 'Ride % not found', p_ride_id; |
| | 450 | END IF; |
| | 451 | |
| | 452 | IF v_status != 'in_progress' THEN |
| | 453 | RAISE EXCEPTION 'Ride % cannot be completed (status: %)', |
| | 454 | p_ride_id, v_status; |
| | 455 | END IF; |
| | 456 | |
| | 457 | UPDATE rides |
| | 458 | SET status = 'completed' |
| | 459 | WHERE id = p_ride_id; |
| | 460 | |
| | 461 | FOR rec IN |
| | 462 | SELECT id FROM bookings |
| | 463 | WHERE ride_id = p_ride_id |
| | 464 | AND status IN ('confirmed', 'picked_up') |
| | 465 | LOOP |
| | 466 | UPDATE bookings |
| | 467 | SET status = 'completed', |
| | 468 | dropoff_confirmed_at = NOW() |
| | 469 | WHERE id = rec.id; |
| | 470 | |
| | 471 | v_fare := calculate_booking_fare(rec.id); |
| | 472 | |
| | 473 | INSERT INTO booking_final_fare (booking_id, total_amount, calculated_at) |
| | 474 | VALUES (rec.id, v_fare, NOW()) |
| | 475 | ON CONFLICT (booking_id) |
| | 476 | DO UPDATE SET |
| | 477 | total_amount = EXCLUDED.total_amount, |
| | 478 | calculated_at = EXCLUDED.calculated_at; |
| | 479 | |
| | 480 | INSERT INTO booking_status_history (booking_id, status, changed_at) |
| | 481 | VALUES (rec.id, 'completed', NOW()); |
| | 482 | END LOOP; |
| | 483 | |
| | 484 | RAISE NOTICE 'Ride % completed successfully', p_ride_id; |
| | 485 | END; |
| | 486 | $$; |
| | 487 | }}} |
| | 488 | |
| | 489 | ==== Пример за употреба: |
| | 490 | {{{ |
| | 491 | CALL complete_ride(1); |
| | 492 | }}} |
| | 493 | |
| | 494 | ---- |
| | 495 | |
| | 496 | = ТРИГЕРИ = |
| | 497 | |
| | 498 | === 7. trg_booking_status_history === |
| | 499 | |
| | 500 | ==== Опис: |
| | 501 | |
| | 502 | Автоматски запишува во табелата `booking_status_history` при секоја промена на статусот на резервација. Обезбедува целосна историја на промени без да мора апликацијата рачно да ги следи. |
| | 503 | |
| | 504 | ==== Тип: `AFTER UPDATE` на табела `bookings` |
| | 505 | |
| | 506 | ==== Логика: |
| | 507 | |
| | 508 | 1. Се активира по секој `UPDATE` на табелата `bookings` |
| | 509 | 2. Проверува дали `OLD.status IS DISTINCT FROM NEW.status` |
| | 510 | 3. Доколку статусот се сменил, вметнува нов ред во `booking_status_history` |
| | 511 | |
| | 512 | ==== Имплементација: |
| | 513 | {{{ |
| | 514 | CREATE OR REPLACE FUNCTION fn_booking_status_history() |
| | 515 | RETURNS TRIGGER |
| | 516 | LANGUAGE plpgsql |
| | 517 | AS $$ |
| | 518 | BEGIN |
| | 519 | IF OLD.status IS DISTINCT FROM NEW.status THEN |
| | 520 | INSERT INTO booking_status_history (booking_id, status, changed_at) |
| | 521 | VALUES (NEW.id, NEW.status, NOW()); |
| | 522 | END IF; |
| | 523 | RETURN NEW; |
| | 524 | END; |
| | 525 | $$; |
| | 526 | |
| | 527 | CREATE TRIGGER trg_booking_status_history |
| | 528 | AFTER UPDATE ON bookings |
| | 529 | FOR EACH ROW |
| | 530 | EXECUTE FUNCTION fn_booking_status_history(); |
| | 531 | }}} |
| | 532 | |
| | 533 | ==== Пример — активирање: |
| | 534 | {{{ |
| | 535 | -- Овој UPDATE автоматски ќе вметне ред во booking_status_history |
| | 536 | UPDATE bookings SET status = 'picked_up' WHERE id = 1; |
| | 537 | }}} |
| | 538 | |
| | 539 | ---- |
| | 540 | |
| | 541 | === 8. trg_ride_status_history === |
| | 542 | |
| | 543 | ==== Опис: |
| | 544 | |
| | 545 | Автоматски запишува во табелата `ride_status_history` при секоја промена на статусот на возење. Обезбедува целосна хронологија на состојбите на возењето. |
| | 546 | |
| | 547 | ==== Тип: `AFTER UPDATE` на табела `rides` |
| | 548 | |
| | 549 | ==== Логика: |
| | 550 | |
| | 551 | 1. Се активира по секој `UPDATE` на табелата `rides` |
| | 552 | 2. Проверува дали `OLD.status IS DISTINCT FROM NEW.status` |
| | 553 | 3. Доколку статусот се сменил, вметнува нов ред во `ride_status_history` |
| | 554 | |
| | 555 | ==== Имплементација: |
| | 556 | {{{ |
| | 557 | CREATE OR REPLACE FUNCTION fn_ride_status_history() |
| | 558 | RETURNS TRIGGER |
| | 559 | LANGUAGE plpgsql |
| | 560 | AS $$ |
| | 561 | BEGIN |
| | 562 | IF OLD.status IS DISTINCT FROM NEW.status THEN |
| | 563 | INSERT INTO ride_status_history (ride_id, status, changed_at) |
| | 564 | VALUES (NEW.id, NEW.status, NOW()); |
| | 565 | END IF; |
| | 566 | RETURN NEW; |
| | 567 | END; |
| | 568 | $$; |
| | 569 | |
| | 570 | CREATE TRIGGER trg_ride_status_history |
| | 571 | AFTER UPDATE ON rides |
| | 572 | FOR EACH ROW |
| | 573 | EXECUTE FUNCTION fn_ride_status_history(); |
| | 574 | }}} |
| | 575 | |
| | 576 | ==== Пример — активирање: |
| | 577 | {{{ |
| | 578 | -- Овој UPDATE автоматски ќе вметне ред во ride_status_history |
| | 579 | UPDATE rides SET status = 'in_progress' WHERE id = 1; |
| | 580 | }}} |
| | 581 | |
| | 582 | ---- |
| | 583 | |
| | 584 | === 9. trg_audit_log === |
| | 585 | |
| | 586 | ==== Опис: |
| | 587 | |
| | 588 | Автоматски запишува во табелата `audit_logs` за сите `INSERT`, `UPDATE` и `DELETE` операции на критичните табели: `rides`, `bookings` и `users`. Го чува и старото и новото состојба на редот во JSON формат. |
| | 589 | |
| | 590 | ==== Тип: `AFTER INSERT OR UPDATE OR DELETE` на табели `rides`, `bookings`, `users` |
| | 591 | |
| | 592 | ==== Логика: |
| | 593 | |
| | 594 | 1. За `INSERT` — зачувува `new_data = row_to_json(NEW)`, `old_data = NULL` |
| | 595 | 2. За `UPDATE` — зачувува `old_data = row_to_json(OLD)` и `new_data = row_to_json(NEW)` |
| | 596 | 3. За `DELETE` — зачувува `old_data = row_to_json(OLD)`, `new_data = NULL` |
| | 597 | |
| | 598 | ==== Имплементација: |
| | 599 | {{{ |
| | 600 | CREATE OR REPLACE FUNCTION fn_audit_log() |
| | 601 | RETURNS TRIGGER |
| | 602 | LANGUAGE plpgsql |
| | 603 | AS $$ |
| | 604 | BEGIN |
| | 605 | IF TG_OP = 'INSERT' THEN |
| | 606 | INSERT INTO audit_logs ( |
| | 607 | table_name, record_id, operation, |
| | 608 | old_data, new_data, changed_at |
| | 609 | ) |
| | 610 | VALUES ( |
| | 611 | TG_TABLE_NAME, NEW.id, 'INSERT', |
| | 612 | NULL, row_to_json(NEW)::TEXT, NOW() |
| | 613 | ); |
| | 614 | RETURN NEW; |
| | 615 | |
| | 616 | ELSIF TG_OP = 'UPDATE' THEN |
| | 617 | INSERT INTO audit_logs ( |
| | 618 | table_name, record_id, operation, |
| | 619 | old_data, new_data, changed_at |
| | 620 | ) |
| | 621 | VALUES ( |
| | 622 | TG_TABLE_NAME, NEW.id, 'UPDATE', |
| | 623 | row_to_json(OLD)::TEXT, row_to_json(NEW)::TEXT, NOW() |
| | 624 | ); |
| | 625 | RETURN NEW; |
| | 626 | |
| | 627 | ELSIF TG_OP = 'DELETE' THEN |
| | 628 | INSERT INTO audit_logs ( |
| | 629 | table_name, record_id, operation, |
| | 630 | old_data, new_data, changed_at |
| | 631 | ) |
| | 632 | VALUES ( |
| | 633 | TG_TABLE_NAME, OLD.id, 'DELETE', |
| | 634 | row_to_json(OLD)::TEXT, NULL, NOW() |
| | 635 | ); |
| | 636 | RETURN OLD; |
| | 637 | END IF; |
| | 638 | END; |
| | 639 | $$; |
| | 640 | |
| | 641 | CREATE TRIGGER trg_audit_log_rides |
| | 642 | AFTER INSERT OR UPDATE OR DELETE ON rides |
| | 643 | FOR EACH ROW EXECUTE FUNCTION fn_audit_log(); |
| | 644 | |
| | 645 | CREATE TRIGGER trg_audit_log_bookings |
| | 646 | AFTER INSERT OR UPDATE OR DELETE ON bookings |
| | 647 | FOR EACH ROW EXECUTE FUNCTION fn_audit_log(); |
| | 648 | |
| | 649 | CREATE TRIGGER trg_audit_log_users |
| | 650 | AFTER INSERT OR UPDATE OR DELETE ON users |
| | 651 | FOR EACH ROW EXECUTE FUNCTION fn_audit_log(); |
| | 652 | }}} |
| | 653 | |
| | 654 | ==== Пример — активирање: |
| | 655 | {{{ |
| | 656 | -- Секој од овие автоматски ќе вметне ред во audit_logs |
| | 657 | INSERT INTO rides (...) VALUES (...); |
| | 658 | UPDATE bookings SET status = 'cancelled' WHERE id = 1; |
| | 659 | DELETE FROM users WHERE id = 999; |
| | 660 | }}} |
| | 661 | |
| | 662 | ---- |
| | 663 | |
| | 664 | = Резиме = |
| | 665 | |
| | 666 | || **#** || **Име** || **Тип** || **Опис** || |
| | 667 | || 1 || `calculate_booking_fare` || Функција || Пресметува вкупна цена за резервација || |
| | 668 | || 2 || `is_driver_available` || Функција || Проверува конфликт на термин за возач || |
| | 669 | || 3 || `get_available_seats` || Функција || Враќа број на слободни места за возење || |
| | 670 | || 4 || `book_ride` || Процедура || Целосен процес на резервација || |
| | 671 | || 5 || `cancel_booking` || Процедура || Откажување на резервација со проверки || |
| | 672 | || 6 || `complete_ride` || Процедура || Завршување на возење и пресметка на тарифи || |
| | 673 | || 7 || `trg_booking_status_history` || Тригер || Автоматски лог на промени на статус на резервација || |
| | 674 | || 8 || `trg_ride_status_history` || Тригер || Автоматски лог на промени на статус на возење || |
| | 675 | || 9 || `trg_audit_log` || Тригер || Audit лог за rides, bookings и users || |