Changes between Initial Version and Version 1 of dbdevelopment


Ignore:
Timestamp:
01/30/26 17:36:28 (4 hours ago)
Author:
231175
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • dbdevelopment

    v1 v1  
     1= Напреден развој на базата =
     2
     3== Правила за запишување на курс (Enrollment) ==
     4
     5=== Опис на барањата за податочни ограничувања ===
     6
     7Системот мора да обезбеди дека:
     8* Кога корисникот се запишува на курс по даден course_id, автоматски се избира и запишува моментално активната верзија на курсот (course_version.active = true) за тој курс.
     9* Не е дозволено запишување на курс за кој воопшто нема активна верзија.
     10* Секоја верзија се третира независно: корисникот може да има повеќе запишувања на различни верзии на истиот курс, без разлика дали претходните се завршени или не.
     11
     12=== Имплементација ===
     13
     14==== Тригери ====
     15
     16{{{
     17CREATE OR REPLACE FUNCTION set_active_course_version_on_enrollment()
     18RETURNS TRIGGER
     19AS $$
     20DECLARE
     21    v_course_version_id INTEGER;
     22BEGIN
     23    SELECT cv.id
     24    INTO v_course_version_id
     25    FROM course_version cv
     26    WHERE cv.course_id = NEW.course_id
     27      AND cv.active = TRUE
     28    ORDER BY cv.version_number DESC
     29    LIMIT 1;
     30   
     31    IF v_course_version_id IS NULL THEN
     32        RAISE EXCEPTION 'No active course version found for course_id=%', NEW.course_id;
     33    END IF;
     34   
     35    NEW.course_version_id := v_course_version_id;
     36   
     37    RETURN NEW;
     38END;
     39$$
     40LANGUAGE plpgsql;
     41
     42CREATE TRIGGER trg_set_active_course_version_on_enrollment
     43BEFORE INSERT ON enrollment
     44FOR EACH ROW
     45EXECUTE FUNCTION set_active_course_version_on_enrollment();
     46}}}
     47
     48==== Функции / Stored Procedures ====
     49
     50{{{
     51CREATE OR REPLACE FUNCTION create_enrollment_for_active_version(p_user_id INTEGER, p_course_id INTEGER)
     52RETURNS INTEGER
     53AS $$
     54DECLARE
     55    v_course_version_id INTEGER;
     56    v_enrollment_id INTEGER;
     57BEGIN
     58    SELECT cv.id
     59    INTO v_course_version_id
     60    FROM course_version cv
     61    WHERE cv.course_id = p_course_id
     62      AND cv.active = TRUE
     63    ORDER BY cv.version_number DESC
     64    LIMIT 1;
     65   
     66    IF v_course_version_id IS NULL THEN
     67        RAISE EXCEPTION 'No active course version found for course_id=%', p_course_id;
     68    END IF;
     69   
     70    INSERT INTO enrollment (user_id, course_id, course_version_id, enrollment_purchase_date, status)
     71    VALUES (p_user_id, p_course_id, v_course_version_id, NOW(), 'PENDING')
     72    RETURNING id INTO v_enrollment_id;
     73   
     74    RETURN v_enrollment_id;
     75END;
     76$$
     77LANGUAGE plpgsql;
     78}}}
     79
     80==== Погледи (Views) ====
     81
     82{{{
     83CREATE OR REPLACE VIEW enrollments_with_active_version AS
     84SELECT
     85    e.id AS enrollment_id,
     86    u.id AS user_id,
     87    u.name AS user_name,
     88    c.id AS course_id,
     89    ct.title_short AS course_title,
     90    cv.id AS course_version_id,
     91    cv.version_number,
     92    cv.active,
     93    e.enrollment_purchase_date,
     94    e.status
     95FROM enrollment e
     96JOIN "user" u ON e.user_id = u.id
     97JOIN course c ON e.course_id = c.id
     98JOIN course_version cv ON e.course_version_id = cv.id
     99JOIN course_translate ct ON c.id = ct.course_id AND ct.language = 'mk';
     100}}}
     101
     102----
     103
     104== Менаџирање на верзии на курс (Course Version Management) ==
     105
     106=== Опис на барањата за податочни ограничувања ===
     107
     108Системот мора да обезбеди дека:
     109* Само една верзија може да биде активна (active = true) по курс во исто време
     110* Кога се активира нова верзија, претходните активни верзии на истиот курс автоматски се деактивираат
     111* Не смее да се избрише верзија која има активни (незавршени) запишувања (enrollments)
     112
     113=== Имплементација ===
     114
     115==== Тригери ====
     116
     117{{{
     118CREATE OR REPLACE FUNCTION ensure_single_active_version()
     119RETURNS TRIGGER AS $$
     120BEGIN
     121    IF NEW.active = TRUE THEN
     122        UPDATE course_version
     123        SET active = FALSE
     124        WHERE course_id = NEW.course_id
     125          AND id != NEW.id
     126          AND active = TRUE;
     127    END IF;
     128   
     129    RETURN NEW;
     130END;
     131$$ LANGUAGE plpgsql;
     132
     133CREATE TRIGGER trg_ensure_single_active_version
     134BEFORE UPDATE OF active ON course_version
     135FOR EACH ROW
     136WHEN (NEW.active = TRUE)
     137EXECUTE FUNCTION ensure_single_active_version();
     138}}}
     139
     140{{{
     141CREATE OR REPLACE FUNCTION prevent_version_deletion_with_active_enrollments()
     142RETURNS TRIGGER AS $$
     143DECLARE
     144    v_active_enrollments INTEGER;
     145BEGIN
     146    SELECT COUNT(*) INTO v_active_enrollments
     147    FROM enrollment
     148    WHERE course_version_id = OLD.id
     149      AND completion_date IS NULL;
     150   
     151    IF v_active_enrollments > 0 THEN
     152        RAISE EXCEPTION 'Cannot delete course version with % active enrollments', v_active_enrollments;
     153    END IF;
     154   
     155    RETURN OLD;
     156END;
     157$$ LANGUAGE plpgsql;
     158
     159CREATE TRIGGER trg_prevent_version_deletion
     160BEFORE DELETE ON course_version
     161FOR EACH ROW
     162EXECUTE FUNCTION prevent_version_deletion_with_active_enrollments();
     163}}}
     164
     165==== Погледи (Views) ====
     166
     167{{{
     168CREATE OR REPLACE VIEW course_latest_versions AS
     169SELECT
     170    c.id AS course_id,
     171    ct.title_short AS course_title,
     172    cv.id AS version_id,
     173    cv.version_number,
     174    cv.active,
     175    cv.version_creation_date,
     176    COUNT(DISTINCT e.id) FILTER (WHERE e.completion_date IS NULL) AS active_enrollments,
     177    COUNT(DISTINCT e.id) AS total_enrollments
     178FROM course c
     179JOIN course_version cv ON c.id = cv.course_id
     180JOIN course_translate ct ON c.id = ct.course_id AND ct.language = 'mk'
     181LEFT JOIN enrollment e ON cv.id = e.course_version_id
     182WHERE cv.version_number = (
     183    SELECT MAX(version_number)
     184    FROM course_version
     185    WHERE course_id = c.id
     186)
     187GROUP BY c.id, ct.title_short, cv.id, cv.version_number, cv.active, cv.version_creation_date;
     188}}}
     189
     190----
     191
     192== Валидација на рецензии и рејтинг (Review Validation & Rating) ==
     193
     194=== Опис на барањата за податочни ограничувања ===
     195
     196Системот мора да обезбеди дека:
     197* Корисникот може да остави рецензија само ако enrollment е завршен (completion_date IS NOT NULL)
     198* Еден корисник може да остави само една рецензија по enrollment
     199* Рејтингот мора да биде валиден број од 1 до 5
     200* Сите промени на рецензии се евидентираат во audit табела
     201
     202=== Имплементација ===
     203
     204==== Прилагодени домени (Custom Domains) ====
     205
     206{{{
     207CREATE DOMAIN rating_scale AS INTEGER
     208CHECK (VALUE >= 1 AND VALUE <= 5);
     209
     210ALTER TABLE review ALTER COLUMN rating TYPE rating_scale;
     211}}}
     212
     213==== Тригери ====
     214
     215{{{
     216CREATE OR REPLACE FUNCTION validate_review_before_insert()
     217RETURNS TRIGGER AS $$
     218DECLARE
     219    v_completion_date TIMESTAMP;
     220    v_existing_review_count INTEGER;
     221BEGIN
     222    SELECT completion_date INTO v_completion_date
     223    FROM enrollment
     224    WHERE id = NEW.enrollment_id;
     225   
     226    IF v_completion_date IS NULL THEN
     227        RAISE EXCEPTION 'Cannot create review for incomplete enrollment';
     228    END IF;
     229   
     230    SELECT COUNT(*) INTO v_existing_review_count
     231    FROM review
     232    WHERE enrollment_id = NEW.enrollment_id;
     233   
     234    IF v_existing_review_count > 0 THEN
     235        RAISE EXCEPTION 'Review already exists for this enrollment';
     236    END IF;
     237   
     238    RETURN NEW;
     239END;
     240$$ LANGUAGE plpgsql;
     241
     242CREATE TRIGGER trg_validate_review
     243BEFORE INSERT ON review
     244FOR EACH ROW
     245EXECUTE FUNCTION validate_review_before_insert();
     246}}}
     247
     248{{{
     249CREATE OR REPLACE FUNCTION audit_review_changes()
     250RETURNS TRIGGER AS $$
     251BEGIN
     252    IF TG_OP = 'INSERT' THEN
     253        INSERT INTO review_audit (review_id, new_rating, new_comment, action)
     254        VALUES (NEW.id, NEW.rating, NEW.comment, 'INSERT');
     255    ELSIF TG_OP = 'UPDATE' THEN
     256        INSERT INTO review_audit (review_id, old_rating, new_rating, old_comment, new_comment, action)
     257        VALUES (NEW.id, OLD.rating, NEW.rating, OLD.comment, NEW.comment, 'UPDATE');
     258    ELSIF TG_OP = 'DELETE' THEN
     259        INSERT INTO review_audit (review_id, old_rating, old_comment, action)
     260        VALUES (OLD.id, OLD.rating, OLD.comment, 'DELETE');
     261    END IF;
     262   
     263    RETURN NEW;
     264END;
     265$$ LANGUAGE plpgsql;
     266
     267CREATE TRIGGER trg_audit_review
     268AFTER INSERT OR UPDATE OR DELETE ON review
     269FOR EACH ROW
     270EXECUTE FUNCTION audit_review_changes();
     271}}}
     272
     273==== Погледи (Views) ====
     274
     275{{{
     276CREATE OR REPLACE VIEW course_average_ratings AS
     277SELECT
     278    c.id AS course_id,
     279    ct.title_short AS course_title,
     280    COUNT(r.id) AS total_reviews,
     281    AVG(r.rating)::NUMERIC(3,2) AS average_rating,
     282    COUNT(r.id) FILTER (WHERE r.rating = 5) AS five_star_count,
     283    COUNT(r.id) FILTER (WHERE r.rating = 4) AS four_star_count,
     284    COUNT(r.id) FILTER (WHERE r.rating = 3) AS three_star_count,
     285    COUNT(r.id) FILTER (WHERE r.rating = 2) AS two_star_count,
     286    COUNT(r.id) FILTER (WHERE r.rating = 1) AS one_star_count
     287FROM course c
     288JOIN course_translate ct ON c.id = ct.course_id AND ct.language = 'mk'
     289LEFT JOIN course_version cv ON c.id = cv.course_id
     290LEFT JOIN enrollment e ON cv.id = e.course_version_id
     291LEFT JOIN review r ON e.id = r.enrollment_id
     292GROUP BY c.id, ct.title_short;
     293}}}
     294
     295==== Помошни табели (Audit) ====
     296
     297{{{
     298CREATE TABLE review_audit (
     299    id SERIAL PRIMARY KEY,
     300    review_id INTEGER NOT NULL REFERENCES review(id),
     301    old_rating INTEGER,
     302    new_rating INTEGER,
     303    old_comment TEXT,
     304    new_comment TEXT,
     305    changed_at TIMESTAMP DEFAULT NOW(),
     306    action VARCHAR(20)
     307);
     308
     309CREATE INDEX idx_review_audit_review ON review_audit(review_id);
     310CREATE INDEX idx_review_audit_changed_at ON review_audit(changed_at);
     311}}}
     312
     313----
     314
     315== Следење и ограничување на бесплатни консултации ==
     316
     317=== Опис на барањата за податочни ограничувања ===
     318
     319Системот мора да обезбеди дека:
     320* Корисникот може да користи само една бесплатна консултација
     321* Може да се креира meeting_reminder за бесплатна консултација само ако корисникот сè уште не ја искористил
     322* После креирање на meeting_reminder за бесплатна консултација, флагот user_used_free_consultation автоматски се поставува на TRUE
     323
     324=== Имплементација ===
     325
     326==== Тригери ====
     327
     328{{{
     329CREATE OR REPLACE FUNCTION check_free_consultation_eligibility()
     330RETURNS TRIGGER AS $$
     331DECLARE
     332    v_used_free_consultation BOOLEAN;
     333BEGIN
     334    SELECT user_used_free_consultation INTO v_used_free_consultation
     335    FROM "user"
     336    WHERE id = NEW.user_id;
     337   
     338    IF v_used_free_consultation = TRUE THEN
     339        RAISE EXCEPTION 'User has already used their free consultation';
     340    END IF;
     341   
     342    RETURN NEW;
     343END;
     344$$ LANGUAGE plpgsql;
     345
     346CREATE TRIGGER trg_check_free_consultation_before_meeting
     347BEFORE INSERT ON meeting_reminder
     348FOR EACH ROW
     349EXECUTE FUNCTION check_free_consultation_eligibility();
     350}}}
     351
     352{{{
     353CREATE OR REPLACE FUNCTION mark_free_consultation_as_used()
     354RETURNS TRIGGER AS $$
     355BEGIN
     356    UPDATE "user"
     357    SET user_used_free_consultation = TRUE
     358    WHERE id = NEW.user_id
     359      AND user_used_free_consultation = FALSE;
     360   
     361    RETURN NEW;
     362END;
     363$$ LANGUAGE plpgsql;
     364
     365CREATE TRIGGER trg_mark_free_consultation_used
     366AFTER INSERT ON meeting_reminder
     367FOR EACH ROW
     368EXECUTE FUNCTION mark_free_consultation_as_used();
     369}}}
     370
     371==== Функции / Stored Procedures ====
     372
     373{{{
     374CREATE OR REPLACE FUNCTION can_user_schedule_free_consultation(p_user_id INTEGER)
     375RETURNS TABLE(
     376    can_schedule BOOLEAN,
     377    reason TEXT
     378) AS $$
     379DECLARE
     380    v_used_free_consultation BOOLEAN;
     381    v_pending_meetings INTEGER;
     382BEGIN
     383    SELECT user_used_free_consultation INTO v_used_free_consultation
     384    FROM "user"
     385    WHERE id = p_user_id;
     386   
     387    IF v_used_free_consultation = TRUE THEN
     388        RETURN QUERY SELECT FALSE, 'Free consultation already used';
     389        RETURN;
     390    END IF;
     391   
     392    SELECT COUNT(*) INTO v_pending_meetings
     393    FROM meeting_reminder
     394    WHERE user_id = p_user_id
     395      AND meeting_at > NOW();
     396   
     397    IF v_pending_meetings > 0 THEN
     398        RETURN QUERY SELECT FALSE, 'Already has pending meeting scheduled';
     399        RETURN;
     400    END IF;
     401   
     402    RETURN QUERY SELECT TRUE, 'Eligible for free consultation';
     403END;
     404$$ LANGUAGE plpgsql;
     405}}}
     406