| 179 | | Appointments must obey real scheduling constraints: |
| 180 | | * The responsible_owner_id for an appointment must match the owner of the animal. |
| 181 | | * The same owner cannot have overlapping appointments. |
| 182 | | * The same animal cannot have overlapping appointments. |
| 183 | | * Confirming appointments in the past is blocked. |
| | 179 | Certificates must obey completion and consistency rules: |
| | 180 | * A certificate can only be issued for an enrollment whose completion status is COMPLETED. |
| | 181 | * The same enrollment cannot have more than one certificate. |
| | 182 | * A certificate must always refer to a valid enrollment. |
| | 183 | * Issuing a certificate before course completion is blocked. |
| 192 | | v_animal_owner bigint; |
| 193 | | BEGIN |
| 194 | | SELECT owner_id INTO v_animal_owner |
| 195 | | FROM animals |
| 196 | | WHERE animal_id = NEW.animal_id; |
| 197 | | |
| 198 | | IF v_animal_owner IS NULL THEN |
| 199 | | RAISE EXCEPTION 'Animal % not found for appointment', NEW.animal_id; |
| 200 | | END IF; |
| 201 | | |
| 202 | | IF NEW.responsible_owner_id <> v_animal_owner THEN |
| 203 | | RAISE EXCEPTION |
| 204 | | 'Appointment responsible_owner_id (%) must match animals.owner_id (%) for animal %', |
| 205 | | NEW.responsible_owner_id, v_animal_owner, NEW.animal_id; |
| 206 | | END IF; |
| 207 | | |
| 208 | | IF NEW.status = 'CONFIRMED' AND NEW.date_time < now() THEN |
| 209 | | RAISE EXCEPTION 'Cannot CONFIRM an appointment in the past (date_time=%)', NEW.date_time; |
| 210 | | END IF; |
| 211 | | |
| 212 | | IF NEW.status = 'DONE' AND NEW.date_time > now() THEN |
| 213 | | RAISE EXCEPTION 'Cannot mark DONE for an appointment that is in the future (date_time=%)', NEW.date_time; |
| 214 | | END IF; |
| 215 | | |
| 216 | | RETURN NEW; |
| 217 | | END; |
| 218 | | $$; |
| 219 | | |
| 220 | | DO $$ |
| 221 | | BEGIN |
| 222 | | IF NOT EXISTS ( |
| 223 | | SELECT 1 |
| 224 | | FROM information_schema.columns |
| 225 | | WHERE table_name = 'appointments' |
| 226 | | AND column_name = 'slot' |
| 227 | | ) THEN |
| 228 | | ALTER TABLE appointments |
| 229 | | ADD COLUMN slot tstzrange |
| 230 | | GENERATED ALWAYS AS ( |
| 231 | | tstzrange(date_time, date_time + interval '30 minutes', '[)') |
| 232 | | ) STORED; |
| 233 | | END IF; |
| 234 | | END $$; |
| 235 | | |
| 236 | | CREATE OR REPLACE FUNCTION petify_trg_appointments_no_overlap() |
| 237 | | RETURNS trigger |
| 238 | | LANGUAGE plpgsql |
| 239 | | AS $$ |
| 240 | | BEGIN |
| 241 | | |
| 242 | | IF NEW.status NOT IN ('CONFIRMED','DONE') THEN |
| | 192 | v_completion_status VARCHAR(30); |
| | 193 | BEGIN |
| | 194 | -- find the status of the referenced enrollment |
| | 195 | SELECT e.completion_status |
| | 196 | INTO v_completion_status |
| | 197 | FROM enrollment e |
| | 198 | WHERE e.enrollment_id = NEW.enrollment_id; |
| | 199 | |
| | 200 | IF v_completion_status IS NULL THEN |
| | 201 | RAISE EXCEPTION 'Enrollment % does not exist or has no completion status', NEW.enrollment_id; |
| | 202 | END IF; |
| | 203 | |
| | 204 | -- certificate can only be issued for completed enrollments |
| | 205 | IF v_completion_status <> 'COMPLETED' THEN |
| | 206 | RAISE EXCEPTION |
| | 207 | 'Certificate cannot be issued because enrollment % is not COMPLETED', |
| | 208 | NEW.enrollment_id; |
| | 209 | END IF; |
| | 210 | |
| | 211 | -- prevent more than one certificate for the same enrollment |
| | 212 | IF EXISTS ( |
| | 213 | SELECT 1 |
| | 214 | FROM certificate c |
| | 215 | WHERE c.enrollment_id = NEW.enrollment_id |
| | 216 | AND (TG_OP <> 'UPDATE' OR c.certificate_id <> NEW.certificate_id) |
| | 217 | ) THEN |
| | 218 | RAISE EXCEPTION |
| | 219 | 'Enrollment % already has a certificate', |
| | 220 | NEW.enrollment_id; |
| | 221 | END IF; |
| | 222 | |
| 244 | | END IF; |
| 245 | | |
| 246 | | |
| 247 | | IF EXISTS ( |
| 248 | | SELECT 1 |
| 249 | | FROM appointments a |
| 250 | | WHERE a.responsible_owner_id = NEW.responsible_owner_id |
| 251 | | AND a.status IN ('CONFIRMED','DONE') |
| 252 | | AND a.slot && NEW.slot |
| 253 | | AND (TG_OP <> 'UPDATE' OR a.appointment_id <> NEW.appointment_id) |
| 254 | | ) THEN |
| 255 | | RAISE EXCEPTION |
| 256 | | 'Overlapping appointment for owner % at %', |
| 257 | | NEW.responsible_owner_id, NEW.date_time; |
| 258 | | END IF; |
| 259 | | |
| 260 | | |
| 261 | | IF EXISTS ( |
| 262 | | SELECT 1 |
| 263 | | FROM appointments a |
| 264 | | WHERE a.animal_id = NEW.animal_id |
| 265 | | AND a.status IN ('CONFIRMED','DONE') |
| 266 | | AND a.slot && NEW.slot |
| 267 | | AND (TG_OP <> 'UPDATE' OR a.appointment_id <> NEW.appointment_id) |
| 268 | | ) THEN |
| 269 | | RAISE EXCEPTION |
| 270 | | 'Overlapping appointment for animal % at %', |
| 271 | | NEW.animal_id, NEW.date_time; |
| 272 | | END IF; |
| 273 | | |
| 274 | | RETURN NEW; |
| 275 | | END; |
| 276 | | $$; |
| 277 | | |
| 278 | | |
| 279 | | DROP TRIGGER IF EXISTS trg_appointments_enforce ON appointments; |
| 280 | | CREATE TRIGGER trg_appointments_enforce |
| | 224 | END; |
| | 225 | $$; |
| | 226 | |
| | 227 | DROP TRIGGER IF EXISTS trg_certificate_enforce ON certificate; |
| | 228 | |
| | 229 | CREATE TRIGGER trg_certificate_enforce |
| 299 | | clinic_id, |
| 300 | | date_trunc('month', date_time) AS month, |
| 301 | | COUNT(*) FILTER (WHERE status='CONFIRMED') AS confirmed_cnt, |
| 302 | | COUNT(*) FILTER (WHERE status='DONE') AS done_cnt, |
| 303 | | COUNT(*) FILTER (WHERE status='NO_SHOW') AS no_show_cnt, |
| 304 | | COUNT(*) FILTER (WHERE status='CANCELLED') AS cancelled_cnt, |
| 305 | | COUNT(*) AS total_cnt |
| 306 | | FROM appointments |
| 307 | | GROUP BY clinic_id, date_trunc('month', date_time) |
| 308 | | ORDER BY month DESC; |
| | 241 | c.certificate_id, |
| | 242 | c.enrollment_id, |
| | 243 | c.issue_date, |
| | 244 | c.certificate_code, |
| | 245 | c.status AS certificate_status, |
| | 246 | e.user_id, |
| | 247 | ue.first_name, |
| | 248 | ue.last_name, |
| | 249 | ue.email, |
| | 250 | e.course_id, |
| | 251 | cr.name AS course_name, |
| | 252 | e.enroll_date, |
| | 253 | e.completion_status, |
| | 254 | e.progress_percentage |
| | 255 | FROM certificate c |
| | 256 | JOIN enrollment e |
| | 257 | ON e.enrollment_id = c.enrollment_id |
| | 258 | JOIN users u |
| | 259 | ON u.id = e.user_id |
| | 260 | JOIN user_entity ue |
| | 261 | ON ue.id = u.id |
| | 262 | JOIN course cr |
| | 263 | ON cr.course_id = e.course_id; |