| 1 | | = DatabaseProgramming = |
| 2 | | |
| 3 | | == Опис == |
| 4 | | |
| 5 | | Покрај основната структура на базата, имплементирана е и дополнителна PostgreSQL логика со функции, процедури и тригери. |
| 6 | | |
| 7 | | Оваа логика се користи за: |
| 8 | | |
| 9 | | * проверка на валидноста на податоците |
| 10 | | * извршување на почести операции над повеќе табели |
| 11 | | * автоматско одржување на историја и timestamps |
| 12 | | * зачувување на интегритетот на податоците |
| 13 | | * спроведување на дел од бизнис правилата на системот |
| 14 | | |
| 15 | | Кодот е поделен во: |
| 16 | | |
| 17 | | * [attachment:functions.sql functions.sql] |
| 18 | | * [attachment:procedures.sql procedures.sql] |
| 19 | | * [attachment:triggers.sql triggers.sql] |
| | 1 | = Функции, процедури и тригери (!DatabaseProgramming) = |
| | 2 | |
| | 3 | Апликациската логика е имплементирана во PL/pgSQL: 11 процедури ([attachment:procedures.sql]), 18 функции ([attachment:functions.sql]) и 18 тригери со 10 тригер-функции ([attachment:triggers.sql]). Правилата се поделени во три слоја. Ограничувањата од шемата ги гарантираат правилата за секоја редица. Процедурите ги проверуваат истите правила пред запишувањето, за апликацијата да добие описна порака наместо грешка на ограничување; тие проверки се издвоени во функции. Правилата што читаат други табели ги гарантираат тригерите за секој пат на запишување; процедурите не ги повторуваат, туку се потпираат на тригерот. |
| | 4 | |
| | 5 | [[PageOutline(2-3,Содржина,inline)]] |
| | 6 | |
| | 7 | ---- |
| | 8 | |
| | 9 | == Процедури == |
| | 10 | |
| | 11 | === Процедура 1: Регистрација на корисник (sp_register_user) === |
| | 12 | |
| | 13 | Регистрација на сметка: во една трансакција запишува нов корисник во `"User"` и соодветната редица во табелата на неговиот подтип (`Client_User`, `Vendor_User` или `Management_User`). Типот определува кој идентификатор е задолжителен (клиент, софтверска агенција или улога) и тој мора да постои; текстуалните полиња не смеат да бидат празни. Корисникот се креира неактивен и се активира по потврда. |
| | 14 | |
| | 15 | {{{#!sql |
| | 16 | CREATE OR REPLACE PROCEDURE sp_register_user( |
| | 17 | OUT p_user_id int4, |
| | 18 | IN p_type text, |
| | 19 | IN p_first_name text, |
| | 20 | IN p_last_name text, |
| | 21 | IN p_email text, |
| | 22 | IN p_password_hash text, |
| | 23 | IN p_client_id int4 DEFAULT NULL, |
| | 24 | IN p_vendor_id int4 DEFAULT NULL, |
| | 25 | IN p_role_id int4 DEFAULT NULL |
| | 26 | ) |
| | 27 | LANGUAGE plpgsql AS $$ |
| | 28 | BEGIN |
| | 29 | -- Типот одредува кој идентификатор е задолжителен; секоја гранка го проверува својот |
| | 30 | CASE p_type |
| | 31 | WHEN 'client' THEN |
| | 32 | IF p_client_id IS NULL THEN |
| | 33 | RAISE EXCEPTION 'p_client_id is required when registering a client user'; |
| | 34 | END IF; |
| | 35 | PERFORM fn_assert_client_exists(p_client_id); |
| | 36 | WHEN 'vendor' THEN |
| | 37 | IF p_vendor_id IS NULL THEN |
| | 38 | RAISE EXCEPTION 'p_vendor_id is required when registering a vendor user'; |
| | 39 | END IF; |
| | 40 | PERFORM fn_assert_vendor_exists(p_vendor_id); |
| | 41 | WHEN 'management' THEN |
| | 42 | IF p_role_id IS NULL THEN |
| | 43 | RAISE EXCEPTION 'p_role_id is required when registering a management user'; |
| | 44 | END IF; |
| | 45 | PERFORM fn_assert_role_exists(p_role_id); |
| | 46 | ELSE |
| | 47 | RAISE EXCEPTION 'Invalid user type: %. Must be client, vendor, or management', p_type; |
| | 48 | END CASE; |
| | 49 | |
| | 50 | -- Задолжителните текстуални полиња не смеат да бидат празни |
| | 51 | PERFORM fn_assert_not_blank(p_first_name, 'first_name'); |
| | 52 | PERFORM fn_assert_not_blank(p_last_name, 'last_name'); |
| | 53 | PERFORM fn_assert_not_blank(p_email, 'email'); |
| | 54 | PERFORM fn_assert_not_blank(p_password_hash, 'password_hash'); |
| | 55 | |
| | 56 | -- Корисникот се креира неактивен (is_active DEFAULT false); |
| | 57 | -- sp_activate_user е чекорот на потврда што го активира |
| | 58 | INSERT INTO "User" (type, first_name, last_name, email, password_hash) |
| | 59 | VALUES (p_type, p_first_name, p_last_name, p_email, p_password_hash) |
| | 60 | RETURNING user_id INTO p_user_id; -- RETURNING го дава новиот клуч во OUT-параметарот |
| | 61 | |
| | 62 | -- Редицата на подтипот; тригерот trg_enforce_user_subtype ја проверува според "User".type |
| | 63 | CASE p_type |
| | 64 | WHEN 'client' THEN |
| | 65 | INSERT INTO Client_User (user_id, client_id) |
| | 66 | VALUES (p_user_id, p_client_id); |
| | 67 | WHEN 'vendor' THEN |
| | 68 | INSERT INTO Vendor_User (user_id, vendor_id) |
| | 69 | VALUES (p_user_id, p_vendor_id); |
| | 70 | WHEN 'management' THEN |
| | 71 | INSERT INTO Management_User (user_id, role_id) |
| | 72 | VALUES (p_user_id, p_role_id); |
| | 73 | ELSE |
| | 74 | RAISE EXCEPTION 'Invalid user type: %. Must be client, vendor, or management', p_type; |
| | 75 | END CASE; |
| | 76 | END; |
| | 77 | $$; |
| | 78 | |
| | 79 | CALL sp_register_user(NULL, 'client', 'Ана', 'Петрова', 'ana.petrova@example.com', 'sha256:...', p_client_id => 17); |
| | 80 | }}} |
| | 81 | |
| | 82 | ---- |
| | 83 | |
| | 84 | === Процедура 2: Активирање на корисник (sp_activate_user) === |
| | 85 | |
| | 86 | Потврда на регистрираната сметка: го активира корисникот. Непостоечки или веќе активен корисник се одбива. Заедно со регистрацијата и деактивирањето го затвора животниот циклус на сметката. |
| | 87 | |
| | 88 | {{{#!sql |
| | 89 | CREATE OR REPLACE PROCEDURE sp_activate_user( |
| | 90 | IN p_user_id int4 |
| | 91 | ) |
| | 92 | LANGUAGE plpgsql AS $$ |
| | 93 | BEGIN |
| | 94 | -- Состојбата е дел од условот WHERE: |
| | 95 | -- една наредба истовремено проверува и запишува |
| | 96 | UPDATE "User" |
| | 97 | SET is_active = true |
| | 98 | WHERE user_id = p_user_id |
| | 99 | AND is_active = false; |
| | 100 | |
| | 101 | IF NOT FOUND THEN |
| | 102 | RAISE EXCEPTION 'User % does not exist or is already active', p_user_id; |
| | 103 | END IF; |
| | 104 | END; |
| | 105 | $$; |
| | 106 | |
| | 107 | CALL sp_activate_user(77001); |
| | 108 | }}} |
| | 109 | |
| | 110 | ---- |
| | 111 | |
| | 112 | === Процедура 3: Деактивирање на корисник (sp_deactivate_user) === |
| | 113 | |
| | 114 | Ја деактивира сметката наместо да ја брише, бидејќи историјата (рецензии, спорови, промени на статус) упатува на корисникот што дејствувал. Ако корисникот е менаџмент корисник, отворените спорови што му се доделени се ослободуваат, за ниту еден спор да не остане кај корисник што заминал. |
| | 115 | |
| | 116 | {{{#!sql |
| | 117 | CREATE OR REPLACE PROCEDURE sp_deactivate_user( |
| | 118 | IN p_user_id int4 |
| | 119 | ) |
| | 120 | LANGUAGE plpgsql AS $$ |
| | 121 | DECLARE |
| | 122 | v_is_management bool; |
| | 123 | BEGIN |
| | 124 | -- Состојбата е дел од условот WHERE, како кај sp_activate_user |
| | 125 | UPDATE "User" |
| | 126 | SET is_active = false |
| | 127 | WHERE user_id = p_user_id |
| | 128 | AND is_active = true; |
| | 129 | |
| | 130 | IF NOT FOUND THEN |
| | 131 | RAISE EXCEPTION 'User % does not exist or is already inactive', p_user_id; |
| | 132 | END IF; |
| | 133 | |
| | 134 | -- Менаџмент корисник што заминува не смее да остане доделен на отворени спорови |
| | 135 | SELECT EXISTS ( |
| | 136 | SELECT 1 FROM Management_User WHERE user_id = p_user_id |
| | 137 | ) INTO v_is_management; |
| | 138 | |
| | 139 | IF v_is_management THEN |
| | 140 | UPDATE Dispute_Ticket |
| | 141 | SET assigned_management_user_id = NULL |
| | 142 | WHERE assigned_management_user_id = p_user_id |
| | 143 | AND is_resolved = false; |
| | 144 | END IF; |
| | 145 | END; |
| | 146 | $$; |
| | 147 | |
| | 148 | CALL sp_deactivate_user(77001); |
| | 149 | }}} |
| | 150 | |
| | 151 | ---- |
| | 152 | |
| | 153 | === Процедура 4: Договор со прв проект (sp_create_contract_with_project) === |
| | 154 | |
| | 155 | Склучување договор меѓу клиент и софтверска агенција заедно со првиот проект на договорот, во една трансакција, бидејќи во моделот проектот секогаш припаѓа на договор. Пред запишувањето проверува дека насловот, името на проектот и буџетот се валидни и дека клиентот, софтверската агенција и почетниот статус постојат; ги враќа двата нови клуча. |
| | 156 | |
| | 157 | {{{#!sql |
| | 158 | CREATE OR REPLACE PROCEDURE sp_create_contract_with_project( |
| | 159 | OUT p_contract_id int4, |
| | 160 | OUT p_project_id int4, |
| | 161 | IN p_client_id int4, |
| | 162 | IN p_vendor_id int4, |
| | 163 | IN p_contract_title text, |
| | 164 | IN p_project_name text, |
| | 165 | IN p_status_id int4, |
| | 166 | IN p_budget numeric(10,2), |
| | 167 | IN p_contract_number text DEFAULT NULL, |
| | 168 | IN p_cvc_start_date date DEFAULT CURRENT_DATE, |
| | 169 | IN p_cvc_end_date date DEFAULT NULL, |
| | 170 | IN p_total_value numeric(10,2) DEFAULT NULL, |
| | 171 | IN p_currency_code text DEFAULT NULL, |
| | 172 | IN p_terms_summary text DEFAULT NULL, |
| | 173 | IN p_project_start_date date DEFAULT CURRENT_DATE, |
| | 174 | IN p_project_end_date date DEFAULT NULL |
| | 175 | ) |
| | 176 | LANGUAGE plpgsql AS $$ |
| | 177 | BEGIN |
| | 178 | -- Задолжителните текстуални полиња не смеат да бидат празни |
| | 179 | PERFORM fn_assert_not_blank(p_contract_title, 'contract_title'); |
| | 180 | PERFORM fn_assert_not_blank(p_project_name, 'project_name'); |
| | 181 | |
| | 182 | -- Буџетот мора да биде даден и позитивен |
| | 183 | PERFORM fn_assert_positive(p_budget, 'Budget'); |
| | 184 | |
| | 185 | -- Референцираните редици мора да постојат пред запишувањето |
| | 186 | PERFORM fn_assert_client_exists(p_client_id); |
| | 187 | PERFORM fn_assert_vendor_exists(p_vendor_id); |
| | 188 | PERFORM fn_assert_status_exists(p_status_id); |
| | 189 | |
| | 190 | INSERT INTO Client_Vendor_Contract ( |
| | 191 | client_id, vendor_id, contract_title, contract_number, |
| | 192 | start_date, end_date, total_value, currency_code, terms_summary |
| | 193 | ) |
| | 194 | VALUES ( |
| | 195 | p_client_id, p_vendor_id, p_contract_title, p_contract_number, |
| | 196 | p_cvc_start_date, p_cvc_end_date, p_total_value, p_currency_code, p_terms_summary |
| | 197 | ) |
| | 198 | RETURNING contract_id INTO p_contract_id; |
| | 199 | |
| | 200 | -- Името на проектот е уникатно внатре во договорот (uq_project_contract_name); |
| | 201 | -- договорот штотуку е создаден, па првиот проект не може да се судри со друг |
| | 202 | INSERT INTO Project ( |
| | 203 | contract_id, status_id, project_name, start_date, end_date, budget |
| | 204 | ) |
| | 205 | VALUES ( |
| | 206 | p_contract_id, p_status_id, p_project_name, |
| | 207 | p_project_start_date, p_project_end_date, p_budget |
| | 208 | ) |
| | 209 | RETURNING project_id INTO p_project_id; |
| | 210 | END; |
| | 211 | $$; |
| | 212 | |
| | 213 | CALL sp_create_contract_with_project(NULL, NULL, 17, 42, 'Развој на веб-портал', 'Веб-портал, фаза 1', 1, 125000.00, |
| | 214 | p_total_value => 500000.00, p_currency_code => 'EUR'); |
| | 215 | }}} |
| | 216 | |
| | 217 | ---- |
| | 218 | |
| | 219 | === Процедура 5: Промена на статус на проект (sp_update_project_status) === |
| | 220 | |
| | 221 | Го води животниот циклус на проектот: го менува статусот и запишува редица во историјата на статуси со точно еден актер, корисник на софтверската агенција или менаџмент корисник. Новиот статус мора да постои; дека проектот и актерот постојат и дека корисникот на агенцијата ѝ припаѓа на агенцијата на проектот го проверува тригерот при запишувањето во историјата. Ако проектот веќе е во целниот статус, не се запишува ништо. Проект што веќе има рецензија не може да се врати во отворен статус. |
| | 222 | |
| | 223 | {{{#!sql |
| | 224 | CREATE OR REPLACE PROCEDURE sp_update_project_status( |
| | 225 | IN p_project_id int4, |
| | 226 | IN p_new_status_id int4, |
| | 227 | IN p_vendor_user_id int4 DEFAULT NULL, |
| | 228 | IN p_management_user_id int4 DEFAULT NULL, |
| | 229 | IN p_comment text DEFAULT NULL |
| | 230 | ) |
| | 231 | LANGUAGE plpgsql AS $$ |
| | 232 | DECLARE |
| | 233 | v_current_status_id int4; |
| | 234 | BEGIN |
| | 235 | -- Точно еден актер: корисник на агенцијата или менаџмент корисник, никогаш двата или ниту еден |
| | 236 | IF (p_vendor_user_id IS NULL) = (p_management_user_id IS NULL) THEN |
| | 237 | RAISE EXCEPTION |
| | 238 | 'Provide exactly one actor: p_vendor_user_id or p_management_user_id'; |
| | 239 | END IF; |
| | 240 | |
| | 241 | PERFORM fn_assert_status_exists(p_new_status_id); |
| | 242 | |
| | 243 | -- Дека проектот и актерот постојат и дека актерот од агенцијата ѝ припаѓа |
| | 244 | -- на агенцијата на проектот го проверува тригерот trg_status_history_actor |
| | 245 | -- при INSERT во Project_Status_History подолу |
| | 246 | |
| | 247 | -- Ако проектот веќе е во целниот статус, не се запишува ништо |
| | 248 | SELECT status_id INTO v_current_status_id |
| | 249 | FROM Project |
| | 250 | WHERE project_id = p_project_id; |
| | 251 | |
| | 252 | IF v_current_status_id = p_new_status_id THEN |
| | 253 | RAISE NOTICE 'Project % is already at status % – no update performed', |
| | 254 | p_project_id, p_new_status_id; |
| | 255 | RETURN; |
| | 256 | END IF; |
| | 257 | |
| | 258 | -- Рецензиран проект останува завршен: не може да се врати во отворен статус |
| | 259 | IF EXISTS (SELECT 1 FROM Review WHERE project_id = p_project_id) |
| | 260 | AND (SELECT status_name FROM Project_Status WHERE status_id = p_new_status_id) |
| | 261 | NOT IN ('Completed', 'Cancelled') THEN |
| | 262 | RAISE EXCEPTION 'Project % has a review and cannot return to an open status', p_project_id; |
| | 263 | END IF; |
| | 264 | |
| | 265 | UPDATE Project |
| | 266 | SET status_id = p_new_status_id |
| | 267 | WHERE project_id = p_project_id; |
| | 268 | |
| | 269 | INSERT INTO Project_Status_History ( |
| | 270 | project_id, status_id, vendor_user_id, management_user_id, comment |
| | 271 | ) |
| | 272 | VALUES ( |
| | 273 | p_project_id, p_new_status_id, p_vendor_user_id, p_management_user_id, p_comment |
| | 274 | ); |
| | 275 | END; |
| | 276 | $$; |
| | 277 | |
| | 278 | CALL sp_update_project_status(1400001, 3, NULL, 75001, 'Одобрено од менаџмент'); |
| | 279 | }}} |
| | 280 | |
| | 281 | ---- |
| | 282 | |
| | 283 | === Процедура 6: Промена на буџет на проект (sp_update_project_budget) === |
| | 284 | |
| | 285 | Го менува буџетот на проектот; новиот буџет мора да биде позитивен, а проектот да постои. Ревизиската редица со стариот и новиот буџет ја запишува тригерот за ревизија на буџетот, така што и промена надвор од процедурата останува забележана. |
| | 286 | |
| | 287 | {{{#!sql |
| | 288 | CREATE OR REPLACE PROCEDURE sp_update_project_budget( |
| | 289 | IN p_project_id int4, |
| | 290 | IN p_new_budget numeric(10,2) |
| | 291 | ) |
| | 292 | LANGUAGE plpgsql AS $$ |
| | 293 | BEGIN |
| | 294 | PERFORM fn_assert_positive(p_new_budget, 'Budget'); |
| | 295 | |
| | 296 | -- Ревизиската редица ја запишува trg_capture_budget_change, преку тригерот |
| | 297 | -- trg_project_budget_audit и неговата клаузула WHEN (само кога буџетот се менува) |
| | 298 | UPDATE Project |
| | 299 | SET budget = p_new_budget |
| | 300 | WHERE project_id = p_project_id; |
| | 301 | |
| | 302 | -- Ниту една погодена редица значи дека проектот не постои; проверката ја дава пораката |
| | 303 | IF NOT FOUND THEN |
| | 304 | PERFORM fn_assert_project_exists(p_project_id); |
| | 305 | END IF; |
| | 306 | END; |
| | 307 | $$; |
| | 308 | |
| | 309 | CALL sp_update_project_budget(1400001, 175000.00); |
| | 310 | }}} |
| | 311 | |
| | 312 | ---- |
| | 313 | |
| | 314 | === Процедура 7: Поднесување рецензија (sp_submit_review) === |
| | 315 | |
| | 316 | Поднесување рецензија од клиент по завршен проект: една рецензија и нејзините оценки по димензии, во една трансакција; оценките се предаваат како JSON низа со барем една димензија (не мора да бидат оценети сите десет). Авторот мора да биде корисник на клиентот од договорот на проектот, проектот да биде завршен (Completed или Cancelled, што го гарантира тригерот 18) и да нема веќе рецензија, димензиите да постојат без повторување и оценките да се меѓу 1 и 5. Рецензијата се создава необјавена. |
| | 317 | |
| | 318 | {{{#!sql |
| | 319 | CREATE OR REPLACE PROCEDURE sp_submit_review( |
| | 320 | OUT p_review_id int4, |
| | 321 | IN p_project_id int4, |
| | 322 | IN p_client_user_id int4, |
| | 323 | IN p_summary_text text, |
| | 324 | IN p_scores jsonb |
| | 325 | ) |
| | 326 | LANGUAGE plpgsql AS $$ |
| | 327 | DECLARE |
| | 328 | v_dimension_id int4; |
| | 329 | v_score_value int4; |
| | 330 | BEGIN |
| | 331 | -- Резимето не смее да биде празно |
| | 332 | PERFORM fn_assert_not_blank(p_summary_text, 'summary_text'); |
| | 333 | |
| | 334 | -- Обликот на p_scores: непразна JSON низа од објекти, секој со |
| | 335 | -- нумерички dimension_id и score_value |
| | 336 | -- jsonb_typeof го враќа JSON типот на вредноста ('array', 'object', 'number', ...) |
| | 337 | IF p_scores IS NULL OR jsonb_typeof(p_scores) <> 'array' THEN |
| | 338 | RAISE EXCEPTION 'p_scores must be a JSON array of {dimension_id, score_value} objects'; |
| | 339 | END IF; |
| | 340 | |
| | 341 | IF jsonb_array_length(p_scores) = 0 THEN |
| | 342 | RAISE EXCEPTION 'p_scores must contain at least one dimension score'; |
| | 343 | END IF; |
| | 344 | |
| | 345 | -- jsonb_array_elements ја разложува низата во по една редица за секој елемент (v) |
| | 346 | IF EXISTS ( |
| | 347 | SELECT 1 |
| | 348 | FROM jsonb_array_elements(p_scores) AS t(v) |
| | 349 | WHERE jsonb_typeof(v) <> 'object' |
| | 350 | OR jsonb_typeof(v->'dimension_id') IS DISTINCT FROM 'number' |
| | 351 | OR jsonb_typeof(v->'score_value') IS DISTINCT FROM 'number' |
| | 352 | ) THEN |
| | 353 | RAISE EXCEPTION |
| | 354 | 'Each element of p_scores must be an object with numeric dimension_id and score_value'; |
| | 355 | END IF; |
| | 356 | |
| | 357 | -- Проектот и клиентскиот корисник мора да постојат |
| | 358 | PERFORM fn_assert_project_exists(p_project_id); |
| | 359 | PERFORM fn_assert_client_user_exists(p_client_user_id); |
| | 360 | |
| | 361 | -- Корисникот мора да припаѓа на клиентот од договорот на проектот; |
| | 362 | -- NULL (непознат корисник или проект) се смета за несовпаѓање |
| | 363 | IF NOT coalesce(fn_is_client_user_of_project(p_client_user_id, p_project_id), false) THEN |
| | 364 | RAISE EXCEPTION |
| | 365 | 'Client user % does not belong to the client associated with project %', |
| | 366 | p_client_user_id, p_project_id; |
| | 367 | END IF; |
| | 368 | |
| | 369 | -- Дека проектот е завршен (Completed или Cancelled) го проверува тригерот |
| | 370 | -- trg_review_finished_project при INSERT во Review подолу |
| | 371 | |
| | 372 | -- Најмногу една рецензија по проект (UNIQUE врз Review.project_id) |
| | 373 | IF EXISTS (SELECT 1 FROM Review WHERE project_id = p_project_id) THEN |
| | 374 | RAISE EXCEPTION 'A review already exists for project %', p_project_id; |
| | 375 | END IF; |
| | 376 | |
| | 377 | -- Ниту една димензија не смее да се повторува во низата |
| | 378 | IF EXISTS ( |
| | 379 | SELECT 1 |
| | 380 | FROM ( |
| | 381 | SELECT (v->>'dimension_id')::int4 AS dim_id |
| | 382 | FROM jsonb_array_elements(p_scores) AS t(v) |
| | 383 | ) dims |
| | 384 | GROUP BY dim_id |
| | 385 | HAVING COUNT(*) > 1 |
| | 386 | ) THEN |
| | 387 | RAISE EXCEPTION 'Duplicate dimension_id found in p_scores'; |
| | 388 | END IF; |
| | 389 | |
| | 390 | -- Секоја димензија мора да постои: се бара првата што не постои |
| | 391 | -- и проверката fn_assert_dimension_exists ја дава пораката за неа |
| | 392 | SELECT (v->>'dimension_id')::int4 |
| | 393 | INTO v_dimension_id |
| | 394 | FROM jsonb_array_elements(p_scores) AS t(v) |
| | 395 | WHERE NOT EXISTS ( |
| | 396 | SELECT 1 FROM Rating_Dimension rd |
| | 397 | WHERE rd.dimension_id = (v->>'dimension_id')::int4 |
| | 398 | ) |
| | 399 | LIMIT 1; |
| | 400 | |
| | 401 | IF FOUND THEN |
| | 402 | PERFORM fn_assert_dimension_exists(v_dimension_id); |
| | 403 | END IF; |
| | 404 | |
| | 405 | -- Секоја оценка мора да биде меѓу 1 и 5; ->> го чита полето како текст, ::int4 го претвора во број |
| | 406 | SELECT (v->>'score_value')::int4 |
| | 407 | INTO v_score_value |
| | 408 | FROM jsonb_array_elements(p_scores) AS t(v) |
| | 409 | WHERE (v->>'score_value')::int4 NOT BETWEEN 1 AND 5 |
| | 410 | LIMIT 1; |
| | 411 | |
| | 412 | IF FOUND THEN |
| | 413 | RAISE EXCEPTION 'Score value must be between 1 and 5 (received: %)', v_score_value; |
| | 414 | END IF; |
| | 415 | |
| | 416 | INSERT INTO Review (project_id, client_user_id, summary_text) |
| | 417 | VALUES (p_project_id, p_client_user_id, p_summary_text) |
| | 418 | RETURNING review_id INTO p_review_id; |
| | 419 | |
| | 420 | -- По една редица за секој елемент од низата, со еден INSERT ... SELECT |
| | 421 | INSERT INTO Review_Score (review_id, dimension_id, score_value) |
| | 422 | SELECT p_review_id, (v->>'dimension_id')::int4, (v->>'score_value')::int4 |
| | 423 | FROM jsonb_array_elements(p_scores) AS t(v); |
| | 424 | END; |
| | 425 | $$; |
| | 426 | |
| | 427 | CALL sp_submit_review(NULL, 1400001, 5001, 'Solid delivery, good communication.', |
| | 428 | '[{"dimension_id": 1, "score_value": 5}, {"dimension_id": 2, "score_value": 4}]'); |
| | 429 | }}} |
| | 430 | |
| | 431 | ---- |
| | 432 | |
| | 433 | === Процедура 8: Поднесување спор (sp_file_dispute) === |
| | 434 | |
| | 435 | Оспорување на рецензија од страна на софтверската агенција: нејзин корисник отвора спор врз рецензијата. Корисникот мора да припаѓа на агенцијата што го испорачала рецензираниот проект и не смее веќе да има отворен спор за истата рецензија. Додека спорот е отворен, рецензијата не може да се објави. |
| | 436 | |
| | 437 | {{{#!sql |
| | 438 | CREATE OR REPLACE PROCEDURE sp_file_dispute( |
| | 439 | OUT p_ticket_id int4, |
| | 440 | IN p_review_id int4, |
| | 441 | IN p_vendor_user_id int4, |
| | 442 | IN p_reason text |
| | 443 | ) |
| | 444 | LANGUAGE plpgsql AS $$ |
| | 445 | DECLARE |
| | 446 | v_project_id int4; |
| | 447 | BEGIN |
| | 448 | -- Причината не смее да биде празна |
| | 449 | PERFORM fn_assert_not_blank(p_reason, 'reason'); |
| | 450 | |
| | 451 | PERFORM fn_assert_review_exists(p_review_id); |
| | 452 | |
| | 453 | -- Корисникот на агенцијата мора да постои |
| | 454 | PERFORM fn_assert_vendor_user_exists(p_vendor_user_id); |
| | 455 | |
| | 456 | -- Истиот корисник не смее да има два отворени спора за иста рецензија |
| | 457 | IF EXISTS ( |
| | 458 | SELECT 1 FROM Dispute_Ticket |
| | 459 | WHERE review_id = p_review_id |
| | 460 | AND vendor_user_id = p_vendor_user_id |
| | 461 | AND is_resolved = false |
| | 462 | ) THEN |
| | 463 | RAISE EXCEPTION |
| | 464 | 'Vendor user % already has an unresolved dispute on review %', |
| | 465 | p_vendor_user_id, p_review_id; |
| | 466 | END IF; |
| | 467 | |
| | 468 | -- Корисникот мора да припаѓа на агенцијата што го испорачала рецензираниот |
| | 469 | -- проект; NULL (непознат корисник или проект) се смета за несовпаѓање |
| | 470 | SELECT project_id |
| | 471 | INTO v_project_id |
| | 472 | FROM Review |
| | 473 | WHERE review_id = p_review_id; |
| | 474 | |
| | 475 | IF NOT coalesce(fn_is_vendor_user_of_project(p_vendor_user_id, v_project_id), false) THEN |
| | 476 | RAISE EXCEPTION |
| | 477 | 'Vendor user % does not belong to the vendor associated with review %', |
| | 478 | p_vendor_user_id, p_review_id; |
| | 479 | END IF; |
| | 480 | |
| | 481 | INSERT INTO Dispute_Ticket (review_id, vendor_user_id, reason) |
| | 482 | VALUES (p_review_id, p_vendor_user_id, p_reason) |
| | 483 | RETURNING ticket_id INTO p_ticket_id; |
| | 484 | END; |
| | 485 | $$; |
| | 486 | |
| | 487 | CALL sp_file_dispute(NULL, 1000001, 55001, 'Оценките не го одразуваат договорениот обем на работа.'); |
| | 488 | }}} |
| | 489 | |
| | 490 | ---- |
| | 491 | |
| | 492 | === Процедура 9: Решавање на спор (sp_resolve_dispute) === |
| | 493 | |
| | 494 | Менаџмент корисник решава отворен спор: се доделува корисникот, се запишува белешката и спорот се означува како решен; непостоечки или веќе решен спор се одбива. Времето на решавање го пополнува тригерот за решавање на спор. |
| | 495 | |
| | 496 | {{{#!sql |
| | 497 | CREATE OR REPLACE PROCEDURE sp_resolve_dispute( |
| | 498 | IN p_ticket_id int4, |
| | 499 | IN p_assigned_management_user_id int4, |
| | 500 | IN p_resolution_note text |
| | 501 | ) |
| | 502 | LANGUAGE plpgsql AS $$ |
| | 503 | BEGIN |
| | 504 | -- Белешката за решавање не смее да биде празна |
| | 505 | PERFORM fn_assert_not_blank(p_resolution_note, 'resolution_note'); |
| | 506 | |
| | 507 | -- Дека доделениот менаџмент корисник постои го проверува тригерот |
| | 508 | -- trg_dispute_ticket_resolve при UPDATE подолу, кој го запишува и |
| | 509 | -- времето на решавање (resolved_at); само отворен спор може да се реши |
| | 510 | UPDATE Dispute_Ticket |
| | 511 | SET assigned_management_user_id = p_assigned_management_user_id, |
| | 512 | resolution_note = p_resolution_note, |
| | 513 | is_resolved = true |
| | 514 | WHERE ticket_id = p_ticket_id |
| | 515 | AND is_resolved = false; |
| | 516 | |
| | 517 | IF NOT FOUND THEN |
| | 518 | RAISE EXCEPTION |
| | 519 | 'Ticket % does not exist or is already resolved', p_ticket_id; |
| | 520 | END IF; |
| | 521 | END; |
| | 522 | $$; |
| | 523 | |
| | 524 | CALL sp_resolve_dispute(143895, 75001, 'Разгледано: оценките остануваат, рецензијата се објавува.'); |
| | 525 | }}} |
| | 526 | |
| | 527 | ---- |
| | 528 | |
| | 529 | === Процедура 10: Објавување рецензија (sp_publish_review) === |
| | 530 | |
| | 531 | Ја објавува рецензијата, по што таа влегува во јавната просечна оценка на софтверската агенција. Непостоечка или веќе објавена рецензија се одбива; рецензија со отворен спор ја одбива тригерот за објавување. |
| | 532 | |
| | 533 | {{{#!sql |
| | 534 | CREATE OR REPLACE PROCEDURE sp_publish_review( |
| | 535 | IN p_review_id int4 |
| | 536 | ) |
| | 537 | LANGUAGE plpgsql AS $$ |
| | 538 | BEGIN |
| | 539 | -- Тригерот trg_review_publish_guard ја одбива промената додека |
| | 540 | -- рецензијата има отворен спор |
| | 541 | UPDATE Review |
| | 542 | SET is_published = true |
| | 543 | WHERE review_id = p_review_id |
| | 544 | AND is_published = false; |
| | 545 | |
| | 546 | IF NOT FOUND THEN |
| | 547 | RAISE EXCEPTION |
| | 548 | 'Review % does not exist or is already published', p_review_id; |
| | 549 | END IF; |
| | 550 | END; |
| | 551 | $$; |
| | 552 | |
| | 553 | CALL sp_publish_review(1000001); |
| | 554 | }}} |
| | 555 | |
| | 556 | ---- |
| | 557 | |
| | 558 | === Процедура 11: Обновување на претплата (sp_renew_vendor_subscription) === |
| | 559 | |
| | 560 | Обновување на претплатата на софтверската агенција: тековниот активен период се затвора најдоцна на денот кога почнува новиот (порано зададен крај се задржува), а новиот период се отвора со избраното ниво. Агенцијата и нивото мора да постојат, а новиот период мора да почне по почетокот на тековниот; договорната цена ја проверува тригерот за цени на претплата. |
| | 561 | |
| | 562 | {{{#!sql |
| | 563 | CREATE OR REPLACE PROCEDURE sp_renew_vendor_subscription( |
| | 564 | OUT p_new_contract_id int4, -- клучот на новиот период; contract_id е примарниот клуч на Vendor_Subscription |
| | 565 | IN p_vendor_id int4, |
| | 566 | IN p_new_tier_id int4, |
| | 567 | IN p_new_start_date date DEFAULT CURRENT_DATE, |
| | 568 | IN p_new_end_date date DEFAULT NULL, |
| | 569 | IN p_new_negotiated_price numeric(10,2) DEFAULT NULL |
| | 570 | ) |
| | 571 | LANGUAGE plpgsql AS $$ |
| | 572 | DECLARE |
| | 573 | v_current_start date; |
| | 574 | BEGIN |
| | 575 | PERFORM fn_assert_vendor_exists(p_vendor_id); |
| | 576 | |
| | 577 | -- Дека нивото постои и дека договорената цена одговара на неговата ценовна |
| | 578 | -- политика го проверува тригерот trg_vendor_subscription_pricing при INSERT подолу |
| | 579 | |
| | 580 | IF p_new_start_date IS NULL THEN |
| | 581 | RAISE EXCEPTION 'start_date is required'; |
| | 582 | END IF; |
| | 583 | |
| | 584 | -- Крајниот датум, ако е даден, мора да биде по почетниот |
| | 585 | IF p_new_end_date IS NOT NULL AND p_new_end_date <= p_new_start_date THEN |
| | 586 | RAISE EXCEPTION |
| | 587 | 'end_date (%) must be after start_date (%)', p_new_end_date, p_new_start_date; |
| | 588 | END IF; |
| | 589 | |
| | 590 | -- Тековниот период се затвора на почетокот на новиот (end_date е |
| | 591 | -- ексклузивен; порано зададен крај се задржува), па новиот период мора да |
| | 592 | -- почне по почетокот на тековниот, инаку затворениот период би бил празен |
| | 593 | SELECT start_date |
| | 594 | INTO v_current_start |
| | 595 | FROM Vendor_Subscription |
| | 596 | WHERE vendor_id = p_vendor_id |
| | 597 | AND is_active = true; |
| | 598 | |
| | 599 | IF v_current_start IS NOT NULL AND v_current_start >= p_new_start_date THEN |
| | 600 | RAISE EXCEPTION |
| | 601 | 'The current period of vendor % began on %; the new period must start after that', |
| | 602 | p_vendor_id, v_current_start; |
| | 603 | END IF; |
| | 604 | |
| | 605 | -- LEAST: отворен период добива крај; период што веќе истекол го задржува својот |
| | 606 | UPDATE Vendor_Subscription |
| | 607 | SET is_active = false, |
| | 608 | end_date = LEAST(end_date, p_new_start_date) |
| | 609 | WHERE vendor_id = p_vendor_id |
| | 610 | AND is_active = true; |
| | 611 | |
| | 612 | INSERT INTO Vendor_Subscription ( |
| | 613 | vendor_id, tier_id, negotiated_price, start_date, end_date |
| | 614 | ) |
| | 615 | VALUES ( |
| | 616 | p_vendor_id, p_new_tier_id, p_new_negotiated_price, |
| | 617 | p_new_start_date, p_new_end_date |
| | 618 | ) |
| | 619 | RETURNING contract_id INTO p_new_contract_id; |
| | 620 | END; |
| | 621 | $$; |
| | 622 | |
| | 623 | CALL sp_renew_vendor_subscription(NULL, 42, 3, CURRENT_DATE, NULL, 1234.50); |
| | 624 | }}} |
| | 625 | |
| | 626 | ---- |
| 67 | | На истиот принцип се имплементирани проверки за `Vendor`, `Client`, `Review`, `Project_Status`, `Subscription_Tier`, корисничките типови и `Rating_Dimension`. |
| 68 | | |
| 69 | | == Процедури == |
| 70 | | |
| 71 | | Процедурите ги имплементираат главните операции кои менуваат податоци во повеќе поврзани табели. |
| 72 | | |
| 73 | | === Регистрација на корисник === |
| 74 | | |
| 75 | | `sp_register_user` прво креира запис во `"User"`, а потоа го додава корисникот во соодветната subtype табела. |
| 76 | | |
| 77 | | {{{#!sql |
| 78 | | INSERT INTO "User" ( |
| 79 | | type, first_name, last_name, email, password_hash |
| 80 | | ) |
| 81 | | VALUES ( |
| 82 | | p_type, p_first_name, p_last_name, |
| 83 | | p_email, p_password_hash |
| 84 | | ) |
| 85 | | RETURNING user_id INTO p_user_id; |
| 86 | | |
| 87 | | IF p_type = 'client' THEN |
| 88 | | INSERT INTO Client_User (user_id, client_id) |
| 89 | | VALUES (p_user_id, p_client_id); |
| 90 | | |
| 91 | | ELSIF p_type = 'vendor' THEN |
| 92 | | INSERT INTO Vendor_User (user_id, vendor_id) |
| 93 | | VALUES (p_user_id, p_vendor_id); |
| 94 | | |
| 95 | | ELSIF p_type = 'management' THEN |
| 96 | | INSERT INTO Management_User (user_id, role_id) |
| 97 | | VALUES (p_user_id, p_role_id); |
| 98 | | END IF; |
| 99 | | }}} |
| 100 | | |
| 101 | | Пред внесувањето се проверуваат типот на корисникот, задолжителните параметри и референците кон останатите табели. |
| 102 | | |
| 103 | | === Креирање договор и проект === |
| 104 | | |
| 105 | | `sp_create_contract_with_project` овозможува со една операција да се креира договор и неговиот почетен проект: |
| 106 | | |
| 107 | | {{{#!sql |
| 108 | | INSERT INTO Client_Vendor_Contract ( |
| 109 | | client_id, vendor_id, contract_title, |
| 110 | | contract_number, start_date, end_date, |
| 111 | | total_value, currency_code, terms_summary |
| 112 | | ) |
| 113 | | VALUES ( |
| 114 | | p_client_id, p_vendor_id, p_contract_title, |
| 115 | | p_contract_number, p_cvc_start_date, p_cvc_end_date, |
| 116 | | p_total_value, p_currency_code, p_terms_summary |
| 117 | | ) |
| 118 | | RETURNING contract_id INTO p_contract_id; |
| 119 | | |
| 120 | | INSERT INTO Project ( |
| 121 | | contract_id, status_id, project_name, |
| 122 | | start_date, end_date, budget |
| 123 | | ) |
| 124 | | VALUES ( |
| 125 | | p_contract_id, p_status_id, p_project_name, |
| 126 | | p_project_start_date, p_project_end_date, p_budget |
| 127 | | ) |
| 128 | | RETURNING project_id INTO p_project_id; |
| 129 | | }}} |
| 130 | | |
| 131 | | Пред креирањето се проверуваат client, vendor и project status, како и буџетот и потребните текстуални полиња. |
| 132 | | |
| 133 | | === Промена на статус на проект === |
| 134 | | |
| 135 | | `sp_update_project_status` го менува тековниот статус и истовремено додава запис во историјата: |
| 136 | | |
| 137 | | {{{#!sql |
| 138 | | UPDATE Project |
| 139 | | SET status_id = p_new_status_id |
| 140 | | WHERE project_id = p_project_id; |
| 141 | | |
| 142 | | INSERT INTO Project_Status_History ( |
| 143 | | project_id, |
| 144 | | status_id, |
| 145 | | vendor_user_id, |
| 146 | | management_user_id, |
| 147 | | comment |
| 148 | | ) |
| 149 | | VALUES ( |
| 150 | | p_project_id, |
| 151 | | p_new_status_id, |
| 152 | | p_vendor_user_id, |
| 153 | | p_management_user_id, |
| 154 | | p_comment |
| 155 | | ); |
| 156 | | }}} |
| 157 | | |
| 158 | | Со ова секоја промена на статусот останува евидентирана. |
| 159 | | |
| 160 | | === Внесување review === |
| 161 | | |
| 162 | | `sp_submit_review` креира review и неговите оценки во една операција. |
| 163 | | |
| 164 | | Оценките се предаваат како JSONB низа: |
| 165 | | |
| 166 | | {{{#!sql |
| 167 | | FOR v_score IN |
| 168 | | SELECT * FROM jsonb_array_elements(p_scores) |
| 169 | | LOOP |
| 170 | | PERFORM fn_assert_dimension_exists( |
| 171 | | (v_score->>'dimension_id')::int4 |
| 172 | | ); |
| 173 | | |
| 174 | | IF ((v_score->>'score_value')::int4 < 1) |
| 175 | | OR ((v_score->>'score_value')::int4 > 5) THEN |
| 176 | | RAISE EXCEPTION |
| 177 | | 'Score value must be between 1 and 5'; |
| 178 | | END IF; |
| 179 | | |
| 180 | | INSERT INTO Review_Score ( |
| 181 | | review_id, |
| 182 | | dimension_id, |
| 183 | | score_value |
| 184 | | ) |
| 185 | | VALUES ( |
| 186 | | p_review_id, |
| 187 | | (v_score->>'dimension_id')::int4, |
| 188 | | (v_score->>'score_value')::int4 |
| 189 | | ); |
| 190 | | END LOOP; |
| 191 | | }}} |
| 192 | | |
| 193 | | Процедурата дополнително проверува дали корисникот припаѓа на клиентот на проектот и дали за проектот веќе постои review. |
| 194 | | |
| 195 | | Останатите процедури се користат за: |
| 196 | | |
| 197 | | * деактивирање на корисник |
| 198 | | * промена на project budget |
| 199 | | * отворање dispute ticket |
| 200 | | * разрешување dispute ticket |
| 201 | | * објавување review |
| 202 | | * обновување vendor subscription |
| | 746 | Останатите десет се `fn_assert_vendor_exists`, `fn_assert_review_exists`, `fn_assert_client_exists`, `fn_assert_status_exists`, `fn_assert_tier_exists`, `fn_assert_role_exists`, `fn_assert_client_user_exists`, `fn_assert_vendor_user_exists`, `fn_assert_management_user_exists` и `fn_assert_dimension_exists`. |
| | 747 | |
| | 748 | ---- |