Changes between Version 4 and Version 5 of AdvancedDB


Ignore:
Timestamp:
03/28/26 13:45:51 (2 weeks ago)
Author:
221296
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedDB

    v4 v5  
    375375    ON c.course_id = m.course_id;
    376376}}}
    377 == Data constraints requirements: Review Consistency
     377
     378== Data constraints requirements: Support ticket status consistency + validated ticket attributes
    378379=== Data requirements description
    379 Reviews are split across multiple tables and require consistency checks:
    380 * A base reviews row can represent either a user_review or a clinic_review, never both.
    381 * A reviewer cannot repeatedly review the same target too frequently (30-day cooldown).
    382 * Prevent self-review.
     380Support tickets must remain consistent and well-structured throughout their lifecycle:
     381* Ticket statuses must follow a controlled set of allowed values.
     382* Ticket subjects must not be empty.
     383* Ticket handling should support valid status transitions.
     384* Ticket data should be easy to inspect together with the related user and administrator information.
    383385=== Implementation
    384 {{{Triggers}}}
    385 {{{
    386 CREATE OR REPLACE FUNCTION petify_trg_reviews_no_update()
    387 RETURNS trigger
    388 LANGUAGE plpgsql
    389 AS $$
    390 BEGIN
    391   RAISE EXCEPTION 'Reviews are immutable. Updates are not allowed (review_id=%).', OLD.review_id;
    392 END;
    393 $$;
    394 
    395 DROP TRIGGER IF EXISTS trg_reviews_no_update ON reviews;
    396 CREATE TRIGGER trg_reviews_no_update
    397 BEFORE UPDATE
    398 ON reviews
    399 FOR EACH ROW
    400 EXECUTE FUNCTION petify_trg_reviews_no_update();
    401 
    402 DROP TRIGGER IF EXISTS trg_clinic_reviews_no_update ON clinic_reviews;
    403 CREATE TRIGGER trg_clinic_reviews_no_update
    404 BEFORE UPDATE
    405 ON clinic_reviews
    406 FOR EACH ROW
    407 EXECUTE FUNCTION petify_trg_no_update_generic();
    408 
    409 CREATE OR REPLACE FUNCTION petify_trg_user_review_exclusive()
    410 RETURNS trigger
    411 LANGUAGE plpgsql
    412 AS $$
    413 BEGIN
    414   IF EXISTS (SELECT 1 FROM clinic_reviews cr WHERE cr.review_id = NEW.review_id) THEN
    415     RAISE EXCEPTION 'review_id % already used as clinic review (cannot also be user review)', NEW.review_id;
    416   END IF;
    417   RETURN NEW;
    418 END;
    419 $$;
    420 
    421 DROP TRIGGER IF EXISTS trg_user_review_exclusive ON user_reviews;
    422 CREATE TRIGGER trg_user_review_exclusive
    423 BEFORE INSERT
    424 ON user_reviews
    425 FOR EACH ROW
    426 EXECUTE FUNCTION petify_trg_user_review_exclusive();
    427 
    428 
    429 CREATE OR REPLACE FUNCTION petify_trg_clinic_review_exclusive()
    430 RETURNS trigger
    431 LANGUAGE plpgsql
    432 AS $$
    433 BEGIN
    434   IF EXISTS (SELECT 1 FROM user_reviews ur WHERE ur.review_id = NEW.review_id) THEN
    435     RAISE EXCEPTION 'review_id % already used as user review (cannot also be clinic review)', NEW.review_id;
    436   END IF;
    437   RETURN NEW;
    438 END;
    439 $$;
    440 
    441 DROP TRIGGER IF EXISTS trg_clinic_review_exclusive ON clinic_reviews;
    442 CREATE TRIGGER trg_clinic_review_exclusive
    443 BEFORE INSERT
    444 ON clinic_reviews
    445 FOR EACH ROW
    446 EXECUTE FUNCTION petify_trg_clinic_review_exclusive();
    447 
    448 CREATE OR REPLACE FUNCTION petify_trg_user_reviews_cooldown()
    449 RETURNS trigger
    450 LANGUAGE plpgsql
    451 AS $$
    452 DECLARE
    453   v_reviewer bigint;
    454   v_created  timestamp;
    455 BEGIN
    456   SELECT reviewer_id, created_at INTO v_reviewer, v_created
    457   FROM reviews
    458   WHERE review_id = NEW.review_id;
    459 
    460   IF v_reviewer IS NULL THEN
    461     RAISE EXCEPTION 'Base review % not found', NEW.review_id;
    462   END IF;
    463 
    464   IF v_reviewer = NEW.target_user_id THEN
    465     RAISE EXCEPTION 'User cannot review themselves (user_id=%)', v_reviewer;
    466   END IF;
    467 
    468   IF EXISTS (
    469     SELECT 1
    470     FROM user_reviews ur
    471     JOIN reviews r ON r.review_id = ur.review_id
    472     WHERE r.reviewer_id = v_reviewer
    473       AND ur.target_user_id = NEW.target_user_id
    474       AND r.is_deleted = false
    475       AND r.created_at >= v_created - interval '30 days'
    476   ) THEN
    477     RAISE EXCEPTION 'Cooldown: reviewer % already reviewed user % within last 30 days',
    478       v_reviewer, NEW.target_user_id;
    479   END IF;
    480 
    481   RETURN NEW;
    482 END;
    483 $$;
    484 
    485 DROP TRIGGER IF EXISTS trg_user_reviews_cooldown ON user_reviews;
    486 CREATE TRIGGER trg_user_reviews_cooldown
    487 BEFORE INSERT
    488 ON user_reviews
    489 FOR EACH ROW
    490 EXECUTE FUNCTION petify_trg_user_reviews_cooldown();
    491 
    492 CREATE OR REPLACE FUNCTION petify_trg_clinic_reviews_cooldown()
    493 RETURNS trigger
    494 LANGUAGE plpgsql
    495 AS $$
    496 DECLARE
    497   v_reviewer bigint;
    498   v_created  timestamp;
    499 BEGIN
    500   SELECT reviewer_id, created_at INTO v_reviewer, v_created
    501   FROM reviews
    502   WHERE review_id = NEW.review_id;
    503 
    504   IF v_reviewer IS NULL THEN
    505     RAISE EXCEPTION 'Base review % not found', NEW.review_id;
    506   END IF;
    507 
    508   IF EXISTS (
    509     SELECT 1
    510     FROM clinic_reviews cr
    511     JOIN reviews r ON r.review_id = cr.review_id
    512     WHERE r.reviewer_id = v_reviewer
    513       AND cr.target_clinic_id = NEW.target_clinic_id
    514       AND r.is_deleted = false
    515       AND r.created_at >= v_created - interval '30 days'
    516   ) THEN
    517     RAISE EXCEPTION 'Cooldown: reviewer % already reviewed clinic % within last 30 days',
    518       v_reviewer, NEW.target_clinic_id;
    519   END IF;
    520 
    521   RETURN NEW;
    522 END;
    523 $$;
    524 
    525 DROP TRIGGER IF EXISTS trg_clinic_reviews_cooldown ON clinic_reviews;
    526 CREATE TRIGGER trg_clinic_reviews_cooldown
    527 BEFORE INSERT
    528 ON clinic_reviews
    529 FOR EACH ROW
    530 EXECUTE FUNCTION petify_trg_clinic_reviews_cooldown();
    531 
     386{{{Stored procedures / functions}}}
     387{{{
     388CREATE OR REPLACE FUNCTION olpms_is_valid_ticket_transition(p_old text, p_new text)
     389RETURNS boolean
     390LANGUAGE sql
     391AS $$
     392    SELECT CASE
     393        WHEN p_old = p_new THEN true
     394        WHEN p_old = 'OPEN' AND p_new IN ('IN_PROGRESS', 'RESOLVED') THEN true
     395        WHEN p_old = 'IN_PROGRESS' AND p_new IN ('RESOLVED') THEN true
     396        WHEN p_old = 'RESOLVED' AND p_new = 'RESOLVED' THEN true
     397        ELSE false
     398    END;
     399$$;
    532400}}}
    533401{{{Views}}}
    534402{{{
    535 CREATE OR REPLACE VIEW v_user_ratings AS
     403CREATE OR REPLACE VIEW v_support_tickets_enriched AS
    536404SELECT
    537   ur.target_user_id,
    538   COUNT(*) FILTER (WHERE r.is_deleted = false) AS review_count,
    539   ROUND(AVG(r.rating)::numeric, 2) FILTER (WHERE r.is_deleted = false) AS avg_rating
    540 FROM user_reviews ur
    541 JOIN reviews r ON r.review_id = ur.review_id
    542 GROUP BY ur.target_user_id;
    543 
    544 CREATE OR REPLACE VIEW v_clinic_ratings AS
    545 SELECT
    546   cr.target_clinic_id,
    547   COUNT(*) FILTER (WHERE r.is_deleted = false) AS review_count,
    548   ROUND(AVG(r.rating)::numeric, 2) FILTER (WHERE r.is_deleted = false) AS avg_rating
    549 FROM clinic_reviews cr
    550 JOIN reviews r ON r.review_id = cr.review_id
    551 GROUP BY cr.target_clinic_id;
     405    st.ticket_id,
     406    st.subject,
     407    st.description,
     408    st.status,
     409    st.created_at,
     410    st.user_id,
     411    u.first_name AS user_first_name,
     412    u.last_name AS user_last_name,
     413    u.email AS user_email,
     414    st.admin_id,
     415    a.first_name AS admin_first_name,
     416    a.last_name AS admin_last_name,
     417    a.email AS admin_email
     418FROM support_ticket st
     419JOIN user_entity u
     420    ON u.id = st.user_id
     421JOIN user_entity a
     422    ON a.id = st.admin_id;
    552423}}}
    553424{{{Custom domains}}}
    554425{{{
    555 CREATE DOMAIN rating_1_5 AS int CHECK (VALUE BETWEEN 1 AND 5);
    556 }}}
    557 == Data constraints requirements: Background Jobs
    558 === Data requirements description
    559 These are time-based business rules that must be enforced asynchronously:
    560 * If an appointment is still CONFIRMED long after its scheduled time, mark it as NO_SHOW.
    561 * If an Archive listing is older than 30days its status is draft.
    562 === Implementation
    563 {{{Stored procedures}}}
    564 {{{
    565 CREATE EXTENSION IF NOT EXISTS pg_cron;
    566 
    567 CREATE OR REPLACE PROCEDURE job_mark_no_show()
    568 LANGUAGE plpgsql
    569 AS $$
    570 BEGIN
    571   UPDATE appointments
    572   SET status = 'NO_SHOW'
    573   WHERE status = 'CONFIRMED'
    574     AND date_time < now() - interval '45 minutes';
    575 END;
    576 $$;
    577 
    578 CREATE OR REPLACE PROCEDURE job_archive_stale_drafts()
    579 LANGUAGE plpgsql
    580 AS $$
    581 BEGIN
    582   UPDATE listings
    583   SET status = 'ARCHIVED'
    584   WHERE status = 'DRAFT'
    585     AND created_at < now() - interval '30 days';
    586 END;
    587 $$;
    588 }}}
    589 {{{Views}}}
    590 {{{
    591 CREATE OR REPLACE VIEW v_overdue_confirmed_appointments AS
    592 SELECT *
    593 FROM appointments
    594 WHERE status='CONFIRMED'
    595   AND date_time < now() - interval '45 minutes';
    596 
    597 CREATE OR REPLACE VIEW v_stale_draft_listings AS
    598 SELECT *
    599 FROM listings
    600 WHERE status='DRAFT'
    601   AND created_at < now() - interval '30 days';
    602 }}}
    603 {{{Scheduling}}}
    604 {{{
    605 SELECT cron.schedule('petify_mark_no_show', '*/10 * * * *', $$CALL job_mark_no_show();$$);
    606 SELECT cron.schedule('petify_archive_stale_drafts_daily', '10 2 * * *', $$CALL job_archive_stale_drafts();$$);
    607 }}}
     426CREATE DOMAIN ticket_status_domain AS VARCHAR(30)
     427CHECK (VALUE IN ('OPEN', 'IN_PROGRESS', 'RESOLVED'));
     428
     429CREATE DOMAIN non_empty_subject_domain AS VARCHAR(200)
     430CHECK (LENGTH(TRIM(VALUE)) > 0);
     431}}}