| | 1 | = Напреден развој на базата = |
| | 2 | |
| | 3 | == Финансиски месечни пресметки и распределба на буџет (Finance Monthly Calculations & Allocation) == |
| | 4 | |
| | 5 | === Опис на барањата за податочни ограничувања === |
| | 6 | |
| | 7 | Системот мора да обезбеди дека: |
| | 8 | * Финансискиот корисник мора да внесе точно 5 процентуални вредности за распределба на буџетот. |
| | 9 | * Секоја од процентуалните вредности мора да биде во опсег од 0 до 100. |
| | 10 | * Збирот на сите 5 проценти мора да биде 100. |
| | 11 | * Месечните приходи по корисник мора да можат да се пресметуваат без дополнителна логика во апликацијата. |
| | 12 | * За тековниот месец мора да постои преглед на вкупно заработените средства и распределбата по буџетски категории. |
| | 13 | |
| | 14 | === Имплементација === |
| | 15 | |
| | 16 | ==== Тригери ==== |
| | 17 | |
| | 18 | BEFORE INSERT и BEFORE UPDATE тригер на finance_users за валидација на 5-те процентуални полиња. |
| | 19 | Тригeрот гарантира дека сите вредности се внесени, се во дозволен опсег и дека збирот е 100. |
| | 20 | {{{ |
| | 21 | CREATE OR REPLACE FUNCTION trekr.fn_validate_finance_percentages() |
| | 22 | RETURNS trigger |
| | 23 | LANGUAGE plpgsql |
| | 24 | AS $$ |
| | 25 | DECLARE |
| | 26 | s NUMERIC; |
| | 27 | v NUMERIC; |
| | 28 | vals NUMERIC[] := ARRAY[ |
| | 29 | NEW.spending_budget, |
| | 30 | NEW.saving_budget, |
| | 31 | NEW.investing_budget, |
| | 32 | NEW.donation_budget, |
| | 33 | NEW.credit |
| | 34 | ]; |
| | 35 | eps CONSTANT NUMERIC := 0.01; |
| | 36 | BEGIN |
| | 37 | FOREACH v IN ARRAY vals LOOP |
| | 38 | IF v IS NULL THEN |
| | 39 | RAISE EXCEPTION 'All 5 finance percentage values are required'; |
| | 40 | END IF; |
| | 41 | |
| | 42 | IF v < 0 OR v > 100 THEN |
| | 43 | RAISE EXCEPTION 'Finance percentage values must be between 0 and 100'; |
| | 44 | END IF; |
| | 45 | END LOOP; |
| | 46 | |
| | 47 | s := ( |
| | 48 | NEW.spending_budget + |
| | 49 | NEW.saving_budget + |
| | 50 | NEW.investing_budget + |
| | 51 | NEW.donation_budget + |
| | 52 | NEW.credit |
| | 53 | )::numeric; |
| | 54 | |
| | 55 | IF abs(s - 100) > eps THEN |
| | 56 | RAISE EXCEPTION 'Finance percentages must sum to 100 (got: %)', s; |
| | 57 | END IF; |
| | 58 | |
| | 59 | RETURN NEW; |
| | 60 | END; |
| | 61 | $$; |
| | 62 | |
| | 63 | CREATE TRIGGER trg_validate_finance_percentages |
| | 64 | BEFORE INSERT OR UPDATE ON trekr.finance_users |
| | 65 | FOR EACH ROW |
| | 66 | EXECUTE FUNCTION trekr.fn_validate_finance_percentages(); |
| | 67 | }}} |
| | 68 | |
| | 69 | ==== Функции / Stored Procedures ==== |
| | 70 | |
| | 71 | Функција за пресметка на вкупно заработени средства во тековниот месец за даден финансиски корисник. |
| | 72 | {{{ |
| | 73 | CREATE OR REPLACE FUNCTION trekr.fn_finance_total_earned_this_month(p_user_id bigint) |
| | 74 | RETURNS numeric |
| | 75 | LANGUAGE sql |
| | 76 | AS $$ |
| | 77 | SELECT COALESCE(SUM(i.amount), 0) |
| | 78 | FROM trekr.incomes i |
| | 79 | WHERE i.finance_user_id = p_user_id |
| | 80 | AND date_trunc('month', i.date) = date_trunc('month', current_date); |
| | 81 | $$; |
| | 82 | }}} |
| | 83 | |
| | 84 | Функција за пресметка на распределбата на тековниот месечен приход по 5-те финансиски категории. |
| | 85 | {{{ |
| | 86 | CREATE OR REPLACE FUNCTION trekr.fn_finance_monthly_allocation(p_user_id bigint) |
| | 87 | RETURNS TABLE ( |
| | 88 | spending_amount numeric, |
| | 89 | saving_amount numeric, |
| | 90 | investing_amount numeric, |
| | 91 | donation_amount numeric, |
| | 92 | credit_amount numeric |
| | 93 | ) |
| | 94 | LANGUAGE sql |
| | 95 | AS $$ |
| | 96 | SELECT |
| | 97 | COALESCE(f.spending_budget, 0) * COALESCE(v.total_earned_this_month, 0) / 100.0 AS spending_amount, |
| | 98 | COALESCE(f.saving_budget, 0) * COALESCE(v.total_earned_this_month, 0) / 100.0 AS saving_amount, |
| | 99 | COALESCE(f.investing_budget, 0) * COALESCE(v.total_earned_this_month, 0) / 100.0 AS investing_amount, |
| | 100 | COALESCE(f.donation_budget, 0) * COALESCE(v.total_earned_this_month, 0) / 100.0 AS donation_amount, |
| | 101 | COALESCE(f.credit, 0) * COALESCE(v.total_earned_this_month, 0) / 100.0 AS credit_amount |
| | 102 | FROM trekr.finance_users f |
| | 103 | LEFT JOIN trekr.vw_finance_current_month v |
| | 104 | ON v.user_id = f.user_id |
| | 105 | WHERE f.user_id = p_user_id; |
| | 106 | $$; |
| | 107 | }}} |
| | 108 | |
| | 109 | ==== Погледи (Views) ==== |
| | 110 | |
| | 111 | Поглед за месечен приход по корисник, по година и месец. |
| | 112 | Овој поглед се користи за историјски извештаи и агрегирани графици. |
| | 113 | {{{ |
| | 114 | CREATE OR REPLACE VIEW trekr.vw_finance_monthly_summary AS |
| | 115 | SELECT |
| | 116 | i.finance_user_id AS user_id, |
| | 117 | EXTRACT(YEAR FROM i.date)::int AS year, |
| | 118 | EXTRACT(MONTH FROM i.date)::int AS month, |
| | 119 | SUM(i.amount) AS total_income |
| | 120 | FROM trekr.incomes i |
| | 121 | GROUP BY i.finance_user_id, EXTRACT(YEAR FROM i.date), EXTRACT(MONTH FROM i.date); |
| | 122 | }}} |
| | 123 | |
| | 124 | Поглед за вкупно заработени средства во тековниот месец по корисник. |
| | 125 | Овој поглед е основа за пресметка на распределбата на буџетот. |
| | 126 | {{{ |
| | 127 | CREATE OR REPLACE VIEW trekr.vw_finance_current_month AS |
| | 128 | SELECT |
| | 129 | f.user_id, |
| | 130 | COALESCE(SUM(i.amount), 0) AS total_earned_this_month |
| | 131 | FROM trekr.finance_users f |
| | 132 | LEFT JOIN trekr.incomes i |
| | 133 | ON i.finance_user_id = f.user_id |
| | 134 | AND date_trunc('month', i.date) = date_trunc('month', current_date) |
| | 135 | GROUP BY f.user_id; |
| | 136 | }}} |
| | 137 | |
| | 138 | Поглед за распределба на тековниот месечен приход по буџетски категории. |
| | 139 | Погледот прикажува колку пари од вкупниот месечен приход припаѓаат на секоја категорија. |
| | 140 | {{{ |
| | 141 | CREATE OR REPLACE VIEW trekr.vw_finance_allocations AS |
| | 142 | SELECT |
| | 143 | f.user_id, |
| | 144 | v.total_earned_this_month, |
| | 145 | (f.spending_budget * v.total_earned_this_month / 100.0) AS spending_amount, |
| | 146 | (f.saving_budget * v.total_earned_this_month / 100.0) AS saving_amount, |
| | 147 | (f.investing_budget * v.total_earned_this_month / 100.0) AS investing_amount, |
| | 148 | (f.donation_budget * v.total_earned_this_month / 100.0) AS donation_amount, |
| | 149 | (f.credit * v.total_earned_this_month / 100.0) AS credit_amount |
| | 150 | FROM trekr.finance_users f |
| | 151 | LEFT JOIN trekr.vw_finance_current_month v |
| | 152 | ON v.user_id = f.user_id; |
| | 153 | }}} |
| | 154 | |
| | 155 | ---- |
| | 156 | |
| | 157 | == Следење на дневна завршеност на дисциплински задачи (Daily Discipline Completion) == |
| | 158 | |
| | 159 | === Опис на барањата за податочни ограничувања === |
| | 160 | |
| | 161 | Системот мора да обезбеди дека: |
| | 162 | * Дневната завршеност се пресметува за конкретен корисник и конкретен датум. |
| | 163 | * Ако дневната завршеност за тој корисник и датум веќе постои, не смее да се креира дупликат. |
| | 164 | * По пресметка на дневната завршеност, сите дисциплински задачи на тој корисник мора да се ресетираат на `is_finished = false`. |
| | 165 | * Решението мора да може да се извршува и без апликациски сервер, преку складирана функција што може да се повикува од cron. |
| | 166 | |
| | 167 | === Имплементација === |
| | 168 | |
| | 169 | ==== Тригери / Functions ==== |
| | 170 | |
| | 171 | Функција за пресметка на дневна завршеност за еден корисник и еден ден. |
| | 172 | Функцијата пресметува процент на завршени задачи и ја зачувува дневната завршеност само ако не постои веќе запис за тој датум. |
| | 173 | {{{ |
| | 174 | CREATE OR REPLACE FUNCTION trekr.fn_compute_daily_completion(p_user_id bigint, p_day date) |
| | 175 | RETURNS TABLE(created boolean, daily_completion_id bigint, procent numeric) |
| | 176 | LANGUAGE plpgsql |
| | 177 | AS $$ |
| | 178 | DECLARE |
| | 179 | total_count bigint; |
| | 180 | finished_count bigint; |
| | 181 | pct numeric; |
| | 182 | dc_id bigint; |
| | 183 | BEGIN |
| | 184 | IF NOT EXISTS ( |
| | 185 | SELECT 1 |
| | 186 | FROM trekr.discipline_users du |
| | 187 | WHERE du.user_id = p_user_id |
| | 188 | ) THEN |
| | 189 | RAISE EXCEPTION 'Discipline tracking is not enabled for this user'; |
| | 190 | END IF; |
| | 191 | |
| | 192 | SELECT dc.daily_completion_id, dc.procent |
| | 193 | INTO dc_id, pct |
| | 194 | FROM trekr.daily_completions dc |
| | 195 | WHERE dc.user_id = p_user_id |
| | 196 | AND dc.date = p_day |
| | 197 | LIMIT 1; |
| | 198 | |
| | 199 | IF dc_id IS NOT NULL THEN |
| | 200 | RETURN QUERY SELECT false, dc_id, pct; |
| | 201 | RETURN; |
| | 202 | END IF; |
| | 203 | |
| | 204 | SELECT COUNT(*) |
| | 205 | INTO total_count |
| | 206 | FROM trekr.tasks t |
| | 207 | WHERE t.discipline_user_id = p_user_id; |
| | 208 | |
| | 209 | SELECT COUNT(*) |
| | 210 | INTO finished_count |
| | 211 | FROM trekr.tasks t |
| | 212 | WHERE t.discipline_user_id = p_user_id |
| | 213 | AND t.is_finished = TRUE; |
| | 214 | |
| | 215 | pct := CASE |
| | 216 | WHEN total_count = 0 THEN 0 |
| | 217 | ELSE ROUND((finished_count::numeric / total_count::numeric) * 100, 2) |
| | 218 | END; |
| | 219 | |
| | 220 | INSERT INTO trekr.daily_completions (user_id, date, procent) |
| | 221 | VALUES (p_user_id, p_day, pct) |
| | 222 | RETURNING daily_completion_id INTO dc_id; |
| | 223 | |
| | 224 | UPDATE trekr.tasks |
| | 225 | SET is_finished = FALSE |
| | 226 | WHERE discipline_user_id = p_user_id |
| | 227 | AND is_finished = TRUE; |
| | 228 | |
| | 229 | RETURN QUERY SELECT true, dc_id, pct; |
| | 230 | END; |
| | 231 | $$; |
| | 232 | }}} |
| | 233 | |
| | 234 | Функција за пресметка на дневна завршеност за сите дисциплински корисници за даден датум. |
| | 235 | Оваа функција е погодна за автоматско стартување преку cron job. |
| | 236 | {{{ |
| | 237 | CREATE OR REPLACE FUNCTION trekr.fn_compute_daily_completion_for_all(p_day date) |
| | 238 | RETURNS void |
| | 239 | LANGUAGE plpgsql |
| | 240 | AS $$ |
| | 241 | DECLARE |
| | 242 | u RECORD; |
| | 243 | BEGIN |
| | 244 | FOR u IN |
| | 245 | SELECT user_id |
| | 246 | FROM trekr.discipline_users |
| | 247 | LOOP |
| | 248 | BEGIN |
| | 249 | PERFORM trekr.fn_compute_daily_completion(u.user_id, p_day); |
| | 250 | EXCEPTION |
| | 251 | WHEN OTHERS THEN |
| | 252 | RAISE NOTICE 'compute_daily_completion failed for user %: %', u.user_id, SQLERRM; |
| | 253 | END; |
| | 254 | END LOOP; |
| | 255 | END; |
| | 256 | $$; |
| | 257 | }}} |
| | 258 | |
| | 259 | ==== Погледи (Views) ==== |
| | 260 | |
| | 261 | Поглед за преглед на сите дневни завршености по корисник. |
| | 262 | Ги прикажува историјските проценти, датумите и корисникот на кој припаѓаат. |
| | 263 | {{{ |
| | 264 | CREATE OR REPLACE VIEW trekr.vw_daily_completion_history AS |
| | 265 | SELECT |
| | 266 | dc.daily_completion_id, |
| | 267 | dc.user_id, |
| | 268 | dc.date, |
| | 269 | dc.procent |
| | 270 | FROM trekr.daily_completions dc |
| | 271 | ORDER BY dc.date DESC, dc.daily_completion_id DESC; |
| | 272 | }}} |
| | 273 | |
| | 274 | ---- |
| | 275 | |
| | 276 | == Интеграција и употреба во апликацијата == |
| | 277 | |
| | 278 | === Опис === |
| | 279 | |
| | 280 | Овие тригери, функции и погледи се користат за: |
| | 281 | * валидација на внесот во финансискиот модул, |
| | 282 | * автоматска пресметка на месечни финансиски суми, |
| | 283 | * пресметка на распределба на буџет според тековниот приход, |
| | 284 | * пресметка и архивирање на дневна дисциплинска завршеност, |
| | 285 | * прикажување на агрегирани извештаи во frontend без дополнителни сложени пресметки. |
| | 286 | |
| | 287 | === Забелешка === |
| | 288 | |
| | 289 | SQL објектите се дизајнирани да бидат: |
| | 290 | * идемпотентни каде што е можно, |
| | 291 | * лесни за користење од backend и frontend, |
| | 292 | * погодни за annual / monthly reporting, |
| | 293 | * компатибилни со директно повикување од апликацијата или преку scheduled job. |