-- Migration 00003: Transactional Stock Adjustment RPC (adjust_product_stock with FEFO) CREATE OR REPLACE FUNCTION public.adjust_product_stock( p_household_id UUID, p_product_id UUID, p_quantity_change NUMERIC, p_reason public.stock_movement_reason, p_batch_id UUID DEFAULT NULL, p_note TEXT DEFAULT NULL ) RETURNS NUMERIC LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_user_id UUID; v_total_before NUMERIC := 0.0; v_total_after NUMERIC := 0.0; v_target_batch RECORD; v_remaining_to_deduct NUMERIC; v_deduct_amount NUMERIC; BEGIN v_user_id := auth.uid(); -- 1. Check permissions IF NOT public.has_household_role(p_household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]) THEN RAISE EXCEPTION 'PERMISSION_DENIED: User is not authorized to modify stock in household %', p_household_id; END IF; -- 2. Lock product and calculate current total stock PERFORM 1 FROM public.products WHERE id = p_product_id AND household_id = p_household_id FOR UPDATE; SELECT COALESCE(SUM(quantity), 0.0) INTO v_total_before FROM public.inventory_batches WHERE household_id = p_household_id AND product_id = p_product_id; -- 3. Check for negative stock IF (v_total_before + p_quantity_change) < 0 THEN RAISE EXCEPTION 'INSUFFICIENT_STOCK: Required quantity reduction exceeds current total stock (%)', v_total_before; END IF; -- 4. Apply changes IF p_batch_id IS NOT NULL THEN -- Target specific batch SELECT * INTO v_target_batch FROM public.inventory_batches WHERE id = p_batch_id AND product_id = p_product_id AND household_id = p_household_id FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'BATCH_NOT_FOUND: Specified batch % does not exist', p_batch_id; END IF; IF (v_target_batch.quantity + p_quantity_change) < 0 THEN RAISE EXCEPTION 'INSUFFICIENT_BATCH_STOCK: Batch quantity cannot be reduced below 0'; END IF; UPDATE public.inventory_batches SET quantity = quantity + p_quantity_change, updated_at = NOW() WHERE id = p_batch_id; ELSE -- No explicit batch provided IF p_quantity_change > 0 THEN -- Increase stock: Add to default batch (batch without expiration_date or newest) or create new batch SELECT id INTO p_batch_id FROM public.inventory_batches WHERE household_id = p_household_id AND product_id = p_product_id AND expiration_date IS NULL ORDER BY created_at DESC LIMIT 1; IF p_batch_id IS NOT NULL THEN UPDATE public.inventory_batches SET quantity = quantity + p_quantity_change, updated_at = NOW() WHERE id = p_batch_id; ELSE INSERT INTO public.inventory_batches (household_id, product_id, quantity) VALUES (p_household_id, p_product_id, p_quantity_change) RETURNING id INTO p_batch_id; END IF; ELSIF p_quantity_change < 0 THEN -- FEFO Deduction (First Expire, First Out) v_remaining_to_deduct := ABS(p_quantity_change); FOR v_target_batch IN SELECT id, quantity FROM public.inventory_batches WHERE household_id = p_household_id AND product_id = p_product_id AND quantity > 0 ORDER BY expiration_date ASC NULLS LAST, created_at ASC FOR UPDATE LOOP IF v_remaining_to_deduct <= 0 THEN EXIT; END IF; v_deduct_amount := LEAST(v_target_batch.quantity, v_remaining_to_deduct); UPDATE public.inventory_batches SET quantity = quantity - v_deduct_amount, updated_at = NOW() WHERE id = v_target_batch.id; v_remaining_to_deduct := v_remaining_to_deduct - v_deduct_amount; END LOOP; END IF; END IF; -- 5. Calculate new total stock SELECT COALESCE(SUM(quantity), 0.0) INTO v_total_after FROM public.inventory_batches WHERE household_id = p_household_id AND product_id = p_product_id; -- 6. Insert stock_movements record INSERT INTO public.stock_movements ( household_id, product_id, batch_id, quantity_before, quantity_change, quantity_after, reason, note, created_by ) VALUES ( p_household_id, p_product_id, p_batch_id, v_total_before, p_quantity_change, v_total_after, p_reason, p_note, v_user_id ); RETURN v_total_after; END; $$;