307 lines
11 KiB
PL/PgSQL
307 lines
11 KiB
PL/PgSQL
-- +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
|