| 5 | | || Type || Name || Description || Step || |
| 6 | | || Trigger || trg_appointment_status_audit || |
| 7 | | || Function || fn_get_active_diagnoses || |
| 8 | | || Function || fn_prescribe_medicine || |
| 9 | | || Function || fn_check_doctor_availability || |
| 10 | | || Procedure || prc_create_referral || |
| 11 | | || Procedure || prc_add_appointment_for_referral || |
| 12 | | || Procedure || prc_complete_examination || |
| 13 | | || Procedure || prc_add_new_diagnosis || |
| 14 | | || Procedure || prc_create_prescription || |
| 15 | | || Procedure || prc_buy_prescription || |
| | 5 | ||= Type =||= Name =||= Description =||= Step =|| |
| | 6 | ||=Trigger=|| trg_appointment_status_audit || Logs every change in appointment status || - || |
| | 7 | ||=Function=|| fn_get_active_diagnoses || Returns active diagnoses for patient || before 5 || |
| | 8 | ||=Function=|| fn_prescribe_medicine || Validates and returns data for prescription || in 5 || |
| | 9 | ||=Function=|| fn_check_doctor_availability || Checks doctor availability for date || in 2 || |
| | 10 | ||=Procedure=|| prc_create_referral || Creates a referral || 1 || |
| | 11 | ||=Procedure=|| prc_add_appointment_for_referral || Schedules an appointment after a referral || 2 || |
| | 12 | ||=Procedure=|| prc_complete_examination || Completes an appointment, creates an examination || 3 || |
| | 13 | ||=Procedure=|| prc_add_new_diagnosis || Adds a diagnosis after an examination || 4 || |
| | 14 | ||=Procedure=|| prc_create_prescription || Creates a prescription for a diagnosis || 5 || |
| | 15 | ||=Procedure=|| prc_buy_prescription || Dispenses a medicine at a pharmacy || 6 || |
| | 16 | |
| | 17 | == Application workflow |
| | 18 | |
| | 19 | ||= Step =||= Description =|| |
| | 20 | ||=1=|| Doctor refers patient to department || |
| | 21 | ||=2=|| Schedule appointment by referral || |
| | 22 | ||=3=|| Complete appointment + create examination || |
| | 23 | ||=4=|| Doctor adds diagnosis by examination || |
| | 24 | ||=5=|| Doctor issues prescription for diagnosis || |
| | 25 | ||=6=|| Patient picks up medication at pharmacy || |
| | 26 | |
| | 27 | === Trigger 1 : trg_appointment_status_audit |
| | 28 | |
| | 29 | The trigger is automatically fired after every UPDATE on the Appointment table. When the old and new status values differ, a new record is inserted into appointment_status_audit with the old status, the new status, the time of change, and the user. This provides a complete history of appointment changes without any additional logic in the application — the trigger is transparent and fires both on direct UPDATE operations and when calling prc_complete_examination. |
| | 30 | |
| | 31 | **Supporting table:** |
| | 32 | |
| | 33 | CREATE TABLE IF NOT EXISTS appointment_status_audit ( |
| | 34 | |
| | 35 | audit_id SERIAL PRIMARY KEY, |
| | 36 | |
| | 37 | appointment_id INT NOT NULL, |
| | 38 | |
| | 39 | old_status VARCHAR(30), |
| | 40 | |
| | 41 | new_status VARCHAR(30), |
| | 42 | |
| | 43 | changed_at TIMESTAMP NOT NULL DEFAULT NOW(), |
| | 44 | |
| | 45 | changed_by TEXT DEFAULT CURRENT_USER |
| | 46 | |
| | 47 | ); |
| | 48 | |
| | 49 | **Implementation:** |
| | 50 | |
| | 51 | CREATE OR REPLACE FUNCTION fn_audit_appointment_status() |
| | 52 | |
| | 53 | RETURNS TRIGGER AS $$ |
| | 54 | |
| | 55 | BEGIN |
| | 56 | |
| | 57 | IF OLD.status IS DISTINCT FROM NEW.status THEN |
| | 58 | |
| | 59 | INSERT INTO appointment_status_audit (appointment_id, old_status, new_status, changed_at, changed_by) |
| | 60 | |
| | 61 | VALUES (NEW.appointment_id, OLD.status, NEW.status, NOW(), CURRENT_USER); |
| | 62 | |
| | 63 | END IF; |
| | 64 | |
| | 65 | RETURN NEW; |
| | 66 | |
| | 67 | END; |
| | 68 | |
| | 69 | $$ LANGUAGE plpgsql; |
| | 70 | |
| | 71 | CREATE TRIGGER trg_appointment_status_audit |
| | 72 | |
| | 73 | AFTER UPDATE ON Appointment |
| | 74 | |
| | 75 | FOR EACH ROW |
| | 76 | |
| | 77 | EXECUTE FUNCTION fn_audit_appointment_status(); |
| | 78 | |
| | 79 | === Function 1 : fn_get_active_diagnoses |
| | 80 | |
| | 81 | The function returns a tabular result with all active diagnoses for a given patient. A diagnosis whose date_to is NULL or is in the future is active. For each diagnosis, the ICD code, description, whether it is primary, and the name of the doctor who prescribed it are returned. Before prescribing a medication, the application calls this function to show the doctor for which diagnosis it is prescribing. |
| | 82 | |
| | 83 | **Implementation:** |
| | 84 | |
| | 85 | CREATE OR REPLACE FUNCTION fn_get_active_diagnoses(p_patient_id INT) |
| | 86 | |
| | 87 | RETURNS TABLE (patient_diagnosis_id INT, diagnosis_name VARCHAR(255), |
| | 88 | |
| | 89 | icd_code VARCHAR(20), icd_description VARCHAR(255), |
| | 90 | |
| | 91 | is_primary BOOLEAN, date_from DATE, date_to DATE, |
| | 92 | |
| | 93 | doctor_name TEXT) AS $$ |
| | 94 | |
| | 95 | BEGIN |
| | 96 | |
| | 97 | IF NOT EXISTS (SELECT 1 FROM Patient WHERE patient_id = p_patient_id) THEN |
| | 98 | |
| | 99 | RAISE EXCEPTION 'Patient with ID % does not exist.', p_patient_id; |
| | 100 | |
| | 101 | END IF; |
| | 102 | |
| | 103 | RETURN QUERY |
| | 104 | |
| | 105 | SELECT pd.patient_diagnosis_id, pd.diagnosis_name, |
| | 106 | |
| | 107 | i.code, i.description, pd.is_primary, |
| | 108 | |
| | 109 | pd.date_from, pd.date_to, |
| | 110 | |
| | 111 | (d.first_name | | ' ' | | d.last_name)::TEXT |
| | 112 | |
| | 113 | FROM Patient_diagnosis pd |
| | 114 | |
| | 115 | JOIN ICD i ON i.icd_id = pd.icd_id |
| | 116 | |
| | 117 | JOIN Doctor d ON d.doctor_id = pd.doctor_id |
| | 118 | |
| | 119 | WHERE pd.patient_id = p_patient_id AND (pd.date_to IS NULL OR pd.date_to >= CURRENT_DATE) |
| | 120 | |
| | 121 | ORDER BY pd.is_primary DESC, pd.date_from DESC; |
| | 122 | |
| | 123 | END; $$ LANGUAGE plpgsql; |
| | 124 | |
| | 125 | === Function 2 : fn_prescribe_medicine |
| | 126 | |
| | 127 | The function is the central validation logic for prescribing medications. It checks that the patient and doctor exist, that the diagnosis belongs to the patient and is active, that the ATC code exists, that the dosage and duration are positive, and that there is sufficient stock for that ATC code. If all hecks pass, it returns a single row with all the data needed to create a prescription. The prc_create_prescription procedure calls this function and directly uses the returned values. |
| | 128 | |
| | 129 | **Implementation:** |
| | 130 | |
| | 131 | CREATE OR REPLACE FUNCTION fn_prescribe_medicine( |
| | 132 | |
| | 133 | p_patient_id INT, p_doctor_id INT, |
| | 134 | |
| | 135 | p_patient_diagnosis_id INT, p_atc_id INT, |
| | 136 | |
| | 137 | p_dosage INT, p_duration INT |
| | 138 | |
| | 139 | ) |
| | 140 | |
| | 141 | RETURNS TABLE (patient_id INT, doctor_id INT, |
| | 142 | |
| | 143 | patient_diagnosis_id INT, atc_id INT, |
| | 144 | |
| | 145 | inventory_id INT, dosage INT, duration INT, |
| | 146 | |
| | 147 | presc_date DATE, available_stock INT, |
| | 148 | |
| | 149 | drug_name TEXT) AS $$ |
| | 150 | |
| | 151 | DECLARE |
| | 152 | |
| | 153 | v_inventory_id INT; v_stock INT; v_drug_name TEXT; |
| | 154 | |
| | 155 | BEGIN |
| | 156 | |
| | 157 | IF NOT EXISTS (SELECT 1 FROM Patient WHERE patient_id = p_patient_id) |
| | 158 | |
| | 159 | THEN RAISE EXCEPTION 'Patient % does not exist.', p_patient_id; |
| | 160 | |
| | 161 | END IF; |
| | 162 | |
| | 163 | IF NOT EXISTS (SELECT 1 FROM Doctor WHERE doctor_id = p_doctor_id) |
| | 164 | |
| | 165 | THEN RAISE EXCEPTION 'Doctor % does not exist.', p_doctor_id; |
| | 166 | |
| | 167 | END IF; |
| | 168 | |
| | 169 | IF NOT EXISTS ( |
| | 170 | |
| | 171 | SELECT 1 FROM Patient_diagnosis |
| | 172 | |
| | 173 | WHERE patient_diagnosis_id = p_patient_diagnosis_id |
| | 174 | |
| | 175 | AND patient_id = p_patient_id |
| | 176 | |
| | 177 | AND (date_to IS NULL OR date_to >= CURRENT_DATE) |
| | 178 | |
| | 179 | ) THEN |
| | 180 | |
| | 181 | RAISE EXCEPTION 'Diagnosis % not active for patient %.', p_patient_diagnosis_id, p_patient_id; |
| | 182 | |
| | 183 | END IF; |
| | 184 | |
| | 185 | SELECT i.inventory_id, i.quantity, (dp.producer_name | | ' - ' | | ac.atc_code)::TEXT |
| | 186 | |
| | 187 | INTO v_inventory_id, v_stock, v_drug_name |
| | 188 | |
| | 189 | FROM Inventory i |
| | 190 | |
| | 191 | JOIN Drug d ON d.product_id = i.product_id |
| | 192 | |
| | 193 | JOIN ATC_code ac ON ac.atc_id = d.atc_id |
| | 194 | |
| | 195 | JOIN Drug_producers dp ON dp.drug_prod_id = d.drug_prod_id |
| | 196 | |
| | 197 | WHERE d.atc_id = p_atc_id AND i.quantity >= p_dosage |
| | 198 | |
| | 199 | ORDER BY i.quantity DESC LIMIT 1; |
| | 200 | |
| | 201 | IF NOT FOUND THEN |
| | 202 | |
| | 203 | RAISE EXCEPTION 'No stock for ATC ID %. Required: %.', p_atc_id, p_dosage; |
| | 204 | |
| | 205 | END IF; |
| | 206 | |
| | 207 | RETURN QUERY SELECT p_patient_id, p_doctor_id, p_patient_diagnosis_id, p_atc_id, v_inventory_id, p_dosage, p_duration, CURRENT_DATE, v_stock, v_drug_name; |
| | 208 | |
| | 209 | END; $$ LANGUAGE plpgsql; |
| | 210 | |
| | 211 | === Function 3 : fn_check_doctor_availability |
| | 212 | |
| | 213 | The function counts the number of active appointments (excluding CANCELLED and NO_SHOW) that a doctor has on a given date. If the number is less than 30, it returns TRUE (available), otherwise FALSE. The prc_add_appointment_for_referral procedure calls this function and throws an exception if the doctor is not available, instead of directly inserting and then detecting a problem. It can also be used directly from the application to display available appointments. |
| | 214 | |
| | 215 | **Implementation:** |
| | 216 | |
| | 217 | CREATE OR REPLACE FUNCTION fn_check_doctor_availability( |
| | 218 | |
| | 219 | p_doctor_id INT, |
| | 220 | |
| | 221 | p_appointment_date DATE |
| | 222 | |
| | 223 | ) |
| | 224 | |
| | 225 | RETURNS BOOLEAN AS $$ |
| | 226 | |
| | 227 | DECLARE v_count INT; |
| | 228 | |
| | 229 | BEGIN |
| | 230 | |
| | 231 | IF NOT EXISTS (SELECT 1 FROM Doctor WHERE doctor_id = p_doctor_id) THEN |
| | 232 | |
| | 233 | RAISE EXCEPTION 'Doctor with ID % does not exist.', p_doctor_id; |
| | 234 | |
| | 235 | END IF; |
| | 236 | |
| | 237 | SELECT COUNT(*) INTO v_count |
| | 238 | |
| | 239 | FROM Appointment |
| | 240 | |
| | 241 | WHERE doctor_id = p_doctor_id AND appointment_date = p_appointment_date AND status NOT IN ('CANCELLED', 'NO_SHOW'); |
| | 242 | |
| | 243 | RETURN v_count < 30; |
| | 244 | |
| | 245 | END; |
| | 246 | |
| | 247 | $$ LANGUAGE plpgsql; |
| | 248 | |
| | 249 | === Procedure 1 : prc_create_referral [Step 1] |
| | 250 | |
| | 251 | The procedure does the following: (1) validates that the patient, department, and referring doctor exist, (2) if a specific doctor is given — validates that it exists and is not the same as the referring doctor, (3) inserts a new record into Referral. This is the entry point into the medical process — without a referral, there is no appointment. |
| | 252 | |
| | 253 | **Implementation:** |
| | 254 | |
| | 255 | CREATE OR REPLACE PROCEDURE prc_create_referral( |
| | 256 | |
| | 257 | p_patient_id INT, |
| | 258 | |
| | 259 | p_department_id INT, |
| | 260 | |
| | 261 | p_referring_doctor_id INT, |
| | 262 | |
| | 263 | p_referred_doctor_id INT DEFAULT NULL |
| | 264 | |
| | 265 | ) |
| | 266 | |
| | 267 | LANGUAGE plpgsql AS $$ |
| | 268 | |
| | 269 | DECLARE v_new_referral_id INT; |
| | 270 | |
| | 271 | BEGIN |
| | 272 | |
| | 273 | IF NOT EXISTS (SELECT 1 FROM Patient WHERE patient_id = p_patient_id) THEN |
| | 274 | |
| | 275 | RAISE EXCEPTION 'Patient % does not exist.', p_patient_id; |
| | 276 | |
| | 277 | END IF; |
| | 278 | |
| | 279 | IF NOT EXISTS (SELECT 1 FROM Department WHERE department_id = p_department_id) THEN |
| | 280 | |
| | 281 | RAISE EXCEPTION 'Department % does not exist.', p_department_id; |
| | 282 | |
| | 283 | END IF; |
| | 284 | |
| | 285 | IF NOT EXISTS (SELECT 1 FROM Doctor WHERE doctor_id = p_referring_doctor_id) THEN |
| | 286 | |
| | 287 | RAISE EXCEPTION 'Referring doctor % does not exist.', p_referring_doctor_id; |
| | 288 | |
| | 289 | END IF; |
| | 290 | |
| | 291 | IF p_referred_doctor_id IS NOT NULL THEN |
| | 292 | |
| | 293 | IF NOT EXISTS (SELECT 1 FROM Doctor WHERE doctor_id = p_referred_doctor_id) THEN |
| | 294 | |
| | 295 | RAISE EXCEPTION 'Referred doctor % does not exist.', p_referred_doctor_id; |
| | 296 | |
| | 297 | END IF; |
| | 298 | |
| | 299 | IF p_referred_doctor_id = p_referring_doctor_id THEN |
| | 300 | |
| | 301 | RAISE EXCEPTION 'Referring and referred doctor cannot be the same (ID: %).', |
| | 302 | |
| | 303 | p_referring_doctor_id; |
| | 304 | |
| | 305 | END IF; |
| | 306 | |
| | 307 | END IF; |
| | 308 | |
| | 309 | SELECT COALESCE(MAX(referral_id),0)+1 INTO v_new_referral_id FROM Referral; |
| | 310 | |
| | 311 | INSERT INTO Referral(referral_id,patient_id,department_id, referred_doctor_id,referring_doctor_id) |
| | 312 | |
| | 313 | VALUES(v_new_referral_id,p_patient_id,p_department_id, p_referred_doctor_id,p_referring_doctor_id); |
| | 314 | |
| | 315 | RAISE NOTICE 'Referral % created. Patient % → Department % by Doctor %.', v_new_referral_id, p_patient_id, p_department_id, p_referring_doctor_id; |
| | 316 | |
| | 317 | END; $$; |
| | 318 | |
| | 319 | === Procedure 2 : prc_add_appointment_for_referral [Step 2] |
| | 320 | |
| | 321 | The procedure does the following: (1) takes the patient_id from the referral, (2) validates the doctor, date (not in the past), type, and priority, (3) calls fn_check_doctor_availability — if the doctor is busy it throws an exception, (4) checks that the referral does not already have an active appointment on that date, (5) inserts a new Appointment with status='SCHEDULED'. This procedure is the second in the flow — a referral must exist first. |
| | 322 | |
| | 323 | **Implemenetation:** |
| | 324 | |
| | 325 | CREATE OR REPLACE PROCEDURE prc_add_appointment_for_referral( |
| | 326 | |
| | 327 | p_referral_id INT, p_doctor_id INT, p_appointment_date DATE, |
| | 328 | |
| | 329 | p_appointment_type VARCHAR(30) DEFAULT 'REGULAR', |
| | 330 | |
| | 331 | p_priority_level VARCHAR(30) DEFAULT 'MEDIUM' |
| | 332 | |
| | 333 | ) |
| | 334 | |
| | 335 | LANGUAGE plpgsql AS $$ |
| | 336 | |
| | 337 | DECLARE |
| | 338 | |
| | 339 | v_patient_id INT; v_new_appt_id INT; v_available BOOLEAN; |
| | 340 | |
| | 341 | BEGIN |
| | 342 | |
| | 343 | SELECT patient_id INTO v_patient_id FROM Referral |
| | 344 | |
| | 345 | WHERE referral_id = p_referral_id; |
| | 346 | |
| | 347 | IF NOT FOUND THEN |
| | 348 | |
| | 349 | RAISE EXCEPTION 'Referral % does not exist.', p_referral_id; |
| | 350 | |
| | 351 | END IF; |
| | 352 | |
| | 353 | IF p_appointment_date < CURRENT_DATE THEN |
| | 354 | |
| | 355 | RAISE EXCEPTION 'Date % cannot be in the past.', p_appointment_date; |
| | 356 | |
| | 357 | END IF; |
| | 358 | |
| | 359 | SELECT fn_check_doctor_availability(p_doctor_id, p_appointment_date) |
| | 360 | |
| | 361 | INTO v_available; |
| | 362 | |
| | 363 | IF NOT v_available THEN |
| | 364 | |
| | 365 | RAISE EXCEPTION 'Doctor % has no availability on %.', p_doctor_id, p_appointment_date; |
| | 366 | |
| | 367 | END IF; |
| | 368 | |
| | 369 | IF EXISTS (SELECT 1 FROM Appointment |
| | 370 | |
| | 371 | WHERE referral_id=p_referral_id |
| | 372 | |
| | 373 | AND appointment_date=p_appointment_date |
| | 374 | |
| | 375 | AND status NOT IN ('CANCELLED','NO_SHOW')) THEN |
| | 376 | |
| | 377 | RAISE EXCEPTION 'Referral % already has an appointment on %.', p_referral_id, p_appointment_date; |
| | 378 | |
| | 379 | END IF; |
| | 380 | |
| | 381 | SELECT COALESCE(MAX(appointment_id),0)+1 INTO v_new_appt_id FROM Appointment; |
| | 382 | |
| | 383 | INSERT INTO Appointment(appointment_id,referral_id,doctor_id,patient_id, prescription_value,appointment_date,status,appointment_type,priority_level) |
| | 384 | |
| | 385 | VALUES(v_new_appt_id,p_referral_id,p_doctor_id,v_patient_id, FALSE,p_appointment_date,'SCHEDULED',p_appointment_type,p_priority_level); |
| | 386 | |
| | 387 | RAISE NOTICE 'Appointment % for patient % (referral %) with doctor % on %.', v_new_appt_id,v_patient_id,p_referral_id,p_doctor_id,p_appointment_date; |
| | 388 | |
| | 389 | END; $$; |
| | 390 | |
| | 391 | === Procedure 3 : prc_complete_examination [Step 3] |
| | 392 | |
| | 393 | The procedure: (1) takes the appointment details, (2) validates that it is not already completed, cancelled, or in the future, (3) checks that there is no other examination for that appointment, (4) sets status='COMPLETED' — this automatically triggers trg_appointment_status_audit, (5) |
| | 394 | inserts a new record into Medical_examination. After completing this procedure, the doctor can |
| | 395 | add a diagnosis. |
| | 396 | |
| | 397 | **Implementation:** |
| | 398 | |
| | 399 | CREATE OR REPLACE PROCEDURE prc_complete_examination( |
| | 400 | |
| | 401 | p_appointment_id INT, |
| | 402 | |
| | 403 | p_notes TEXT DEFAULT 'Examination completed.' |
| | 404 | |
| | 405 | ) |
| | 406 | |
| | 407 | LANGUAGE plpgsql AS $$ |
| | 408 | |
| | 409 | DECLARE |
| | 410 | |
| | 411 | v_doctor_id INT; v_exam_date DATE; |
| | 412 | |
| | 413 | v_new_exam_id INT; v_current_status VARCHAR(30); |
| | 414 | |
| | 415 | BEGIN |
| | 416 | |
| | 417 | SELECT doctor_id, appointment_date, status |
| | 418 | |
| | 419 | INTO v_doctor_id, v_exam_date, v_current_status |
| | 420 | |
| | 421 | FROM Appointment WHERE appointment_id = p_appointment_id; |
| | 422 | |
| | 423 | IF NOT FOUND THEN |
| | 424 | |
| | 425 | RAISE EXCEPTION 'Appointment % does not exist.', p_appointment_id; |
| | 426 | |
| | 427 | END IF; |
| | 428 | |
| | 429 | IF v_current_status = 'COMPLETED' THEN |
| | 430 | |
| | 431 | RAISE EXCEPTION 'Appointment % already completed.', p_appointment_id; |
| | 432 | |
| | 433 | END IF; |
| | 434 | |
| | 435 | IF v_current_status = 'CANCELLED' THEN |
| | 436 | |
| | 437 | RAISE EXCEPTION 'Cannot complete CANCELLED appointment %.', p_appointment_id; |
| | 438 | |
| | 439 | END IF; |
| | 440 | |
| | 441 | IF v_exam_date > CURRENT_DATE THEN |
| | 442 | |
| | 443 | RAISE EXCEPTION 'Cannot complete future appointment % on %.', p_appointment_id, v_exam_date; |
| | 444 | |
| | 445 | END IF; |
| | 446 | |
| | 447 | IF EXISTS (SELECT 1 FROM Medical_examination WHERE appointment_id=p_appointment_id) THEN |
| | 448 | |
| | 449 | RAISE EXCEPTION 'Exam already exists for appointment %.', p_appointment_id; |
| | 450 | |
| | 451 | END IF; |
| | 452 | |
| | 453 | UPDATE Appointment SET status='COMPLETED' WHERE appointment_id=p_appointment_id; |
| | 454 | |
| | 455 | SELECT COALESCE(MAX(exam_id),0)+1 INTO v_new_exam_id FROM Medical_examination; |
| | 456 | |
| | 457 | INSERT INTO Medical_examination(exam_id,exam_date,notes,doctor_id,appointment_id) |
| | 458 | |
| | 459 | VALUES(v_new_exam_id,v_exam_date,p_notes,v_doctor_id,p_appointment_id); |
| | 460 | |
| | 461 | RAISE NOTICE 'Appointment % completed. Examination % created.', p_appointment_id, v_new_exam_id; |
| | 462 | |
| | 463 | END; $$; |
| | 464 | |
| | 465 | === Procedure 4 : prc_add_new_diagnosis [Step 4] |
| | 466 | |
| | 467 | The procedure: (1) takes the patient_id and the exam date via exam_id, (2) validates that the doctor and ICD code exist, (3) validates that date_to (if specified) is not before the exam date, (4) if the diagnosis is set as primary — reports if the patient already has an active primary, (5) inserts a new record into Patient_diagnosis. This procedure is a prerequisite for prc_create_prescription. |
| | 468 | |
| | 469 | **Implementation:** |
| | 470 | |
| | 471 | CREATE OR REPLACE PROCEDURE prc_add_new_diagnosis( |
| | 472 | |
| | 473 | p_exam_id INT, |
| | 474 | |
| | 475 | p_icd_id INT, |
| | 476 | |
| | 477 | p_doctor_id INT, |
| | 478 | |
| | 479 | p_diagnosis_name VARCHAR(255), |
| | 480 | |
| | 481 | p_is_primary BOOLEAN DEFAULT FALSE, |
| | 482 | |
| | 483 | p_date_to DATE DEFAULT NULL |
| | 484 | |
| | 485 | ) |
| | 486 | |
| | 487 | LANGUAGE plpgsql AS $$ |
| | 488 | |
| | 489 | DECLARE |
| | 490 | |
| | 491 | v_patient_id INT; v_exam_date DATE; v_new_diagnosis_id INT; |
| | 492 | |
| | 493 | BEGIN |
| | 494 | |
| | 495 | SELECT a.patient_id, me.exam_date INTO v_patient_id, v_exam_date |
| | 496 | |
| | 497 | FROM Medical_examination me |
| | 498 | |
| | 499 | JOIN Appointment a ON a.appointment_id = me.appointment_id |
| | 500 | |
| | 501 | WHERE me.exam_id = p_exam_id; |
| | 502 | |
| | 503 | IF NOT FOUND THEN |
| | 504 | |
| | 505 | RAISE EXCEPTION 'Exam % does not exist.', p_exam_id; |
| | 506 | |
| | 507 | END IF; |
| | 508 | |
| | 509 | IF NOT EXISTS (SELECT 1 FROM Doctor WHERE doctor_id = p_doctor_id) THEN |
| | 510 | |
| | 511 | RAISE EXCEPTION 'Doctor % does not exist.', p_doctor_id; |
| | 512 | |
| | 513 | END IF; |
| | 514 | |
| | 515 | IF NOT EXISTS (SELECT 1 FROM ICD WHERE icd_id = p_icd_id) THEN |
| | 516 | |
| | 517 | RAISE EXCEPTION 'ICD code % does not exist.', p_icd_id; |
| | 518 | |
| | 519 | END IF; |
| | 520 | |
| | 521 | IF p_date_to IS NOT NULL AND p_date_to < v_exam_date THEN |
| | 522 | |
| | 523 | RAISE EXCEPTION 'date_to (%) cannot be before exam date (%).', p_date_to, v_exam_date; |
| | 524 | |
| | 525 | END IF; |
| | 526 | |
| | 527 | SELECT COALESCE(MAX(patient_diagnosis_id),0)+1 |
| | 528 | |
| | 529 | INTO v_new_diagnosis_id FROM Patient_diagnosis; |
| | 530 | |
| | 531 | INSERT INTO Patient_diagnosis(patient_diagnosis_id,patient_id,doctor_id, exam_id,icd_id,diagnosis_name,is_primary,date_from,date_to) |
| | 532 | |
| | 533 | VALUES(v_new_diagnosis_id,v_patient_id,p_doctor_id, p_exam_id,p_icd_id,p_diagnosis_name,p_is_primary,v_exam_date,p_date_to); |
| | 534 | |
| | 535 | RAISE NOTICE 'Diagnosis % (ID: %) added for patient % from exam %.', p_diagnosis_name, v_new_diagnosis_id, v_patient_id, p_exam_id; |
| | 536 | |
| | 537 | END; $$; |
| | 538 | |
| | 539 | === Procedure 5 : prc_create_prescription [Step 5] |
| | 540 | |
| | 541 | The procedure calls fn_prescribe_medicine which performs all the validation (patient, doctor, diagnosis, ATC, stock). If the function succeeds, the procedure additionally checks that there is no existing prescription for that diagnosis and ATC code, and inserts a new entry into Prescription. This procedure is a prerequisite for prc_buy_prescription. |
| | 542 | |
| | 543 | **Implementation:** |
| | 544 | |
| | 545 | CREATE OR REPLACE PROCEDURE prc_create_prescription( |
| | 546 | |
| | 547 | p_patient_id INT, |
| | 548 | |
| | 549 | p_doctor_id INT, |
| | 550 | |
| | 551 | p_patient_diagnosis_id INT, |
| | 552 | |
| | 553 | p_atc_id INT, |
| | 554 | |
| | 555 | p_dosage INT, |
| | 556 | |
| | 557 | p_duration INT |
| | 558 | |
| | 559 | ) |
| | 560 | |
| | 561 | LANGUAGE plpgsql AS $$ |
| | 562 | |
| | 563 | DECLARE |
| | 564 | |
| | 565 | v_new_presc_id INT; v_inventory_id INT; |
| | 566 | |
| | 567 | v_drug_name TEXT; v_stock INT; |
| | 568 | |
| | 569 | BEGIN |
| | 570 | |
| | 571 | SELECT r.inventory_id, r.available_stock, r.drug_name |
| | 572 | |
| | 573 | INTO v_inventory_id, v_stock, v_drug_name |
| | 574 | |
| | 575 | FROM fn_prescribe_medicine(p_patient_id, p_doctor_id, p_patient_diagnosis_id, p_atc_id, p_dosage, p_duration) r; |
| | 576 | |
| | 577 | IF EXISTS (SELECT 1 FROM Prescription |
| | 578 | |
| | 579 | WHERE patient_diagnosis_id = p_patient_diagnosis_id AND atc_id = p_atc_id AND patient_id = p_patient_id) THEN |
| | 580 | |
| | 581 | RAISE EXCEPTION 'Prescription for ATC % already exists for diagnosis %.', p_atc_id, p_patient_diagnosis_id; |
| | 582 | |
| | 583 | END IF; |
| | 584 | |
| | 585 | SELECT COALESCE(MAX(presc_id),0)+1 INTO v_new_presc_id FROM Prescription; |
| | 586 | |
| | 587 | INSERT INTO Prescription(presc_id,atc_id,doctor_id,patient_id, presc_date,duration,dosage,inventory_id,patient_diagnosis_id) |
| | 588 | |
| | 589 | VALUES(v_new_presc_id,p_atc_id,p_doctor_id,p_patient_id, CURRENT_DATE,p_duration,p_dosage,v_inventory_id,p_patient_diagnosis_id); |
| | 590 | |
| | 591 | RAISE NOTICE 'Prescription % created. Drug: %. Dosage: %. Duration: % days.', v_new_presc_id, v_drug_name, p_dosage, p_duration; |
| | 592 | |
| | 593 | END; $$; |
| | 594 | |
| | 595 | === Procedure 6 : prc_buy_prescription [Step 6] |
| | 596 | |
| | 597 | The procedure: (1) takes the prescription details, (2) validates that the pharmacy exists, (3) checks that the prescription is not already filled at that pharmacy, (4) checks inventory, (5) takes the last sale price, (6) inserts Pharmacy_sale, (7) inserts Sale_item, and (8) decrements the quantity in Inventory. This is the final step in the entire medical process and closes the referral → medication cycle. |
| | 598 | |
| | 599 | **Implementation:** |
| | 600 | |
| | 601 | CREATE OR REPLACE PROCEDURE prc_buy_prescription( |
| | 602 | |
| | 603 | p_presc_id INT, p_pharmacy_id INT |
| | 604 | |
| | 605 | ) |
| | 606 | |
| | 607 | LANGUAGE plpgsql AS $$ |
| | 608 | |
| | 609 | DECLARE |
| | 610 | |
| | 611 | v_patient_id INT; v_inventory_id INT; v_dosage INT; |
| | 612 | |
| | 613 | v_current_qty INT; v_sale_price NUMERIC; |
| | 614 | |
| | 615 | v_total_amount NUMERIC; v_new_sale_id INT; v_new_item_id INT; |
| | 616 | |
| | 617 | BEGIN |
| | 618 | |
| | 619 | SELECT patient_id, inventory_id, dosage INTO v_patient_id, v_inventory_id, v_dosage |
| | 620 | |
| | 621 | FROM Prescription WHERE presc_id = p_presc_id; |
| | 622 | |
| | 623 | IF NOT FOUND THEN |
| | 624 | |
| | 625 | RAISE EXCEPTION 'Prescription % does not exist.', p_presc_id; |
| | 626 | |
| | 627 | END IF; |
| | 628 | |
| | 629 | IF NOT EXISTS (SELECT 1 FROM Pharmacy WHERE pharmacy_id = p_pharmacy_id) THEN |
| | 630 | |
| | 631 | RAISE EXCEPTION 'Pharmacy % does not exist.', p_pharmacy_id; |
| | 632 | |
| | 633 | END IF; |
| | 634 | |
| | 635 | IF EXISTS (SELECT 1 FROM Pharmacy_sale WHERE presc_id=p_presc_id AND pharmacy_id=p_pharmacy_id) THEN |
| | 636 | |
| | 637 | RAISE EXCEPTION 'Prescription % already dispensed at pharmacy %.', p_presc_id, p_pharmacy_id; |
| | 638 | |
| | 639 | END IF; |
| | 640 | |
| | 641 | SELECT quantity INTO v_current_qty FROM Inventory WHERE inventory_id=v_inventory_id; |
| | 642 | |
| | 643 | IF v_current_qty < v_dosage THEN |
| | 644 | |
| | 645 | RAISE EXCEPTION 'Insufficient stock. Required: %, Available: %.', v_dosage, v_current_qty; |
| | 646 | |
| | 647 | END IF; |
| | 648 | |
| | 649 | SELECT sale_price INTO v_sale_price FROM Inventory_price |
| | 650 | |
| | 651 | WHERE inventory_id=v_inventory_id ORDER BY date DESC LIMIT 1; |
| | 652 | |
| | 653 | v_total_amount := ROUND(v_sale_price * v_dosage, 2); |
| | 654 | |
| | 655 | SELECT COALESCE(MAX(sale_id),0)+1 INTO v_new_sale_id FROM Pharmacy_sale; |
| | 656 | |
| | 657 | INSERT INTO Pharmacy_sale(sale_id,patient_id,pharmacy_id,presc_id,sale_date,total_amount) |
| | 658 | |
| | 659 | VALUES(v_new_sale_id,v_patient_id,p_pharmacy_id,p_presc_id,CURRENT_DATE,v_total_amount); |
| | 660 | |
| | 661 | SELECT COALESCE(MAX(sale_item_id),0)+1 INTO v_new_item_id FROM Sale_item; |
| | 662 | |
| | 663 | INSERT INTO Sale_item(sale_item_id,sale_id,presc_id,inventory_id,date) |
| | 664 | |
| | 665 | VALUES(v_new_item_id,v_new_sale_id,p_presc_id,v_inventory_id,CURRENT_DATE); |
| | 666 | |
| | 667 | UPDATE Inventory SET quantity=quantity-v_dosage WHERE inventory_id=v_inventory_id; |
| | 668 | |
| | 669 | RAISE NOTICE 'Prescription % dispensed at pharmacy %. Sale: %. Total: %. Stock left: %.', p_presc_id,p_pharmacy_id,v_new_sale_id,v_total_amount,v_current_qtyv_dosage; |
| | 670 | |
| | 671 | END; $$; |