FunkciiTrigeri: Funkcii,proceduri i indeksi.sql

File Funkcii,proceduri i indeksi.sql, 16.9 KB (added by 231004, 4 days ago)
Line 
1-- =========================================================
2-- SETUP FIXES / INDEXES
3-- =========================================================
4
5SELECT setval(
6 'car_dealership.configurationpackage_id_seq',
7 COALESCE((SELECT MAX(id) FROM car_dealership.ConfigurationPackage), 0) + 1,
8 false
9);
10
11CREATE INDEX IF NOT EXISTS idx_testdrive_vin_date_times
12ON car_dealership.TestDrive (vin, date, time_start, time_end);
13
14
15-- =========================================================
16-- CONSTRAINTS
17-- =========================================================
18
19DO $$
20BEGIN
21 IF NOT EXISTS (
22 SELECT 1
23 FROM pg_constraint
24 WHERE conname = 'chk_payment_amount_positive'
25 ) THEN
26 ALTER TABLE car_dealership.Payment
27 ADD CONSTRAINT chk_payment_amount_positive
28 CHECK (amount > 0);
29 END IF;
30END;
31$$;
32
33-- Prevent duplicate contracts per order and per vehicle.
34-- These back the existence checks in complete_sale_from_order and
35-- also guard against concurrent calls slipping through the IF EXISTS checks.
36DO $$
37BEGIN
38 IF NOT EXISTS (
39 SELECT 1 FROM pg_constraint WHERE conname = 'uq_contract_order_id'
40 ) THEN
41 ALTER TABLE car_dealership.Contract
42 ADD CONSTRAINT uq_contract_order_id UNIQUE (order_id);
43 END IF;
44
45 IF NOT EXISTS (
46 SELECT 1 FROM pg_constraint WHERE conname = 'uq_contract_vin'
47 ) THEN
48 ALTER TABLE car_dealership.Contract
49 ADD CONSTRAINT uq_contract_vin UNIQUE (vin);
50 END IF;
51END;
52$$;
53
54
55-- =========================================================
56-- FUNCTION 1: Calculate configuration total
57-- =========================================================
58
59CREATE OR REPLACE FUNCTION car_dealership.calculate_configuration_total(
60 p_configuration_id INT
61)
62RETURNS NUMERIC(10, 2)
63LANGUAGE sql
64AS $$
65 SELECT v.price + COALESCE(SUM(ep.price), 0)
66 FROM car_dealership.configuration c
67 JOIN car_dealership.vehicle v
68 ON v.vin = c.vin
69 LEFT JOIN car_dealership.configurationpackage cp
70 ON cp.configuration_id = c.id
71 LEFT JOIN car_dealership.equipmentpackage ep
72 ON ep.id = cp.package_id
73 WHERE c.id = p_configuration_id
74 GROUP BY v.price;
75$$;
76
77
78-- =========================================================
79-- FUNCTION 2: Check vehicle availability
80-- =========================================================
81
82CREATE OR REPLACE FUNCTION car_dealership.is_vehicle_available(
83 p_vin VARCHAR
84)
85RETURNS BOOLEAN
86LANGUAGE sql
87AS $$
88 SELECT EXISTS (
89 SELECT 1
90 FROM car_dealership.vehicle v
91 JOIN car_dealership.status s
92 ON s.id = v.status_id
93 WHERE v.vin = p_vin
94 AND s.status = 'In Stock'
95 );
96$$;
97
98
99-- =========================================================
100-- FUNCTION 3: Get customer total spent
101-- =========================================================
102
103CREATE OR REPLACE FUNCTION car_dealership.get_customer_total_spent(
104 p_customer_id INT
105)
106RETURNS NUMERIC(10, 2)
107LANGUAGE sql
108AS $$
109 SELECT COALESCE(SUM(p.amount), 0)
110 FROM car_dealership.sale s
111 JOIN car_dealership.payment p
112 ON p.sale_id = s.id
113 WHERE s.customer_id = p_customer_id;
114$$;
115
116
117-- =========================================================
118-- PROCEDURE 1: Create customer configuration
119-- =========================================================
120
121CREATE OR REPLACE PROCEDURE car_dealership.create_customer_configuration(
122 p_customer_id INT,
123 p_vin VARCHAR,
124 p_description VARCHAR
125)
126LANGUAGE plpgsql
127AS $$
128DECLARE
129 v_vehicle_price NUMERIC(10, 2);
130 v_configuration_id INT;
131BEGIN
132 IF NOT EXISTS (
133 SELECT 1 FROM car_dealership.Customer WHERE id = p_customer_id
134 ) THEN
135 RAISE EXCEPTION 'Customer with id % does not exist.', p_customer_id;
136 END IF;
137
138 -- is_vehicle_available returns false for non-existent VINs as well,
139 -- so a single check covers both the existence and availability cases.
140 IF NOT car_dealership.is_vehicle_available(p_vin) THEN
141 RAISE EXCEPTION 'Vehicle % does not exist or is not available for configuration.', p_vin;
142 END IF;
143
144 SELECT price INTO v_vehicle_price
145 FROM car_dealership.Vehicle
146 WHERE vin = p_vin;
147
148 INSERT INTO car_dealership.Configuration(vin, description, total_price, created_at, customer_id)
149 VALUES (p_vin, p_description, v_vehicle_price, CURRENT_DATE, p_customer_id)
150 RETURNING id INTO v_configuration_id;
151
152 RAISE NOTICE 'Configuration % created for customer %.', v_configuration_id, p_customer_id;
153END;
154$$;
155
156
157-- =========================================================
158-- PROCEDURE 2: Add package to configuration
159-- =========================================================
160
161CREATE OR REPLACE PROCEDURE car_dealership.add_package_to_configuration(
162 p_configuration_id INT,
163 p_package_id INT
164)
165LANGUAGE plpgsql
166AS $$
167BEGIN
168 IF NOT EXISTS (
169 SELECT 1 FROM car_dealership.Configuration WHERE id = p_configuration_id
170 ) THEN
171 RAISE EXCEPTION 'Configuration with id % does not exist.', p_configuration_id;
172 END IF;
173
174 IF NOT EXISTS (
175 SELECT 1 FROM car_dealership.EquipmentPackage WHERE id = p_package_id
176 ) THEN
177 RAISE EXCEPTION 'Equipment package with id % does not exist.', p_package_id;
178 END IF;
179
180 IF EXISTS (
181 SELECT 1
182 FROM car_dealership.ConfigurationPackage
183 WHERE configuration_id = p_configuration_id
184 AND package_id = p_package_id
185 ) THEN
186 RAISE EXCEPTION 'Package % already exists in configuration %.', p_package_id, p_configuration_id;
187 END IF;
188
189 INSERT INTO car_dealership.ConfigurationPackage(configuration_id, package_id)
190 VALUES (p_configuration_id, p_package_id);
191
192 RAISE NOTICE 'Package % added to configuration %.', p_package_id, p_configuration_id;
193END;
194$$;
195
196
197-- =========================================================
198-- PROCEDURE 3: Create order from configuration
199--
200-- NOTE: The configuration ownership check (customer_id = p_customer_id
201-- AND vin = p_vin) is intentional. A configuration is created by a
202-- specific customer for a specific vehicle; the order must match both.
203-- If the business requirement changes so that any customer can reuse an
204-- existing configuration against a different vehicle, remove the vin and
205-- customer_id predicates from the check below.
206-- =========================================================
207
208CREATE OR REPLACE PROCEDURE car_dealership.create_order_from_configuration(
209 p_customer_id INT,
210 p_configuration_id INT,
211 p_employee_id INT,
212 p_vin VARCHAR
213)
214LANGUAGE plpgsql
215AS $$
216DECLARE
217 v_reserved_status_id INT;
218 v_order_id INT;
219BEGIN
220 IF NOT EXISTS (
221 SELECT 1 FROM car_dealership.Customer WHERE id = p_customer_id
222 ) THEN
223 RAISE EXCEPTION 'Customer with id % does not exist.', p_customer_id;
224 END IF;
225
226 IF NOT EXISTS (
227 SELECT 1 FROM car_dealership.Employee WHERE id = p_employee_id
228 ) THEN
229 RAISE EXCEPTION 'Employee with id % does not exist.', p_employee_id;
230 END IF;
231
232 IF NOT EXISTS (
233 SELECT 1
234 FROM car_dealership.Configuration
235 WHERE id = p_configuration_id
236 AND customer_id = p_customer_id
237 AND vin = p_vin
238 ) THEN
239 RAISE EXCEPTION 'Configuration % does not belong to customer % or does not match vehicle %.',
240 p_configuration_id, p_customer_id, p_vin;
241 END IF;
242
243 -- is_vehicle_available covers both existence and In Stock status.
244 IF NOT car_dealership.is_vehicle_available(p_vin) THEN
245 RAISE EXCEPTION 'Vehicle % does not exist or is not available for ordering.', p_vin;
246 END IF;
247
248 SELECT id INTO v_reserved_status_id
249 FROM car_dealership.Status
250 WHERE status = 'Reserved';
251
252 IF v_reserved_status_id IS NULL THEN
253 RAISE EXCEPTION 'Status ''Reserved'' does not exist in the Status table.';
254 END IF;
255
256 INSERT INTO car_dealership."Order"(customer_id, configuration_id, employee_id, date, status)
257 VALUES (p_customer_id, p_configuration_id, p_employee_id, CURRENT_DATE, 'Pending')
258 RETURNING id INTO v_order_id;
259
260 UPDATE car_dealership.Vehicle
261 SET status_id = v_reserved_status_id
262 WHERE vin = p_vin;
263
264 RAISE NOTICE 'Order % created for customer %, vehicle % reserved.', v_order_id, p_customer_id, p_vin;
265END;
266$$;
267
268
269-- =========================================================
270-- PROCEDURE 4: Complete sale from order
271--
272-- NOTE: Business logic (marking the order Completed and the vehicle Sold)
273-- lives here rather than in a trigger so that the execution path is explicit
274-- and auditable. Sale rows must only ever be inserted through this procedure;
275-- direct inserts will leave orders and vehicles in an inconsistent state.
276-- The UNIQUE constraints on Contract(order_id) and Contract(vin) added above
277-- provide a hard concurrency guard on top of the IF EXISTS soft checks.
278-- =========================================================
279
280CREATE OR REPLACE PROCEDURE car_dealership.complete_sale_from_order(
281 p_order_id INT,
282 p_contract_type VARCHAR,
283 p_payment_type VARCHAR,
284 p_notes VARCHAR DEFAULT NULL
285)
286LANGUAGE plpgsql
287AS $$
288DECLARE
289 v_customer_id INT;
290 v_employee_id INT;
291 v_configuration_id INT;
292 v_order_status VARCHAR;
293 v_vin VARCHAR(17);
294 v_total_price NUMERIC(10, 2);
295 v_contract_id INT;
296 v_sale_id INT;
297 v_sold_status_id INT;
298BEGIN
299 IF p_contract_type NOT IN ('Standard', 'Finance', 'Fleet') THEN
300 RAISE EXCEPTION 'Invalid contract type: %', p_contract_type;
301 END IF;
302
303 IF p_payment_type NOT IN ('Cash', 'Installment', 'Leasing') THEN
304 RAISE EXCEPTION 'Invalid payment type: %', p_payment_type;
305 END IF;
306
307 SELECT o.customer_id, o.employee_id, o.configuration_id, o.status, c.vin
308 INTO v_customer_id, v_employee_id, v_configuration_id, v_order_status, v_vin
309 FROM car_dealership."Order" o
310 JOIN car_dealership.Configuration c ON c.id = o.configuration_id
311 WHERE o.id = p_order_id;
312
313 IF NOT FOUND THEN
314 RAISE EXCEPTION 'Order with id % does not exist.', p_order_id;
315 END IF;
316
317 IF v_order_status NOT IN ('Pending', 'Confirmed') THEN
318 RAISE EXCEPTION 'Only Pending or Confirmed orders can be completed. Current status: %', v_order_status;
319 END IF;
320
321 -- Soft duplicate guards; the UNIQUE constraints are the hard concurrency backstop.
322 IF EXISTS (SELECT 1 FROM car_dealership.Contract WHERE order_id = p_order_id) THEN
323 RAISE EXCEPTION 'Contract already exists for order %.', p_order_id;
324 END IF;
325
326 IF EXISTS (SELECT 1 FROM car_dealership.Contract WHERE vin = v_vin) THEN
327 RAISE EXCEPTION 'Vehicle with VIN % already has a contract and cannot be sold again.', v_vin;
328 END IF;
329
330 SELECT id INTO v_sold_status_id
331 FROM car_dealership.Status
332 WHERE status = 'Sold';
333
334 IF v_sold_status_id IS NULL THEN
335 RAISE EXCEPTION 'Status ''Sold'' does not exist in the Status table.';
336 END IF;
337
338 v_total_price := car_dealership.calculate_configuration_total(v_configuration_id);
339
340 INSERT INTO car_dealership.Contract(employee_id, notes, date, type, order_id, customer_id, vin)
341 VALUES (v_employee_id, p_notes, CURRENT_DATE, p_contract_type, p_order_id, v_customer_id, v_vin)
342 RETURNING id INTO v_contract_id;
343
344 INSERT INTO car_dealership.Sale(date, employee_id, contract_id, customer_id)
345 VALUES (CURRENT_DATE, v_employee_id, v_contract_id, v_customer_id)
346 RETURNING id INTO v_sale_id;
347
348 INSERT INTO car_dealership.Payment(type, amount, sale_id)
349 VALUES (p_payment_type, v_total_price, v_sale_id);
350
351 UPDATE car_dealership."Order"
352 SET status = 'Completed'
353 WHERE id = p_order_id;
354
355 UPDATE car_dealership.Vehicle
356 SET status_id = v_sold_status_id
357 WHERE vin = v_vin;
358
359 RAISE NOTICE 'Sale completed. Contract %, Sale %, Amount %.', v_contract_id, v_sale_id, v_total_price;
360END;
361$$;
362
363
364-- =========================================================
365-- PROCEDURE 5: Create test drive
366-- =========================================================
367
368CREATE OR REPLACE PROCEDURE car_dealership.create_test_drive(
369 p_customer_id INT,
370 p_vin VARCHAR,
371 p_date DATE,
372 p_time_start TIMESTAMP,
373 p_time_end TIMESTAMP,
374 p_result VARCHAR DEFAULT NULL
375)
376LANGUAGE plpgsql
377AS $$
378DECLARE
379 v_test_drive_id INT;
380BEGIN
381 IF NOT EXISTS (
382 SELECT 1 FROM car_dealership.Customer WHERE id = p_customer_id
383 ) THEN
384 RAISE EXCEPTION 'Customer with id % does not exist.', p_customer_id;
385 END IF;
386
387 -- is_vehicle_available returns false for non-existent VINs,
388 -- but we check existence separately to give a clearer error message.
389 IF NOT EXISTS (
390 SELECT 1 FROM car_dealership.Vehicle WHERE vin = p_vin
391 ) THEN
392 RAISE EXCEPTION 'Vehicle with VIN % does not exist.', p_vin;
393 END IF;
394
395 IF NOT car_dealership.is_vehicle_available(p_vin) THEN
396 RAISE EXCEPTION 'Vehicle % is not available for a test drive.', p_vin;
397 END IF;
398
399 IF p_time_end <= p_time_start THEN
400 RAISE EXCEPTION 'Test drive end time must be after start time.';
401 END IF;
402
403 IF EXISTS (
404 SELECT 1
405 FROM car_dealership.TestDrive td
406 WHERE td.vin = p_vin
407 AND td.date = p_date
408 AND p_time_start < td.time_end
409 AND p_time_end > td.time_start
410 ) THEN
411 RAISE EXCEPTION 'Vehicle % already has a test drive scheduled in this time period.', p_vin;
412 END IF;
413
414 INSERT INTO car_dealership.TestDrive(customer_id, vin, date, time_start, time_end, result)
415 VALUES (p_customer_id, p_vin, p_date, p_time_start, p_time_end, p_result)
416 RETURNING id INTO v_test_drive_id;
417
418 RAISE NOTICE 'Test drive % created for customer %, vehicle %.', v_test_drive_id, p_customer_id, p_vin;
419END;
420$$;
421
422
423-- =========================================================
424-- PROCEDURE 6: Refresh dashboard materialized views
425-- =========================================================
426
427CREATE OR REPLACE PROCEDURE car_dealership.refresh_dealership_dashboard_views()
428LANGUAGE plpgsql
429AS $$
430BEGIN
431 REFRESH MATERIALIZED VIEW car_dealership.BrandPopularityView;
432 REFRESH MATERIALIZED VIEW car_dealership.PopularConfigurationsView;
433 REFRESH MATERIALIZED VIEW car_dealership.AvailableVehiclesByBudgetView;
434
435 RAISE NOTICE 'Dealership dashboard materialized views refreshed.';
436END;
437$$;
438
439
440-- =========================================================
441-- TRIGGER FUNCTION 1: Update configuration total
442-- Kept as a trigger because it is pure derived-data maintenance with
443-- no business rules — the correct use case for a trigger.
444-- =========================================================
445
446CREATE OR REPLACE FUNCTION car_dealership.trg_update_configuration_total()
447RETURNS TRIGGER
448LANGUAGE plpgsql
449AS $$
450BEGIN
451 IF TG_OP = 'DELETE' THEN
452 UPDATE car_dealership.Configuration
453 SET total_price = car_dealership.calculate_configuration_total(OLD.configuration_id)
454 WHERE id = OLD.configuration_id;
455 RETURN OLD;
456 END IF;
457
458 UPDATE car_dealership.Configuration
459 SET total_price = car_dealership.calculate_configuration_total(NEW.configuration_id)
460 WHERE id = NEW.configuration_id;
461
462 -- If a row is moved to a different configuration, recalculate the old one too.
463 IF TG_OP = 'UPDATE' AND OLD.configuration_id <> NEW.configuration_id THEN
464 UPDATE car_dealership.Configuration
465 SET total_price = car_dealership.calculate_configuration_total(OLD.configuration_id)
466 WHERE id = OLD.configuration_id;
467 END IF;
468
469 RETURN NEW;
470END;
471$$;
472
473DROP TRIGGER IF EXISTS update_configuration_total_after_package_change
474ON car_dealership.ConfigurationPackage;
475
476CREATE TRIGGER update_configuration_total_after_package_change
477AFTER INSERT OR UPDATE OR DELETE
478ON car_dealership.ConfigurationPackage
479FOR EACH ROW
480EXECUTE FUNCTION car_dealership.trg_update_configuration_total();
481
482
483-- =========================================================
484-- REMOVE OLD TRIGGERS AND THEIR FUNCTIONS
485-- Business logic has been moved into procedures.
486-- =========================================================
487
488DROP TRIGGER IF EXISTS validate_test_drive ON car_dealership.TestDrive;
489DROP TRIGGER IF EXISTS complete_order_and_sell_vehicle ON car_dealership.Sale;
490DROP TRIGGER IF EXISTS validate_payment ON car_dealership.Payment;
491
492DROP FUNCTION IF EXISTS car_dealership.trg_validate_test_drive();
493DROP FUNCTION IF EXISTS car_dealership.trg_complete_order_and_sell_vehicle();
494DROP FUNCTION IF EXISTS car_dealership.trg_validate_payment();