-- complete_order: transisi delivering -> completed secara atomik & idempotent. -- Idempotensi: partial unique index stock_movements_order_out_unique_idx -- ON (reference_type, reference_id) WHERE type='order_out' AND reference_type='order' -- membuat insert mutasi ganda otomatis dilewati (ON CONFLICT DO NOTHING). CREATE OR REPLACE FUNCTION public.complete_order(p_order_id uuid, p_actor uuid DEFAULT NULL) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $fn$ DECLARE v_order public.orders%ROWTYPE; v_item record; v_new_balance integer; BEGIN SELECT * INTO v_order FROM public.orders WHERE id = p_order_id FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'ORDER_NOT_FOUND'; END IF; -- Sudah selesai: anggap sukses (idempotent), tanpa efek stok. IF v_order.order_status = 'completed' THEN RETURN; END IF; IF v_order.deleted_at IS NOT NULL THEN RAISE EXCEPTION 'ORDER_DELETED'; END IF; IF v_order.order_status <> 'delivering' THEN RAISE EXCEPTION 'INVALID_TRANSITION:%_TO_COMPLETED', v_order.order_status; END IF; FOR v_item IN SELECT oi.product_id, oi.quantity FROM public.order_items oi WHERE oi.order_id = p_order_id ORDER BY oi.product_id FOR UPDATE OF oi LOOP SELECT s.quantity INTO v_new_balance FROM public.stocks s WHERE s.product_id = v_item.product_id FOR UPDATE; v_new_balance := COALESCE(v_new_balance, 0) - v_item.quantity; IF v_new_balance < 0 THEN RAISE EXCEPTION 'INSUFFICIENT_STOCK:%', v_item.product_id; END IF; INSERT INTO public.stock_movements (product_id, type, quantity, reference_type, reference_id, notes, created_by) VALUES (v_item.product_id, 'order_out', -v_item.quantity, 'order', p_order_id, NULL, p_actor) ON CONFLICT DO NOTHING; UPDATE public.stocks s SET quantity = v_new_balance, updated_at = now() WHERE s.product_id = v_item.product_id; END LOOP; UPDATE public.orders SET order_status = 'completed', completed_at = now(), updated_at = now() WHERE id = p_order_id; INSERT INTO public.order_status_histories (order_id, old_status, new_status, changed_by) VALUES (p_order_id, v_order.order_status, 'completed', COALESCE(p_actor, v_order.created_by)); END; $fn$;