| 1 |
|
|---|
| 2 | --T/F na is_active
|
|---|
| 3 | CREATE OR REPLACE PROCEDURE activate_deactivate_service(
|
|---|
| 4 | p_service_id INT,
|
|---|
| 5 | p_is_active BOOLEAN
|
|---|
| 6 | )
|
|---|
| 7 | LANGUAGE plpgsql
|
|---|
| 8 | AS
|
|---|
| 9 | $$
|
|---|
| 10 | BEGIN
|
|---|
| 11 | UPDATE service
|
|---|
| 12 | SET is_active = p_is_active
|
|---|
| 13 | WHERE service_id = p_service_id;
|
|---|
| 14 |
|
|---|
| 15 | IF NOT FOUND THEN
|
|---|
| 16 | RAISE EXCEPTION 'Service % does not exist.', p_service_id;
|
|---|
| 17 | END IF;
|
|---|
| 18 |
|
|---|
| 19 | RAISE NOTICE 'Service % is now %.', p_service_id,
|
|---|
| 20 | CASE WHEN p_is_active THEN 'active' ELSE 'inactive' END;
|
|---|
| 21 | END;
|
|---|
| 22 | $$;
|
|---|
| 23 |
|
|---|
| 24 | CALL activate_deactivate_service(1, FALSE);
|
|---|
| 25 |
|
|---|
| 26 | CALL activate_deactivate_service(1, TRUE);
|
|---|
| 27 |
|
|---|
| 28 | CALL activate_deactivate_service(9999, TRUE);
|
|---|
| 29 |
|
|---|
| 30 | CREATE OR REPLACE PROCEDURE confirm_appointment(
|
|---|
| 31 | p_appointment_id INT
|
|---|
| 32 | )
|
|---|
| 33 | LANGUAGE plpgsql
|
|---|
| 34 | AS
|
|---|
| 35 | $$
|
|---|
| 36 | BEGIN
|
|---|
| 37 | UPDATE appointment
|
|---|
| 38 | SET status = 'confirmed'
|
|---|
| 39 | WHERE appointment_id = p_appointment_id
|
|---|
| 40 | AND status = 'pending';
|
|---|
| 41 |
|
|---|
| 42 | IF NOT FOUND THEN
|
|---|
| 43 | RAISE EXCEPTION 'Appointment % does not exist or is not in pending status.', p_appointment_id;
|
|---|
| 44 | END IF;
|
|---|
| 45 |
|
|---|
| 46 | RAISE NOTICE 'Appointment % confirmed.', p_appointment_id;
|
|---|
| 47 | END;
|
|---|
| 48 | $$;
|
|---|
| 49 |
|
|---|
| 50 | -- Confirm na pending appointment
|
|---|
| 51 | -- (prosledi go istoto id sto ke go stavis i vo testot za complete)
|
|---|
| 52 | CALL confirm_appointment(2751692);
|
|---|
| 53 |
|
|---|
| 54 |
|
|---|
| 55 |
|
|---|
| 56 | CREATE OR REPLACE PROCEDURE complete_appointment(
|
|---|
| 57 | p_appointment_id INT
|
|---|
| 58 | )
|
|---|
| 59 | LANGUAGE plpgsql
|
|---|
| 60 | AS
|
|---|
| 61 | $$
|
|---|
| 62 | BEGIN
|
|---|
| 63 | UPDATE appointment
|
|---|
| 64 | SET status = 'completed'
|
|---|
| 65 | WHERE appointment_id = p_appointment_id
|
|---|
| 66 | AND status = 'confirmed';
|
|---|
| 67 |
|
|---|
| 68 | IF NOT FOUND THEN
|
|---|
| 69 | RAISE EXCEPTION 'Appointment % does not exist or is not in confirmed status.', p_appointment_id;
|
|---|
| 70 | END IF;
|
|---|
| 71 |
|
|---|
| 72 | RAISE NOTICE 'Appointment % completed.', p_appointment_id;
|
|---|
| 73 | END;
|
|---|
| 74 | $$;
|
|---|
| 75 |
|
|---|
| 76 | -- Complete na confirmed appointment
|
|---|
| 77 | CALL complete_appointment(2751692);
|
|---|
| 78 |
|
|---|
| 79 |
|
|---|
| 80 | -- vazno e deka staff i service treba da se od ISTA company
|
|---|
| 81 | CREATE OR REPLACE PROCEDURE add_staff_to_service(
|
|---|
| 82 | p_staff_id INT,
|
|---|
| 83 | p_service_id INT
|
|---|
| 84 | )
|
|---|
| 85 | LANGUAGE plpgsql
|
|---|
| 86 | AS
|
|---|
| 87 | $$
|
|---|
| 88 | DECLARE
|
|---|
| 89 | v_exists BOOLEAN;
|
|---|
| 90 | BEGIN
|
|---|
| 91 | IF NOT EXISTS (SELECT 1 FROM staff WHERE staff_id = p_staff_id) THEN
|
|---|
| 92 | RAISE EXCEPTION 'Staff % does not exist.', p_staff_id;
|
|---|
| 93 | END IF;
|
|---|
| 94 |
|
|---|
| 95 | IF NOT EXISTS (SELECT 1 FROM service WHERE service_id = p_service_id) THEN
|
|---|
| 96 | RAISE EXCEPTION 'Service % does not exist.', p_service_id;
|
|---|
| 97 | END IF;
|
|---|
| 98 |
|
|---|
| 99 | IF NOT EXISTS (SELECT 1
|
|---|
| 100 | FROM staff s
|
|---|
| 101 | JOIN company_location cl ON cl.location_id = s.location_id
|
|---|
| 102 | WHERE s.staff_id = p_staff_id
|
|---|
| 103 | AND cl.company_id = (SELECT company_id FROM service WHERE service_id = p_service_id)) THEN
|
|---|
| 104 | RAISE EXCEPTION 'Staff % and service % do not belong to the same company.', p_staff_id, p_service_id;
|
|---|
| 105 | END IF;
|
|---|
| 106 |
|
|---|
| 107 | -- INSERT INTO staff_service (staff_id, service_id)
|
|---|
| 108 | -- VALUES (p_staff_id, p_service_id)
|
|---|
| 109 | -- ON CONFLICT DO NOTHING;
|
|---|
| 110 |
|
|---|
| 111 | SELECT EXISTS (SELECT 1
|
|---|
| 112 | FROM staff_service
|
|---|
| 113 | WHERE staff_id = p_staff_id
|
|---|
| 114 | AND service_id = p_service_id)
|
|---|
| 115 | INTO v_exists;
|
|---|
| 116 |
|
|---|
| 117 | IF v_exists THEN
|
|---|
| 118 | RAISE NOTICE 'Staff % is ALREADY assigned to service %.', p_staff_id, p_service_id;
|
|---|
| 119 | ELSE
|
|---|
| 120 | INSERT INTO staff_service (staff_id, service_id)
|
|---|
| 121 | VALUES (p_staff_id, p_service_id);
|
|---|
| 122 |
|
|---|
| 123 | RAISE NOTICE 'Staff % SUCCESSFULLY assigned to service %.', p_staff_id, p_service_id;
|
|---|
| 124 | END IF;
|
|---|
| 125 |
|
|---|
| 126 | END;
|
|---|
| 127 | $$;
|
|---|
| 128 |
|
|---|
| 129 |
|
|---|
| 130 |
|
|---|
| 131 | CREATE OR REPLACE PROCEDURE remove_staff_from_service(
|
|---|
| 132 | p_staff_id INT,
|
|---|
| 133 | p_service_id INT
|
|---|
| 134 | )
|
|---|
| 135 | LANGUAGE plpgsql
|
|---|
| 136 | AS
|
|---|
| 137 | $$
|
|---|
| 138 | BEGIN
|
|---|
| 139 | DELETE
|
|---|
| 140 | FROM staff_service
|
|---|
| 141 | WHERE staff_id = p_staff_id
|
|---|
| 142 | AND service_id = p_service_id;
|
|---|
| 143 |
|
|---|
| 144 | IF NOT FOUND THEN
|
|---|
| 145 | RAISE EXCEPTION 'Assignment between staff % and service % does not exist.',
|
|---|
| 146 | p_staff_id, p_service_id;
|
|---|
| 147 | END IF;
|
|---|
| 148 |
|
|---|
| 149 | RAISE NOTICE 'Staff % removed from service %.', p_staff_id, p_service_id;
|
|---|
| 150 | END;
|
|---|
| 151 | $$;
|
|---|
| 152 |
|
|---|
| 153 | -- Assign
|
|---|
| 154 | --243 company_id
|
|---|
| 155 | --3347 location_id
|
|---|
| 156 | --173 service_id
|
|---|
| 157 |
|
|---|
| 158 | CALL add_staff_to_service(108615, 173);
|
|---|
| 159 | CALL remove_staff_from_service(108615, 173);
|
|---|
| 160 | CALL add_staff_to_service(108615, 19);
|
|---|
| 161 |
|
|---|
| 162 | CREATE OR REPLACE FUNCTION get_appointment_total(
|
|---|
| 163 | p_appointment_id INT
|
|---|
| 164 | )
|
|---|
| 165 | RETURNS NUMERIC(10, 2)
|
|---|
| 166 | LANGUAGE plpgsql
|
|---|
| 167 | AS
|
|---|
| 168 | $$
|
|---|
| 169 | DECLARE
|
|---|
| 170 | v_total NUMERIC(10, 2);
|
|---|
| 171 | BEGIN
|
|---|
| 172 | SELECT COALESCE(SUM(price), 0)
|
|---|
| 173 | INTO v_total
|
|---|
| 174 | FROM appointment_service
|
|---|
| 175 | WHERE appointment_id = p_appointment_id;
|
|---|
| 176 |
|
|---|
| 177 | IF NOT FOUND THEN
|
|---|
| 178 | RAISE EXCEPTION 'Appointment % does not exist.', p_appointment_id;
|
|---|
| 179 | END IF;
|
|---|
| 180 |
|
|---|
| 181 | RETURN v_total;
|
|---|
| 182 | END;
|
|---|
| 183 | $$;
|
|---|
| 184 |
|
|---|
| 185 | SELECT get_appointment_total(986317);
|
|---|
| 186 | -- rez 85
|
|---|
| 187 | SELECT get_appointment_total(986318);
|
|---|
| 188 | -- rez 240
|
|---|
| 189 |
|
|---|
| 190 | --COMPLEX
|
|---|
| 191 |
|
|---|
| 192 | CREATE OR REPLACE PROCEDURE staff_add_blocked_time(
|
|---|
| 193 | p_staff_id INT,
|
|---|
| 194 | p_start_datetime TIMESTAMPTZ,
|
|---|
| 195 | p_end_datetime TIMESTAMPTZ,
|
|---|
| 196 | p_reason TEXT DEFAULT NULL
|
|---|
| 197 | )
|
|---|
| 198 | LANGUAGE plpgsql
|
|---|
| 199 | AS
|
|---|
| 200 | $$
|
|---|
| 201 | BEGIN
|
|---|
| 202 |
|
|---|
| 203 |
|
|---|
| 204 | IF p_end_datetime <= p_start_datetime THEN
|
|---|
| 205 | RAISE EXCEPTION 'end_datetime must be after start_datetime.';
|
|---|
| 206 | END IF;
|
|---|
| 207 |
|
|---|
| 208 |
|
|---|
| 209 | IF NOT EXISTS (SELECT 1
|
|---|
| 210 | FROM staff s
|
|---|
| 211 | JOIN "user" u ON u.user_id = s.staff_id
|
|---|
| 212 | WHERE s.staff_id = p_staff_id
|
|---|
| 213 | AND u.is_active = TRUE) THEN
|
|---|
| 214 | RAISE EXCEPTION 'Staff % does not exist or is not active.', p_staff_id;
|
|---|
| 215 | END IF;
|
|---|
| 216 |
|
|---|
| 217 |
|
|---|
| 218 | IF EXISTS (SELECT 1
|
|---|
| 219 | FROM blocked_time
|
|---|
| 220 | WHERE staff_id = p_staff_id
|
|---|
| 221 | AND start_datetime < p_end_datetime
|
|---|
| 222 | AND end_datetime > p_start_datetime) THEN
|
|---|
| 223 | RAISE EXCEPTION 'Blocked time overlaps with an existing blocked period for staff %.', p_staff_id;
|
|---|
| 224 | END IF;
|
|---|
| 225 |
|
|---|
| 226 |
|
|---|
| 227 | IF EXISTS (SELECT 1
|
|---|
| 228 | FROM appointment
|
|---|
| 229 | WHERE staff_id = p_staff_id
|
|---|
| 230 | AND status <> 'cancelled'
|
|---|
| 231 | AND (appointment_date + appointment_time)::TIMESTAMP AT TIME ZONE 'UTC' < p_end_datetime
|
|---|
| 232 | AND (appointment_date + end_time)::TIMESTAMP AT TIME ZONE 'UTC' > p_start_datetime) THEN
|
|---|
| 233 | RAISE EXCEPTION 'Blocked time overlaps with an existing appointment for staff %.', p_staff_id;
|
|---|
| 234 | END IF;
|
|---|
| 235 |
|
|---|
| 236 |
|
|---|
| 237 | INSERT INTO blocked_time (staff_id, start_datetime, end_datetime, reason)
|
|---|
| 238 | VALUES (p_staff_id, p_start_datetime, p_end_datetime, p_reason);
|
|---|
| 239 |
|
|---|
| 240 |
|
|---|
| 241 | DELETE
|
|---|
| 242 | FROM staff_time_slot
|
|---|
| 243 | WHERE staff_id = p_staff_id
|
|---|
| 244 | AND appointment_id IS NULL
|
|---|
| 245 | AND slot_start >= (p_start_datetime AT TIME ZONE 'UTC')
|
|---|
| 246 | AND slot_start < (p_end_datetime AT TIME ZONE 'UTC');
|
|---|
| 247 |
|
|---|
| 248 | END;
|
|---|
| 249 | $$;
|
|---|
| 250 |
|
|---|
| 251 |
|
|---|
| 252 |
|
|---|
| 253 | -- dodaj block i izbrisi (utre 12-13)
|
|---|
| 254 | CALL staff_add_blocked_time(
|
|---|
| 255 | 108615,
|
|---|
| 256 | (CURRENT_DATE + 1 + TIME '12:00')::TIMESTAMPTZ,
|
|---|
| 257 | (CURRENT_DATE + 1 + TIME '13:00')::TIMESTAMPTZ,
|
|---|
| 258 | 'Personal errand'
|
|---|
| 259 | );
|
|---|
| 260 |
|
|---|
| 261 | -- Test error: end before start
|
|---|
| 262 | CALL staff_add_blocked_time(
|
|---|
| 263 | 108615,
|
|---|
| 264 | (CURRENT_DATE + 1 + TIME '12:00')::TIMESTAMPTZ,
|
|---|
| 265 | (CURRENT_DATE + 1 + TIME '10:00')::TIMESTAMPTZ,
|
|---|
| 266 | NULL
|
|---|
| 267 | );
|
|---|
| 268 |
|
|---|
| 269 | -- Test error: overlaps with the block we just inserted
|
|---|
| 270 | CALL staff_add_blocked_time(
|
|---|
| 271 | 108615,
|
|---|
| 272 | (CURRENT_DATE + 1 + TIME '12:30')::TIMESTAMPTZ,
|
|---|
| 273 | (CURRENT_DATE + 1 + TIME '12:40')::TIMESTAMPTZ,
|
|---|
| 274 | NULL
|
|---|
| 275 | );
|
|---|
| 276 |
|
|---|
| 277 | CREATE OR REPLACE PROCEDURE client_book_appointment(
|
|---|
| 278 | p_client_id INT,
|
|---|
| 279 | p_staff_id INT,
|
|---|
| 280 | p_location_id INT,
|
|---|
| 281 | p_date DATE,
|
|---|
| 282 | p_time TIME,
|
|---|
| 283 | p_service_ids INT[]
|
|---|
| 284 | )
|
|---|
| 285 | LANGUAGE plpgsql
|
|---|
| 286 | AS
|
|---|
| 287 | $$
|
|---|
| 288 | DECLARE
|
|---|
| 289 | v_total_duration INT;
|
|---|
| 290 | v_end_time TIME;
|
|---|
| 291 | v_day_of_week day_of_week_enum;
|
|---|
| 292 | v_appointment_id INT;
|
|---|
| 293 | v_avail_start TIME;
|
|---|
| 294 | v_avail_end TIME;
|
|---|
| 295 | v_company_id INT;
|
|---|
| 296 | BEGIN
|
|---|
| 297 |
|
|---|
| 298 | -- 1. Service list must not be empty
|
|---|
| 299 | IF p_service_ids IS NULL OR array_length(p_service_ids, 1) = 0 THEN
|
|---|
| 300 | RAISE EXCEPTION 'At least one service must be provided.';
|
|---|
| 301 | END IF;
|
|---|
| 302 |
|
|---|
| 303 | -- 2. Date/time must not be in the past
|
|---|
| 304 | IF p_date < CURRENT_DATE OR (p_date = CURRENT_DATE AND p_time <= CURRENT_TIME) THEN
|
|---|
| 305 | RAISE EXCEPTION 'Appointment date/time is in the past.';
|
|---|
| 306 | END IF;
|
|---|
| 307 |
|
|---|
| 308 | -- 3. Client exists and is active
|
|---|
| 309 | IF NOT EXISTS (SELECT 1
|
|---|
| 310 | FROM client
|
|---|
| 311 | JOIN "user" u ON u.user_id = client_id
|
|---|
| 312 | WHERE client_id = p_client_id
|
|---|
| 313 | AND u.is_active = TRUE) THEN
|
|---|
| 314 | RAISE EXCEPTION 'Client % does not exist or is not active.', p_client_id;
|
|---|
| 315 | END IF;
|
|---|
| 316 |
|
|---|
| 317 | -- 4. Staff exists, is active, and works at the given location
|
|---|
| 318 | IF NOT EXISTS (SELECT 1
|
|---|
| 319 | FROM staff s
|
|---|
| 320 | JOIN "user" u ON u.user_id = s.staff_id
|
|---|
| 321 | WHERE s.staff_id = p_staff_id
|
|---|
| 322 | AND s.location_id = p_location_id
|
|---|
| 323 | AND u.is_active = TRUE) THEN
|
|---|
| 324 | RAISE EXCEPTION 'Staff % does not exist, is not active, or does not work at location %.', p_staff_id, p_location_id;
|
|---|
| 325 | END IF;
|
|---|
| 326 |
|
|---|
| 327 | -- 5. Resolve company_id from location
|
|---|
| 328 | SELECT company_id
|
|---|
| 329 | INTO v_company_id
|
|---|
| 330 | FROM company_location
|
|---|
| 331 | WHERE location_id = p_location_id;
|
|---|
| 332 |
|
|---|
| 333 | -- 6. All services valid, active, and belong to this company
|
|---|
| 334 | IF (SELECT COUNT(*)
|
|---|
| 335 | FROM service
|
|---|
| 336 | WHERE service_id = ANY (p_service_ids)
|
|---|
| 337 | AND is_active = TRUE
|
|---|
| 338 | AND company_id = v_company_id) <> array_length(p_service_ids, 1) THEN
|
|---|
| 339 | RAISE EXCEPTION 'One or more services are invalid, inactive, or do not belong to this company.';
|
|---|
| 340 | END IF;
|
|---|
| 341 |
|
|---|
| 342 | -- 7. Staff is assigned to all requested services
|
|---|
| 343 | IF (SELECT COUNT(*)
|
|---|
| 344 | FROM staff_service
|
|---|
| 345 | WHERE staff_id = p_staff_id
|
|---|
| 346 | AND service_id = ANY (p_service_ids)) <> array_length(p_service_ids, 1) THEN
|
|---|
| 347 | RAISE EXCEPTION 'Staff % is not assigned to one or more of the requested services.', p_staff_id;
|
|---|
| 348 | END IF;
|
|---|
| 349 |
|
|---|
| 350 | -- 8. Calculate total duration and derive end_time
|
|---|
| 351 | SELECT COALESCE(SUM(duration_minutes), 0)
|
|---|
| 352 | INTO v_total_duration
|
|---|
| 353 | FROM service
|
|---|
| 354 | WHERE service_id = ANY (p_service_ids);
|
|---|
| 355 |
|
|---|
| 356 | v_end_time := p_time + (v_total_duration || ' minutes')::INTERVAL;
|
|---|
| 357 | v_day_of_week := LOWER(TRIM(TO_CHAR(p_date, 'FMDay')))::day_of_week_enum;
|
|---|
| 358 |
|
|---|
| 359 | -- 9. Staff availability covers the full window
|
|---|
| 360 | SELECT start_time, end_time
|
|---|
| 361 | INTO v_avail_start, v_avail_end
|
|---|
| 362 | FROM staff_availability
|
|---|
| 363 | WHERE staff_id = p_staff_id
|
|---|
| 364 | AND day_of_week = v_day_of_week;
|
|---|
| 365 |
|
|---|
| 366 | IF NOT FOUND THEN
|
|---|
| 367 | RAISE EXCEPTION 'Staff % has no availability on %.', p_staff_id, v_day_of_week;
|
|---|
| 368 | END IF;
|
|---|
| 369 |
|
|---|
| 370 | IF p_time < v_avail_start OR v_end_time > v_avail_end THEN
|
|---|
| 371 | RAISE EXCEPTION 'Appointment window %–% falls outside staff availability %–% on %.',
|
|---|
| 372 | p_time, v_end_time, v_avail_start, v_avail_end, v_day_of_week;
|
|---|
| 373 | END IF;
|
|---|
| 374 |
|
|---|
| 375 | -- 10. Location is open and appointment fits within business hours
|
|---|
| 376 | IF NOT EXISTS (SELECT 1
|
|---|
| 377 | FROM business_hours
|
|---|
| 378 | WHERE location_id = p_location_id
|
|---|
| 379 | AND day_of_week = v_day_of_week
|
|---|
| 380 | AND is_closed = FALSE
|
|---|
| 381 | AND open_time <= p_time
|
|---|
| 382 | AND close_time >= v_end_time) THEN
|
|---|
| 383 | RAISE EXCEPTION 'Appointment window %–% falls outside business hours for location % on %.',
|
|---|
| 384 | p_time, v_end_time, p_location_id, v_day_of_week;
|
|---|
| 385 | END IF;
|
|---|
| 386 |
|
|---|
| 387 | -- 11. No overlap with blocked_time
|
|---|
| 388 | IF EXISTS (SELECT 1
|
|---|
| 389 | FROM blocked_time
|
|---|
| 390 | WHERE staff_id = p_staff_id
|
|---|
| 391 | AND (p_date + p_time)::TIMESTAMP AT TIME ZONE 'UTC' < end_datetime
|
|---|
| 392 | AND (p_date + v_end_time)::TIMESTAMP AT TIME ZONE 'UTC' > start_datetime) THEN
|
|---|
| 393 | RAISE EXCEPTION 'Time slot overlaps with a blocked period for staff %.', p_staff_id;
|
|---|
| 394 | END IF;
|
|---|
| 395 |
|
|---|
| 396 | -- 12. No overlap with existing appointments
|
|---|
| 397 | IF EXISTS (SELECT 1
|
|---|
| 398 | FROM appointment
|
|---|
| 399 | WHERE staff_id = p_staff_id
|
|---|
| 400 | AND appointment_date = p_date
|
|---|
| 401 | AND status <> 'cancelled'
|
|---|
| 402 | AND appointment_time < v_end_time
|
|---|
| 403 | AND end_time > p_time) THEN
|
|---|
| 404 | RAISE EXCEPTION 'Time slot %–% conflicts with an existing appointment for staff %.',
|
|---|
| 405 | p_time, v_end_time, p_staff_id;
|
|---|
| 406 | END IF;
|
|---|
| 407 |
|
|---|
| 408 | -- 13. Insert appointment
|
|---|
| 409 | INSERT INTO appointment (client_id, staff_id, location_id, appointment_date, appointment_time, end_time, status)
|
|---|
| 410 | VALUES (p_client_id, p_staff_id, p_location_id, p_date, p_time, v_end_time, 'pending')
|
|---|
| 411 | RETURNING appointment_id INTO v_appointment_id;
|
|---|
| 412 |
|
|---|
| 413 | -- 14. Snapshot services
|
|---|
| 414 | INSERT INTO appointment_service (appointment_id, service_id, duration_minutes, price)
|
|---|
| 415 | SELECT v_appointment_id, service_id, duration_minutes, price
|
|---|
| 416 | FROM service
|
|---|
| 417 | WHERE service_id = ANY (p_service_ids);
|
|---|
| 418 |
|
|---|
| 419 | -- 15. Mark time slots as occupied
|
|---|
| 420 | UPDATE staff_time_slot
|
|---|
| 421 | SET appointment_id = v_appointment_id
|
|---|
| 422 | WHERE staff_id = p_staff_id
|
|---|
| 423 | AND slot_start >= (p_date + p_time)::TIMESTAMP
|
|---|
| 424 | AND slot_start < (p_date + v_end_time)::TIMESTAMP
|
|---|
| 425 | AND appointment_id IS NULL;
|
|---|
| 426 |
|
|---|
| 427 | END;
|
|---|
| 428 | $$;
|
|---|
| 429 |
|
|---|
| 430 | -- Valid booking:
|
|---|
| 431 | CALL client_book_appointment(710096, 23918, 3, CURRENT_DATE + 1, '13:00', ARRAY [18, 19]);
|
|---|
| 432 |
|
|---|
| 433 | -- Test error: date in the past
|
|---|
| 434 | CALL client_book_appointment(710096, 23918, 3, CURRENT_DATE - 1, '10:00', ARRAY [18, 19]);
|
|---|
| 435 |
|
|---|
| 436 | -- Test error: empty service list
|
|---|
| 437 | CALL client_book_appointment(710096, 23918, 3, CURRENT_DATE + 1, '10:00', ARRAY []::INT[]);
|
|---|
| 438 |
|
|---|
| 439 | -- Test error: overlapping slot (run the valid booking twice)
|
|---|
| 440 | CALL client_book_appointment(710096, 23918, 3, CURRENT_DATE + 1, '10:00', ARRAY [18, 19]);
|
|---|
| 441 |
|
|---|
| 442 | -- Test error: staff not assigned to service
|
|---|
| 443 | CALL client_book_appointment(710096, 23918, 3, CURRENT_DATE + 1, '14:00', ARRAY [99]);
|
|---|
| 444 |
|
|---|
| 445 | CREATE OR REPLACE PROCEDURE client_cancel_appointment(
|
|---|
| 446 | p_appointment_id INT,
|
|---|
| 447 | p_client_id INT,
|
|---|
| 448 | p_cancellation_reason TEXT DEFAULT NULL
|
|---|
| 449 | )
|
|---|
| 450 | LANGUAGE plpgsql
|
|---|
| 451 | AS
|
|---|
| 452 | $$
|
|---|
| 453 | DECLARE
|
|---|
| 454 | v_rec RECORD;
|
|---|
| 455 | BEGIN
|
|---|
| 456 |
|
|---|
| 457 | -- 1. Fetch appointment, verify ownership and cancellability
|
|---|
| 458 | SELECT a.status, a.appointment_date, a.appointment_time, a.staff_id
|
|---|
| 459 | INTO v_rec
|
|---|
| 460 | FROM appointment a
|
|---|
| 461 | WHERE a.appointment_id = p_appointment_id
|
|---|
| 462 | AND a.client_id = p_client_id;
|
|---|
| 463 |
|
|---|
| 464 | IF NOT FOUND THEN
|
|---|
| 465 | RAISE EXCEPTION 'Appointment % not found or does not belong to client %.',
|
|---|
| 466 | p_appointment_id, p_client_id;
|
|---|
| 467 | END IF;
|
|---|
| 468 |
|
|---|
| 469 | IF v_rec.status NOT IN ('pending', 'confirmed') THEN
|
|---|
| 470 | RAISE EXCEPTION 'Appointment % cannot be cancelled (current status: %).',
|
|---|
| 471 | p_appointment_id, v_rec.status;
|
|---|
| 472 | END IF;
|
|---|
| 473 |
|
|---|
| 474 | IF (v_rec.appointment_date + v_rec.appointment_time) <= (CURRENT_DATE + CURRENT_TIME) THEN
|
|---|
| 475 | RAISE EXCEPTION 'Appointment % has already started or passed.', p_appointment_id;
|
|---|
| 476 | END IF;
|
|---|
| 477 |
|
|---|
| 478 | -- 2. Cancel it
|
|---|
| 479 | UPDATE appointment
|
|---|
| 480 | SET status = 'cancelled',
|
|---|
| 481 | cancelled_at = NOW(),
|
|---|
| 482 | cancellation_reason = p_cancellation_reason
|
|---|
| 483 | WHERE appointment_id = p_appointment_id;
|
|---|
| 484 |
|
|---|
| 485 | -- 3. Free up the time slots
|
|---|
| 486 | UPDATE staff_time_slot
|
|---|
| 487 | SET appointment_id = NULL
|
|---|
| 488 | WHERE appointment_id = p_appointment_id;
|
|---|
| 489 |
|
|---|
| 490 | RAISE NOTICE 'Appointment % cancelled.', p_appointment_id;
|
|---|
| 491 |
|
|---|
| 492 | END;
|
|---|
| 493 | $$;
|
|---|
| 494 |
|
|---|
| 495 | -- Cancel appointment 1 belonging to client 1
|
|---|
| 496 | CALL client_cancel_appointment(5917661, 710096, 'Changed my mind');
|
|---|
| 497 |
|
|---|
| 498 | -- Test error: already cancelled (run twice)
|
|---|
| 499 | CALL client_cancel_appointment(5917661, 710096, 'Changed my mind');
|
|---|
| 500 |
|
|---|
| 501 | -- Test error: appointment belongs to a different client or does not exist
|
|---|
| 502 | CALL client_cancel_appointment(1, 99, NULL);
|
|---|
| 503 |
|
|---|
| 504 | CREATE OR REPLACE PROCEDURE generate_invoice(
|
|---|
| 505 | p_appointment_id INT,
|
|---|
| 506 | p_payment_method payment_method_enum,
|
|---|
| 507 | p_promo_codes TEXT[] DEFAULT NULL,
|
|---|
| 508 | p_tax NUMERIC(10, 2) DEFAULT 0
|
|---|
| 509 | )
|
|---|
| 510 | LANGUAGE plpgsql
|
|---|
| 511 | AS
|
|---|
| 512 | $$
|
|---|
| 513 | DECLARE
|
|---|
| 514 | v_client_id INT;
|
|---|
| 515 | v_company_id INT;
|
|---|
| 516 | v_subtotal NUMERIC(10, 2);
|
|---|
| 517 | v_discount_total NUMERIC(10, 2) := 0;
|
|---|
| 518 | v_invoice_id INT;
|
|---|
| 519 | v_promo RECORD;
|
|---|
| 520 | BEGIN
|
|---|
| 521 |
|
|---|
| 522 | -- 1. Appointment exists, is completed, resolve client and company
|
|---|
| 523 | SELECT a.client_id, cl.company_id
|
|---|
| 524 | INTO v_client_id, v_company_id
|
|---|
| 525 | FROM appointment a
|
|---|
| 526 | JOIN company_location cl ON cl.location_id = a.location_id
|
|---|
| 527 | WHERE a.appointment_id = p_appointment_id
|
|---|
| 528 | AND a.status = 'completed';
|
|---|
| 529 |
|
|---|
| 530 | IF NOT FOUND THEN
|
|---|
| 531 | RAISE EXCEPTION 'Appointment % does not exist or is not completed.', p_appointment_id;
|
|---|
| 532 | END IF;
|
|---|
| 533 |
|
|---|
| 534 | -- 2. No invoice already exists
|
|---|
| 535 | IF EXISTS (SELECT 1 FROM invoice WHERE appointment_id = p_appointment_id) THEN
|
|---|
| 536 | RAISE EXCEPTION 'An invoice already exists for appointment %.', p_appointment_id;
|
|---|
| 537 | END IF;
|
|---|
| 538 |
|
|---|
| 539 | -- 3. Subtotal from the appointment_service snapshot
|
|---|
| 540 | SELECT COALESCE(SUM(price), 0)
|
|---|
| 541 | INTO v_subtotal
|
|---|
| 542 | FROM appointment_service
|
|---|
| 543 | WHERE appointment_id = p_appointment_id;
|
|---|
| 544 |
|
|---|
| 545 | -- 4. Apply promo codes
|
|---|
| 546 | FOR v_promo IN
|
|---|
| 547 | SELECT pc.promo_id, pc.discount_type, pc.discount_value
|
|---|
| 548 | FROM promo_code pc
|
|---|
| 549 | WHERE pc.code = ANY (p_promo_codes)
|
|---|
| 550 | AND pc.company_id = v_company_id
|
|---|
| 551 | AND CURRENT_DATE BETWEEN pc.valid_from AND pc.valid_until
|
|---|
| 552 | LOOP
|
|---|
| 553 | v_discount_total := v_discount_total + CASE
|
|---|
| 554 | WHEN v_promo.discount_type = 'percentage'
|
|---|
| 555 | THEN ROUND(v_subtotal * (v_promo.discount_value / 100), 2)
|
|---|
| 556 | ELSE v_promo.discount_value
|
|---|
| 557 | END;
|
|---|
| 558 | END LOOP;
|
|---|
| 559 |
|
|---|
| 560 | -- Cap discount at subtotal so total never goes negative
|
|---|
| 561 | v_discount_total := LEAST(v_discount_total, v_subtotal);
|
|---|
| 562 |
|
|---|
| 563 | -- 5. Insert invoice
|
|---|
| 564 | INSERT INTO invoice (appointment_id, client_id, invoice_date,
|
|---|
| 565 | subtotal, discount_total, tax, payment_method)
|
|---|
| 566 | VALUES (p_appointment_id, v_client_id, CURRENT_DATE,
|
|---|
| 567 | v_subtotal, v_discount_total, p_tax, p_payment_method)
|
|---|
| 568 | RETURNING invoice_id INTO v_invoice_id;
|
|---|
| 569 |
|
|---|
| 570 | -- 6. Link promos to invoice
|
|---|
| 571 | INSERT INTO invoice_promo (invoice_id, promo_id)
|
|---|
| 572 | SELECT v_invoice_id, pc.promo_id
|
|---|
| 573 | FROM promo_code pc
|
|---|
| 574 | WHERE pc.code = ANY (p_promo_codes)
|
|---|
| 575 | AND pc.company_id = v_company_id
|
|---|
| 576 | AND CURRENT_DATE BETWEEN pc.valid_from AND pc.valid_until;
|
|---|
| 577 |
|
|---|
| 578 | RAISE NOTICE 'Invoice % created for appointment %.', v_invoice_id, p_appointment_id;
|
|---|
| 579 |
|
|---|
| 580 | END;
|
|---|
| 581 | $$;
|
|---|
| 582 |
|
|---|
| 583 | -- Valid invoice: completed appointment 1, cash, no promos
|
|---|
| 584 | CALL generate_invoice(5025426, 'cash', NULL, 0);
|
|---|
| 585 |
|
|---|
| 586 | -- With a promo code
|
|---|
| 587 | CALL generate_invoice(4945256, 'card', ARRAY ['SUMMER10'], 1.50);
|
|---|
| 588 |
|
|---|
| 589 | -- Test error: invoice already exists (run twice)
|
|---|
| 590 | CALL generate_invoice(5025426, 'cash', NULL, 0);
|
|---|
| 591 |
|
|---|
| 592 | -- Test error: appointment not completed
|
|---|
| 593 | CALL generate_invoice(3, 'cash', NULL, 0);
|
|---|
| 594 |
|
|---|
| 595 |
|
|---|
| 596 |
|
|---|
| 597 | CREATE OR REPLACE PROCEDURE generate_staff_time_slots_from_to(
|
|---|
| 598 | p_staff_id INT,
|
|---|
| 599 | p_from_date DATE,
|
|---|
| 600 | p_to_date DATE
|
|---|
| 601 | )
|
|---|
| 602 | LANGUAGE plpgsql
|
|---|
| 603 | AS $$
|
|---|
| 604 | BEGIN
|
|---|
| 605 |
|
|---|
| 606 | IF NOT EXISTS (
|
|---|
| 607 | SELECT 1 FROM staff s
|
|---|
| 608 | JOIN "user" u ON u.user_id = s.staff_id
|
|---|
| 609 | WHERE s.staff_id = p_staff_id AND u.is_active = TRUE
|
|---|
| 610 | ) THEN
|
|---|
| 611 | RAISE EXCEPTION 'Staff % does not exist or is not active.', p_staff_id;
|
|---|
| 612 | END IF;
|
|---|
| 613 |
|
|---|
| 614 | IF p_to_date < p_from_date THEN
|
|---|
| 615 | RAISE EXCEPTION 'p_to_date must be on or after p_from_date.';
|
|---|
| 616 | END IF;
|
|---|
| 617 |
|
|---|
| 618 | INSERT INTO staff_time_slot (staff_id, slot_start)
|
|---|
| 619 | SELECT p_staff_id, slot
|
|---|
| 620 | FROM generate_series(
|
|---|
| 621 | p_from_date::TIMESTAMP,
|
|---|
| 622 | p_to_date::TIMESTAMP + INTERVAL '1 day' - INTERVAL '15 minutes',
|
|---|
| 623 | INTERVAL '15 minutes'
|
|---|
| 624 | ) AS slot
|
|---|
| 625 | JOIN staff_availability sa
|
|---|
| 626 | ON sa.staff_id = p_staff_id
|
|---|
| 627 | AND sa.day_of_week = LOWER(TRIM(TO_CHAR(slot, 'FMDay')))::day_of_week_enum
|
|---|
| 628 | AND slot::TIME >= sa.start_time
|
|---|
| 629 | AND slot::TIME < sa.end_time
|
|---|
| 630 | WHERE NOT EXISTS (
|
|---|
| 631 | SELECT 1 FROM blocked_time bt
|
|---|
| 632 | WHERE bt.staff_id = p_staff_id
|
|---|
| 633 | AND bt.start_datetime <= slot
|
|---|
| 634 | AND bt.end_datetime > slot
|
|---|
| 635 | )
|
|---|
| 636 | ON CONFLICT DO NOTHING;
|
|---|
| 637 |
|
|---|
| 638 | RAISE NOTICE 'Slots generated for staff % — % to %.', p_staff_id, p_from_date, p_to_date;
|
|---|
| 639 |
|
|---|
| 640 | END;
|
|---|
| 641 | $$;
|
|---|
| 642 |
|
|---|
| 643 |
|
|---|
| 644 |
|
|---|
| 645 | CREATE OR REPLACE FUNCTION trg_check_appointment_conflicts()
|
|---|
| 646 | RETURNS TRIGGER
|
|---|
| 647 | LANGUAGE plpgsql
|
|---|
| 648 | AS $$
|
|---|
| 649 | BEGIN
|
|---|
| 650 | IF EXISTS (
|
|---|
| 651 | SELECT 1
|
|---|
| 652 | FROM appointment
|
|---|
| 653 | WHERE staff_id = NEW.staff_id
|
|---|
| 654 | AND appointment_date = NEW.appointment_date
|
|---|
| 655 | AND appointment_time = NEW.appointment_time
|
|---|
| 656 | AND appointment_id <> NEW.appointment_id
|
|---|
| 657 | AND status <> 'cancelled'
|
|---|
| 658 | ) THEN
|
|---|
| 659 | RAISE EXCEPTION
|
|---|
| 660 | 'Staff member % is already booked on % at %.',
|
|---|
| 661 | NEW.staff_id, NEW.appointment_date, NEW.appointment_time
|
|---|
| 662 | USING ERRCODE = 'unique_violation';
|
|---|
| 663 | END IF;
|
|---|
| 664 |
|
|---|
| 665 | IF EXISTS (
|
|---|
| 666 | SELECT 1
|
|---|
| 667 | FROM appointment
|
|---|
| 668 | WHERE client_id = NEW.client_id
|
|---|
| 669 | AND appointment_date = NEW.appointment_date
|
|---|
| 670 | AND appointment_id <> NEW.appointment_id
|
|---|
| 671 | AND status <> 'cancelled'
|
|---|
| 672 | AND NEW.appointment_time < end_time
|
|---|
| 673 | AND appointment_time < NEW.end_time
|
|---|
| 674 | ) THEN
|
|---|
| 675 | RAISE EXCEPTION
|
|---|
| 676 | 'Client % already has an overlapping appointment on % between % and %.',
|
|---|
| 677 | NEW.client_id, NEW.appointment_date, NEW.appointment_time, NEW.end_time
|
|---|
| 678 | USING ERRCODE = 'unique_violation';
|
|---|
| 679 | END IF;
|
|---|
| 680 |
|
|---|
| 681 | RETURN NEW;
|
|---|
| 682 | END;
|
|---|
| 683 | $$;
|
|---|
| 684 |
|
|---|
| 685 |
|
|---|
| 686 |
|
|---|
| 687 |
|
|---|
| 688 | CREATE TRIGGER trg_appointment_conflicts
|
|---|
| 689 | BEFORE INSERT OR UPDATE OF
|
|---|
| 690 | staff_id, client_id, appointment_date, appointment_time, end_time, status
|
|---|
| 691 | ON appointment
|
|---|
| 692 | FOR EACH ROW
|
|---|
| 693 | EXECUTE FUNCTION trg_check_appointment_conflicts();
|
|---|