DatabaseProgramming: 13_routines.sql

File 13_routines.sql, 14.7 KB (added by 231102, 6 days ago)
Line 
1-- ==============================================================================
2-- 13. ФУНКЦИИ / ПРОЦЕДУРИ / ТРИГЕРИ (3 + 3 + 3)
3-- ==============================================================================
4
5-- ============================== ФУНКЦИИ =======================================
6
7-- F1: Кредитно-пондериран просек на студент (опционално по систем на оценување).
8CREATE
9OR REPLACE FUNCTION fn_prosek_student(p_student_id integer,
10 p_sistem_ocenuvanje_id integer DEFAULT NULL)
11 RETURNS numeric
12 LANGUAGE sql
13 STABLE
14AS
15$$
16SELECT ROUND(SUM(sov.ocena_vrednost::numeric * ak.krediti)
17 / NULLIF(SUM(ak.krediti), 0), 2)
18FROM sm_student_ocena_zapis soz
19 JOIN sm_student_upisan_kurs suk ON soz.student_upisan_kurs_id = suk.id
20 JOIN sm_student_semestar_upis ssu ON suk.student_semestar_upis_id = ssu.id
21 JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id
22 JOIN sm_kurs_realizacija kr ON suk.kurs_realizacija_id = kr.id
23 JOIN sm_akreditacija ak ON kr.akreditacija_id = ak.id
24 JOIN sm_sistem_ocenuvanje_vrednost sov ON soz.sistem_ocenuvanje_vrednost_id = sov.id
25WHERE ss.student_id = p_student_id
26 AND sov.polozitelna
27 AND (p_sistem_ocenuvanje_id IS NULL OR sov.sistem_ocenuvanje_id = p_sistem_ocenuvanje_id)
28 $$;
29
30-- F2: Вкупен отворен долг на корисник во дадена валута.
31CREATE
32OR REPLACE FUNCTION fn_otvoren_dolg(p_user_id integer, p_vid_valuta_id integer)
33 RETURNS numeric
34 LANGUAGE sql
35 STABLE
36AS
37$$
38SELECT COALESCE(SUM(fo.otvorena_suma), 0)
39FROM fin_finansiska_obvrska fo
40WHERE fo.user_id = p_user_id
41 AND fo.vid_valuta_id = p_vid_valuta_id
42 AND fo.otvorena_suma > 0
43 AND fo.status_finansiska_obvrska_id IN
44 (SELECT id FROM fin_status_finansiska_obvrska WHERE ime IN ('Izdadena', 'Delumno platena'))
45 $$;
46
47-- F3: Слободни места на реализација на курс (NULL капацитет = неограничено).
48CREATE
49OR REPLACE FUNCTION fn_slobodni_mesta(p_kurs_realizacija_id integer)
50 RETURNS integer
51 LANGUAGE sql
52 STABLE
53AS
54$$
55SELECT kr.kapacitet_studenti
56 - (SELECT COUNT(*) ::int
57 FROM sm_student_upisan_kurs suk
58 WHERE suk.kurs_realizacija_id = kr.id
59 AND suk.status_slushanje = 'UPISAN')
60FROM sm_kurs_realizacija kr
61WHERE kr.id = p_kurs_realizacija_id
62 $$;
63
64-- ============================== ПРОЦЕДУРИ =====================================
65
66-- P1: Регистрирање наплата + автоматска алокација кон отворени обврски
67-- (најстаро доспевање прво). Ажурирањето на otvorena_suma го врши тригерот T2.
68CREATE
69OR REPLACE PROCEDURE pr_registriraj_naplata(p_user_id integer,
70 p_suma numeric,
71 p_vid_valuta_id integer,
72 p_organizaciona_edinica_id integer,
73 p_referenca varchar DEFAULT NULL,
74 p_korisnik_naplata_id integer DEFAULT NULL)
75 LANGUAGE plpgsql
76AS
77$$
78DECLARE
79v_naplata_id integer;
80 v_smetka_id
81integer;
82 v_ostatok
83numeric := p_suma;
84 v_obvrska
85record;
86 v_alocirano
87numeric;
88BEGIN
89 IF
90p_suma <= 0 THEN
91 RAISE EXCEPTION 'Сумата мора да биде позитивна (добиено: %)', p_suma;
92END IF;
93
94 -- активна трансакциска сметка на организационата единица (приоритет: иста валута)
95SELECT oes.transakciska_smetka_id
96INTO v_smetka_id
97FROM or_organizaciona_edinica_smetki oes
98 JOIN fin_transakciski_smetki ts ON oes.transakciska_smetka_id = ts.id
99WHERE oes.organizaciona_edinica_id = p_organizaciona_edinica_id
100 AND oes.aktivna
101 AND ts.aktivna
102ORDER BY (ts.vid_valuta_id = p_vid_valuta_id) DESC, ts.id LIMIT 1;
103IF
104v_smetka_id IS NULL THEN
105 RAISE EXCEPTION 'Организационата единица % нема активна трансакциска сметка',
106 p_organizaciona_edinica_id;
107END IF;
108
109INSERT INTO fin_naplati (user_id, korisnik_naplata_id, transakciska_smetka_id,
110 organizaciona_edinica_id, naplatena_suma, vid_valuta_id,
111 status_transakcija_id, nadvoresna_referenca,
112 requested_at, confirmed_at, data)
113VALUES (p_user_id, COALESCE(p_korisnik_naplata_id, p_user_id), v_smetka_id,
114 p_organizaciona_edinica_id, p_suma, p_vid_valuta_id,
115 (SELECT id FROM fin_status_transakcija WHERE ime_status = 'ACCEPTED'),
116 p_referenca, now(), now(), now()) RETURNING id
117INTO v_naplata_id;
118
119FOR v_obvrska IN
120SELECT fo.id, fo.otvorena_suma
121FROM fin_finansiska_obvrska fo
122WHERE fo.user_id = p_user_id
123 AND fo.vid_valuta_id = p_vid_valuta_id
124 AND fo.otvorena_suma > 0
125ORDER BY fo.datum_dospevanje NULLS LAST, fo.id
126 FOR UPDATE LOOP
127 EXIT WHEN v_ostatok <= 0;
128v_alocirano
129:= LEAST(v_ostatok, v_obvrska.otvorena_suma);
130INSERT INTO fin_naplata_alokacija (naplata_id, finansiska_obvrska_id,
131 alocirana_suma, created_at)
132VALUES (v_naplata_id, v_obvrska.id, v_alocirano, now());
133v_ostatok
134:= v_ostatok - v_alocirano;
135END LOOP;
136
137 IF
138v_ostatok > 0 THEN
139 RAISE NOTICE 'Наплата %: % останува неалоцирано (преплата).', v_naplata_id, v_ostatok;
140END IF;
141END;
142$$;
143
144-- P2: Запишување студент на курс — проверува долг и слободни места,
145-- па внесува во sm_student_upisan_kurs (тригерот T3 е последна одбрана).
146CREATE
147OR REPLACE PROCEDURE pr_zapisi_student_na_kurs(p_student_id integer,
148 p_kurs_realizacija_id integer)
149 LANGUAGE plpgsql
150AS
151$$
152DECLARE
153v_ssu_id integer;
154 v_user_id
155integer;
156 v_dolg
157numeric;
158 v_valuta
159integer;
160 v_slobodni
161integer;
162BEGIN
163SELECT ssu.id
164INTO v_ssu_id
165FROM sm_student_semestar_upis ssu
166 JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id
167 JOIN sm_kurs_realizacija kr ON kr.id = p_kurs_realizacija_id
168WHERE ss.student_id = p_student_id
169 AND ssu.akademski_semestar_id = kr.akademski_semestar_id
170ORDER BY ssu.datum_upis DESC LIMIT 1;
171IF
172v_ssu_id IS NULL THEN
173 RAISE EXCEPTION 'Студентот % нема упис во семестарот на курсот %',
174 p_student_id, p_kurs_realizacija_id;
175END IF;
176
177 IF
178EXISTS (SELECT 1
179 FROM sm_student_upisan_kurs
180 WHERE student_semestar_upis_id = v_ssu_id
181 AND kurs_realizacija_id = p_kurs_realizacija_id) THEN
182 RAISE EXCEPTION 'Студентот % е веќе запишан на курсот %',
183 p_student_id, p_kurs_realizacija_id;
184END IF;
185
186SELECT s.user_id
187INTO v_user_id
188FROM sm_studenti s
189WHERE s.id = p_student_id;
190FOR v_valuta IN
191SELECT DISTINCT vid_valuta_id
192FROM fin_finansiska_obvrska
193WHERE user_id = v_user_id
194 AND otvorena_suma > 0 LOOP
195 v_dolg := fn_otvoren_dolg(v_user_id, v_valuta);
196IF
197v_dolg > 0 THEN
198 RAISE EXCEPTION 'Студентот % има отворен долг % (валута %) — уписот е блокиран',
199 p_student_id, v_dolg, v_valuta;
200END IF;
201END LOOP;
202
203 v_slobodni
204:= fn_slobodni_mesta(p_kurs_realizacija_id);
205 IF
206v_slobodni IS NOT NULL AND v_slobodni <= 0 THEN
207 RAISE EXCEPTION 'Нема слободни места на курсот %', p_kurs_realizacija_id;
208END IF;
209
210INSERT INTO sm_student_upisan_kurs (student_semestar_upis_id, kurs_realizacija_id, status_slushanje)
211VALUES (v_ssu_id, p_kurs_realizacija_id, 'UPISAN');
212END;
213$$;
214
215-- P3: Сериско генерирање обврски за школарина за сите студенти запишани
216-- во даден академски семестар, според активниот ценовник. COMMIT на
217-- секои 1000 студенти — можно само во процедура.
218CREATE
219OR REPLACE PROCEDURE pr_generiraj_obvrski_za_semestar(p_akademski_semestar_id integer)
220 LANGUAGE plpgsql
221AS
222$$
223DECLARE
224v_cenovnik record;
225 v_oe
226integer;
227 v_opis
228varchar;
229 v_student
230record;
231 v_brojac
232integer := 0;
233BEGIN
234SELECT asem.organizaciona_edinica_id,
235 c.id AS cenovnik_id,
236 c.iznos,
237 c.vid_valuta_id,
238 asem.tip_naplata_organizaciona_edinica_id
239INTO v_cenovnik
240FROM sm_akademski_semestar asem
241 JOIN fin_tip_naplati_cenovnik c
242 ON c.tip_naplata_organizaciona_edinica_id = asem.tip_naplata_organizaciona_edinica_id
243 AND c.aktivna
244WHERE asem.id = p_akademski_semestar_id
245ORDER BY c.prioritet LIMIT 1;
246IF
247v_cenovnik IS NULL THEN
248 RAISE EXCEPTION 'Нема активен ценовник за семестар %', p_akademski_semestar_id;
249END IF;
250
251 v_oe
252:= v_cenovnik.organizaciona_edinica_id;
253 v_opis
254:= 'Школарина семестар ' || p_akademski_semestar_id;
255
256FOR v_student IN
257SELECT DISTINCT s.user_id
258FROM sm_student_semestar_upis ssu
259 JOIN sm_studenti_smerovi ss ON ssu.studenti_smerovi_id = ss.id
260 JOIN sm_studenti s ON ss.student_id = s.id
261WHERE ssu.akademski_semestar_id = p_akademski_semestar_id
262 AND NOT EXISTS (SELECT 1
263 FROM fin_finansiska_obvrska fo
264 WHERE fo.user_id = s.user_id
265 AND fo.opis = 'Школарина семестар ' || p_akademski_semestar_id)
266 LOOP
267INSERT
268INTO fin_finansiska_obvrska (user_id, organizaciona_edinica_id,
269 tip_naplata_organizaciona_edinica_id,
270 tip_naplati_cenovnik_id,
271 status_finansiska_obvrska_id,
272 datum_kreiranje, datum_dospevanje,
273 originalna_suma, otvorena_suma,
274 vid_valuta_id, opis)
275VALUES (v_student.user_id, v_oe, v_cenovnik.tip_naplata_organizaciona_edinica_id, v_cenovnik.cenovnik_id, (SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Izdadena'), now(), CURRENT_DATE + 30, v_cenovnik.iznos, v_cenovnik.iznos, v_cenovnik.vid_valuta_id, v_opis);
276v_brojac
277:= v_brojac + 1;
278 IF
279v_brojac % 1000 = 0 THEN
280 COMMIT; -- дозволено само во процедура
281END IF;
282END LOOP;
283
284 RAISE
285NOTICE 'Генерирани % обврски за семестар %', v_brojac, p_akademski_semestar_id;
286END;
287$$;
288
289-- ============================== ТРИГЕРИ =======================================
290
291-- T1: Валидација при внес на оценка — уписот мора да е активен (UPISAN),
292-- поените во опсег 0–100, датумот не во иднина.
293CREATE
294OR REPLACE FUNCTION trgf_validna_ocena()
295 RETURNS trigger
296 LANGUAGE plpgsql
297AS
298$$
299DECLARE
300v_status varchar;
301BEGIN
302SELECT status_slushanje
303INTO v_status
304FROM sm_student_upisan_kurs
305WHERE id = NEW.student_upisan_kurs_id;
306IF
307v_status IS DISTINCT FROM 'UPISAN' THEN
308 RAISE EXCEPTION 'Оценка може да се внесе само за активен упис (статус: %)', v_status;
309END IF;
310 IF
311NEW.osvoeni_poeni IS NOT NULL AND (NEW.osvoeni_poeni < 0 OR NEW.osvoeni_poeni > 100) THEN
312 RAISE EXCEPTION 'Освоени поени мора да бидат 0–100 (добиено: %)', NEW.osvoeni_poeni;
313END IF;
314 IF
315NEW.datum_zapis > now() THEN
316 RAISE EXCEPTION 'Датумот на запис не смее да биде во иднина';
317END IF;
318RETURN NEW;
319END;
320$$;
321
322DROP TRIGGER IF EXISTS trg_validna_ocena ON sm_student_ocena_zapis;
323CREATE TRIGGER trg_validna_ocena
324 BEFORE INSERT
325 ON sm_student_ocena_zapis
326 FOR EACH ROW
327 EXECUTE FUNCTION trgf_validna_ocena();
328
329-- T2: По алокација на наплата — намали otvorena_suma и постави статус
330-- (Delumno platena / Platena). Изведената вредност ја одржува тригерот,
331-- без разлика кој ја внел алокацијата.
332CREATE
333OR REPLACE FUNCTION trgf_azuriraj_otvorena_suma()
334 RETURNS trigger
335 LANGUAGE plpgsql
336AS
337$$
338DECLARE
339v_nova numeric;
340BEGIN
341SELECT otvorena_suma - NEW.alocirana_suma
342INTO v_nova
343FROM fin_finansiska_obvrska
344WHERE id = NEW.finansiska_obvrska_id
345 FOR UPDATE;
346
347IF
348v_nova < 0 THEN
349 RAISE EXCEPTION 'Алокацијата (%) ја надминува отворената сума на обврската %',
350 NEW.alocirana_suma, NEW.finansiska_obvrska_id;
351END IF;
352
353UPDATE fin_finansiska_obvrska
354SET otvorena_suma = v_nova,
355 status_finansiska_obvrska_id = CASE
356 WHEN v_nova = 0 THEN
357 (SELECT id FROM fin_status_finansiska_obvrska WHERE ime = 'Platena')
358 ELSE
359 (SELECT id
360 FROM fin_status_finansiska_obvrska
361 WHERE ime = 'Delumno platena')
362 END
363WHERE id = NEW.finansiska_obvrska_id;
364
365RETURN NEW;
366END;
367$$;
368
369DROP TRIGGER IF EXISTS trg_azuriraj_otvorena_suma ON fin_naplata_alokacija;
370CREATE TRIGGER trg_azuriraj_otvorena_suma
371 AFTER INSERT
372 ON fin_naplata_alokacija
373 FOR EACH ROW
374 EXECUTE FUNCTION trgf_azuriraj_otvorena_suma();
375
376-- T3: Капацитет на курс — одбиј упис над kapacitet_studenti, и при
377-- конкурентни инсерти (FOR UPDATE врз реализацијата).
378CREATE
379OR REPLACE FUNCTION trgf_kapacitet_kurs()
380 RETURNS trigger
381 LANGUAGE plpgsql
382AS
383$$
384DECLARE
385v_kapacitet integer;
386 v_upisani
387integer;
388BEGIN
389 IF
390NEW.status_slushanje <> 'UPISAN' THEN
391 RETURN NEW;
392END IF;
393
394SELECT kapacitet_studenti
395INTO v_kapacitet
396FROM sm_kurs_realizacija
397WHERE id = NEW.kurs_realizacija_id
398 FOR UPDATE; -- сериализира конкурентни уписи на иста реализација
399
400IF
401v_kapacitet IS NULL THEN
402 RETURN NEW; -- неограничен капацитет
403END IF;
404
405SELECT COUNT(*)
406INTO v_upisani
407FROM sm_student_upisan_kurs
408WHERE kurs_realizacija_id = NEW.kurs_realizacija_id
409 AND status_slushanje = 'UPISAN';
410
411IF
412v_upisani >= v_kapacitet THEN
413 RAISE EXCEPTION 'Курсот % е полн (капацитет %)', NEW.kurs_realizacija_id, v_kapacitet;
414END IF;
415
416RETURN NEW;
417END;
418$$;
419
420DROP TRIGGER IF EXISTS trg_kapacitet_kurs ON sm_student_upisan_kurs;
421CREATE TRIGGER trg_kapacitet_kurs
422 BEFORE INSERT
423 ON sm_student_upisan_kurs
424 FOR EACH ROW
425 EXECUTE FUNCTION trgf_kapacitet_kurs();