-- Migration: Additional SQL Functions for Inventory Management -- Week 2: Helper functions for common queries -- Function: Get inventory items with full details (tags, product info, unit conversion) CREATE OR REPLACE FUNCTION get_inventory_details() RETURNS TABLE ( item_id UUID, item_name TEXT, quantity DECIMAL, unit_abbreviation TEXT, unit_name TEXT, expiry_date DATE, days_until_expiry INTEGER, tags TEXT[], product_brand TEXT, product_image_url TEXT, product_barcode TEXT, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ ) AS $$ BEGIN RETURN QUERY SELECT i.id AS item_id, i.name AS item_name, i.quantity, u.abbreviation AS unit_abbreviation, u.name AS unit_name, i.expiry_date, (i.expiry_date - CURRENT_DATE) AS days_until_expiry, COALESCE(ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL), '{}') AS tags, p.brand AS product_brand, p.image_url AS product_image_url, p.barcode AS product_barcode, i.created_at, i.updated_at FROM inventory_items i JOIN units u ON i.unit_id = u.id LEFT JOIN products p ON i.product_id = p.id LEFT JOIN item_tags it ON i.id = it.item_id LEFT JOIN tags t ON it.tag_id = t.id GROUP BY i.id, i.name, i.quantity, u.abbreviation, u.name, i.expiry_date, p.brand, p.image_url, p.barcode, i.created_at, i.updated_at ORDER BY i.created_at DESC; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION get_inventory_details() IS 'Returns all inventory items with denormalized data for display'; -- Function: Get items expiring soon CREATE OR REPLACE FUNCTION get_expiring_items(days_ahead INTEGER DEFAULT 7) RETURNS TABLE ( item_id UUID, item_name TEXT, quantity DECIMAL, unit_abbreviation TEXT, expiry_date DATE, days_until_expiry INTEGER, tags TEXT[] ) AS $$ BEGIN RETURN QUERY SELECT i.id AS item_id, i.name AS item_name, i.quantity, u.abbreviation AS unit_abbreviation, i.expiry_date, (i.expiry_date - CURRENT_DATE) AS days_until_expiry, COALESCE(ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL), '{}') AS tags FROM inventory_items i JOIN units u ON i.unit_id = u.id LEFT JOIN item_tags it ON i.id = it.item_id LEFT JOIN tags t ON it.tag_id = t.id WHERE i.expiry_date IS NOT NULL AND i.expiry_date <= CURRENT_DATE + MAKE_INTERVAL(days => days_ahead) GROUP BY i.id, i.name, i.quantity, u.abbreviation, i.expiry_date ORDER BY i.expiry_date ASC; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION get_expiring_items(INTEGER) IS 'Returns items expiring within specified days (default 7)'; -- Function: Get items by tag CREATE OR REPLACE FUNCTION get_items_by_tag(tag_name TEXT) RETURNS TABLE ( item_id UUID, item_name TEXT, quantity DECIMAL, unit_abbreviation TEXT, expiry_date DATE, created_at TIMESTAMPTZ ) AS $$ BEGIN RETURN QUERY SELECT i.id AS item_id, i.name AS item_name, i.quantity, u.abbreviation AS unit_abbreviation, i.expiry_date, i.created_at FROM inventory_items i JOIN units u ON i.unit_id = u.id JOIN item_tags it ON i.id = it.item_id JOIN tags t ON it.tag_id = t.id WHERE t.name ILIKE tag_name ORDER BY i.created_at DESC; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION get_items_by_tag(TEXT) IS 'Returns all items with specified tag (case-insensitive)'; -- Function: Get low stock items (quantity <= threshold) CREATE OR REPLACE FUNCTION get_low_stock_items(threshold DECIMAL DEFAULT 1.0) RETURNS TABLE ( item_id UUID, item_name TEXT, quantity DECIMAL, unit_abbreviation TEXT, tags TEXT[] ) AS $$ BEGIN RETURN QUERY SELECT i.id AS item_id, i.name AS item_name, i.quantity, u.abbreviation AS unit_abbreviation, COALESCE(ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL), '{}') AS tags FROM inventory_items i JOIN units u ON i.unit_id = u.id LEFT JOIN item_tags it ON i.id = it.item_id LEFT JOIN tags t ON it.tag_id = t.id WHERE i.quantity <= threshold GROUP BY i.id, i.name, i.quantity, u.abbreviation ORDER BY i.quantity ASC, i.name ASC; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION get_low_stock_items(DECIMAL) IS 'Returns items with quantity at or below threshold'; -- Function: Update item quantity (consume or restock) CREATE OR REPLACE FUNCTION update_item_quantity( item_uuid UUID, quantity_change DECIMAL, delete_if_zero BOOLEAN DEFAULT TRUE ) RETURNS BOOLEAN AS $$ DECLARE new_quantity DECIMAL; BEGIN -- Calculate new quantity SELECT quantity + quantity_change INTO new_quantity FROM inventory_items WHERE id = item_uuid; IF new_quantity IS NULL THEN RETURN FALSE; -- Item not found END IF; -- Delete if zero and flag is set IF new_quantity <= 0 AND delete_if_zero THEN DELETE FROM inventory_items WHERE id = item_uuid; RETURN TRUE; END IF; -- Update quantity (ensure non-negative) UPDATE inventory_items SET quantity = GREATEST(new_quantity, 0), updated_at = NOW() WHERE id = item_uuid; RETURN TRUE; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION update_item_quantity(UUID, DECIMAL, BOOLEAN) IS 'Updates item quantity (positive for restock, negative for consume). Optionally deletes if zero.'; -- Function: Get inventory statistics CREATE OR REPLACE FUNCTION get_inventory_stats() RETURNS TABLE ( total_items BIGINT, total_unique_products BIGINT, items_expiring_week BIGINT, items_expired BIGINT, total_tags_used BIGINT ) AS $$ BEGIN RETURN QUERY SELECT COUNT(DISTINCT i.id) AS total_items, COUNT(DISTINCT i.product_id) FILTER (WHERE i.product_id IS NOT NULL) AS total_unique_products, COUNT(i.id) FILTER ( WHERE i.expiry_date IS NOT NULL AND i.expiry_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '7 days' ) AS items_expiring_week, COUNT(i.id) FILTER ( WHERE i.expiry_date IS NOT NULL AND i.expiry_date < CURRENT_DATE ) AS items_expired, COUNT(DISTINCT it.tag_id) AS total_tags_used FROM inventory_items i LEFT JOIN item_tags it ON i.id = it.item_id; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION get_inventory_stats() IS 'Returns summary statistics for the entire inventory'; -- Function: Search inventory (full-text search on items and products) CREATE OR REPLACE FUNCTION search_inventory(search_query TEXT) RETURNS TABLE ( item_id UUID, item_name TEXT, quantity DECIMAL, unit_abbreviation TEXT, product_brand TEXT, tags TEXT[], relevance REAL ) AS $$ BEGIN RETURN QUERY SELECT i.id AS item_id, i.name AS item_name, i.quantity, u.abbreviation AS unit_abbreviation, p.brand AS product_brand, COALESCE(ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL), '{}') AS tags, ts_rank( to_tsvector('english', i.name || ' ' || COALESCE(p.brand, '') || ' ' || COALESCE(p.name, '')), plainto_tsquery('english', search_query) ) AS relevance FROM inventory_items i JOIN units u ON i.unit_id = u.id LEFT JOIN products p ON i.product_id = p.id LEFT JOIN item_tags it ON i.id = it.item_id LEFT JOIN tags t ON it.tag_id = t.id WHERE to_tsvector('english', i.name || ' ' || COALESCE(p.brand, '') || ' ' || COALESCE(p.name, '')) @@ plainto_tsquery('english', search_query) GROUP BY i.id, i.name, i.quantity, u.abbreviation, p.brand, p.name ORDER BY relevance DESC, i.created_at DESC LIMIT 50; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION search_inventory(TEXT) IS 'Full-text search across inventory items and products';