-- Stock Management Stored Procedures - UPDATED VERSION -- Centralized inventory mutation logic in PostgreSQL with proper schema references -- Simplified stock movement function with idempotency and proper schema references CREATE OR REPLACE FUNCTION apply_stock_movement( p_merchant_id TEXT, p_warehouse_id TEXT, p_sku TEXT, p_batch_no TEXT, p_qty NUMERIC, p_txn_type TEXT, p_ref_type TEXT, p_ref_id UUID, p_ref_no TEXT, p_user TEXT ) RETURNS VOID AS $$ BEGIN -- Idempotency check - prevent duplicate processing IF EXISTS ( SELECT 1 FROM trans.scm_stock_ledger WHERE ref_type = p_ref_type AND ref_id = p_ref_id AND sku = p_sku AND batch_no = p_batch_no ) THEN RETURN; END IF; -- Insert ledger entry (immutable audit trail) INSERT INTO trans.scm_stock_ledger ( ledger_id, merchant_id, warehouse_id, sku, batch_no, qty, txn_type, ref_type, ref_id, ref_no, created_by, created_at ) VALUES ( gen_random_uuid(), p_merchant_id, p_warehouse_id, p_sku, p_batch_no, p_qty, p_txn_type, p_ref_type, p_ref_id, p_ref_no, p_user, NOW() ); -- Stock snapshot upsert (create or update) INSERT INTO trans.scm_stock ( stock_id, merchant_id, warehouse_id, sku, batch_no, qty_on_hand, qty_reserved, qty_available, created_at, updated_at, last_updated_at ) VALUES ( gen_random_uuid(), p_merchant_id, p_warehouse_id, p_sku, p_batch_no, GREATEST(p_qty, 0), 0, GREATEST(p_qty, 0), NOW(), NOW(), NOW() ) ON CONFLICT (merchant_id, warehouse_id, sku, batch_no) DO UPDATE SET qty_on_hand = trans.scm_stock.qty_on_hand + p_qty, qty_available = GREATEST(trans.scm_stock.qty_on_hand + p_qty - trans.scm_stock.qty_reserved, 0), updated_at = NOW(), last_updated_at = NOW(); END; $$ LANGUAGE plpgsql; -- Enhanced bulk stock movement function with proper error handling CREATE OR REPLACE FUNCTION apply_bulk_stock_movements( p_movements JSONB ) RETURNS JSONB LANGUAGE plpgsql AS $$ DECLARE v_movement JSONB; v_results JSONB := '[]'::JSONB; v_result JSONB; v_success BOOLEAN := TRUE; v_error_msg TEXT; BEGIN -- Process each movement in the array FOR v_movement IN SELECT * FROM jsonb_array_elements(p_movements) LOOP BEGIN -- Apply individual stock movement PERFORM apply_stock_movement( (v_movement->>'merchant_id')::TEXT, (v_movement->>'warehouse_id')::TEXT, (v_movement->>'sku')::TEXT, (v_movement->>'batch_no')::TEXT, (v_movement->>'qty')::NUMERIC, (v_movement->>'txn_type')::TEXT, (v_movement->>'ref_type')::TEXT, (v_movement->>'ref_id')::UUID, (v_movement->>'ref_no')::TEXT, (v_movement->>'created_by')::TEXT ); -- Build success result object v_result := jsonb_build_object( 'ledger_id', gen_random_uuid(), 'sku', v_movement->>'sku', 'qty', v_movement->>'qty', 'success', true, 'error', null ); EXCEPTION WHEN OTHERS THEN -- Handle individual movement errors v_success := FALSE; v_error_msg := SQLERRM; v_result := jsonb_build_object( 'ledger_id', null, 'sku', v_movement->>'sku', 'qty', v_movement->>'qty', 'success', false, 'error', v_error_msg ); END; -- Add to results array v_results := v_results || v_result; END LOOP; -- If any movement failed, rollback the entire transaction IF NOT v_success THEN RAISE EXCEPTION 'One or more stock movements failed. Transaction rolled back.'; END IF; RETURN v_results; END; $$; -- Function to reserve stock (for sales orders) with proper schema CREATE OR REPLACE FUNCTION reserve_stock( p_merchant_id VARCHAR(64), p_warehouse_id VARCHAR(64), p_sku VARCHAR(64), p_batch_no VARCHAR(50), p_qty NUMERIC(14,3), p_ref_id UUID, p_created_by VARCHAR(64) ) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ DECLARE v_available_qty NUMERIC(14,3); BEGIN -- Get current available quantity SELECT qty_available INTO v_available_qty FROM trans.scm_stock WHERE merchant_id = p_merchant_id AND warehouse_id = p_warehouse_id AND sku = p_sku AND batch_no = p_batch_no; -- Check if sufficient stock available IF v_available_qty IS NULL OR v_available_qty < p_qty THEN RAISE EXCEPTION 'Insufficient available stock for SKU: %. Available: %, Required: %', p_sku, COALESCE(v_available_qty, 0), p_qty; END IF; -- Update stock reservation UPDATE trans.scm_stock SET qty_reserved = qty_reserved + p_qty, qty_available = qty_available - p_qty, updated_at = NOW(), last_updated_at = NOW() WHERE merchant_id = p_merchant_id AND warehouse_id = p_warehouse_id AND sku = p_sku AND batch_no = p_batch_no; RETURN TRUE; END; $$; -- Function to release stock reservation with proper schema CREATE OR REPLACE FUNCTION release_stock_reservation( p_merchant_id VARCHAR(64), p_warehouse_id VARCHAR(64), p_sku VARCHAR(64), p_batch_no VARCHAR(50), p_qty NUMERIC(14,3), p_ref_id UUID, p_created_by VARCHAR(64) ) RETURNS BOOLEAN LANGUAGE plpgsql AS $$ BEGIN -- Update stock reservation UPDATE trans.scm_stock SET qty_reserved = GREATEST(qty_reserved - p_qty, 0), qty_available = qty_on_hand - GREATEST(qty_reserved - p_qty, 0), updated_at = NOW(), last_updated_at = NOW() WHERE merchant_id = p_merchant_id AND warehouse_id = p_warehouse_id AND sku = p_sku AND batch_no = p_batch_no; RETURN TRUE; END; $$; -- Function to get current stock summary with proper schema CREATE OR REPLACE FUNCTION get_stock_summary( p_merchant_id VARCHAR(64), p_warehouse_id VARCHAR(64) DEFAULT NULL, p_sku VARCHAR(64) DEFAULT NULL ) RETURNS TABLE ( stock_id UUID, warehouse_id VARCHAR(64), catalogue_id UUID, sku VARCHAR(64), batch_no VARCHAR(50), qty_on_hand NUMERIC(14,3), qty_reserved NUMERIC(14,3), qty_available NUMERIC(14,3), uom VARCHAR(10), last_updated_at TIMESTAMP ) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT s.stock_id, s.warehouse_id, s.catalogue_id, s.sku, s.batch_no, s.qty_on_hand, s.qty_reserved, s.qty_available, s.uom, s.last_updated_at FROM trans.scm_stock s WHERE s.merchant_id = p_merchant_id AND (p_warehouse_id IS NULL OR s.warehouse_id = p_warehouse_id) AND (p_sku IS NULL OR s.sku = p_sku) ORDER BY s.sku, s.batch_no; END; $$; -- Function to get stock ledger history with proper schema CREATE OR REPLACE FUNCTION get_stock_ledger( p_merchant_id VARCHAR(64), p_sku VARCHAR(64) DEFAULT NULL, p_batch_no VARCHAR(50) DEFAULT NULL, p_limit INTEGER DEFAULT 100 ) RETURNS TABLE ( ledger_id UUID, warehouse_id VARCHAR(64), catalogue_id UUID, sku VARCHAR(64), batch_no VARCHAR(50), txn_type VARCHAR(30), qty NUMERIC(14,3), uom VARCHAR(10), ref_type VARCHAR(30), ref_id UUID, ref_no VARCHAR(50), remarks TEXT, created_by VARCHAR(64), created_at TIMESTAMP ) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT l.ledger_id, l.warehouse_id, l.catalogue_id, l.sku, l.batch_no, l.txn_type, l.qty, l.uom, l.ref_type, l.ref_id, l.ref_no, l.remarks, l.created_by, l.created_at FROM trans.scm_stock_ledger l WHERE l.merchant_id = p_merchant_id AND (p_sku IS NULL OR l.sku = p_sku) AND (p_batch_no IS NULL OR l.batch_no = p_batch_no) ORDER BY l.created_at DESC LIMIT p_limit; END; $$;