wiki:AdvancedDatabaseDevelopment

Напреден развој на базата

Валидација на финансиски проценти (Finance Percentage Validation)

Опис на барањата за податочни ограничувања

Системот мора да обезбеди дека:

  • Сите пет вредности за буџет мора да бидат внесени (не смеат да бидат NULL)
  • Секоја вредност мора да биде во опсегот [0, 100]
  • Збирот на сите пет вредности мора да биде 100 (со толеранција од 0.01 за мали разлики при заокружување)
  • Ограничувањето се применува и при INSERT и при UPDATE на табелата finance_users

Имплементација

Тригери

BEFORE INSERT OR UPDATE тригер на finance_users за валидација дека сите пет буџетски проценти се валидни и нивниот збир е 100.

CREATE OR REPLACE FUNCTION trekr.fn_validate_finance_percentages()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
    s NUMERIC;
    eps CONSTANT NUMERIC := 0.01;
    vals NUMERIC[] := ARRAY[NEW.spending_budget, NEW.saving_budget, NEW.investing_budget, NEW.donation_budget, NEW.credit];
    v NUMERIC;
BEGIN
    FOREACH v IN ARRAY vals LOOP
        IF v IS NULL THEN
            RAISE EXCEPTION 'All 5 finance percentage values are required';
        END IF;
        IF v < 0 OR v > 100 THEN
            RAISE EXCEPTION 'Finance percentage values must be between 0 and 100';
        END IF;
    END LOOP;

    s := (NEW.spending_budget + NEW.saving_budget + NEW.investing_budget + NEW.donation_budget + NEW.credit)::numeric;
    IF abs(s - 100) > eps THEN
        RAISE EXCEPTION 'Finance percentages must sum to 100 (got: %)', s;
    END IF;

    RETURN NEW;
END;
$$;

DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM pg_trigger t
        JOIN pg_class c ON t.tgrelid = c.oid
        WHERE t.tgname = 'trg_validate_finance_percentages' AND c.relname = 'finance_users'
    ) THEN
        CREATE TRIGGER trg_validate_finance_percentages
        BEFORE INSERT OR UPDATE ON trekr.finance_users
        FOR EACH ROW
        EXECUTE FUNCTION trekr.fn_validate_finance_percentages();
    END IF;
END$$;

Пресметување на дневни завршувања (Daily Completion Computation)

Опис на барањата за податочни ограничувања

Системот мора да обезбеди дека:

  • Дневното завршување може да се пресмета само за корисник со овозможено следење (discipline_users)
  • Не смее да се пресметува за иден датум
  • Ако веќе постои запис за тој корисник и датум, се враќа постоечкиот резултат без дупликат
  • По пресметувањето, завршените задачи се врзуваат за дневниот запис, а потоа нивниот статус се ресетира

Имплементација

Функции / Stored Procedures

Функција за пресметување на дневно завршување за еден корисник и датум. Вметнува ред во daily_completions (доколку не постои), ги поврзува завршените задачи и го ресетира нивниот статус.

CREATE OR REPLACE FUNCTION trekr.fn_compute_daily_completion(user_id bigint, day date)
RETURNS TABLE(created boolean, daily_completion_id bigint, procent numeric)
LANGUAGE plpgsql
AS $$
DECLARE
    total_count bigint;
    finished_count bigint;
    pct numeric;
    dc_id bigint;
    finished_tasks RECORD;
BEGIN
    IF user_id IS NULL THEN
        RAISE EXCEPTION 'user_id is required';
    END IF;
    IF day IS NULL THEN
        RAISE EXCEPTION 'day is required';
    END IF;
    IF day > current_date THEN
        RAISE EXCEPTION 'date cannot be in the future';
    END IF;

    IF NOT EXISTS (SELECT 1 FROM trekr.discipline_users du WHERE du.user_id = user_id) THEN
        RAISE EXCEPTION 'Discipline tracking is not enabled for this user';
    END IF;

    SELECT dc.daily_completion_id, dc.procent INTO dc_id, pct
    FROM trekr.daily_completions dc
    WHERE dc.user_id = user_id AND dc.date = day
    LIMIT 1;

    IF dc_id IS NOT NULL THEN
        RETURN QUERY SELECT false, dc_id, pct;
        RETURN;
    END IF;

    SELECT COUNT(*) INTO total_count FROM trekr.tasks t WHERE t.discipline_user_id = (
        SELECT du.discipline_user_id FROM trekr.discipline_users du WHERE du.user_id = user_id
    );

    SELECT COUNT(*) INTO finished_count FROM trekr.tasks t WHERE t.discipline_user_id = (
        SELECT du.discipline_user_id FROM trekr.discipline_users du WHERE du.user_id = user_id
    ) AND t.finished = true;

    IF total_count <= 0 THEN
        pct := 0;
    ELSE
        pct := round((finished_count::numeric * 100) / total_count::numeric, 2);
    END IF;

    INSERT INTO trekr.daily_completions (user_id, date, procent)
    VALUES (user_id, day, pct)
    RETURNING daily_completion_id INTO dc_id;

    FOR finished_tasks IN
        SELECT t.task_id FROM trekr.tasks t
        WHERE t.discipline_user_id = (
            SELECT du.discipline_user_id FROM trekr.discipline_users du WHERE du.user_id = user_id
        ) AND t.finished = true
    LOOP
        INSERT INTO trekr.task_daily_completions (task_id, daily_completion_id)
        VALUES (finished_tasks.task_id, dc_id)
        ON CONFLICT DO NOTHING;
    END LOOP;

    UPDATE trekr.tasks t SET finished = false
    WHERE t.discipline_user_id = (
        SELECT du.discipline_user_id FROM trekr.discipline_users du WHERE du.user_id = user_id
    );

    RETURN QUERY SELECT true, dc_id, pct;
