| 1 | -- =============================================================================
|
|---|
| 2 | -- PROCEDURE 1: Register a New User
|
|---|
| 3 | -- =============================================================================
|
|---|
| 4 | -- p_type is 'client', 'vendor' or 'management' and decides which of
|
|---|
| 5 | -- p_client_id, p_vendor_id and p_role_id is required. p_password_hash is
|
|---|
| 6 | -- stored as given; hashing is the caller's job. New users start inactive.
|
|---|
| 7 | CREATE OR REPLACE PROCEDURE sp_register_user(
|
|---|
| 8 | OUT p_user_id int4,
|
|---|
| 9 | IN p_type text,
|
|---|
| 10 | IN p_first_name text,
|
|---|
| 11 | IN p_last_name text,
|
|---|
| 12 | IN p_email text,
|
|---|
| 13 | IN p_password_hash text,
|
|---|
| 14 | IN p_client_id int4 DEFAULT NULL,
|
|---|
| 15 | IN p_vendor_id int4 DEFAULT NULL,
|
|---|
| 16 | IN p_role_id int4 DEFAULT NULL
|
|---|
| 17 | )
|
|---|
| 18 | LANGUAGE plpgsql AS $$
|
|---|
| 19 | BEGIN
|
|---|
| 20 | CASE p_type
|
|---|
| 21 | WHEN 'client' THEN
|
|---|
| 22 | IF p_client_id IS NULL THEN
|
|---|
| 23 | RAISE EXCEPTION 'p_client_id is required when registering a client user';
|
|---|
| 24 | END IF;
|
|---|
| 25 | PERFORM fn_assert_client_exists(p_client_id);
|
|---|
| 26 | WHEN 'vendor' THEN
|
|---|
| 27 | IF p_vendor_id IS NULL THEN
|
|---|
| 28 | RAISE EXCEPTION 'p_vendor_id is required when registering a vendor user';
|
|---|
| 29 | END IF;
|
|---|
| 30 | PERFORM fn_assert_vendor_exists(p_vendor_id);
|
|---|
| 31 | WHEN 'management' THEN
|
|---|
| 32 | IF p_role_id IS NULL THEN
|
|---|
| 33 | RAISE EXCEPTION 'p_role_id is required when registering a management user';
|
|---|
| 34 | END IF;
|
|---|
| 35 | PERFORM fn_assert_role_exists(p_role_id);
|
|---|
| 36 | ELSE
|
|---|
| 37 | RAISE EXCEPTION 'Invalid user type: %. Must be client, vendor, or management', p_type;
|
|---|
| 38 | END CASE;
|
|---|
| 39 |
|
|---|
| 40 | PERFORM fn_assert_not_blank(p_first_name, 'first_name');
|
|---|
| 41 | PERFORM fn_assert_not_blank(p_last_name, 'last_name');
|
|---|
| 42 | PERFORM fn_assert_not_blank(p_email, 'email');
|
|---|
| 43 | PERFORM fn_assert_not_blank(p_password_hash, 'password_hash');
|
|---|
| 44 |
|
|---|
| 45 | INSERT INTO "User" (type, first_name, last_name, email, password_hash)
|
|---|
| 46 | VALUES (p_type, p_first_name, p_last_name, p_email, p_password_hash)
|
|---|
| 47 | RETURNING user_id INTO p_user_id;
|
|---|
| 48 |
|
|---|
| 49 | CASE p_type
|
|---|
| 50 | WHEN 'client' THEN
|
|---|
| 51 | INSERT INTO Client_User (user_id, client_id)
|
|---|
| 52 | VALUES (p_user_id, p_client_id);
|
|---|
| 53 | WHEN 'vendor' THEN
|
|---|
| 54 | INSERT INTO Vendor_User (user_id, vendor_id)
|
|---|
| 55 | VALUES (p_user_id, p_vendor_id);
|
|---|
| 56 | WHEN 'management' THEN
|
|---|
| 57 | INSERT INTO Management_User (user_id, role_id)
|
|---|
| 58 | VALUES (p_user_id, p_role_id);
|
|---|
| 59 | ELSE
|
|---|
| 60 | RAISE EXCEPTION 'Invalid user type: %. Must be client, vendor, or management', p_type;
|
|---|
| 61 | END CASE;
|
|---|
| 62 | END;
|
|---|
| 63 | $$;
|
|---|
| 64 |
|
|---|
| 65 |
|
|---|
| 66 | -- =============================================================================
|
|---|
| 67 | -- PROCEDURE 2: Activate a User
|
|---|
| 68 | -- =============================================================================
|
|---|
| 69 | CREATE OR REPLACE PROCEDURE sp_activate_user(
|
|---|
| 70 | IN p_user_id int4
|
|---|
| 71 | )
|
|---|
| 72 | LANGUAGE plpgsql AS $$
|
|---|
| 73 | BEGIN
|
|---|
| 74 | UPDATE "User"
|
|---|
| 75 | SET is_active = true
|
|---|
| 76 | WHERE user_id = p_user_id
|
|---|
| 77 | AND is_active = false;
|
|---|
| 78 |
|
|---|
| 79 | IF NOT FOUND THEN
|
|---|
| 80 | RAISE EXCEPTION 'User % does not exist or is already active', p_user_id;
|
|---|
| 81 | END IF;
|
|---|
| 82 | END;
|
|---|
| 83 | $$;
|
|---|
| 84 |
|
|---|
| 85 |
|
|---|
| 86 | -- =============================================================================
|
|---|
| 87 | -- PROCEDURE 3: Deactivate a User
|
|---|
| 88 | -- =============================================================================
|
|---|
| 89 | CREATE OR REPLACE PROCEDURE sp_deactivate_user(
|
|---|
| 90 | IN p_user_id int4
|
|---|
| 91 | )
|
|---|
| 92 | LANGUAGE plpgsql AS $$
|
|---|
| 93 | DECLARE
|
|---|
| 94 | v_is_management bool;
|
|---|
| 95 | BEGIN
|
|---|
| 96 | UPDATE "User"
|
|---|
| 97 | SET is_active = false
|
|---|
| 98 | WHERE user_id = p_user_id
|
|---|
| 99 | AND is_active = true;
|
|---|
| 100 |
|
|---|
| 101 | IF NOT FOUND THEN
|
|---|
| 102 | RAISE EXCEPTION 'User % does not exist or is already inactive', p_user_id;
|
|---|
| 103 | END IF;
|
|---|
| 104 |
|
|---|
| 105 | -- A management user who leaves must not stay assigned to open tickets
|
|---|
| 106 | SELECT EXISTS (
|
|---|
| 107 | SELECT 1 FROM Management_User WHERE user_id = p_user_id
|
|---|
| 108 | ) INTO v_is_management;
|
|---|
| 109 |
|
|---|
| 110 | IF v_is_management THEN
|
|---|
| 111 | UPDATE Dispute_Ticket
|
|---|
| 112 | SET assigned_management_user_id = NULL
|
|---|
| 113 | WHERE assigned_management_user_id = p_user_id
|
|---|
| 114 | AND is_resolved = false;
|
|---|
| 115 | END IF;
|
|---|
| 116 | END;
|
|---|
| 117 | $$;
|
|---|
| 118 |
|
|---|
| 119 |
|
|---|
| 120 | -- =============================================================================
|
|---|
| 121 | -- PROCEDURE 4: Create a Contract with an Initial Project
|
|---|
| 122 | -- =============================================================================
|
|---|
| 123 | -- The contract (p_cvc_*) and its first project (p_project_*) have separate
|
|---|
| 124 | -- start and end dates. p_contract_number is the contract's external
|
|---|
| 125 | -- reference and p_total_value the framework value stated in the contract
|
|---|
| 126 | -- document. p_currency_code is meant to be an ISO 4217 code; only its
|
|---|
| 127 | -- format (three upper-case letters) is checked.
|
|---|
| 128 | CREATE OR REPLACE PROCEDURE sp_create_contract_with_project(
|
|---|
| 129 | OUT p_contract_id int4,
|
|---|
| 130 | OUT p_project_id int4,
|
|---|
| 131 | IN p_client_id int4,
|
|---|
| 132 | IN p_vendor_id int4,
|
|---|
| 133 | IN p_contract_title text,
|
|---|
| 134 | IN p_project_name text,
|
|---|
| 135 | IN p_status_id int4,
|
|---|
| 136 | IN p_budget numeric(10,2),
|
|---|
| 137 | IN p_contract_number text DEFAULT NULL,
|
|---|
| 138 | IN p_cvc_start_date date DEFAULT CURRENT_DATE,
|
|---|
| 139 | IN p_cvc_end_date date DEFAULT NULL,
|
|---|
| 140 | IN p_total_value numeric(10,2) DEFAULT NULL,
|
|---|
| 141 | IN p_currency_code text DEFAULT NULL,
|
|---|
| 142 | IN p_terms_summary text DEFAULT NULL,
|
|---|
| 143 | IN p_project_start_date date DEFAULT CURRENT_DATE,
|
|---|
| 144 | IN p_project_end_date date DEFAULT NULL
|
|---|
| 145 | )
|
|---|
| 146 | LANGUAGE plpgsql AS $$
|
|---|
| 147 | BEGIN
|
|---|
| 148 | PERFORM fn_assert_not_blank(p_contract_title, 'contract_title');
|
|---|
| 149 | PERFORM fn_assert_not_blank(p_project_name, 'project_name');
|
|---|
| 150 |
|
|---|
| 151 | PERFORM fn_assert_positive(p_budget, 'Budget');
|
|---|
| 152 |
|
|---|
| 153 | PERFORM fn_assert_client_exists(p_client_id);
|
|---|
| 154 | PERFORM fn_assert_vendor_exists(p_vendor_id);
|
|---|
| 155 | PERFORM fn_assert_status_exists(p_status_id);
|
|---|
| 156 |
|
|---|
| 157 | INSERT INTO Client_Vendor_Contract (
|
|---|
| 158 | client_id, vendor_id, contract_title, contract_number,
|
|---|
| 159 | start_date, end_date, total_value, currency_code, terms_summary
|
|---|
| 160 | )
|
|---|
| 161 | VALUES (
|
|---|
| 162 | p_client_id, p_vendor_id, p_contract_title, p_contract_number,
|
|---|
| 163 | p_cvc_start_date, p_cvc_end_date, p_total_value, p_currency_code, p_terms_summary
|
|---|
| 164 | )
|
|---|
| 165 | RETURNING contract_id INTO p_contract_id;
|
|---|
| 166 |
|
|---|
| 167 | INSERT INTO Project (
|
|---|
| 168 | contract_id, status_id, project_name, start_date, end_date, budget
|
|---|
| 169 | )
|
|---|
| 170 | VALUES (
|
|---|
| 171 | p_contract_id, p_status_id, p_project_name,
|
|---|
| 172 | p_project_start_date, p_project_end_date, p_budget
|
|---|
| 173 | )
|
|---|
| 174 | RETURNING project_id INTO p_project_id;
|
|---|
| 175 | END;
|
|---|
| 176 | $$;
|
|---|
| 177 |
|
|---|
| 178 |
|
|---|
| 179 | -- =============================================================================
|
|---|
| 180 | -- PROCEDURE 5: Update Project Status
|
|---|
| 181 | -- =============================================================================
|
|---|
| 182 | -- The change is recorded in Project_Status_History with exactly one actor:
|
|---|
| 183 | -- p_vendor_user_id or p_management_user_id. An unchanged status returns
|
|---|
| 184 | -- without adding history.
|
|---|
| 185 | CREATE OR REPLACE PROCEDURE sp_update_project_status(
|
|---|
| 186 | IN p_project_id int4,
|
|---|
| 187 | IN p_new_status_id int4,
|
|---|
| 188 | IN p_vendor_user_id int4 DEFAULT NULL,
|
|---|
| 189 | IN p_management_user_id int4 DEFAULT NULL,
|
|---|
| 190 | IN p_comment text DEFAULT NULL
|
|---|
| 191 | )
|
|---|
| 192 | LANGUAGE plpgsql AS $$
|
|---|
| 193 | DECLARE
|
|---|
| 194 | v_current_status_id int4;
|
|---|
| 195 | BEGIN
|
|---|
| 196 | -- Exactly one actor: a vendor user or a management user, never both or neither
|
|---|
| 197 | IF (p_vendor_user_id IS NULL) = (p_management_user_id IS NULL) THEN
|
|---|
| 198 | RAISE EXCEPTION
|
|---|
| 199 | 'Provide exactly one actor: p_vendor_user_id or p_management_user_id';
|
|---|
| 200 | END IF;
|
|---|
| 201 |
|
|---|
| 202 | PERFORM fn_assert_status_exists(p_new_status_id);
|
|---|
| 203 |
|
|---|
| 204 | SELECT status_id INTO v_current_status_id
|
|---|
| 205 | FROM Project
|
|---|
| 206 | WHERE project_id = p_project_id;
|
|---|
| 207 |
|
|---|
| 208 | IF v_current_status_id = p_new_status_id THEN
|
|---|
| 209 | RAISE NOTICE 'Project % is already at status % – no update performed',
|
|---|
| 210 | p_project_id, p_new_status_id;
|
|---|
| 211 | RETURN;
|
|---|
| 212 | END IF;
|
|---|
| 213 |
|
|---|
| 214 | -- A reviewed project stays finished: this procedure never returns it to an
|
|---|
| 215 | -- open status
|
|---|
| 216 | IF EXISTS (SELECT 1 FROM Review WHERE project_id = p_project_id)
|
|---|
| 217 | AND (SELECT status_name FROM Project_Status WHERE status_id = p_new_status_id)
|
|---|
| 218 | NOT IN ('Completed', 'Cancelled') THEN
|
|---|
| 219 | RAISE EXCEPTION 'Project % has a review and cannot return to an open status', p_project_id;
|
|---|
| 220 | END IF;
|
|---|
| 221 |
|
|---|
| 222 | UPDATE Project
|
|---|
| 223 | SET status_id = p_new_status_id
|
|---|
| 224 | WHERE project_id = p_project_id;
|
|---|
| 225 |
|
|---|
| 226 | -- The project, the actor and the actor's vendor are checked by trg_status_history_actor
|
|---|
| 227 | INSERT INTO Project_Status_History (
|
|---|
| 228 | project_id, status_id, vendor_user_id, management_user_id, comment
|
|---|
| 229 | )
|
|---|
| 230 | VALUES (
|
|---|
| 231 | p_project_id, p_new_status_id, p_vendor_user_id, p_management_user_id, p_comment
|
|---|
| 232 | );
|
|---|
| 233 | END;
|
|---|
| 234 | $$;
|
|---|
| 235 |
|
|---|
| 236 |
|
|---|
| 237 | -- =============================================================================
|
|---|
| 238 | -- PROCEDURE 6: Update Project Budget
|
|---|
| 239 | -- =============================================================================
|
|---|
| 240 | CREATE OR REPLACE PROCEDURE sp_update_project_budget(
|
|---|
| 241 | IN p_project_id int4,
|
|---|
| 242 | IN p_new_budget numeric(10,2)
|
|---|
| 243 | )
|
|---|
| 244 | LANGUAGE plpgsql AS $$
|
|---|
| 245 | BEGIN
|
|---|
| 246 | PERFORM fn_assert_positive(p_new_budget, 'Budget');
|
|---|
| 247 |
|
|---|
| 248 | -- trg_project_budget_audit records the old and new budget in
|
|---|
| 249 | -- Project_Budget_Audit when the value changes; an unchanged value adds no
|
|---|
| 250 | -- audit row
|
|---|
| 251 | UPDATE Project
|
|---|
| 252 | SET budget = p_new_budget
|
|---|
| 253 | WHERE project_id = p_project_id;
|
|---|
| 254 |
|
|---|
| 255 | IF NOT FOUND THEN
|
|---|
| 256 | PERFORM fn_assert_project_exists(p_project_id);
|
|---|
| 257 | END IF;
|
|---|
| 258 | END;
|
|---|
| 259 | $$;
|
|---|
| 260 |
|
|---|
| 261 |
|
|---|
| 262 | -- =============================================================================
|
|---|
| 263 | -- PROCEDURE 7: Submit a Review with Scores
|
|---|
| 264 | -- =============================================================================
|
|---|
| 265 | -- p_scores is a non-empty JSON array of {dimension_id, score_value} objects
|
|---|
| 266 | -- with distinct, existing dimensions and scores from 1 to 5. A project gets
|
|---|
| 267 | -- at most one review, written by a user of its client.
|
|---|
| 268 | CREATE OR REPLACE PROCEDURE sp_submit_review(
|
|---|
| 269 | OUT p_review_id int4,
|
|---|
| 270 | IN p_project_id int4,
|
|---|
| 271 | IN p_client_user_id int4,
|
|---|
| 272 | IN p_summary_text text,
|
|---|
| 273 | IN p_scores jsonb
|
|---|
| 274 | )
|
|---|
| 275 | LANGUAGE plpgsql AS $$
|
|---|
| 276 | DECLARE
|
|---|
| 277 | v_dimension_id int4;
|
|---|
| 278 | v_score_value int4;
|
|---|
| 279 | BEGIN
|
|---|
| 280 | PERFORM fn_assert_not_blank(p_summary_text, 'summary_text');
|
|---|
| 281 |
|
|---|
| 282 | -- Shape of p_scores: a non-empty array of objects with a JSON number as
|
|---|
| 283 | -- dimension_id and score_value; the casts to int4 come later
|
|---|
| 284 | IF p_scores IS NULL OR jsonb_typeof(p_scores) <> 'array' THEN
|
|---|
| 285 | RAISE EXCEPTION 'p_scores must be a JSON array of {dimension_id, score_value} objects';
|
|---|
| 286 | END IF;
|
|---|
| 287 |
|
|---|
| 288 | IF jsonb_array_length(p_scores) = 0 THEN
|
|---|
| 289 | RAISE EXCEPTION 'p_scores must contain at least one dimension score';
|
|---|
| 290 | END IF;
|
|---|
| 291 |
|
|---|
| 292 | IF EXISTS (
|
|---|
| 293 | SELECT 1
|
|---|
| 294 | FROM jsonb_array_elements(p_scores) AS t(v)
|
|---|
| 295 | WHERE jsonb_typeof(v) <> 'object'
|
|---|
| 296 | OR jsonb_typeof(v->'dimension_id') IS DISTINCT FROM 'number'
|
|---|
| 297 | OR jsonb_typeof(v->'score_value') IS DISTINCT FROM 'number'
|
|---|
| 298 | ) THEN
|
|---|
| 299 | RAISE EXCEPTION
|
|---|
| 300 | 'Each element of p_scores must be an object with numeric dimension_id and score_value';
|
|---|
| 301 | END IF;
|
|---|
| 302 |
|
|---|
| 303 | PERFORM fn_assert_project_exists(p_project_id);
|
|---|
| 304 | PERFORM fn_assert_client_user_exists(p_client_user_id);
|
|---|
| 305 |
|
|---|
| 306 | -- The client user must belong to the client of the project; NULL is a
|
|---|
| 307 | -- mismatch
|
|---|
| 308 | IF NOT coalesce(fn_is_client_user_of_project(p_client_user_id, p_project_id), false) THEN
|
|---|
| 309 | RAISE EXCEPTION
|
|---|
| 310 | 'Client user % does not belong to the client associated with project %',
|
|---|
| 311 | p_client_user_id, p_project_id;
|
|---|
| 312 | END IF;
|
|---|
| 313 |
|
|---|
| 314 | IF EXISTS (SELECT 1 FROM Review WHERE project_id = p_project_id) THEN
|
|---|
| 315 | RAISE EXCEPTION 'A review already exists for project %', p_project_id;
|
|---|
| 316 | END IF;
|
|---|
| 317 |
|
|---|
| 318 | -- Duplicate dimensions
|
|---|
| 319 | IF EXISTS (
|
|---|
| 320 | SELECT 1
|
|---|
| 321 | FROM (
|
|---|
| 322 | SELECT (v->>'dimension_id')::int4 AS dim_id
|
|---|
| 323 | FROM jsonb_array_elements(p_scores) AS t(v)
|
|---|
| 324 | ) dims
|
|---|
| 325 | GROUP BY dim_id
|
|---|
| 326 | HAVING COUNT(*) > 1
|
|---|
| 327 | ) THEN
|
|---|
| 328 | RAISE EXCEPTION 'Duplicate dimension_id found in p_scores';
|
|---|
| 329 | END IF;
|
|---|
| 330 |
|
|---|
| 331 | -- Every dimension must exist
|
|---|
| 332 | SELECT (v->>'dimension_id')::int4
|
|---|
| 333 | INTO v_dimension_id
|
|---|
| 334 | FROM jsonb_array_elements(p_scores) AS t(v)
|
|---|
| 335 | WHERE NOT EXISTS (
|
|---|
| 336 | SELECT 1 FROM Rating_Dimension rd
|
|---|
| 337 | WHERE rd.dimension_id = (v->>'dimension_id')::int4
|
|---|
| 338 | )
|
|---|
| 339 | LIMIT 1;
|
|---|
| 340 |
|
|---|
| 341 | IF FOUND THEN
|
|---|
| 342 | PERFORM fn_assert_dimension_exists(v_dimension_id);
|
|---|
| 343 | END IF;
|
|---|
| 344 |
|
|---|
| 345 | -- Every score must be within 1..5
|
|---|
| 346 | SELECT (v->>'score_value')::int4
|
|---|
| 347 | INTO v_score_value
|
|---|
| 348 | FROM jsonb_array_elements(p_scores) AS t(v)
|
|---|
| 349 | WHERE (v->>'score_value')::int4 NOT BETWEEN 1 AND 5
|
|---|
| 350 | LIMIT 1;
|
|---|
| 351 |
|
|---|
| 352 | IF FOUND THEN
|
|---|
| 353 | RAISE EXCEPTION 'Score value must be between 1 and 5 (received: %)', v_score_value;
|
|---|
| 354 | END IF;
|
|---|
| 355 |
|
|---|
| 356 | -- That the project is finished (Completed or Cancelled) is checked by trg_review_finished_project
|
|---|
| 357 | INSERT INTO Review (project_id, client_user_id, summary_text)
|
|---|
| 358 | VALUES (p_project_id, p_client_user_id, p_summary_text)
|
|---|
| 359 | RETURNING review_id INTO p_review_id;
|
|---|
| 360 |
|
|---|
| 361 | INSERT INTO Review_Score (review_id, dimension_id, score_value)
|
|---|
| 362 | SELECT p_review_id, (v->>'dimension_id')::int4, (v->>'score_value')::int4
|
|---|
| 363 | FROM jsonb_array_elements(p_scores) AS t(v);
|
|---|
| 364 | END;
|
|---|
| 365 | $$;
|
|---|
| 366 |
|
|---|
| 367 |
|
|---|
| 368 | -- =============================================================================
|
|---|
| 369 | -- PROCEDURE 8: File a Dispute Ticket
|
|---|
| 370 | -- =============================================================================
|
|---|
| 371 | -- The dispute is filed by a user of the vendor that delivered the reviewed
|
|---|
| 372 | -- project.
|
|---|
| 373 | CREATE OR REPLACE PROCEDURE sp_file_dispute(
|
|---|
| 374 | OUT p_ticket_id int4,
|
|---|
| 375 | IN p_review_id int4,
|
|---|
| 376 | IN p_vendor_user_id int4,
|
|---|
| 377 | IN p_reason text
|
|---|
| 378 | )
|
|---|
| 379 | LANGUAGE plpgsql AS $$
|
|---|
| 380 | DECLARE
|
|---|
| 381 | v_project_id int4;
|
|---|
| 382 | BEGIN
|
|---|
| 383 | PERFORM fn_assert_not_blank(p_reason, 'reason');
|
|---|
| 384 |
|
|---|
| 385 | PERFORM fn_assert_review_exists(p_review_id);
|
|---|
| 386 | PERFORM fn_assert_vendor_user_exists(p_vendor_user_id);
|
|---|
| 387 |
|
|---|
| 388 | -- At most one unresolved dispute per vendor user and review
|
|---|
| 389 | IF EXISTS (
|
|---|
| 390 | SELECT 1 FROM Dispute_Ticket
|
|---|
| 391 | WHERE review_id = p_review_id
|
|---|
| 392 | AND vendor_user_id = p_vendor_user_id
|
|---|
| 393 | AND is_resolved = false
|
|---|
| 394 | ) THEN
|
|---|
| 395 | RAISE EXCEPTION
|
|---|
| 396 | 'Vendor user % already has an unresolved dispute on review %',
|
|---|
| 397 | p_vendor_user_id, p_review_id;
|
|---|
| 398 | END IF;
|
|---|
| 399 |
|
|---|
| 400 | -- The vendor user must belong to the vendor of the reviewed project; NULL
|
|---|
| 401 | -- is a mismatch
|
|---|
| 402 | SELECT project_id
|
|---|
| 403 | INTO v_project_id
|
|---|
| 404 | FROM Review
|
|---|
| 405 | WHERE review_id = p_review_id;
|
|---|
| 406 |
|
|---|
| 407 | IF NOT coalesce(fn_is_vendor_user_of_project(p_vendor_user_id, v_project_id), false) THEN
|
|---|
| 408 | RAISE EXCEPTION
|
|---|
| 409 | 'Vendor user % does not belong to the vendor associated with review %',
|
|---|
| 410 | p_vendor_user_id, p_review_id;
|
|---|
| 411 | END IF;
|
|---|
| 412 |
|
|---|
| 413 | INSERT INTO Dispute_Ticket (review_id, vendor_user_id, reason)
|
|---|
| 414 | VALUES (p_review_id, p_vendor_user_id, p_reason)
|
|---|
| 415 | RETURNING ticket_id INTO p_ticket_id;
|
|---|
| 416 | END;
|
|---|
| 417 | $$;
|
|---|
| 418 |
|
|---|
| 419 |
|
|---|
| 420 | -- =============================================================================
|
|---|
| 421 | -- PROCEDURE 9: Resolve a Dispute Ticket
|
|---|
| 422 | -- =============================================================================
|
|---|
| 423 | CREATE OR REPLACE PROCEDURE sp_resolve_dispute(
|
|---|
| 424 | IN p_ticket_id int4,
|
|---|
| 425 | IN p_assigned_management_user_id int4,
|
|---|
| 426 | IN p_resolution_note text
|
|---|
| 427 | )
|
|---|
| 428 | LANGUAGE plpgsql AS $$
|
|---|
| 429 | BEGIN
|
|---|
| 430 | PERFORM fn_assert_not_blank(p_resolution_note, 'resolution_note');
|
|---|
| 431 |
|
|---|
| 432 | -- The assigned management user is checked by trg_dispute_ticket_resolve
|
|---|
| 433 | -- on the UPDATE below, which also fills resolved_at
|
|---|
| 434 | UPDATE Dispute_Ticket
|
|---|
| 435 | SET assigned_management_user_id = p_assigned_management_user_id,
|
|---|
| 436 | resolution_note = p_resolution_note,
|
|---|
| 437 | is_resolved = true
|
|---|
| 438 | WHERE ticket_id = p_ticket_id
|
|---|
| 439 | AND is_resolved = false;
|
|---|
| 440 |
|
|---|
| 441 | IF NOT FOUND THEN
|
|---|
| 442 | RAISE EXCEPTION
|
|---|
| 443 | 'Ticket % does not exist or is already resolved', p_ticket_id;
|
|---|
| 444 | END IF;
|
|---|
| 445 | END;
|
|---|
| 446 | $$;
|
|---|
| 447 |
|
|---|
| 448 |
|
|---|
| 449 | -- =============================================================================
|
|---|
| 450 | -- PROCEDURE 10: Publish a Review
|
|---|
| 451 | -- =============================================================================
|
|---|
| 452 | CREATE OR REPLACE PROCEDURE sp_publish_review(
|
|---|
| 453 | IN p_review_id int4
|
|---|
| 454 | )
|
|---|
| 455 | LANGUAGE plpgsql AS $$
|
|---|
| 456 | BEGIN
|
|---|
| 457 | -- trg_review_publish_guard refuses the UPDATE while the review has an
|
|---|
| 458 | -- unresolved dispute
|
|---|
| 459 | UPDATE Review
|
|---|
| 460 | SET is_published = true
|
|---|
| 461 | WHERE review_id = p_review_id
|
|---|
| 462 | AND is_published = false;
|
|---|
| 463 |
|
|---|
| 464 | IF NOT FOUND THEN
|
|---|
| 465 | RAISE EXCEPTION
|
|---|
| 466 | 'Review % does not exist or is already published', p_review_id;
|
|---|
| 467 | END IF;
|
|---|
| 468 | END;
|
|---|
| 469 | $$;
|
|---|
| 470 |
|
|---|
| 471 |
|
|---|
| 472 | -- =============================================================================
|
|---|
| 473 | -- PROCEDURE 11: Renew a Vendor Subscription
|
|---|
| 474 | -- =============================================================================
|
|---|
| 475 | -- p_new_negotiated_price is required for tiers with custom pricing and must
|
|---|
| 476 | -- be NULL for fixed-price tiers. p_new_contract_id is the Vendor_Subscription
|
|---|
| 477 | -- key of the new period.
|
|---|
| 478 | CREATE OR REPLACE PROCEDURE sp_renew_vendor_subscription(
|
|---|
| 479 | OUT p_new_contract_id int4,
|
|---|
| 480 | IN p_vendor_id int4,
|
|---|
| 481 | IN p_new_tier_id int4,
|
|---|
| 482 | IN p_new_start_date date DEFAULT CURRENT_DATE,
|
|---|
| 483 | IN p_new_end_date date DEFAULT NULL,
|
|---|
| 484 | IN p_new_negotiated_price numeric(10,2) DEFAULT NULL
|
|---|
| 485 | )
|
|---|
| 486 | LANGUAGE plpgsql AS $$
|
|---|
| 487 | DECLARE
|
|---|
| 488 | v_current_start date;
|
|---|
| 489 | BEGIN
|
|---|
| 490 | PERFORM fn_assert_vendor_exists(p_vendor_id);
|
|---|
| 491 |
|
|---|
| 492 | IF p_new_start_date IS NULL THEN
|
|---|
| 493 | RAISE EXCEPTION 'start_date is required';
|
|---|
| 494 | END IF;
|
|---|
| 495 |
|
|---|
| 496 | IF p_new_end_date IS NOT NULL AND p_new_end_date <= p_new_start_date THEN
|
|---|
| 497 | RAISE EXCEPTION
|
|---|
| 498 | 'end_date (%) must be after start_date (%)', p_new_end_date, p_new_start_date;
|
|---|
| 499 | END IF;
|
|---|
| 500 |
|
|---|
| 501 | -- The current period is closed at the renewal start (end_date is
|
|---|
| 502 | -- exclusive; an earlier end date is kept), so the new period must start
|
|---|
| 503 | -- after the current one began, or closing it would leave an empty period
|
|---|
| 504 | SELECT start_date
|
|---|
| 505 | INTO v_current_start
|
|---|
| 506 | FROM Vendor_Subscription
|
|---|
| 507 | WHERE vendor_id = p_vendor_id
|
|---|
| 508 | AND is_active = true;
|
|---|
| 509 |
|
|---|
| 510 | IF v_current_start IS NOT NULL AND v_current_start >= p_new_start_date THEN
|
|---|
| 511 | RAISE EXCEPTION
|
|---|
| 512 | 'The current period of vendor % began on %; the new period must start after that',
|
|---|
| 513 | p_vendor_id, v_current_start;
|
|---|
| 514 | END IF;
|
|---|
| 515 |
|
|---|
| 516 | UPDATE Vendor_Subscription
|
|---|
| 517 | SET is_active = false,
|
|---|
| 518 | end_date = LEAST(end_date, p_new_start_date)
|
|---|
| 519 | WHERE vendor_id = p_vendor_id
|
|---|
| 520 | AND is_active = true;
|
|---|
| 521 |
|
|---|
| 522 | -- The tier and the negotiated price are checked by trg_vendor_subscription_pricing
|
|---|
| 523 | INSERT INTO Vendor_Subscription (
|
|---|
| 524 | vendor_id, tier_id, negotiated_price, start_date, end_date
|
|---|
| 525 | )
|
|---|
| 526 | VALUES (
|
|---|
| 527 | p_vendor_id, p_new_tier_id, p_new_negotiated_price,
|
|---|
| 528 | p_new_start_date, p_new_end_date
|
|---|
| 529 | )
|
|---|
| 530 | RETURNING contract_id INTO p_new_contract_id;
|
|---|
| 531 | END;
|
|---|
| 532 | $$;
|
|---|