| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 | -- TRIGGER 1 FUNCTION: fn_guard_closed_order
|
|---|
| 6 |
|
|---|
| 7 | --Prevents adding or modifying OrderItem rows on orders
|
|---|
| 8 | --that are already Completed or Cancelled.
|
|---|
| 9 |
|
|---|
| 10 | CREATE OR REPLACE FUNCTION fn_guard_closed_order()
|
|---|
| 11 | RETURNS TRIGGER
|
|---|
| 12 | LANGUAGE plpgsql AS
|
|---|
| 13 | $$
|
|---|
| 14 | DECLARE
|
|---|
| 15 | v_status TEXT;
|
|---|
| 16 | BEGIN
|
|---|
| 17 | SELECT os.Status
|
|---|
| 18 | INTO v_status
|
|---|
| 19 | FROM "Order" o
|
|---|
| 20 | JOIN OrderStatus os ON os.Id = o.StatusId
|
|---|
| 21 | WHERE o.Id = NEW.OrderId;
|
|---|
| 22 |
|
|---|
| 23 | IF v_status IN ('Completed', 'Cancelled') THEN
|
|---|
| 24 | RAISE EXCEPTION
|
|---|
| 25 | '[OrderItem] Order #% is "%". Cannot be modified.',
|
|---|
| 26 | NEW.OrderId, v_status;
|
|---|
| 27 | END IF;
|
|---|
| 28 |
|
|---|
| 29 | RETURN NEW;
|
|---|
| 30 | END;
|
|---|
| 31 | $$;
|
|---|
| 32 |
|
|---|
| 33 | -- TRIGGER 1: trg_prevent_closed_order_edit
|
|---|
| 34 |
|
|---|
| 35 | CREATE OR REPLACE TRIGGER trg_prevent_closed_order_edit
|
|---|
| 36 | BEFORE INSERT OR UPDATE ON OrderItem
|
|---|
| 37 | FOR EACH ROW
|
|---|
| 38 | EXECUTE FUNCTION fn_guard_closed_order();
|
|---|
| 39 |
|
|---|
| 40 |
|
|---|
| 41 | -- TRIGGER 2 FUNCTION: fn_deduct_inventory_on_finish
|
|---|
| 42 | --When the kitchen marks an OrderItem as Finished = TRUE,
|
|---|
| 43 | --automatically deducts the ingredients (preku consistOF) from StoredProduct inventory.
|
|---|
| 44 |
|
|---|
| 45 | CREATE OR REPLACE FUNCTION fn_deduct_inventory_on_finish()
|
|---|
| 46 | RETURNS TRIGGER
|
|---|
| 47 | LANGUAGE plpgsql AS
|
|---|
| 48 | $$
|
|---|
| 49 | DECLARE
|
|---|
| 50 | v_product_id INT4;
|
|---|
| 51 | v_ingredient RECORD;
|
|---|
| 52 | v_needed INT4;
|
|---|
| 53 | v_available INT4;
|
|---|
| 54 | BEGIN
|
|---|
| 55 | IF OLD.Finished IS DISTINCT FROM NEW.Finished
|
|---|
| 56 | AND NEW.Finished = TRUE
|
|---|
| 57 | THEN
|
|---|
| 58 | v_product_id := NEW.MenuItemId;
|
|---|
| 59 |
|
|---|
| 60 | FOR v_ingredient IN
|
|---|
| 61 | SELECT co.Component AS IngId,
|
|---|
| 62 | co.Amount AS AmtPerUnit
|
|---|
| 63 | FROM ConsistsOf co
|
|---|
| 64 | WHERE co.Parent = v_product_id
|
|---|
| 65 | LOOP
|
|---|
| 66 | v_needed := NEW.Quantity * v_ingredient.AmtPerUnit;
|
|---|
| 67 |
|
|---|
| 68 | SELECT COALESCE(SUM(sp.Quantity), 0)
|
|---|
| 69 | INTO v_available
|
|---|
| 70 | FROM StoredProduct sp
|
|---|
| 71 | WHERE sp.ProductId = v_ingredient.IngId;
|
|---|
| 72 |
|
|---|
| 73 | IF v_available < v_needed THEN
|
|---|
| 74 | RAISE WARNING
|
|---|
| 75 | '[Inventory] ProductId % - needed: %, available: %. Stock will be set to 0.',
|
|---|
| 76 | v_ingredient.IngId, v_needed, v_available;
|
|---|
| 77 | END IF;
|
|---|
| 78 |
|
|---|
| 79 | UPDATE StoredProduct
|
|---|
| 80 | SET Quantity = GREATEST(0, Quantity - v_needed)
|
|---|
| 81 | WHERE ProductId = v_ingredient.IngId
|
|---|
| 82 | AND Id = (
|
|---|
| 83 | SELECT Id FROM StoredProduct
|
|---|
| 84 | WHERE ProductId = v_ingredient.IngId
|
|---|
| 85 | ORDER BY Id
|
|---|
| 86 | LIMIT 1
|
|---|
| 87 | );
|
|---|
| 88 | END LOOP;
|
|---|
| 89 | END IF;
|
|---|
| 90 |
|
|---|
| 91 | RETURN NEW;
|
|---|
| 92 | END;
|
|---|
| 93 | $$;
|
|---|
| 94 |
|
|---|
| 95 | -- TRIGGER 2: trg_deduct_inventory_on_item_finish
|
|---|
| 96 |
|
|---|
| 97 | CREATE OR REPLACE TRIGGER trg_deduct_inventory_on_item_finish
|
|---|
| 98 | AFTER UPDATE OF Finished ON OrderItem
|
|---|
| 99 | FOR EACH ROW
|
|---|
| 100 | EXECUTE FUNCTION fn_deduct_inventory_on_finish();
|
|---|
| 101 |
|
|---|
| 102 |
|
|---|
| 103 | -- TRIGGER 3 FUNCTION: fn_log_stock_change
|
|---|
| 104 | --On every change to StoredProduct.Quantity,
|
|---|
| 105 | -- automatically writes a row into ProductUsageLog.
|
|---|
| 106 |
|
|---|
| 107 | CREATE OR REPLACE FUNCTION fn_log_stock_change()
|
|---|
| 108 | RETURNS TRIGGER
|
|---|
| 109 | LANGUAGE plpgsql AS
|
|---|
| 110 | $$
|
|---|
| 111 | DECLARE
|
|---|
| 112 | v_diff INT4;
|
|---|
| 113 | v_change_type_id INT4;
|
|---|
| 114 | v_base_unit_id INT4;
|
|---|
| 115 | BEGIN
|
|---|
| 116 | v_diff := NEW.Quantity - OLD.Quantity;
|
|---|
| 117 |
|
|---|
| 118 | IF v_diff = 0 THEN
|
|---|
| 119 | RETURN NEW;
|
|---|
| 120 | END IF;
|
|---|
| 121 |
|
|---|
| 122 | SELECT Id INTO v_change_type_id
|
|---|
| 123 | FROM ChangeType
|
|---|
| 124 | WHERE Sign = (v_diff > 0)
|
|---|
| 125 | LIMIT 1;
|
|---|
| 126 |
|
|---|
| 127 | SELECT p.BaseUnitId INTO v_base_unit_id
|
|---|
| 128 | FROM Product p
|
|---|
| 129 | WHERE p.Id = NEW.ProductId;
|
|---|
| 130 |
|
|---|
| 131 | INSERT INTO ProductUsageLog
|
|---|
| 132 | (ProductId, ChangeAmount, InputAmount,
|
|---|
| 133 | Timestamp, ChangeTypeId, BaseUnitId, InputUnitId)
|
|---|
| 134 | VALUES
|
|---|
| 135 | (NEW.ProductId,
|
|---|
| 136 | ABS(v_diff),
|
|---|
| 137 | ABS(v_diff),
|
|---|
| 138 | CURRENT_TIMESTAMP,
|
|---|
| 139 | v_change_type_id,
|
|---|
| 140 | v_base_unit_id,
|
|---|
| 141 | v_base_unit_id);
|
|---|
| 142 |
|
|---|
| 143 | RETURN NEW;
|
|---|
| 144 | END;
|
|---|
| 145 | $$;
|
|---|
| 146 |
|
|---|
| 147 | -- TRIGGER 3: trg_log_stock_change
|
|---|
| 148 |
|
|---|
| 149 | CREATE OR REPLACE TRIGGER trg_log_stock_change
|
|---|
| 150 | AFTER UPDATE OF Quantity ON StoredProduct
|
|---|
| 151 | FOR EACH ROW
|
|---|
| 152 | EXECUTE FUNCTION fn_log_stock_change();
|
|---|
| 153 |
|
|---|
| 154 |
|
|---|
| 155 |
|
|---|
| 156 |
|
|---|
| 157 |
|
|---|
| 158 | -- FUNCTION 4: fn_calculate_order_total
|
|---|
| 159 | --Calculates the total amount of an order.
|
|---|
| 160 | -- Automatically applies active discounts (DiscountItem)
|
|---|
| 161 | -- for the current date if they exist.
|
|---|
| 162 |
|
|---|
| 163 | CREATE OR REPLACE FUNCTION fn_calculate_order_total(p_order_id INT4)
|
|---|
| 164 | RETURNS NUMERIC(10,2)
|
|---|
| 165 | LANGUAGE plpgsql AS
|
|---|
| 166 | $$
|
|---|
| 167 | DECLARE
|
|---|
| 168 | v_total NUMERIC(10,2) := 0;
|
|---|
| 169 | v_item RECORD;
|
|---|
| 170 | v_disc_price NUMERIC(10,2);
|
|---|
| 171 | v_unit_price NUMERIC(10,2);
|
|---|
| 172 | BEGIN
|
|---|
| 173 | FOR v_item IN
|
|---|
| 174 | SELECT oi.Id AS OiId,
|
|---|
| 175 | oi.Quantity,
|
|---|
| 176 | oi.MenuMemberId,
|
|---|
| 177 | oi.MenuItemId,
|
|---|
| 178 | mm.Price AS BasePrice
|
|---|
| 179 | FROM OrderItem oi
|
|---|
| 180 | JOIN MenuMember mm
|
|---|
| 181 | ON mm.Id = oi.MenuMemberId
|
|---|
| 182 | AND mm.MenuItemId = oi.MenuItemId
|
|---|
| 183 | WHERE oi.OrderId = p_order_id
|
|---|
| 184 | LOOP
|
|---|
| 185 | SELECT di.NewPrice INTO v_disc_price
|
|---|
| 186 | FROM DiscountItem di
|
|---|
| 187 | JOIN Discount d ON d.Id = di.DiscountId
|
|---|
| 188 | WHERE di.MenuMemberId = v_item.MenuMemberId
|
|---|
| 189 | AND di.MenuMemberMenuItemId = v_item.MenuItemId
|
|---|
| 190 | AND d.Status = TRUE
|
|---|
| 191 | AND CURRENT_DATE BETWEEN d."From" AND d."To"
|
|---|
| 192 | ORDER BY di.NewPrice ASC
|
|---|
| 193 | LIMIT 1;
|
|---|
| 194 |
|
|---|
| 195 | v_unit_price := COALESCE(v_disc_price, v_item.BasePrice);
|
|---|
| 196 | v_total := v_total + (v_unit_price * v_item.Quantity);
|
|---|
| 197 |
|
|---|
| 198 | v_disc_price := NULL;
|
|---|
| 199 | END LOOP;
|
|---|
| 200 |
|
|---|
| 201 | RETURN v_total;
|
|---|
| 202 | END;
|
|---|
| 203 | $$;
|
|---|
| 204 |
|
|---|
| 205 |
|
|---|
| 206 | -- FUNCTION 5: fn_get_available_tables
|
|---|
| 207 | --Returns all tables that are free for a given date,time range and minimum capacity.
|
|---|
| 208 | -- Also checks the Reservation table for overlaps.
|
|---|
| 209 |
|
|---|
| 210 | CREATE OR REPLACE FUNCTION fn_get_available_tables(
|
|---|
| 211 | p_date DATE,
|
|---|
| 212 | p_start TIME,
|
|---|
| 213 | p_end TIME,
|
|---|
| 214 | p_guests INT4 DEFAULT 1
|
|---|
| 215 | )
|
|---|
| 216 | RETURNS TABLE(
|
|---|
| 217 | TableNumber INT4,
|
|---|
| 218 | Capacity INT4,
|
|---|
| 219 | TableType TEXT
|
|---|
| 220 | )
|
|---|
| 221 | LANGUAGE plpgsql AS
|
|---|
| 222 | $$
|
|---|
| 223 | BEGIN
|
|---|
| 224 | RETURN QUERY
|
|---|
| 225 | SELECT rt.TableNumber,
|
|---|
| 226 | rt.Capacity,
|
|---|
| 227 | tt.Type::TEXT AS TableType
|
|---|
| 228 | FROM RestaurantTable rt
|
|---|
| 229 | JOIN TableType tt ON tt.Id = rt.TableTypeId
|
|---|
| 230 | WHERE rt.Status = TRUE
|
|---|
| 231 | AND rt.Capacity >= p_guests
|
|---|
| 232 | AND rt.TableNumber NOT IN (
|
|---|
| 233 | SELECT r.TableNumber
|
|---|
| 234 | FROM Reservation r
|
|---|
| 235 | WHERE r."Date" = p_date
|
|---|
| 236 | AND r.TableNumber IS NOT NULL
|
|---|
| 237 | AND tsrange(
|
|---|
| 238 | (p_date + p_start)::TIMESTAMP,
|
|---|
| 239 | (p_date + p_end )::TIMESTAMP
|
|---|
| 240 | ) &&
|
|---|
| 241 | tsrange(
|
|---|
| 242 | (r."Date" + r.StartTime)::TIMESTAMP,
|
|---|
| 243 | (r."Date" + r.EndTime )::TIMESTAMP
|
|---|
| 244 | )
|
|---|
| 245 | )
|
|---|
| 246 | ORDER BY rt.Capacity ASC;
|
|---|
| 247 | END;
|
|---|
| 248 | $$;
|
|---|
| 249 |
|
|---|
| 250 |
|
|---|
| 251 | -- FUNCTION 6: fn_low_stock_report
|
|---|
| 252 | --Returns all products whose current stock is below a given threshold.
|
|---|
| 253 | -- Used by the owner to know when a restock is needed.
|
|---|
| 254 |
|
|---|
| 255 | CREATE OR REPLACE FUNCTION fn_low_stock_report(p_threshold INT4 DEFAULT 10)
|
|---|
| 256 | RETURNS TABLE(
|
|---|
| 257 | Product TEXT,
|
|---|
| 258 | ProductType TEXT,
|
|---|
| 259 | Quantity INT4,
|
|---|
| 260 | BaseUnit TEXT
|
|---|
| 261 | )
|
|---|
| 262 | LANGUAGE plpgsql AS
|
|---|
| 263 | $$
|
|---|
| 264 | BEGIN
|
|---|
| 265 | RETURN QUERY
|
|---|
| 266 | SELECT p.Name::TEXT AS Product,
|
|---|
| 267 | pt.Type::TEXT AS ProductType,
|
|---|
| 268 | sp.Quantity,
|
|---|
| 269 | u.Name::TEXT AS BaseUnit
|
|---|
| 270 | FROM StoredProduct sp
|
|---|
| 271 | JOIN Product p ON p.Id = sp.ProductId
|
|---|
| 272 | JOIN ProductType pt ON pt.Id = p.TypeId
|
|---|
| 273 | JOIN Unit u ON u.Id = p.BaseUnitId
|
|---|
| 274 | WHERE sp.Quantity < p_threshold
|
|---|
| 275 | ORDER BY sp.Quantity ASC;
|
|---|
| 276 | END;
|
|---|
| 277 | $$;
|
|---|
| 278 |
|
|---|
| 279 | -- PROCEDURE 7: sp_add_order_item
|
|---|
| 280 | -- Adds a menu item to an existing open order.
|
|---|
| 281 | -- Validates that the order is open, the MenuMember exists
|
|---|
| 282 | -- in an active menu, and the quantity is positive.
|
|---|
| 283 | -- If the same item already exists on the order, it increases
|
|---|
| 284 | -- the quantity instead of inserting a duplicate row.
|
|---|
| 285 | -- Usage:
|
|---|
| 286 | -- CALL sp_add_order_item(5, 3, 3, 2, 1);
|
|---|
| 287 |
|
|---|
| 288 | CREATE OR REPLACE PROCEDURE sp_add_order_item(
|
|---|
| 289 | p_order_id INT4,
|
|---|
| 290 | p_menu_member_id INT4,
|
|---|
| 291 | p_menu_item_id INT4,
|
|---|
| 292 | p_quantity INT4,
|
|---|
| 293 | p_created_by INT4 DEFAULT NULL
|
|---|
| 294 | )
|
|---|
| 295 | LANGUAGE plpgsql AS
|
|---|
| 296 | $$
|
|---|
| 297 | DECLARE
|
|---|
| 298 | v_order_status TEXT;
|
|---|
| 299 | v_existing_id INT4;
|
|---|
| 300 | BEGIN
|
|---|
| 301 | IF p_quantity <= 0 THEN
|
|---|
| 302 | RAISE EXCEPTION '[AddItem] Quantity must be > 0. Provided: %', p_quantity;
|
|---|
| 303 | END IF;
|
|---|
| 304 |
|
|---|
| 305 | -- Check order exists and is open
|
|---|
| 306 | SELECT os.Status INTO v_order_status
|
|---|
| 307 | FROM "Order" o
|
|---|
| 308 | JOIN OrderStatus os ON os.Id = o.StatusId
|
|---|
| 309 | WHERE o.Id = p_order_id;
|
|---|
| 310 |
|
|---|
| 311 | IF NOT FOUND THEN
|
|---|
| 312 | RAISE EXCEPTION '[AddItem] Order #% does not exist.', p_order_id;
|
|---|
| 313 | END IF;
|
|---|
| 314 |
|
|---|
| 315 | IF v_order_status IN ('Completed', 'Cancelled') THEN
|
|---|
| 316 | RAISE EXCEPTION '[AddItem] Order #% is "%". Cannot add items.', p_order_id, v_order_status;
|
|---|
| 317 | END IF;
|
|---|
| 318 |
|
|---|
| 319 | -- Check the MenuMember exists
|
|---|
| 320 | IF NOT EXISTS (
|
|---|
| 321 | SELECT 1 FROM MenuMember
|
|---|
| 322 | WHERE Id = p_menu_member_id
|
|---|
| 323 | AND MenuItemId = p_menu_item_id
|
|---|
| 324 | ) THEN
|
|---|
| 325 | RAISE EXCEPTION '[AddItem] MenuMember (%, %) does not exist.',
|
|---|
| 326 | p_menu_member_id, p_menu_item_id;
|
|---|
| 327 | END IF;
|
|---|
| 328 |
|
|---|
| 329 | -- If the same item is already on the order, just increase quantity
|
|---|
| 330 | SELECT Id INTO v_existing_id
|
|---|
| 331 | FROM OrderItem
|
|---|
| 332 | WHERE OrderId = p_order_id
|
|---|
| 333 | AND MenuMemberId = p_menu_member_id
|
|---|
| 334 | AND MenuItemId = p_menu_item_id
|
|---|
| 335 | AND Finished = FALSE
|
|---|
| 336 | LIMIT 1;
|
|---|
| 337 |
|
|---|
| 338 | IF FOUND THEN
|
|---|
| 339 | UPDATE OrderItem
|
|---|
| 340 | SET Quantity = Quantity + p_quantity
|
|---|
| 341 | WHERE Id = v_existing_id;
|
|---|
| 342 |
|
|---|
| 343 | RAISE NOTICE '[AddItem] Item (MenuMember %) quantity increased by % on Order #%.',
|
|---|
| 344 | p_menu_member_id, p_quantity, p_order_id;
|
|---|
| 345 | ELSE
|
|---|
| 346 | INSERT INTO OrderItem
|
|---|
| 347 | (Quantity, OrderId, CreatedBy, Finished, MenuMemberId, MenuItemId)
|
|---|
| 348 | VALUES
|
|---|
| 349 | (p_quantity, p_order_id, p_created_by, FALSE, p_menu_member_id, p_menu_item_id);
|
|---|
| 350 |
|
|---|
| 351 | RAISE NOTICE '[AddItem] Item (MenuMember %) x% added to Order #%.',
|
|---|
| 352 | p_menu_member_id, p_quantity, p_order_id;
|
|---|
| 353 | END IF;
|
|---|
| 354 | END;
|
|---|
| 355 | $$;
|
|---|
| 356 |
|
|---|
| 357 | -- PROCEDURE 8: sp_create_order
|
|---|
| 358 | -- Creates a new order for a waiter, order type and optionally
|
|---|
| 359 | -- assigns a table. Sets the initial status to the first
|
|---|
| 360 | -- non-terminal status found (e.g. 'Pending' or 'Open').
|
|---|
| 361 | -- If a table is given it marks it as occupied (Status = FALSE).
|
|---|
| 362 | -- Usage:
|
|---|
| 363 | -- CALL sp_create_order(1, 1, 3); -- dine-in table 3
|
|---|
| 364 | -- CALL sp_create_order(1, 2, NULL); -- takeaway, no table
|
|---|
| 365 |
|
|---|
| 366 | CREATE OR REPLACE PROCEDURE sp_create_order(
|
|---|
| 367 | p_waiter_id INT4,
|
|---|
| 368 | p_order_type_id INT4,
|
|---|
| 369 | p_table_number INT4 DEFAULT NULL
|
|---|
| 370 | )
|
|---|
| 371 | LANGUAGE plpgsql AS
|
|---|
| 372 | $$
|
|---|
| 373 | DECLARE
|
|---|
| 374 | v_status_id INT4;
|
|---|
| 375 | v_order_id INT4;
|
|---|
| 376 | BEGIN
|
|---|
| 377 | -- Validate waiter exists and is still active
|
|---|
| 378 | IF NOT EXISTS (
|
|---|
| 379 | SELECT 1 FROM Employee
|
|---|
| 380 | WHERE Id = p_waiter_id
|
|---|
| 381 | AND (DateResignation IS NULL OR DateResignation > CURRENT_DATE)
|
|---|
| 382 | ) THEN
|
|---|
| 383 | RAISE EXCEPTION '[Order] Employee #% does not exist or has resigned.', p_waiter_id;
|
|---|
| 384 | END IF;
|
|---|
| 385 |
|
|---|
| 386 | -- Validate order type
|
|---|
| 387 | IF NOT EXISTS (SELECT 1 FROM OrderType WHERE Id = p_order_type_id) THEN
|
|---|
| 388 | RAISE EXCEPTION '[Order] OrderType #% does not exist.', p_order_type_id;
|
|---|
| 389 | END IF;
|
|---|
| 390 |
|
|---|
| 391 | -- Validate table if provided
|
|---|
| 392 | IF p_table_number IS NOT NULL THEN
|
|---|
| 393 | IF NOT EXISTS (
|
|---|
| 394 | SELECT 1 FROM RestaurantTable
|
|---|
| 395 | WHERE TableNumber = p_table_number
|
|---|
| 396 | AND Status = TRUE
|
|---|
| 397 | ) THEN
|
|---|
| 398 | RAISE EXCEPTION '[Order] Table #% does not exist or is already occupied.', p_table_number;
|
|---|
| 399 | END IF;
|
|---|
| 400 | END IF;
|
|---|
| 401 |
|
|---|
| 402 | -- Get the initial order status (Pending or first available)
|
|---|
| 403 | SELECT Id INTO v_status_id
|
|---|
| 404 | FROM OrderStatus
|
|---|
| 405 | WHERE Status NOT IN ('Completed', 'Cancelled')
|
|---|
| 406 | ORDER BY Id
|
|---|
| 407 | LIMIT 1;
|
|---|
| 408 |
|
|---|
| 409 | IF NOT FOUND THEN
|
|---|
| 410 | RAISE EXCEPTION '[Order] No valid initial OrderStatus found. Check the OrderStatus table.';
|
|---|
| 411 | END IF;
|
|---|
| 412 |
|
|---|
| 413 | -- Create the order
|
|---|
| 414 | INSERT INTO "Order" (WaiterId, DateCreated, TypeId, StatusId, TableNumber)
|
|---|
| 415 | VALUES (p_waiter_id, CURRENT_TIMESTAMP, p_order_type_id, v_status_id, p_table_number)
|
|---|
| 416 | RETURNING Id INTO v_order_id;
|
|---|
| 417 |
|
|---|
| 418 | -- Mark the table as occupied
|
|---|
| 419 | IF p_table_number IS NOT NULL THEN
|
|---|
| 420 | UPDATE RestaurantTable
|
|---|
| 421 | SET Status = FALSE
|
|---|
| 422 | WHERE TableNumber = p_table_number;
|
|---|
| 423 | END IF;
|
|---|
| 424 |
|
|---|
| 425 | RAISE NOTICE '[Order] Order #% created. Waiter: %, Table: %, Status: %.',
|
|---|
| 426 | v_order_id, p_waiter_id, COALESCE(p_table_number::TEXT, 'N/A'), v_status_id;
|
|---|
| 427 | END;
|
|---|
| 428 | $$;
|
|---|
| 429 |
|
|---|
| 430 |
|
|---|
| 431 |
|
|---|
| 432 |
|
|---|
| 433 | -- PROCEDURE 9: sp_create_reservation
|
|---|
| 434 | -- Creates a reservation for a guest after validating that
|
|---|
| 435 | -- the requested table is free for that date and time slot.
|
|---|
| 436 | -- Uses fn_get_available_tables to check availability.
|
|---|
| 437 |
|
|---|
| 438 | -- CALL sp_create_reservation('John Smith', '071234567',
|
|---|
| 439 | -- '2024-12-31', '19:00', '21:00', 3, 2);
|
|---|
| 440 |
|
|---|
| 441 | CREATE OR REPLACE PROCEDURE sp_create_reservation(
|
|---|
| 442 | p_guest_name VARCHAR(50),
|
|---|
| 443 | p_guest_phone VARCHAR(50),
|
|---|
| 444 | p_date DATE,
|
|---|
| 445 | p_start TIME,
|
|---|
| 446 | p_end TIME,
|
|---|
| 447 | p_table_number INT4,
|
|---|
| 448 | p_employee_id INT4 DEFAULT NULL
|
|---|
| 449 | )
|
|---|
| 450 | LANGUAGE plpgsql AS
|
|---|
| 451 | $$
|
|---|
| 452 | DECLARE
|
|---|
| 453 | v_reservation_id INT4;
|
|---|
| 454 | BEGIN
|
|---|
| 455 | -- Basic time validation
|
|---|
| 456 | IF p_start >= p_end THEN
|
|---|
| 457 | RAISE EXCEPTION '[Reservation] Start time must be before end time.';
|
|---|
| 458 | END IF;
|
|---|
| 459 |
|
|---|
| 460 | IF p_date < CURRENT_DATE THEN
|
|---|
| 461 | RAISE EXCEPTION '[Reservation] Cannot make a reservation in the past.';
|
|---|
| 462 | END IF;
|
|---|
| 463 |
|
|---|
| 464 | -- Check the table exists
|
|---|
| 465 | IF NOT EXISTS (
|
|---|
| 466 | SELECT 1 FROM RestaurantTable WHERE TableNumber = p_table_number
|
|---|
| 467 | ) THEN
|
|---|
| 468 | RAISE EXCEPTION '[Reservation] Table #% does not exist.', p_table_number;
|
|---|
| 469 | END IF;
|
|---|
| 470 |
|
|---|
| 471 | -- Check the table is available for this slot using our function
|
|---|
| 472 | IF NOT EXISTS (
|
|---|
| 473 | SELECT 1 FROM fn_get_available_tables(p_date, p_start, p_end, 1)
|
|---|
| 474 | WHERE TableNumber = p_table_number
|
|---|
| 475 | ) THEN
|
|---|
| 476 | RAISE EXCEPTION
|
|---|
| 477 | '[Reservation] Table #% is not available on % from % to %.',
|
|---|
| 478 | p_table_number, p_date, p_start, p_end;
|
|---|
| 479 | END IF;
|
|---|
| 480 |
|
|---|
| 481 | -- Validate employee if provided
|
|---|
| 482 | IF p_employee_id IS NOT NULL AND NOT EXISTS (
|
|---|
| 483 | SELECT 1 FROM Employee WHERE Id = p_employee_id
|
|---|
| 484 | ) THEN
|
|---|
| 485 | RAISE EXCEPTION '[Reservation] Employee #% does not exist.', p_employee_id;
|
|---|
| 486 | END IF;
|
|---|
| 487 |
|
|---|
| 488 | -- Create the reservation
|
|---|
| 489 | INSERT INTO Reservation
|
|---|
| 490 | (GuestName, GuestPhone, CreatedAt, StartTime, EndTime,
|
|---|
| 491 | "Date", EmployeeId, TableNumber)
|
|---|
| 492 | VALUES
|
|---|
| 493 | (p_guest_name, p_guest_phone, CURRENT_TIMESTAMP,
|
|---|
| 494 | p_start, p_end, p_date, p_employee_id, p_table_number)
|
|---|
| 495 | RETURNING Id INTO v_reservation_id;
|
|---|
| 496 |
|
|---|
| 497 | RAISE NOTICE '[Reservation] #% created for "%" on % % - %, Table #%.',
|
|---|
| 498 | v_reservation_id, p_guest_name, p_date, p_start, p_end, p_table_number;
|
|---|
| 499 | END;
|
|---|
| 500 | $$;
|
|---|
| 501 |
|
|---|
| 502 |
|
|---|
| 503 |
|
|---|
| 504 | -- PROCEDURE 10: sp_cancel_order
|
|---|
| 505 | -- Safely cancels an open order.
|
|---|
| 506 | -- Sets status to Cancelled, sets DateFinished, and frees
|
|---|
| 507 | -- the table if no other active order is using it.
|
|---|
| 508 | -- Does NOT restock inventory (items already consumed by kitchen
|
|---|
| 509 | -- are not reversed - sp_restock_product for manual correction).
|
|---|
| 510 | -- Usage:
|
|---|
| 511 | -- CALL sp_cancel_order(7, 'Guest left before food was ready');
|
|---|
| 512 |
|
|---|
| 513 | CREATE OR REPLACE PROCEDURE sp_cancel_order(
|
|---|
| 514 | p_order_id INT4,
|
|---|
| 515 | p_reason TEXT DEFAULT 'No reason provided'
|
|---|
| 516 | )
|
|---|
| 517 | LANGUAGE plpgsql AS
|
|---|
| 518 | $$
|
|---|
| 519 | DECLARE
|
|---|
| 520 | v_cancelled_id INT4;
|
|---|
| 521 | v_cur_status TEXT;
|
|---|
| 522 | v_table_number INT4;
|
|---|
| 523 | v_other_open INT4;
|
|---|
| 524 | BEGIN
|
|---|
| 525 | -- Get current order state
|
|---|
| 526 | SELECT os.Status, o.TableNumber
|
|---|
| 527 | INTO v_cur_status, v_table_number
|
|---|
| 528 | FROM "Order" o
|
|---|
| 529 | JOIN OrderStatus os ON os.Id = o.StatusId
|
|---|
| 530 | WHERE o.Id = p_order_id;
|
|---|
| 531 |
|
|---|
| 532 | IF NOT FOUND THEN
|
|---|
| 533 | RAISE EXCEPTION '[Cancel] Order #% does not exist.', p_order_id;
|
|---|
| 534 | END IF;
|
|---|
| 535 |
|
|---|
| 536 | IF v_cur_status = 'Cancelled' THEN
|
|---|
| 537 | RAISE EXCEPTION '[Cancel] Order #% is already cancelled.', p_order_id;
|
|---|
| 538 | END IF;
|
|---|
| 539 |
|
|---|
| 540 | IF v_cur_status = 'Completed' THEN
|
|---|
| 541 | RAISE EXCEPTION '[Cancel] Cannot cancel a completed order (#%).', p_order_id;
|
|---|
| 542 | END IF;
|
|---|
| 543 |
|
|---|
| 544 | -- Get Cancelled status Id
|
|---|
| 545 | SELECT Id INTO v_cancelled_id
|
|---|
| 546 | FROM OrderStatus
|
|---|
| 547 | WHERE Status = 'Cancelled';
|
|---|
| 548 |
|
|---|
| 549 | IF NOT FOUND THEN
|
|---|
| 550 | RAISE EXCEPTION '[Cancel] OrderStatus "Cancelled" missing from lookup table.';
|
|---|
| 551 | END IF;
|
|---|
| 552 |
|
|---|
| 553 | -- Cancel the order
|
|---|
| 554 | UPDATE "Order"
|
|---|
| 555 | SET StatusId = v_cancelled_id,
|
|---|
| 556 | DateFinished = CURRENT_TIMESTAMP
|
|---|
| 557 | WHERE Id = p_order_id;
|
|---|
| 558 |
|
|---|
| 559 | -- Free the table if no other active order is using it
|
|---|
| 560 | IF v_table_number IS NOT NULL THEN
|
|---|
| 561 | SELECT COUNT(*) INTO v_other_open
|
|---|
| 562 | FROM "Order" o
|
|---|
| 563 | JOIN OrderStatus os ON os.Id = o.StatusId
|
|---|
| 564 | WHERE o.TableNumber = v_table_number
|
|---|
| 565 | AND o.Id <> p_order_id
|
|---|
| 566 | AND os.Status NOT IN ('Completed', 'Cancelled');
|
|---|
| 567 |
|
|---|
| 568 | IF v_other_open = 0 THEN
|
|---|
| 569 | UPDATE RestaurantTable
|
|---|
| 570 | SET Status = TRUE
|
|---|
| 571 | WHERE TableNumber = v_table_number;
|
|---|
| 572 |
|
|---|
| 573 | RAISE NOTICE '[Cancel] Table #% has been freed.', v_table_number;
|
|---|
| 574 | END IF;
|
|---|
| 575 | END IF;
|
|---|
| 576 |
|
|---|
| 577 | RAISE NOTICE '[Cancel] Order #% cancelled. Reason: %', p_order_id, p_reason;
|
|---|
| 578 | END;
|
|---|
| 579 | $$;
|
|---|
| 580 |
|
|---|
| 581 |
|
|---|
| 582 | -- PROCEDURE 11: sp_close_order_and_invoice
|
|---|
| 583 | --Central operation - closing an order and automatically generating a full invoice:
|
|---|
| 584 | -- - Invoice - total amount and tax
|
|---|
| 585 | -- - InvoiceItem - per line item, with original and discounted price
|
|---|
| 586 |
|
|---|
| 587 | CREATE OR REPLACE PROCEDURE sp_close_order_and_invoice(
|
|---|
| 588 | p_order_id INT4,
|
|---|
| 589 | p_tax_rate NUMERIC(5,4) DEFAULT 0.18
|
|---|
| 590 | )
|
|---|
| 591 | LANGUAGE plpgsql AS
|
|---|
| 592 | $$
|
|---|
| 593 | DECLARE
|
|---|
| 594 | v_status_id INT4;
|
|---|
| 595 | v_cur_status TEXT;
|
|---|
| 596 | v_total NUMERIC(10,2);
|
|---|
| 597 | v_tax NUMERIC(10,2);
|
|---|
| 598 | v_invoice_id INT4;
|
|---|
| 599 | v_disc_price NUMERIC(10,2);
|
|---|
| 600 | v_item RECORD;
|
|---|
| 601 | BEGIN
|
|---|
| 602 | SELECT os.Status, os.Id
|
|---|
| 603 | INTO v_cur_status, v_status_id
|
|---|
| 604 | FROM "Order" o
|
|---|
| 605 | JOIN OrderStatus os ON os.Id = o.StatusId
|
|---|
| 606 | WHERE o.Id = p_order_id;
|
|---|
| 607 |
|
|---|
| 608 | IF NOT FOUND THEN
|
|---|
| 609 | RAISE EXCEPTION '[Invoice] Order #% does not exist.', p_order_id;
|
|---|
| 610 | END IF;
|
|---|
| 611 |
|
|---|
| 612 | IF v_cur_status IN ('Completed', 'Cancelled') THEN
|
|---|
| 613 | RAISE EXCEPTION
|
|---|
| 614 | '[Invoice] Order #% is already "%". Cannot close again.',
|
|---|
| 615 | p_order_id, v_cur_status;
|
|---|
| 616 | END IF;
|
|---|
| 617 |
|
|---|
| 618 | SELECT Id INTO v_status_id
|
|---|
| 619 | FROM OrderStatus
|
|---|
| 620 | WHERE Status = 'Completed';
|
|---|
| 621 |
|
|---|
| 622 | IF NOT FOUND THEN
|
|---|
| 623 | RAISE EXCEPTION
|
|---|
| 624 | '[Invoice] OrderStatus "Completed" does not exist. Add it to the lookup table.';
|
|---|
| 625 | END IF;
|
|---|
| 626 |
|
|---|
| 627 | v_total := fn_calculate_order_total(p_order_id);
|
|---|
| 628 | v_tax := ROUND(v_total * p_tax_rate, 2);
|
|---|
| 629 |
|
|---|
| 630 | INSERT INTO Invoice (OrderId, InvoiceDate, TotalAmount, TaxAmount)
|
|---|
| 631 | VALUES (p_order_id, CURRENT_TIMESTAMP, v_total, v_tax)
|
|---|
| 632 | RETURNING Id INTO v_invoice_id;
|
|---|
| 633 |
|
|---|
| 634 | FOR v_item IN
|
|---|
| 635 | SELECT oi.Id AS OiId,
|
|---|
| 636 | oi.Quantity,
|
|---|
| 637 | oi.MenuMemberId,
|
|---|
| 638 | oi.MenuItemId,
|
|---|
| 639 | mm.Price AS OrigPrice
|
|---|
| 640 | FROM OrderItem oi
|
|---|
| 641 | JOIN MenuMember mm
|
|---|
| 642 | ON mm.Id = oi.MenuMemberId
|
|---|
| 643 | AND mm.MenuItemId = oi.MenuItemId
|
|---|
| 644 | WHERE oi.OrderId = p_order_id
|
|---|
| 645 | LOOP
|
|---|
| 646 | SELECT di.NewPrice INTO v_disc_price
|
|---|
| 647 | FROM DiscountItem di
|
|---|
| 648 | JOIN Discount d ON d.Id = di.DiscountId
|
|---|
| 649 | WHERE di.MenuMemberId = v_item.MenuMemberId
|
|---|
| 650 | AND di.MenuMemberMenuItemId = v_item.MenuItemId
|
|---|
| 651 | AND d.Status = TRUE
|
|---|
| 652 | AND CURRENT_DATE BETWEEN d."From" AND d."To"
|
|---|
| 653 | ORDER BY di.NewPrice ASC
|
|---|
| 654 | LIMIT 1;
|
|---|
| 655 |
|
|---|
| 656 | INSERT INTO InvoiceItem
|
|---|
| 657 | (InvoiceId, OrderItemId, OriginalPrice, DiscountedPrice, Quantity)
|
|---|
| 658 | VALUES
|
|---|
| 659 | (v_invoice_id,
|
|---|
| 660 | v_item.OiId,
|
|---|
| 661 | v_item.OrigPrice,
|
|---|
| 662 | v_disc_price,
|
|---|
| 663 | v_item.Quantity);
|
|---|
| 664 |
|
|---|
| 665 | v_disc_price := NULL;
|
|---|
| 666 | END LOOP;
|
|---|
| 667 |
|
|---|
| 668 | UPDATE "Order"
|
|---|
| 669 | SET StatusId = v_status_id,
|
|---|
| 670 | DateFinished = CURRENT_TIMESTAMP
|
|---|
| 671 | WHERE Id = p_order_id;
|
|---|
| 672 |
|
|---|
| 673 | RAISE NOTICE
|
|---|
| 674 | '[Invoice] Order #% closed. Invoice #% generated. Total: % (tax: %).',
|
|---|
| 675 | p_order_id, v_invoice_id, v_total, v_tax;
|
|---|
| 676 | END;
|
|---|
| 677 | $$;
|
|---|
| 678 |
|
|---|
| 679 |
|
|---|
| 680 | -- PROCEDURE 12: sp_restock_product
|
|---|
| 681 | --Restock inventory for a product.
|
|---|
| 682 | -- If no row exists in StoredProduct it creates one.
|
|---|
| 683 | -- The log is written automatically via trg_log_stock_change.
|
|---|
| 684 |
|
|---|
| 685 | CREATE OR REPLACE PROCEDURE sp_restock_product(
|
|---|
| 686 | p_product_id INT4,
|
|---|
| 687 | p_quantity INT4
|
|---|
| 688 | )
|
|---|
| 689 | LANGUAGE plpgsql AS
|
|---|
| 690 | $$
|
|---|
| 691 | DECLARE
|
|---|
| 692 | v_name TEXT;
|
|---|
| 693 | v_old_qty INT4;
|
|---|
| 694 | BEGIN
|
|---|
| 695 | IF p_quantity <= 0 THEN
|
|---|
| 696 | RAISE EXCEPTION
|
|---|
| 697 | '[Restock] Quantity must be > 0. Provided: %', p_quantity;
|
|---|
| 698 | END IF;
|
|---|
| 699 |
|
|---|
| 700 | SELECT Name INTO v_name
|
|---|
| 701 | FROM Product
|
|---|
| 702 | WHERE Id = p_product_id;
|
|---|
| 703 |
|
|---|
| 704 | IF NOT FOUND THEN
|
|---|
| 705 | RAISE EXCEPTION '[Restock] Product #% does not exist.', p_product_id;
|
|---|
| 706 | END IF;
|
|---|
| 707 |
|
|---|
| 708 | SELECT Quantity INTO v_old_qty
|
|---|
| 709 | FROM StoredProduct
|
|---|
| 710 | WHERE ProductId = p_product_id
|
|---|
| 711 | ORDER BY Id
|
|---|
| 712 | LIMIT 1;
|
|---|
| 713 |
|
|---|
| 714 | IF NOT FOUND THEN
|
|---|
| 715 | INSERT INTO StoredProduct (Quantity, ProductId)
|
|---|
| 716 | VALUES (p_quantity, p_product_id);
|
|---|
| 717 | RAISE NOTICE '[Restock] "%" - new record: 0 -> % units.', v_name, p_quantity;
|
|---|
| 718 | ELSE
|
|---|
| 719 | UPDATE StoredProduct
|
|---|
| 720 | SET Quantity = Quantity + p_quantity
|
|---|
| 721 | WHERE ProductId = p_product_id
|
|---|
| 722 | AND Id = (SELECT Id FROM StoredProduct
|
|---|
| 723 | WHERE ProductId = p_product_id ORDER BY Id LIMIT 1);
|
|---|
| 724 |
|
|---|
| 725 | RAISE NOTICE '[Restock] "%" - % -> % units.',
|
|---|
| 726 | v_name, v_old_qty, v_old_qty + p_quantity;
|
|---|
| 727 | END IF;
|
|---|
| 728 | END;
|
|---|
| 729 | $$;
|
|---|
| 730 |
|
|---|
| 731 |
|
|---|
| 732 | -- PROCEDURE 13: sp_apply_discount_to_menu
|
|---|
| 733 | --The owner can apply a percentage discount to all items in a given menu for a set period.
|
|---|
| 734 | -- Automatically calculates the new prices and inserts rows into the Discount and DiscountItem tables.
|
|---|
| 735 | --
|
|---|
| 736 |
|
|---|
| 737 | CREATE OR REPLACE PROCEDURE sp_apply_discount_to_menu(
|
|---|
| 738 | p_menu_id INT4,
|
|---|
| 739 | p_name VARCHAR(30),
|
|---|
| 740 | p_pct NUMERIC(5,2),
|
|---|
| 741 | p_from DATE,
|
|---|
| 742 | p_to DATE
|
|---|
| 743 | )
|
|---|
| 744 | LANGUAGE plpgsql AS
|
|---|
| 745 | $$
|
|---|
| 746 | DECLARE
|
|---|
| 747 | v_disc_id INT4;
|
|---|
| 748 | v_item RECORD;
|
|---|
| 749 | v_new_prc NUMERIC(10,2);
|
|---|
| 750 | v_count INT4 := 0;
|
|---|
| 751 | BEGIN
|
|---|
| 752 | IF p_pct <= 0 OR p_pct >= 100 THEN
|
|---|
| 753 | RAISE EXCEPTION
|
|---|
| 754 | '[Discount] Percentage must be 0 < p_pct < 100. Provided: %', p_pct;
|
|---|
| 755 | END IF;
|
|---|
| 756 |
|
|---|
| 757 | IF p_from > p_to THEN
|
|---|
| 758 | RAISE EXCEPTION '[Discount] "From" date must be before "To" date.';
|
|---|
| 759 | END IF;
|
|---|
| 760 |
|
|---|
| 761 | IF NOT EXISTS (SELECT 1 FROM Menu WHERE Id = p_menu_id) THEN
|
|---|
| 762 | RAISE EXCEPTION '[Discount] Menu #% does not exist.', p_menu_id;
|
|---|
| 763 | END IF;
|
|---|
| 764 |
|
|---|
| 765 | INSERT INTO Discount ("Name", "From", "To", MenuId, Status)
|
|---|
| 766 | VALUES (p_name, p_from, p_to, p_menu_id, TRUE)
|
|---|
| 767 | RETURNING Id INTO v_disc_id;
|
|---|
| 768 |
|
|---|
| 769 | FOR v_item IN
|
|---|
| 770 | SELECT mm.Id AS MemberId, mm.MenuItemId, mm.Price
|
|---|
| 771 | FROM MenuMember mm
|
|---|
| 772 | WHERE mm.MenuId = p_menu_id
|
|---|
| 773 | LOOP
|
|---|
| 774 | v_new_prc := ROUND(v_item.Price * (1.0 - p_pct / 100.0), 2);
|
|---|
| 775 |
|
|---|
| 776 | INSERT INTO DiscountItem
|
|---|
| 777 | (NewPrice, MenuMemberId, MenuMemberMenuItemId, DiscountId)
|
|---|
| 778 | VALUES
|
|---|
| 779 | (v_new_prc, v_item.MemberId, v_item.MenuItemId, v_disc_id);
|
|---|
| 780 |
|
|---|
| 781 | v_count := v_count + 1;
|
|---|
| 782 | END LOOP;
|
|---|
| 783 |
|
|---|
| 784 | IF v_count = 0 THEN
|
|---|
| 785 | RAISE WARNING
|
|---|
| 786 | '[Discount] Menu #% has no items. Discount created but is empty.',
|
|---|
| 787 | p_menu_id;
|
|---|
| 788 | ELSE
|
|---|
| 789 | RAISE NOTICE
|
|---|
| 790 | '[Discount] "%" (% %%) applied to % items. Valid: % to %.',
|
|---|
| 791 | p_name, p_pct, v_count, p_from, p_to;
|
|---|
| 792 | END IF;
|
|---|
| 793 | END;
|
|---|
| 794 | $$;
|
|---|
| 795 |
|
|---|
| 796 |
|
|---|
| 797 |
|
|---|
| 798 | SELECT routine_name, routine_type
|
|---|
| 799 | FROM information_schema.routines
|
|---|
| 800 | WHERE routine_schema = 'public'
|
|---|
| 801 | AND routine_name IN (
|
|---|
| 802 | 'fn_calculate_order_total',
|
|---|
| 803 | 'fn_get_available_tables',
|
|---|
| 804 | 'fn_low_stock_report',
|
|---|
| 805 | 'sp_create_order',
|
|---|
| 806 | 'sp_add_order_item',
|
|---|
| 807 | 'sp_create_reservation',
|
|---|
| 808 | 'sp_cancel_order',
|
|---|
| 809 | 'sp_close_order_and_invoice',
|
|---|
| 810 | 'sp_restock_product',
|
|---|
| 811 | 'sp_apply_discount_to_menu'
|
|---|
| 812 | )
|
|---|
| 813 | ORDER BY routine_type, routine_name;
|
|---|
| 814 |
|
|---|
| 815 |
|
|---|
| 816 |
|
|---|
| 817 | SELECT trigger_name, event_manipulation, event_object_table, action_timing
|
|---|
| 818 | FROM information_schema.triggers
|
|---|
| 819 | WHERE trigger_schema = 'public'
|
|---|
| 820 | AND trigger_name IN (
|
|---|
| 821 | 'trg_prevent_closed_order_edit',
|
|---|
| 822 | 'trg_deduct_inventory_on_item_finish',
|
|---|
| 823 | 'trg_log_stock_change'
|
|---|
| 824 | )
|
|---|
| 825 | ORDER BY trigger_name;
|
|---|