END;
$$;

Функција за пресметување на дневни завршувања за сите корисници со овозможено следење за даден датум. Грешките по корисник се логираат и се продолжува понатаму.

CREATE OR REPLACE FUNCTION trekr.fn_compute_daily_completion_for_all(day date)
RETURNS void
LANGUAGE plpgsql
AS $$
DECLARE
    u RECORD;
BEGIN
    IF day IS NULL THEN
        RAISE EXCEPTION 'day is required';
    END IF;

    FOR u IN SELECT user_id FROM trekr.discipline_users LOOP
        BEGIN
            PERFORM trekr.fn_compute_daily_completion(u.user_id, day);
        EXCEPTION WHEN OTHERS THEN
            RAISE NOTICE 'compute_daily_completion failed for user %: %', u.user_id, SQLERRM;
        END;
    END LOOP;
END;
$$;

-- Опционално: pg_cron задача за секојдневно извршување (бара pg_cron екстензија)
-- CREATE EXTENSION IF NOT EXISTS pg_cron;
-- SELECT cron.schedule('compute_daily_completions_every_day', '59 23 * * *',
--     $$SELECT trekr.fn_compute_daily_completion_for_all(current_date - INTERVAL '1 day')$$);

Дополнителни ограничувања на базата (Additional DB Constraints)

Опис на барањата за податочни ограничувања

Системот мора да обезбеди дека:

  • Корисникот може да има само еден дневен внес (daily intake) по датум
  • Тренинг сесиите не смеат да имаат иден датум

Имплементација

Индекси

Уникатен индекс на daily_intakes за осигурување дека еден корисник може да има најмногу еден внес по датум.

DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM pg_indexes
        WHERE schemaname = 'trekr'
          AND tablename = 'daily_intakes'
          AND indexname = 'uq_daily_intake_user_date'
    ) THEN
        CREATE UNIQUE INDEX uq_daily_intake_user_date
        ON trekr.daily_intakes (weight_user_id, date);
    END IF;
END$$;

Тригери

BEFORE INSERT OR UPDATE тригер на training_sessions за спречување на внес со иден датум.

CREATE OR REPLACE FUNCTION trekr.fn_check_training_date()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    IF NEW.date > current_date THEN
        RAISE EXCEPTION 'Training session date cannot be in the future: %', NEW.date;
    END IF;
    RETURN NEW;
END;
$$;

DO $$
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM pg_trigger t
        JOIN pg_class c ON t.tgrelid = c.oid
        WHERE t.tgname = 'trg_check_training_date' AND c.relname = 'training_sessions'
    ) THEN
        CREATE TRIGGER trg_check_training_date
        BEFORE INSERT OR UPDATE ON trekr.training_sessions
        FOR EACH ROW
        EXECUTE FUNCTION trekr.fn_check_training_date();
    END IF;
END$$;

Прегледи за финансии (Finance Views)

Опис на барањата за податочни ограничувања

Системот мора да обезбеди дека:

  • Постои преглед за месечен приход по корисник и период
  • Постои преглед за вкупниот приход на корисникот во тековниот месец
  • Постои преглед за пресметани апсолутни износи по категорија врз основа на процентите и приходот во тековниот месец

Имплементација

Погледи (Views)

Поглед за месечен приход по корисник — прикажува вкупен приход по корисник, месец и година.

CREATE OR REPLACE VIEW trekr.vw_finance_monthly_summary AS
SELECT
    i.finance_user_id AS user_id,
    EXTRACT(YEAR FROM i.date)::int AS year,
    EXTRACT(MONTH FROM i.date)::int AS month,
    SUM(i.amount) AS total_income
FROM trekr.incomes i
GROUP BY i.finance_user_id, EXTRACT(YEAR FROM i.date), EXTRACT(MONTH FROM i.date);

Поглед за вкупниот приход на секој корисник во тековниот месец.

CREATE OR REPLACE VIEW trekr.vw_finance_current_month AS
SELECT
    f.user_id,
    COALESCE(SUM(i.amount), 0) AS total_earned_this_month
FROM trekr.finance_users f
LEFT JOIN trekr.incomes i
    ON i.finance_user_id = f.user_id
    AND date_trunc('month', i.date) = date_trunc('month', current_date)
GROUP BY f.user_id;

Поглед за пресметување на апсолутни износи по финансиска категорија за тековниот месец, врз основа на буџетските проценти и вкупниот приход.

CREATE OR REPLACE VIEW trekr.vw_finance_allocations_current_month AS
SELECT
    f.user_id,
    fm.total_earned_this_month,
    f.spending_budget,
    f.saving_budget,
    f.investing_budget,
    f.donation_budget,
    f.credit,
    ROUND((COALESCE(f.spending_budget, 0) / 100.0) * fm.total_earned_this_month, 2) AS spending_amount,
    ROUND((COALESCE(f.saving_budget, 0) / 100.0) * fm.total_earned_this_month, 2) AS saving_amount,
    ROUND((COALESCE(f.investing_budget, 0) / 100.0) * fm.total_earned_this_month, 2) AS investing_amount,
    ROUND((COALESCE(f.donation_budget, 0) / 100.0) * fm.total_earned_this_month, 2) AS donation_amount,
    ROUND((COALESCE(f.credit, 0) / 100.0) * fm.total_earned_this_month, 2) AS credit_amount
FROM trekr.finance_users f
LEFT JOIN trekr.vw_finance_current_month fm ON fm.user_id = f.user_id;
Last modified 3 days ago Last modified on 05/07/26 14:12:53
Note: See TracWiki for help on using the wiki.