-- +goose Up -- +goose StatementBegin -- ========================================= -- ФУНКЦИЯ: accept_volunteer_response -- Принятие отклика волонтера с назначением на заявку -- ========================================= CREATE OR REPLACE FUNCTION accept_volunteer_response( p_response_id BIGINT, p_requester_id BIGINT ) RETURNS TABLE ( success BOOLEAN, message TEXT, out_request_id BIGINT, out_volunteer_id BIGINT ) AS $$ DECLARE v_request_id BIGINT; v_volunteer_id BIGINT; v_request_status request_status; v_response_status VARCHAR(20); v_assigned_volunteer_id BIGINT; v_request_requester_id BIGINT; BEGIN -- Получаем информацию об отклике и связанной заявке SELECT vr.request_id, vr.volunteer_id, vr.status, r.status, r.assigned_volunteer_id, r.requester_id INTO v_request_id, v_volunteer_id, v_response_status, v_request_status, v_assigned_volunteer_id, v_request_requester_id FROM volunteer_responses vr JOIN requests r ON r.id = vr.request_id WHERE vr.id = p_response_id AND r.deleted_at IS NULL; -- Проверка: отклик существует IF v_request_id IS NULL THEN RETURN QUERY SELECT FALSE, 'Volunteer response not found'::TEXT, NULL::BIGINT, NULL::BIGINT; RETURN; END IF; -- Проверка: заявка принадлежит заявителю IF v_request_requester_id != p_requester_id THEN RETURN QUERY SELECT FALSE, 'You are not the owner of this request'::TEXT, v_request_id, v_volunteer_id; RETURN; END IF; -- Проверка: заявка в статусе 'approved' IF v_request_status != 'approved' THEN RETURN QUERY SELECT FALSE, format('Request must be in approved status, current status: %s', v_request_status)::TEXT, v_request_id, v_volunteer_id; RETURN; END IF; -- Проверка: отклик в статусе 'pending' IF v_response_status != 'pending' THEN RETURN QUERY SELECT FALSE, format('Response must be in pending status, current status: %s', v_response_status)::TEXT, v_request_id, v_volunteer_id; RETURN; END IF; -- Проверка: заявка еще не взята другим волонтером IF v_assigned_volunteer_id IS NOT NULL THEN RETURN QUERY SELECT FALSE, 'Request already assigned to another volunteer'::TEXT, v_request_id, v_volunteer_id; RETURN; END IF; -- Все проверки пройдены, выполняем операцию -- 1. Принимаем отклик UPDATE volunteer_responses SET status = 'accepted', accepted_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = p_response_id; -- 2. Назначаем волонтера на заявку и меняем статус UPDATE requests SET assigned_volunteer_id = v_volunteer_id, status = 'in_progress', updated_at = CURRENT_TIMESTAMP WHERE id = v_request_id; -- 3. Отклоняем все остальные отклики на эту заявку UPDATE volunteer_responses SET status = 'rejected', rejected_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE request_id = v_request_id AND id != p_response_id AND status = 'pending'; -- Триггер log_request_status_change автоматически создаст запись в request_status_history RETURN QUERY SELECT TRUE, 'Volunteer response accepted successfully'::TEXT, v_request_id, v_volunteer_id; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION accept_volunteer_response IS 'Атомарное принятие отклика волонтера: обновление статусов заявки и отклика, отклонение остальных откликов. Предотвращает race conditions.'; -- ========================================= -- ФУНКЦИЯ: complete_request_with_rating -- Завершение заявки с обязательным выставлением рейтинга -- ========================================= CREATE OR REPLACE FUNCTION complete_request_with_rating( p_request_id BIGINT, p_requester_id BIGINT, p_rating INTEGER, p_comment TEXT DEFAULT NULL ) RETURNS TABLE ( success BOOLEAN, message TEXT, out_rating_id BIGINT ) AS $$ DECLARE v_request_status request_status; v_volunteer_id BIGINT; v_requester_id BIGINT; v_response_id BIGINT; v_rating_id BIGINT; BEGIN -- Получаем информацию о заявке SELECT r.status, r.assigned_volunteer_id, r.requester_id INTO v_request_status, v_volunteer_id, v_requester_id FROM requests r WHERE r.id = p_request_id AND r.deleted_at IS NULL; -- Проверка: заявка существует IF v_request_status IS NULL THEN RETURN QUERY SELECT FALSE, 'Request not found'::TEXT, 0::BIGINT; RETURN; END IF; -- Проверка: заявка принадлежит заявителю IF v_requester_id != p_requester_id THEN RETURN QUERY SELECT FALSE, 'You are not the owner of this request'::TEXT, 0::BIGINT; RETURN; END IF; -- Проверка: заявка в статусе 'in_progress' IF v_request_status != 'in_progress' THEN RETURN QUERY SELECT FALSE, format('Request must be in in_progress status, current status: %s', v_request_status)::TEXT, 0::BIGINT; RETURN; END IF; -- Проверка: есть назначенный волонтер IF v_volunteer_id IS NULL THEN RETURN QUERY SELECT FALSE, 'Request has no assigned volunteer'::TEXT, 0::BIGINT; RETURN; END IF; -- Проверка: рейтинг от 1 до 5 IF p_rating < 1 OR p_rating > 5 THEN RETURN QUERY SELECT FALSE, 'Rating must be between 1 and 5'::TEXT, 0::BIGINT; RETURN; END IF; -- Все проверки пройдены, выполняем операцию -- 1. Завершаем заявку UPDATE requests SET status = 'completed', completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = p_request_id; -- 2. Получаем ID отклика (для связи с рейтингом) SELECT id INTO v_response_id FROM volunteer_responses WHERE request_id = p_request_id AND volunteer_id = v_volunteer_id AND status = 'accepted' LIMIT 1; -- 3. Создаем рейтинг INSERT INTO ratings ( volunteer_response_id, volunteer_id, requester_id, request_id, rating, comment ) VALUES ( v_response_id, v_volunteer_id, p_requester_id, p_request_id, p_rating, p_comment ) RETURNING id INTO v_rating_id; -- Триггер update_volunteer_rating автоматически пересчитает рейтинг волонтера -- Триггер log_request_status_change автоматически создаст запись в request_status_history RETURN QUERY SELECT TRUE, 'Request completed and rating saved successfully'::TEXT, v_rating_id; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION complete_request_with_rating IS 'Атомарное завершение заявки с обязательным выставлением рейтинга волонтеру. Триггер автоматически пересчитает средний рейтинг волонтера.'; -- ========================================= -- ФУНКЦИЯ: moderate_request -- Универсальная функция модерации заявок -- ========================================= CREATE OR REPLACE FUNCTION moderate_request( p_request_id BIGINT, p_moderator_id BIGINT, p_action TEXT, -- 'approve' или 'reject' p_comment TEXT DEFAULT NULL ) RETURNS TABLE ( success BOOLEAN, message TEXT ) AS $$ DECLARE v_request_status request_status; v_has_permission BOOLEAN; v_new_status request_status; BEGIN -- Проверка: модератор имеет право модерировать SELECT has_permission(p_moderator_id, 'moderate_requests') INTO v_has_permission; IF NOT v_has_permission THEN RETURN QUERY SELECT FALSE, 'You do not have permission to moderate requests'::TEXT; RETURN; END IF; -- Получаем текущий статус заявки SELECT status INTO v_request_status FROM requests WHERE id = p_request_id AND deleted_at IS NULL; -- Проверка: заявка существует IF v_request_status IS NULL THEN RETURN QUERY SELECT FALSE, 'Request not found'::TEXT; RETURN; END IF; -- Проверка: заявка в статусе 'pending_moderation' IF v_request_status != 'pending_moderation' THEN RETURN QUERY SELECT FALSE, format('Request must be in pending_moderation status, current status: %s', v_request_status)::TEXT; RETURN; END IF; -- Определяем новый статус IF p_action = 'approve' THEN v_new_status := 'approved'; ELSIF p_action = 'reject' THEN v_new_status := 'rejected'; -- При отклонении комментарий обязателен IF p_comment IS NULL OR trim(p_comment) = '' THEN RETURN QUERY SELECT FALSE, 'Comment is required when rejecting a request'::TEXT; RETURN; END IF; ELSE RETURN QUERY SELECT FALSE, format('Invalid action: %s. Must be approve or reject', p_action)::TEXT; RETURN; END IF; -- Все проверки пройдены, выполняем модерацию UPDATE requests SET status = v_new_status, moderated_by = p_moderator_id, moderated_at = CURRENT_TIMESTAMP, moderation_comment = p_comment, updated_at = CURRENT_TIMESTAMP WHERE id = p_request_id; -- Триггер audit_moderation_action автоматически создаст запись в moderator_actions -- Триггер log_request_status_change автоматически создаст запись в request_status_history RETURN QUERY SELECT TRUE, format('Request %s successfully', p_action)::TEXT; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION moderate_request IS 'Универсальная функция модерации заявок с проверкой прав, валидацией статуса и автоматическим аудитом через триггеры.'; -- +goose StatementEnd -- +goose Down -- +goose StatementBegin DROP FUNCTION IF EXISTS moderate_request; DROP FUNCTION IF EXISTS complete_request_with_rating; DROP FUNCTION IF EXISTS accept_volunteer_response; -- +goose StatementEnd