Untitled

 avatar
4ae4d
plain_text
a month ago
80 kB
5
Indexable
f=services/bot/core/services/event.py
f=services/bot/core/clients/event.py
f=services/bot/core/services/requests.py
f=services/bot/core/clients/request.py
f=services/bot/core/handlers/request.py
f=services/bot/core/markups/request.py
f=services/bot/core/handlers/events_ui.py
f=services/backend/internal/drivers/http/v1/volunteering/events/controller.go
f=services/backend/internal/drivers/http/v1/volunteering/events/routes.go
f=services/backend/internal/domain/volunteering/events/service.go
--- services/bot/core/services/event.py ---
from datetime import datetime, timezone
from typing import Any

from requests import HTTPError

from core.clients import EventClient
from core.schemas import EventCardSchema, ParticipationSchema
from core.services.base import BaseService
from core.services.registry import BaseServiceRegistry
from core.utils import logger


class EventService(BaseService):  # legacy
    """
    сервис мероприятий на стороне бота.

    на данный момент:
    - ходит в backend через EventClient
    - умеет получать список мероприятий по городу
    - умеет получать одно мероприятие по id
    - создает мероприятие через клиент используя мастера создания
    """

    def __init__(self, registry: BaseServiceRegistry, client: EventClient):  # legacy
        # DI может вызвать клиент без аргументов.
        super().__init__(registry)
        self.client = client

    def get_event_cards_page(
        self,
        *,
        requester_messenger_id: int,
        page: int = 1,
        limit: int = 10,
        title_str: str | None = None,
        tag_ids: list[str] | None = None,
        mine: bool = False,
    ) -> tuple[list[EventCardSchema], int, int]:  # меняем тип возвращаемого списка
        resp = (
            self.client.get_my_event_cards_page(
                requester_messenger_id=requester_messenger_id,
                page=page,
                limit=limit,
                title_str=title_str,
                tag_ids=tag_ids,
            )
            if mine
            else self.client.get_event_cards_page(
                requester_messenger_id=requester_messenger_id,
                page=page,
                limit=limit,
                title_str=title_str,
                tag_ids=tag_ids,
            )
        )

        match resp.status_code:
            case 200:
                pass
            case 400:
                raise HTTPError("Error while getting event cards list: invalid parameters")
            case 401:
                raise HTTPError("Error while getting event cards list: unauthorized")
            case 500:
                raise HTTPError("Error while getting event cards list: internal server error")
            case _:
                raise HTTPError(
                    f"Error while getting event cards list: unexpected status {resp.status_code}"
                )

        body = resp.json() or {}
        items = body.get("event_cards") or []
        page_meta = body.get("page_meta") or {}

        result: list[EventCardSchema] = []
        for item in items:
            if not isinstance(item, dict):
                raise ValueError("Invalid event card contract: item must be dict")
            result.append(EventCardSchema.from_dict(item))

        current_page = int(page_meta.get("current_page") or page)
        total_pages = int(page_meta.get("total_pages") or current_page)

        return result, current_page, total_pages

    def sign_up(self, event_id: str, messenger_id: int) -> bool:  # legacy
        """messenger_id = peer.id (как на бэкенде: user.get_by_messenger_id)"""
        resp = self.client.sign_up(event_id=event_id, messenger_id=messenger_id)
        return resp.status_code == 200

    def get_event_by_id(
        self,
        event_id: str,
        requester_messenger_id: int,
        *,
        mine: bool = False,
    ) -> EventCardSchema | None:
        logger.debug(
            "[EventService] get_event_by_id: event_id=%s requester=%s mine=%s",
            event_id,
            requester_messenger_id,
            mine,
        )
        resp = (
            self.client.get_my_event_info(
                event_id=event_id,
                requester_messenger_id=requester_messenger_id,
            )
            if mine
            else self.client.get_event_info(
                event_id=event_id,
                requester_messenger_id=requester_messenger_id,
            )
        )

        match resp.status_code:
            case 200:
                pass
            case 400:
                raise HTTPError("Error while getting event card: invalid parameters")
            case 401:
                raise HTTPError("Error while getting event card: unauthorized")
            case 404:
                return None
            case 500:
                raise HTTPError("Error while getting event card: internal server error")
            case _:
                raise HTTPError(
                    f"Error while getting event card: unexpected status {resp.status_code}"
                )

        logger.debug(
            "[EventService] get_event_by_id: body=%s",
            resp.json(),
        )

        return EventCardSchema.from_dict(resp.json() or {})

    def sign_out_by_event(self, event_id: str, messenger_id: int) -> bool:
        resp = self.client.sign_out(event_id=event_id, messenger_id=messenger_id)
        return resp.status_code == 200

    def enter_code_by_event(
        self,
        event_id: str,
        messenger_id: int,
        code: str,
    ) -> ParticipationSchema | None:
        resp = self.client.confirm_participation(
            event_id=event_id,
            messenger_id=messenger_id,
            code=code,
        )

        match resp.status_code:
            case 200:
                pass
            case 400:
                return None
            case 401:
                raise HTTPError("Error while confirming participation: unauthorized")
            case 404:
                return None
            case 500:
                raise HTTPError("Error while confirming participation: internal server error")
            case _:
                raise HTTPError(
                    f"Error while confirming participation: unexpected status {resp.status_code}"
                )

        return self.get_participation_status(
            event_id=event_id,
            requester_messenger_id=messenger_id,
        )

    def get_participation_status(self, event_id: str, requester_messenger_id: int):
        resp = self.client.get_event_info(event_id, requester_messenger_id)
        card = EventCardSchema.from_dict(resp.json() or {})
        return card.participation

    def add_tag(self, event_id: str, tag_id: str, messenger_id: int) -> bool:
        resp = self.client.add_tag(event_id=event_id, tag_id=tag_id, messenger_id=messenger_id)
        return resp.status_code == 200

    def remove_tag(self, event_id: str, tag_id: str, messenger_id: int) -> bool:
        resp = self.client.remove_tag(event_id=event_id, tag_id=tag_id, messenger_id=messenger_id)
        return resp.status_code == 200

    def create_from_wizard(self, data: dict[str, Any], peer) -> dict[str, Any]:
        title = data.get("event_name", "")
        description = data.get("event_description", "")
        date_obj = data.get("event_date", "")
        time_obj = data.get("event_time", "")
        gosb_id = data.get("gosb_id", "")
        project_id = data.get("project_id", "")
        hours = data.get("hours", "")

        verification_code = data.get("event_verification_code", "")
        # Теги: массив ID, сформированный в хендлере
        tags: list[str] = data.get("event_app_tag_ids") or []
        # === Формирование RFC3339-совместимой строки ===
        event_dt = datetime.combine(date_obj, time_obj).replace(tzinfo=timezone.utc)
        date_time = event_dt.isoformat().replace("+00:00", "Z")

        resp = self.client.create_event(
            title=title,
            description=description,
            date_time=date_time,
            gosb_id=gosb_id,
            project_id=project_id,
            verification_code=verification_code,
            hours=hours,
            requester_messenger_id=peer.id,
            tags=tags if tags else None,
        )

        match resp.status_code:
            case 200:
                pass
            case 401:
                raise HTTPError("Error while creating event: unauthorized")
            case 500:
                raise HTTPError("Error while creating event: internal server error")
            case _:
                raise HTTPError(f"Error while creating event: unexpected status {resp.status_code}")

        return {"TYPE": resp.json().get("code")}  # показывать карточку мероприятия

    def update_event(self, event_id: str, messenger_id: int, payload: dict[str, int | str]):
        resp = self.client.update_event(
            event_id=event_id,
            messenger_id=messenger_id,
            payload=payload,
        )

        match resp.status_code:
            case 200:
                return True
            case 401:
                raise HTTPError("Error while patching event: unauthorized")
            case 403:
                raise HTTPError("Error while patching event: forbidden")
            case 404:
                raise HTTPError("Error while patching event: event not found")
            case 500:
                raise HTTPError("Error while patching event: internal server error")
            case _:
                raise HTTPError(f"Error while patching event: unexpected status {resp.status_code}")

--- services/bot/core/clients/event.py ---
from core.clients.base import BaseApiClient


class EventClient(BaseApiClient):
    def get_event_cards_page(
        self,
        *,
        requester_messenger_id: int,
        page: int = 1,
        limit: int = 10,
        title_str: str | None = None,
        tag_ids: list[str] | None = None,
    ):
        params: dict = {
            "page": page,
            "limit": limit,
        }
        if title_str:
            params["title_str"] = title_str
        if tag_ids:
            params["tag_id"] = tag_ids

        return self.get(
            "/events/card/list",
            headers={"Authorization": str(requester_messenger_id)},
            params=params,
        )

    def get_my_event_cards_page(
        self,
        *,
        requester_messenger_id: int,
        page: int = 1,
        limit: int = 10,
        title_str: str | None = None,
        tag_ids: list[str] | None = None,
    ):
        params: dict = {
            "page": page,
            "limit": limit,
        }
        if title_str:
            params["title_str"] = title_str
        if tag_ids:
            params["tag_id"] = tag_ids

        return self.get(
            "/events/card/list/mine",
            headers={"Authorization": str(requester_messenger_id)},
            params=params,
        )

    def get_event_info(self, event_id: str, requester_messenger_id: int):
        """GET /events/{event_id}/card"""
        return self.get(
            f"/events/{event_id}/card",
            headers={"Authorization": str(requester_messenger_id)},
        )

    def get_my_event_info(self, event_id: str, requester_messenger_id: int):
        """GET /events/{event_id}/card/mine"""
        return self.get(
            f"/events/{event_id}/card/mine",
            headers={"Authorization": str(requester_messenger_id)},
        )

    def sign_up(self, *, event_id: str, messenger_id: int):
        """POST /events/{event_id}/participations/signup"""
        return self.post(
            f"/events/{event_id}/participations/signup",
            headers={"Authorization": str(messenger_id)},
        )

    def sign_out(self, *, event_id: str, messenger_id: int):
        """DELETE /events/{event_id}/participations/cancel"""
        return self.delete(
            f"/events/{event_id}/participations/cancel",
            headers={"Authorization": str(messenger_id)},
        )

    def confirm_participation(self, *, event_id: str, messenger_id: int, code: str):
        """POST /events/{event_id}/participations/confirm"""
        return self.post(
            f"/events/{event_id}/participations/confirm",
            headers={"Authorization": str(messenger_id)},
            json={"code": code},
        )

    def add_tag(self, *, event_id: str, tag_id: str, messenger_id: int):
        """POST /events/{event_id}/tags"""
        return self.post(
            f"/events/{event_id}/tags",
            headers={"Authorization": str(messenger_id)},
            json={"tag_id": tag_id},
        )

    def remove_tag(self, *, event_id: str, tag_id: str, messenger_id: int):
        """DELETE /events/{event_id}/tags"""
        return self.delete(
            f"/events/{event_id}/tags",
            headers={"Authorization": str(messenger_id)},
            json={"tag_id": tag_id},
        )

    def create_event(
        self,
        *,
        title: str,
        description: str,
        date_time: str,
        gosb_id: str,
        project_id: str = "",
        verification_code: str,
        hours: int,
        requester_messenger_id: int,
        tags: list[str] | None = None,
    ):
        """POST /events"""
        payload = {
            "activation_code": verification_code,
            "hours": hours,
            "date_time": date_time,
            "description": description,
            "gosb_id": gosb_id,
            "project_id": project_id,
            "title": title,
        }
        if tags:
            payload["selected_tags"] = tags
        return self.post(
            "/events",
            headers={"Authorization": str(requester_messenger_id)},
            json=payload,
        )

    def update_event(self, event_id: str, messenger_id: int, payload: dict[str, int | str]):
        """PATCH /events/{event_id}"""
        return self.patch(
            f"/events/{event_id}",
            headers={"Authorization": str(messenger_id)},
            json=payload,
        )

--- services/bot/core/services/requests.py ---
from core.clients import RequestClient
from core.schemas import RequestCardSchema, RequestCardsPageSchema
from core.services.base import BaseService
from core.services.registry import BaseServiceRegistry
from core.utils import logger


class RequestService(BaseService):
    """Сервис для работы с заявками"""

    def __init__(self, registry: BaseServiceRegistry, client: RequestClient):
        super().__init__(registry)
        self.client = client

    def get_request_cards_page(
        self,
        *,
        requester_messenger_id: int,
        page: int = 1,
        limit: int = 10,
        status: str | None = None,
    ) -> RequestCardsPageSchema:
        """Получает страницу с заявками"""
        resp = self.client.get_request_cards(
            requester_messenger_id=requester_messenger_id,
            page=page,
            limit=limit,
            status=status,
        )

        if resp.status_code != 200:
            logger.error(f"[RequestService] get_request_cards_page failed: {resp.status_code}")
            return RequestCardsPageSchema(request_cards=[], current_page=1, total_pages=1)

        try:
            return RequestCardsPageSchema.from_response(resp.json())
        except Exception as e:
            logger.error(f"[RequestService] Failed to parse response: {e}")
            return RequestCardsPageSchema(request_cards=[], current_page=1, total_pages=1)

    def get_request_by_id(
        self, request_id: str, requester_messenger_id: int
    ) -> RequestCardSchema | None:
        """Получает заявку по ID"""
        resp = self.client.get_request(request_id, requester_messenger_id)

        if resp.status_code == 404:
            return None
        if resp.status_code != 200:
            logger.error(f"[RequestService] get_request_by_id failed: {resp.status_code}")
            return None

        try:
            return RequestCardSchema.from_dict(resp.json())
        except Exception as e:
            logger.error(f"[RequestService] Failed to parse request: {e}")
            return None

    def approve_request(self, request_id: str, requester_messenger_id: int) -> bool:
        """Одобряет заявку"""
        resp = self.client.approve_request(request_id, requester_messenger_id)
        return resp.status_code == 200

    def reject_request(self, request_id: str, requester_messenger_id: int, reason: str) -> bool:
        """Отклоняет заявку с причиной"""
        resp = self.client.reject_request(request_id, requester_messenger_id, reason)
        return resp.status_code == 200

--- services/bot/core/clients/request.py ---
from requests import Response

from core.clients.base import BaseApiClient


class RequestClient(BaseApiClient):
    """Клиент для работы с API заявок"""

    def get_request_cards(
        self,
        requester_messenger_id: int,
        page: int = 1,
        limit: int = 10,
        status: str | None = None,
    ) -> Response:
        """Получает список заявок"""
        url = "/requests/card/list"
        params = {
            "page": page,
            "limit": limit,
        }
        if status:
            params["status"] = status

        return self.get(
            url,
            headers={"Authorization": str(requester_messenger_id)},
            params=params,
        )

    def get_request(self, request_id: str, requester_messenger_id: int) -> Response:
        """Получает заявку по ID"""
        url = f"/requests/{request_id}/card"
        params = {"messenger_id": requester_messenger_id}

        return self.get(
            url,
            headers={"Authorization": str(requester_messenger_id)},
            params=params,
        )

    def approve_request(self, request_id: str, requester_messenger_id: int) -> Response:
        """Одобряет заявку"""
        url = f"/requests/{request_id}/approve"

        return self.post(url, headers={"Authorization": str(requester_messenger_id)})

    def reject_request(self, request_id: str, requester_messenger_id: int, reason: str) -> Response:
        """Отклоняет заявку с указанием причины"""
        url = f"/requests/{request_id}/reject"
        json_data = {
            "reject_reason": reason,
        }

        return self.post(
            url, headers={"Authorization": str(requester_messenger_id)}, json=json_data
        )

--- services/bot/core/handlers/request.py ---
from __future__ import annotations

from dataclasses import dataclass

from dialog_bot_sdk.entities.messaging import (
    InteractiveMediaGroup,
    UpdateInteractiveMediaEvent,
    UpdateMessage,
)
from dialog_bot_sdk.interactive_media import Button, MediaGroupBuilder

from core.bot_kit.fsm import FSMContext, State, StatesGroup
from core.bot_kit.router import Router
from core.config import bot
from core.markups import (
    format_request_details,
    requests_pagination_keyboard,
    requests_select_keyboard,
)
from core.schemas import RequestCardsPageSchema
from core.services import RequestService
from core.utils import delete_prev_message, delete_prev_message_by_peer

requests_rt = Router()


class RequestRejectState(StatesGroup):
    wait_reason = State()


@dataclass(frozen=True)
class RequestUIContext:
    name: str
    back_value: str
    back_label: str


_UI: dict[str, RequestUIContext] = {
    # модерация -> заявки
    "requests": RequestUIContext(
        name="requests",
        back_value="requests",
        back_label="⬅️ Назад",
    ),
    "my_requests": RequestUIContext(
        name="my_requests",
        back_value="my_requests",
        back_label="⬅️ Назад",
    ),
}


def get_ui(ctx_name: str) -> RequestUIContext:
    return _UI.get(ctx_name, _UI["requests"])


def build_context_back(*, label: str = "❌ Отмена", value: str = "", ctx: str = ""):
    ui = get_ui(ctx)  # noqa: F841


def build_back_keyboard(
    *, value: str = "", label: str = "❌ Отмена", media_id: str = "request_open"
) -> list[InteractiveMediaGroup]:
    return MediaGroupBuilder(
        [Button(media_id=media_id, value=value, label=label)],
    ).build()  # type: ignore


def _send_requests_list_screen(
    peer,
    *,
    ctx_name: str,
    offset: int,
    limit: int,
    page_data: RequestCardsPageSchema,
    header: str | None = None,
    empty_text: str | None = None,
    filter_summary: str | None = None,
    note: str | None = None,
):
    """Отправляет список заявок"""

    request_cards = page_data.request_cards
    has_prev = page_data.has_prev
    has_next = page_data.has_next

    header_part = header if header is not None else "📋 Заявки\n\n"
    header_part = f"{note}\n\n{header_part}" if note else header_part
    filter_part = f"\n\n{filter_summary}" if filter_summary else ""

    if not request_cards:
        text = (
            header_part
            + (
                empty_text
                or "⚠️ Заявок не найдено.\n\nПопробуй изменить критерии или вернись назад."
            )
            + filter_part
        )
        im: list[InteractiveMediaGroup] = []
        im += requests_pagination_keyboard(
            offset=offset,
            limit=limit,
            has_prev=has_prev,
            has_next=has_next,
            leave_value=ctx_name,
            leave_label="⬅️ Назад",
        )
        bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)
        return

    text = (
        header_part
        + f"Страница: {page_data.current_page} / {page_data.total_pages}\n\n"
        + "Выбери заявку для просмотра:"
        + filter_part
    )

    im: list[InteractiveMediaGroup] = []
    im += requests_select_keyboard(
        request_cards,
        open_media_id="request_open",
        start_index=offset + 1,
        per_row=1,
    )

    im += requests_pagination_keyboard(
        offset=offset,
        limit=limit,
        has_prev=has_prev,
        has_next=has_next,
        leave_value=ctx_name,
        leave_label="🛡️ В меню модерации",
    )

    bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)


def send_request_cards_page(
    peer,
    *,
    offset: int,
    request_service: RequestService,
    ctx_name: str,
    note: str | None = None,
    context: FSMContext | None = None,
):
    """Отправляет страницу с заявками"""
    limit = 10
    page = offset // limit + 1

    page_data = request_service.get_request_cards_page(
        requester_messenger_id=peer.id,
        page=page,
        limit=limit,
    )

    _send_requests_list_screen(
        peer,
        ctx_name=ctx_name,
        offset=offset,
        limit=limit,
        page_data=page_data,
        note=note,
    )


@bot.di
def all_requests_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    request_service: RequestService,
):
    """Обработчик для просмотра всех заявок"""
    delete_prev_message_by_peer(bot, event.peer)

    send_request_cards_page(
        event.peer,
        offset=0,
        request_service=request_service,
        ctx_name="requests",
        context=context,
    )


@bot.di
def requests_page_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    request_service: RequestService,
):
    """Обработчик пагинации заявок"""
    delete_prev_message_by_peer(bot, event.peer)

    try:
        offset = int(event.data.value)
        if offset < 0:
            offset = 0
    except Exception:
        offset = 0

    send_request_cards_page(
        event.peer,
        offset=offset,
        request_service=request_service,
        ctx_name="requests",
        context=context,
    )


@bot.di
def request_open_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    request_service: RequestService,
):
    """Обработчик открытия конкретной заявки"""
    delete_prev_message_by_peer(bot, event.peer)
    context.set_state(None)

    request_id = str(event.data.value or "").strip()

    # Получаем детали заявки
    request_card = request_service.get_request_by_id(request_id, event.peer.id)

    if request_card is None:
        bot.messaging.send_message(
            peer=event.peer,
            text="⚠️ Заявка не найдена.",
            interactive_media_groups=build_back_keyboard(media_id="requests"),
        )
        return

    # Формируем детальное сообщение
    text = format_request_details(request_card)

    # Клавиатура для действий с заявкой
    keyboard = _build_request_actions_keyboard(request_card, ctx_name="requests")

    bot.messaging.send_message(
        peer=event.peer,
        text=text,
        interactive_media_groups=keyboard,
    )


def _build_request_actions_keyboard(request_card, ctx_name: str):
    """Строит клавиатуру для действий с заявкой"""

    actions = []
    req = request_card.request

    actions.append(
        Button(
            media_id="leave",
            value=ctx_name,
            label="⬅️ Назад",
        )
    )

    if req.status.lower() == "pending":
        actions.append(
            Button(
                media_id="request_approve",
                value=req.id,
                label="✅ Одобрить",
            )
        )
        actions.append(
            Button(
                media_id="request_reject",
                value=req.id,
                label="❌ Отклонить",
            )
        )

    return MediaGroupBuilder(actions).build()


@bot.di
def requests_prev_page_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    request_service: RequestService,
):
    """Обработчик предыдущей страницы заявок"""
    delete_prev_message_by_peer(bot, event.peer)

    try:
        offset = int(event.data.value)
        if offset < 0:
            offset = 0
    except Exception:
        offset = 0

    send_request_cards_page(
        event.peer,
        offset=offset,
        request_service=request_service,
        ctx_name="moderation",
        context=context,
    )


@bot.di
def requests_next_page_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    request_service: RequestService,
):
    """Обработчик следующей страницы заявок"""
    delete_prev_message_by_peer(bot, event.peer)

    try:
        offset = int(event.data.value)
        if offset < 0:
            offset = 0
    except Exception:
        offset = 0

    send_request_cards_page(
        event.peer,
        offset=offset,
        request_service=request_service,
        ctx_name="moderation",
        context=context,
    )


@bot.di
def request_approve_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    request_service: RequestService,
):
    """Обработчик одобрения заявки"""
    delete_prev_message_by_peer(bot, event.peer)

    request_id = str(event.data.value or "").strip()

    success = request_service.approve_request(request_id, event.peer.id)

    if success:
        note = "✅ Заявка успешно одобрена!"
    else:
        note = "⚠️ Не удалось одобрить заявку. Попробуй позже."

    send_request_cards_page(
        event.peer,
        offset=0,
        request_service=request_service,
        ctx_name="moderation",
        context=context,
        note=note,
    )


@bot.di
def request_reject_handler(
    event: UpdateInteractiveMediaEvent,
    context: FSMContext,
    request_service: RequestService,
):
    """Обработчик отклонения заявки - запрашивает причину"""
    delete_prev_message_by_peer(bot, event.peer)

    request_id = str(event.data.value or "").strip()

    context.update_data({"reject_request_id": request_id})
    context.set_state(RequestRejectState.wait_reason)

    bot.messaging.send_message(
        peer=event.peer,
        text="💬 Укажите причину отклонения заявки:",
        interactive_media_groups=build_back_keyboard(value=request_id, media_id="request_open"),
    )


@requests_rt.message(state=RequestRejectState.wait_reason)
@bot.di
def request_reject_reason_handler(
    message: UpdateMessage,
    context: FSMContext,
    request_service: RequestService,
):
    """Обработчик получения причины отклонения заявки"""
    delete_prev_message(bot, message)

    data = context.get_data()
    request_id = data.get("reject_request_id")
    reason = message.message.text_message.text.strip()

    note = ""
    if not reason:
        note = "⚠️ Причина не может быть пустой."
    else:
        success = request_service.reject_request(request_id, message.peer.id, reason)
        note = "ℹ️ Заявка успешно отклонена." if success else "⚠️ Не удалось отклонить заявку."

    context.set_state(None)
    context.update_data({"reject_request_id": None})

    send_request_cards_page(
        message.peer,
        offset=0,
        request_service=request_service,
        ctx_name="moderation",
        note=note,
    )

--- services/bot/core/markups/request.py ---
from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder

from core.markups.event import format_event_details
from core.markups.pagination import pagination_keyboard
from core.schemas import RequestCardSchema
from core.utils import format_datetime, wrap_text


def _get_status_emoji(status: str) -> str:

    status_emojis = {
        "pending": "⏳",  # на рассмотрении
        "approved": "✅",  # одобрено
        "rejected": "❌",  # отклонено
    }
    return status_emojis.get(status.lower(), "⚪")


def _get_status_label(status: str) -> str:
    status_label = {
        "pending": "на рассмотрении",
        "approved": "одобрено",
        "rejected": "отлконено",
    }
    return status_label.get(status.lower(), "-")


def _get_request_type_label(request_type: str) -> str:
    types = {
        "event": "Мероприятие вне проекта",
        "project_event": "Мероприятие в рамках",
        "project": "Проект",
    }
    return types.get(request_type.lower(), request_type.capitalize())


def _request_button_label(request_card: RequestCardSchema) -> str:
    """Формирует подпись для кнопки заявки (короткий вариант)"""
    req = request_card.request
    resource = request_card.resource

    request_type = _get_request_type_label(req.request_type)

    resource_name = "—"
    if resource.event:
        resource_name = resource.event.title or "—"

    status_emoji = _get_status_emoji(req.status)
    status_label = _get_status_label(req.status)

    return f"{request_type}\nНазвание: {resource_name}\nСтатус: {status_label} {status_emoji}"


def requests_select_keyboard(
    requests_cards: list[RequestCardSchema],
    *,
    open_media_id: str,
    start_index: int = 1,
    per_row: int = 1,
) -> list[InteractiveMediaGroup]:
    """
    Клавиатура для выбора заявки из списка (аналог events_select_keyboard)
    """
    groups: list[InteractiveMediaGroup] = []
    row: list[Button] = []

    idx = start_index
    for request_card in requests_cards:
        label = _request_button_label(request_card)
        row.append(Button(media_id=open_media_id, value=str(request_card.request.id), label=label))
        idx += 1

        if len(row) >= per_row:
            groups.extend(MediaGroupBuilder(row).build())
            row = []

    if row:
        groups.extend(MediaGroupBuilder(row).build())

    return groups


def requests_pagination_keyboard(
    *,
    offset: int,
    limit: int,
    has_prev: bool,
    has_next: bool,
    prev_media_id: str = "requests_prev",
    next_media_id: str = "requests_next",
    leave_value: str = "moderation",
    leave_label: str = "🛡️ В меню модерации",
    keep_layout: bool = True,
) -> list[InteractiveMediaGroup]:
    """
    Клавиатура пагинации для списка заявок
    """
    return pagination_keyboard(
        offset=offset,
        limit=limit,
        has_prev=has_prev,
        has_next=has_next,
        prev_media_id=prev_media_id,
        next_media_id=next_media_id,
        leave_value=leave_value,
        leave_label=leave_label,
        keep_layout=keep_layout,
    )


def requests_filters_keyboard(
    ctx_name: str = "moderation",
    offset: int = 0,
) -> list[InteractiveMediaGroup]:
    """
    Клавиатура фильтров для заявок
    """
    return MediaGroupBuilder(
        [Button(media_id="requests_filters_open", value=f"{ctx_name}|{offset}", label="🔍 Фильтры")]
    ).build()


def format_request_details(request_card: RequestCardSchema) -> str:
    """Форматирует детальную информацию о заявке"""

    def _format_reject_reason(reject_reason: str) -> str:
        reject_reason = wrap_text(reject_reason, 40, "")
        return "❌ Причина отказа:\n```plain\n" + reject_reason + "\n```"

    req = request_card.request
    resource = request_card.resource

    status_emoji = _get_status_emoji(req.status)
    status_label = _get_status_label(req.status)

    lines = [
        f"📋 *Заявка* {status_emoji}",
        "",
        f"🆔 ID: `{req.id}`",
        f"📝 Тип: {_get_request_type_label(req.request_type)}",
        f"👤 Отправитель: `{req.messenger_id}`",
        f"📅 Создана: {format_datetime(req.created_at)}",
        f"🔄 Обновлена: {format_datetime(req.updated_at)}",
        f"📊 Статус: {status_label} {status_emoji}",
        "",
    ]

    if req.status.lower() == "rejected" and req.reject_reason:
        lines.append(_format_reject_reason(req.reject_reason))

    if req.request_type == "event" or req.request_type == "project_event":
        event_formated = format_event_details(
            resource, show_code=True, show_organizer=True, make_frame=True
        )
        lines.append(event_formated)

    return "\n".join(lines)


def request_actions_keyboard(
    request_id: str,
    *,
    status: str,
    back_value: str,
    back_label: str,
    approve_media_id: str = "request_approve",
    reject_media_id: str = "request_reject",
) -> list[InteractiveMediaGroup]:
    """
    Клавиатура для действий с заявкой (аналог event_actions_keyboard)
    """
    actions = []

    if status.lower() == "pending":
        actions.append(Button(media_id=approve_media_id, value=request_id, label="✅ Одобрить"))
        actions.append(Button(media_id=reject_media_id, value=request_id, label="❌ Отклонить"))

    actions.append(Button(media_id="leave", value=back_value, label=back_label))

    return MediaGroupBuilder(actions).build()

--- services/bot/core/handlers/events_ui.py ---
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder

from core.bot_kit.fsm import FSMContext
from core.config import bot
from core.handlers.events_pages import EVENTS_PAGE_SIZE, fetch_events_page
from core.markups import (
    all_events_pagination_keyboard,
    event_actions_keyboard,
    events_filters_button_keyboard,
    events_select_keyboard,
    format_event_details,
    my_events_pagination_keyboard,
    part_events_pagination_keyboard,
    points_events_pagination_keyboard,
    points_menu_keyboard,
    user_events_pagination_keyboard,
)
from core.schemas import EventCardsPageSchema, EventSchema, UserSchema
from core.services import EventService, ReportService
from core.utils import format_events_filter_summary, get_events_filter, logger

if TYPE_CHECKING:
    from core.schemas.event import EventCardSchema

ADMIN_CAN_MANAGE_OTHERS = True  # имеет ли администратор доступ ко всем мероприятиям?


@dataclass(frozen=True)
class EventUIContext:  # legacy
    name: str
    # list
    list_open_media_id: str
    pagination_builder: Callable[..., list[InteractiveMediaGroup]]
    extra_groups_builder: Callable[[], list[InteractiveMediaGroup]] | None
    # card
    back_value: str | None
    back_label: str
    enter_code_media_id: str
    sign_up_media_id: str
    sign_out_media_id: str


_UI: dict[str, EventUIContext] = {
    # дом волонтёра -> все мероприятия
    "events": EventUIContext(
        name="events",
        list_open_media_id="user_events_open",
        pagination_builder=user_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="events",
        back_label="⬅️ К списку мероприятий",
        enter_code_media_id="event_user_enter_code",
        sign_up_media_id="event_user_sign_up",
        sign_out_media_id="event_user_sign_out",
    ),
    # дом волонтёра -> мои меропрития
    "part_events": EventUIContext(
        name="part_events",
        list_open_media_id="user_events_open",
        pagination_builder=part_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="part_events",
        back_label="⬅️ К списку мероприятий",
        enter_code_media_id="event_user_enter_code",
        sign_up_media_id="event_user_sign_up",
        sign_out_media_id="event_user_sign_out",
    ),
    # дом волонтёра -> посмотреть по uuid
    "volunteer_home": EventUIContext(
        name="volunteer_home",
        list_open_media_id="user_events_open",  # не используется в этом контексте
        pagination_builder=user_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="volunteer_home",
        back_label="⬅️ В дом волонтёра",
        enter_code_media_id="event_user_enter_code",
        sign_up_media_id="event_user_sign_up",
        sign_out_media_id="event_user_sign_out",
    ),
    # дом модерации -> все мероприятия
    "moderation": EventUIContext(
        name="moderation",
        list_open_media_id="all_events_open",
        pagination_builder=all_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="all_events",
        back_label="⬅️ К списку мероприятий",
        enter_code_media_id="event_menu_enter_code",
        sign_up_media_id="event_menu_sign_up",
        sign_out_media_id="event_menu_sign_out",
    ),
    # меню баллов -> мероприятия с начисленными баллами
    "points": EventUIContext(
        name="points",
        list_open_media_id="points_event_open",
        pagination_builder=points_events_pagination_keyboard,
        extra_groups_builder=points_menu_keyboard,  # лидерборд + назад
        back_value="points",
        back_label="⬅️ К баллам",
        enter_code_media_id="event_user_enter_code",
        sign_up_media_id="event_user_sign_up",
        sign_out_media_id="event_user_sign_out",
    ),
    # дом модерации -> посмотреть по uuid
    "moderation_menu": EventUIContext(
        name="moderation_menu",
        list_open_media_id="all_events_open",  # не используется в этом контексте
        pagination_builder=all_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="moderation",
        back_label="⬅️ В меню модерации",
        enter_code_media_id="event_menu_enter_code",
        sign_up_media_id="event_menu_sign_up",
        sign_out_media_id="event_menu_sign_out",
    ),
    # дом модерации -> мои мероприятия
    "my_events": EventUIContext(
        name="my_events",
        list_open_media_id="my_events_open",
        pagination_builder=my_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="my_events",
        back_label="⬅️ К моим мероприятиям",
        enter_code_media_id="event_menu_enter_code",
        sign_up_media_id="event_menu_sign_up",
        sign_out_media_id="event_menu_sign_out",
    ),
    # ответ нейронки -> карточка мероприятия
    "ai_dialog_event": EventUIContext(
        name="ai_dialog_event",
        list_open_media_id="user_events_open",
        pagination_builder=user_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value=None,
        back_label="",
        enter_code_media_id="event_user_enter_code",
        sign_up_media_id="event_user_sign_up",
        sign_out_media_id="event_user_sign_out",
    ),
}


def get_ui(ctx_name: str) -> EventUIContext:  # legacy
    return _UI.get(ctx_name, _UI["events"])


def build_back_keyboard(ctx_name: str) -> list[InteractiveMediaGroup]:
    ui = get_ui(ctx_name)
    if not ui.back_value:
        return []
    return MediaGroupBuilder(
        [Button(media_id="leave", value=ui.back_value, label=ui.back_label)]
    ).build()


def split_event_value(raw: Any) -> tuple[str, str | None]:  # legacy
    """
    ожидаем:
      - "<event_id>"
      - "<event_id>|<ctx>"
    """
    s = str(raw or "").strip()
    if "|" in s:
        ev_id, ctx = s.split("|", 1)
        return ev_id.strip(), (ctx.strip() or None)
    return s, None


def can_manage_event(event: EventSchema, user: UserSchema | None) -> bool:
    if user is not None and user.admin and ADMIN_CAN_MANAGE_OTHERS:
        logger.debug("[events_ui] can_manage_event admin -> True")
        return True
    uid = user.messenger_id if user is not None else None
    if not uid:
        logger.debug("[events_ui] can_manage_event no uid -> False")
        return False
    result = bool(event.creator_id) and (str(event.creator_id) == str(uid))
    logger.debug(
        "[events_ui] can_manage_event user=%s event=%s result=%s",
        user.__repr__(),
        event.__repr__(),
        result,
    )
    return result


def can_view_code(event: EventSchema, user: UserSchema | None) -> bool:
    # кто может управлять - тот видит код
    return can_manage_event(event, user)


def _send_events_list_screen(  # legacy
    peer,
    *,
    ui: EventUIContext,
    ctx_name: str,
    offset: int,
    limit: int,
    page_data: EventCardsPageSchema,
    note: str | None = None,
    header: str | None = None,
    empty_text: str | None = None,
    empty_groups: list[InteractiveMediaGroup] | None = None,
    filter_summary: str | None = None,
    show_active: bool = False,
):
    # ИСПРАВЛЕНО: используем event_cards вместо events
    event_cards = page_data.event_cards  # list[EventCardSchema]
    has_prev = page_data.has_prev
    has_next = page_data.has_next

    prefix = f"{note}\n\n" if note else ""
    header_part = (header if header is not None else "📅 Мероприятия") + "\n\n"
    filter_part = f"\n\n{filter_summary}" if filter_summary else ""

    if not event_cards:  # проверяем карточки
        text = (
            prefix
            + header_part
            + (
                empty_text
                or "⚠️ Мероприятий не найдено.\n\nПопробуй изменить критерии или вернись назад."
            )
            + filter_part
        )
        im: list[InteractiveMediaGroup] = []
        footer: list[InteractiveMediaGroup] = []
        if empty_groups is not None:
            footer = empty_groups
        elif ui.extra_groups_builder:
            footer = ui.extra_groups_builder()
        im += events_filters_button_keyboard(ctx_name=ctx_name, offset=offset)
        im += ui.pagination_builder(
            offset=offset,
            limit=limit,
            has_prev=has_prev,
            has_next=has_next,
        )
        im += footer
        bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)
        return

    text = (
        prefix
        + header_part
        + f"Страница: {page_data.current_page} / {page_data.total_pages}\n\n"
        + "Выбери мероприятие:"
        + filter_part
    )

    im: list[InteractiveMediaGroup] = []
    im += events_select_keyboard(
        event_cards,
        ctx=ctx_name,
        open_media_id=ui.list_open_media_id,
        start_index=offset + 1,
        per_row=1,
    )
    im += events_filters_button_keyboard(ctx_name=ctx_name, offset=offset)
    im += ui.pagination_builder(offset=offset, limit=limit, has_prev=has_prev, has_next=has_next)

    if ui.extra_groups_builder:
        im += ui.extra_groups_builder()

    bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)


def send_event_cards_page(
    peer,
    *,
    offset: int,
    event_service: EventService,
    ctx_name: str,
    note: str | None = None,
    context: FSMContext | None = None,
):
    ui = get_ui(ctx_name)
    limit = EVENTS_PAGE_SIZE

    flt = get_events_filter(context, ctx_name=ctx_name) if context is not None else None
    summary = format_events_filter_summary(flt) if context is not None else None

    page_data = fetch_events_page(
        event_service=event_service,
        offset=offset,
        limit=limit,
        requester_messenger_id=peer.id,
        filters=flt,
    )

    _send_events_list_screen(
        peer,
        ui=ui,
        ctx_name=ctx_name,
        offset=offset,
        limit=limit,
        page_data=page_data,
        note=note,
        filter_summary=summary,
    )


def send_part_events_page(  # legacy
    peer,
    *,
    offset: int,
    event_service: EventService,
    context: FSMContext | None = None,
    note: str | None = None,
):
    bot.messaging.send_message(
        peer=peer,
        text=("f{note}\n\n" if note else "Мои мероприятия\n\nПока не реализовано"),
        interactive_media_groups=build_back_keyboard("part_events"),
    )


def send_points_page(  # legacy
    peer,
    *,
    offset: int,
    event_service: EventService,
    context: FSMContext | None = None,
):
    bot.messaging.send_message(
        peer=peer,
        text=("Меорприятия за которые я получил баллы\n\nПока не реализовано"),
        interactive_media_groups=build_back_keyboard("part_events"),
    )


def send_my_events_page(  # legacy
    peer,
    *,
    offset: int,
    event_service: EventService,
    requester_messenger_id: int,
    ctx_name: str,
    note: str | None = None,
    context: FSMContext | None = None,
):
    ui = get_ui(ctx_name)
    limit = EVENTS_PAGE_SIZE

    flt = get_events_filter(context, ctx_name=ctx_name) if context is not None else None
    summary = format_events_filter_summary(flt) if context is not None else None

    page_data = fetch_events_page(
        event_service=event_service,
        offset=offset,
        limit=limit,
        requester_messenger_id=peer.id,
        filters=flt,
        mine=True,
    )

    _send_events_list_screen(
        peer,
        ui=ui,
        ctx_name=ctx_name,
        offset=offset,
        limit=limit,
        page_data=page_data,
        note=note,
        header="👤 Мои мероприятия",
        filter_summary=summary,
        show_active=True,
    )


def send_event_card(
    peer,
    *,
    event_id: str,
    event_service: EventService,
    report_service: ReportService | None = None,
    user: UserSchema | None,
    ctx_name: str,
    note: str | None = None,
    show_organizer: bool = True,
):
    ui = get_ui(ctx_name)

    card: EventCardSchema | None = event_service.get_event_by_id(event_id, peer.id)
    if card is None:
        text = f"{note}\n\n⚠️ Мероприятие не найдено." if note else "⚠️ Мероприятие не найдено."
        bot.messaging.send_message(
            peer=peer,
            text=text,
            interactive_media_groups=build_back_keyboard(ctx_name),
        )
        return

    # TODO: 1. Убрать проверку права редактирования в сервис
    # TODO: 2. Бекенд будет возвращать список организаторов события
    # TODO: 3. Редактировать может не creator_id, а волонтер, входящий в список организаторов
    can_manage = can_manage_event(card.event, user)
    if can_manage:
        managed_ev = event_service.get_event_by_id(event_id, peer.id, mine=True)
        if managed_ev is not None:
            card = managed_ev

    is_participant = card.participation is not None

    # TODO: Код не приходит, если его нет, эта логика на бекенде, подумать, как упростить это здесь (Optional?)
    show_code = can_view_code(card.event, user)

    can_access_reports = can_manage

    report_exists: bool | None = None
    if can_access_reports and report_service is not None:
        report_exists = report_service.report_exists(
            event_id=str(card.event.id),
            requester_messenger_id=peer.id,
        )
    else:
        report_exists = None

    can_create_report = bool(can_manage and (report_exists is False))
    can_view_report = bool(can_access_reports and (report_exists is True))

    show_active = ctx_name == "my_events"

    base = format_event_details(
        card,
        show_active=show_active,
        show_code=show_code,
        show_organizer=show_organizer,
        show_report_status=bool(can_access_reports and report_exists is not None),
        report_exists=bool(report_exists) if report_exists is not None else None,
    )
    text = f"{note}\n\n{base}" if note else base

    bot.messaging.send_message(
        peer=peer,
        text=text,
        interactive_media_groups=event_actions_keyboard(
            card.event.id,
            is_participant=is_participant,
            can_manage=can_manage,
            back_value=ui.back_value,
            back_label=ui.back_label,
            enter_code_media_id=ui.enter_code_media_id,
            sign_up_media_id=ui.sign_up_media_id,
            sign_out_media_id=ui.sign_out_media_id,
            ctx=ui.name,  # важно: чтобы value был "<event_id>|<ctx>"
            can_create_report=can_create_report,
            can_view_report=can_view_report,
        ),
    )


# TODO перенести клавиатуру туда где ей место
def build_back_to_event_keyboard(
    event_id: str, ctx_name: str
) -> list[InteractiveMediaGroup]:  # legacy
    ui = get_ui(ctx_name)
    return MediaGroupBuilder(
        [
            Button(
                media_id=ui.list_open_media_id,
                value=f"{event_id}|{ui.name}",
                label="⬅️ К мероприятию",
            )
        ]
    ).build()


# TODO перенести клавиатуру туда где ей место
def build_back_to_event_editing_keyboard(
    event_id: str, ctx_name: str
) -> list[InteractiveMediaGroup]:  # legacy
    return MediaGroupBuilder(
        [
            Button(
                media_id="event_menu_edit",
                value=f"{event_id}|{ctx_name}",
                label="⬅️ К редактированию мероприятия",
            )
        ]
    ).build()

Error: 'services/backend/internal/drivers/http/v1/volunteering/events/controller.go' is not a valid file
Error: 'services/backend/internal/drivers/http/v1/volunteering/events/routes.go' is not a valid file
Error: 'services/backend/internal/domain/volunteering/events/service.go' is not a valid file
Error: 'services/backend/internal/domain/volunteering/requests/service.go' is not a valid file



во список файлов:

.env.example
.gitignore
Makefile
docker-compose.yml
docs/TODO.md
docs/buisiness-model.md
docs/devops-instruction.md
docs/domain-model.md
docs/event-access-policy.md
docs/integration-notes.md
docs/specification.pdf
pyproject.toml
pyrightconfig.json
python
seed.yml
services/ai/Dockerfile
services/ai/main.py
services/ai/requirements.txt
services/ai/russian_trusted_root_ca_pem.crt
services/ai/src/api/__init__.py
services/ai/src/api/controllers/__init__.py
services/ai/src/api/controllers/assistant_controller.py
services/ai/src/api/controllers/health_controller.py
services/ai/src/api/controllers/index_controller.py
services/ai/src/api/middlewares/auth.py
services/ai/src/api/models/__init__.py
services/ai/src/api/models/request.py
services/ai/src/api/models/response.py
services/ai/src/api/routes.py
services/ai/src/app.py
services/ai/src/clients/embedding_model.py
services/ai/src/clients/language_model.py
services/ai/src/clients/vector_store.py
services/ai/src/config/env.py
services/ai/src/db/database.py
services/ai/src/repositories/__init__.py
services/ai/src/repositories/conversation.py
services/ai/src/run.py
services/ai/src/services/__init__.py
services/ai/src/services/assistant.py
services/ai/src/services/indexing.py
services/ai/src/services/intents.py
services/ai/src/utils/__init__.py
services/ai/src/utils/asterisks.py
services/ai/src/utils/logger.py
services/backend/.dockerignore
services/backend/Dockerfile
services/backend/bootstrap/modules.go
services/backend/cmd/root.go
services/backend/docs/docs.go
services/backend/docs/swagger.json
services/backend/docs/swagger.yaml
services/backend/go.mod
services/backend/go.sum
services/backend/internal/adapters/assistant/assistant_client.go
services/backend/internal/adapters/assistant/module.go
services/backend/internal/adapters/assistant/requests.go
services/backend/internal/adapters/reports/collection_mapper.go
services/backend/internal/adapters/reports/collection_schema.go
services/backend/internal/adapters/reports/module.go
services/backend/internal/adapters/reports/report_query.go
services/backend/internal/adapters/reports/report_repo.go
services/backend/internal/adapters/reports/report_schema.go
services/backend/internal/adapters/volunteering/event_card/event_card_mappers.go
services/backend/internal/adapters/volunteering/event_card/event_card_query.go
services/backend/internal/adapters/volunteering/event_card/event_card_schema.go
services/backend/internal/adapters/volunteering/event_card/module.go
services/backend/internal/adapters/volunteering/event_organizer_card/module.go
services/backend/internal/adapters/volunteering/event_organizer_card/organizer_card_mappers.go
services/backend/internal/adapters/volunteering/event_organizer_card/organizer_card_query.go
services/backend/internal/adapters/volunteering/event_organizer_card/organizer_card_schema.go
services/backend/internal/adapters/volunteering/event_organizers/event_organizer_mappers.go
services/backend/internal/adapters/volunteering/event_organizers/event_organizer_repo.go
services/backend/internal/adapters/volunteering/event_organizers/event_organizer_schema.go
services/backend/internal/adapters/volunteering/event_organizers/module.go
services/backend/internal/adapters/volunteering/event_tags/event_tag_mappers.go
services/backend/internal/adapters/volunteering/event_tags/event_tag_repo.go
services/backend/internal/adapters/volunteering/event_tags/event_tag_schema.go
services/backend/internal/adapters/volunteering/event_tags/module.go
services/backend/internal/adapters/volunteering/events/event_mappers.go
services/backend/internal/adapters/volunteering/events/event_repository.go
services/backend/internal/adapters/volunteering/events/event_schema.go
services/backend/internal/adapters/volunteering/events/module.go
services/backend/internal/adapters/volunteering/gosbs/gosb_mappers.go
services/backend/internal/adapters/volunteering/gosbs/gosb_repo.go
services/backend/internal/adapters/volunteering/gosbs/gosb_schema.go
services/backend/internal/adapters/volunteering/gosbs/module.go
services/backend/internal/adapters/volunteering/module.go
services/backend/internal/adapters/volunteering/participation/module.go
services/backend/internal/adapters/volunteering/participation/participation_repo.go
services/backend/internal/adapters/volunteering/participation/participation_schema.go
services/backend/internal/adapters/volunteering/participation_card/participation_card_mapper.go
services/backend/internal/adapters/volunteering/participation_card/participation_card_schema.go
services/backend/internal/adapters/volunteering/project/module.go
services/backend/internal/adapters/volunteering/project/project_mappers.go
services/backend/internal/adapters/volunteering/project/project_repository.go
services/backend/internal/adapters/volunteering/project/project_schema.go
services/backend/internal/adapters/volunteering/request_card/module.go
services/backend/internal/adapters/volunteering/request_card/request_card_mappers.go
services/backend/internal/adapters/volunteering/request_card/request_card_query.go
services/backend/internal/adapters/volunteering/request_card/request_card_schema.go
services/backend/internal/adapters/volunteering/requests/module.go
services/backend/internal/adapters/volunteering/requests/request_mappers.go
services/backend/internal/adapters/volunteering/requests/request_repo.go
services/backend/internal/adapters/volunteering/requests/request_schema.go
services/backend/internal/adapters/volunteering/tags/module.go
services/backend/internal/adapters/volunteering/tags/tag_mapper.go
services/backend/internal/adapters/volunteering/tags/tag_schema.go
services/backend/internal/adapters/volunteering/tags/tags_repository.go
services/backend/internal/adapters/volunteering/terbank_moderators/module.go
services/backend/internal/adapters/volunteering/terbank_moderators/terbank_moderator_schema.go
services/backend/internal/adapters/volunteering/terbank_moderators/terbank_moderators_mappers.go
services/backend/internal/adapters/volunteering/terbank_moderators/terbank_moderators_repo.go
services/backend/internal/adapters/volunteering/terbanks/module.go
services/backend/internal/adapters/volunteering/terbanks/terbank_repo.go
services/backend/internal/adapters/volunteering/terbanks/terbank_schema.go
services/backend/internal/adapters/volunteering/terbanks/terbanks_mappers.go
services/backend/internal/adapters/volunteering/volunteer/module.go
services/backend/internal/adapters/volunteering/volunteer/volunteer_mapper.go
services/backend/internal/adapters/volunteering/volunteer/volunteer_repository.go
services/backend/internal/adapters/volunteering/volunteer/volunteer_schema.go
services/backend/internal/adapters/volunteering/volunteer_card/module.go
services/backend/internal/adapters/volunteering/volunteer_card/volunteer_card_mapper.go
services/backend/internal/adapters/volunteering/volunteer_card/volunteer_card_query.go
services/backend/internal/adapters/volunteering/volunteer_card/volunteer_card_schema.go
services/backend/internal/adapters/volunteering/volunteer_tags/module.go
services/backend/internal/adapters/volunteering/volunteer_tags/volunteer_tag_mappers.go
services/backend/internal/adapters/volunteering/volunteer_tags/volunteer_tag_repo.go
services/backend/internal/adapters/volunteering/volunteer_tags/volunteer_tag_schema.go
services/backend/internal/config/env.go
services/backend/internal/config/module.go
services/backend/internal/config/seed_config.go
services/backend/internal/domain/assistant/ask_response.go
services/backend/internal/domain/assistant/assistant_answer.go
services/backend/internal/domain/assistant/assistent_client.go
services/backend/internal/domain/assistant/assistent_errors.go
services/backend/internal/domain/assistant/assistent_service.go
services/backend/internal/domain/assistant/event_rules.go
services/backend/internal/domain/assistant/module.go
services/backend/internal/domain/assistant/rules.go
services/backend/internal/domain/event_bus.go
services/backend/internal/domain/policy/auth.go
services/backend/internal/domain/policy/context.go
services/backend/internal/domain/policy/module.go
services/backend/internal/domain/policy/permission_resolver.go
services/backend/internal/domain/policy/policy_matrix.go
services/backend/internal/domain/policy/role_resolver.go
services/backend/internal/domain/reports/module.go
services/backend/internal/domain/reports/report.go
services/backend/internal/domain/reports/report_collection.go
services/backend/internal/domain/reports/report_errors.go
services/backend/internal/domain/reports/report_query.go
services/backend/internal/domain/reports/report_repository.go
services/backend/internal/domain/reports/report_service.go
services/backend/internal/domain/volunteering/event_card/event_card.go
services/backend/internal/domain/volunteering/event_card/event_card_errors.go
services/backend/internal/domain/volunteering/event_card/event_card_page.go
services/backend/internal/domain/volunteering/event_card/event_card_page_view.go
services/backend/internal/domain/volunteering/event_card/event_card_query.go
services/backend/internal/domain/volunteering/event_card/event_card_service.go
services/backend/internal/domain/volunteering/event_card/event_card_view.go
services/backend/internal/domain/volunteering/event_card/filter.go
services/backend/internal/domain/volunteering/event_card/module.go
services/backend/internal/domain/volunteering/event_card/page_meta.go
services/backend/internal/domain/volunteering/event_organizer_card/card_service.go
services/backend/internal/domain/volunteering/event_organizer_card/event_organizer_card.go
services/backend/internal/domain/volunteering/event_organizer_card/module.go
services/backend/internal/domain/volunteering/event_organizer_card/organizer_query.go
services/backend/internal/domain/volunteering/event_organizers/event_organizer.go
services/backend/internal/domain/volunteering/event_organizers/event_organizer_repository.go
services/backend/internal/domain/volunteering/event_organizers/event_organizer_service.go
services/backend/internal/domain/volunteering/event_organizers/event_roganizer_errors.go
services/backend/internal/domain/volunteering/event_organizers/module.go
services/backend/internal/domain/volunteering/event_organizers/organizer_request.go
services/backend/internal/domain/volunteering/event_tags/event_tag.go
services/backend/internal/domain/volunteering/event_tags/event_tag_errors.go
services/backend/internal/domain/volunteering/event_tags/event_tag_repository.go
services/backend/internal/domain/volunteering/event_tags/event_tag_service.go
services/backend/internal/domain/volunteering/event_tags/module.go
services/backend/internal/domain/volunteering/events/create_event_request.go
services/backend/internal/domain/volunteering/events/event.go
services/backend/internal/domain/volunteering/events/event_errors.go
services/backend/internal/domain/volunteering/events/event_events.go
services/backend/internal/domain/volunteering/events/event_repository.go
services/backend/internal/domain/volunteering/events/event_service.go
services/backend/internal/domain/volunteering/events/event_update.go
services/backend/internal/domain/volunteering/events/event_view.go
services/backend/internal/domain/volunteering/events/module.go
services/backend/internal/domain/volunteering/events/verificaion_result.go
services/backend/internal/domain/volunteering/gosbs/actual_gosb.go
services/backend/internal/domain/volunteering/gosbs/gosb.go
services/backend/internal/domain/volunteering/gosbs/gosb_errors.go
services/backend/internal/domain/volunteering/gosbs/gosb_repository.go
services/backend/internal/domain/volunteering/gosbs/gosb_service.go
services/backend/internal/domain/volunteering/gosbs/module.go
services/backend/internal/domain/volunteering/module.go
services/backend/internal/domain/volunteering/participation_card/participation_card.go
services/backend/internal/domain/volunteering/participation_card/participation_card_query.go
services/backend/internal/domain/volunteering/participations/module.go
services/backend/internal/domain/volunteering/participations/participation.go
services/backend/internal/domain/volunteering/participations/participation_errors.go
services/backend/internal/domain/volunteering/participations/participation_repository.go
services/backend/internal/domain/volunteering/participations/participation_service.go
services/backend/internal/domain/volunteering/projects/module.go
services/backend/internal/domain/volunteering/projects/project.go
services/backend/internal/domain/volunteering/projects/project_errors.go
services/backend/internal/domain/volunteering/projects/project_repository.go
services/backend/internal/domain/volunteering/projects/project_service.go
services/backend/internal/domain/volunteering/registry/create_type.go
services/backend/internal/domain/volunteering/registry/module.go
services/backend/internal/domain/volunteering/registry/registry_service.go
services/backend/internal/domain/volunteering/registry/resource_activator.go
services/backend/internal/domain/volunteering/request_card/filter.go
services/backend/internal/domain/volunteering/request_card/module.go
services/backend/internal/domain/volunteering/request_card/page_meta.go
services/backend/internal/domain/volunteering/request_card/request_card.go
services/backend/internal/domain/volunteering/request_card/request_card_errors.go
services/backend/internal/domain/volunteering/request_card/request_card_page.go
services/backend/internal/domain/volunteering/request_card/request_card_query.go
services/backend/internal/domain/volunteering/request_card/request_card_service.go
services/backend/internal/domain/volunteering/requests/module.go
services/backend/internal/domain/volunteering/requests/request.go
services/backend/internal/domain/volunteering/requests/request_errors.go
services/backend/internal/domain/volunteering/requests/request_repository.go
services/backend/internal/domain/volunteering/requests/request_service.go
services/backend/internal/domain/volunteering/roles/module.go
services/backend/internal/domain/volunteering/roles/role_service.go
services/backend/internal/domain/volunteering/tags/actual_tag.go
services/backend/internal/domain/volunteering/tags/module.go
services/backend/internal/domain/volunteering/tags/tag.go
services/backend/internal/domain/volunteering/tags/tag_errors.go
services/backend/internal/domain/volunteering/tags/tag_repository.go
services/backend/internal/domain/volunteering/tags/tag_service.go
services/backend/internal/domain/volunteering/terbank_moderators/module.go
services/backend/internal/domain/volunteering/terbank_moderators/terbank_moderator.go
services/backend/internal/domain/volunteering/terbank_moderators/terbank_moderator_errors.go
services/backend/internal/domain/volunteering/terbank_moderators/terbank_moderator_repository.go
services/backend/internal/domain/volunteering/terbank_moderators/terbank_moderator_service.go
services/backend/internal/domain/volunteering/terbanks/actual_terbank.go
services/backend/internal/domain/volunteering/terbanks/module.go
services/backend/internal/domain/volunteering/terbanks/terbank.go
services/backend/internal/domain/volunteering/terbanks/terbank_errors.go
services/backend/internal/domain/volunteering/terbanks/terbank_repository.go
services/backend/internal/domain/volunteering/terbanks/terbank_service.go
services/backend/internal/domain/volunteering/volunteer_card/module.go
services/backend/internal/domain/volunteering/volunteer_card/volunteer_card.go
services/backend/internal/domain/volunteering/volunteer_card/volunteer_card_query.go
services/backend/internal/domain/volunteering/volunteer_card/volunteer_card_service.go
services/backend/internal/domain/volunteering/volunteer_tags/module.go
services/backend/internal/domain/volunteering/volunteer_tags/volunteer_tag.go
services/backend/internal/domain/volunteering/volunteer_tags/volunteer_tag_errors.go
services/backend/internal/domain/volunteering/volunteer_tags/volunteer_tag_repository.go
services/backend/internal/domain/volunteering/volunteer_tags/volunteer_tag_service.go
services/backend/internal/domain/volunteering/volunteers/module.go
services/backend/internal/domain/volunteering/volunteers/volunteer.go
services/backend/internal/domain/volunteering/volunteers/volunteer_errors.go
services/backend/internal/domain/volunteering/volunteers/volunteer_repository.go
services/backend/internal/domain/volunteering/volunteers/volunteer_service.go
services/backend/internal/drivers/http/middlewares/auth_middleware.go
services/backend/internal/drivers/http/middlewares/context_middleware.go
services/backend/internal/drivers/http/middlewares/module.go
services/backend/internal/drivers/http/middlewares/policy_middleware.go
services/backend/internal/drivers/http/middlewares/role_middleware.go
services/backend/internal/drivers/http/middlewares/test_auth_middleware.go
services/backend/internal/drivers/http/v1/assistant/assistant_controller.go
services/backend/internal/drivers/http/v1/assistant/assistant_requests.go
services/backend/internal/drivers/http/v1/assistant/assistant_routes.go
services/backend/internal/drivers/http/v1/health/health_controller.go
services/backend/internal/drivers/http/v1/health/health_routes.go
services/backend/internal/drivers/http/v1/reports/event_report_builder.go
services/backend/internal/drivers/http/v1/reports/report_controller.go
services/backend/internal/drivers/http/v1/reports/report_request.go
services/backend/internal/drivers/http/v1/reports/report_routes.go
services/backend/internal/drivers/http/v1/routes.go
services/backend/internal/drivers/http/v1/swagger/swagger_routes.go
services/backend/internal/drivers/http/v1/volunteering/event_card/event_card_controller.go
services/backend/internal/drivers/http/v1/volunteering/event_card/event_card_request.go
services/backend/internal/drivers/http/v1/volunteering/event_organizers/event_organizer_controller.go
services/backend/internal/drivers/http/v1/volunteering/event_organizers/event_organizer_requests.go
services/backend/internal/drivers/http/v1/volunteering/event_tags/event_tag_controller.go
services/backend/internal/drivers/http/v1/volunteering/event_tags/event_tag_request.go
services/backend/internal/drivers/http/v1/volunteering/events/event_controller.go
services/backend/internal/drivers/http/v1/volunteering/gosbs/gosb_controller.go
services/backend/internal/drivers/http/v1/volunteering/participation/participation_controller.go
services/backend/internal/drivers/http/v1/volunteering/participation/participation_requests.go
services/backend/internal/drivers/http/v1/volunteering/registry/registry_controller.go
services/backend/internal/drivers/http/v1/volunteering/request_card/request_card_controller.go
services/backend/internal/drivers/http/v1/volunteering/request_card/request_card_request.go
services/backend/internal/drivers/http/v1/volunteering/requests/request_contoller.go
services/backend/internal/drivers/http/v1/volunteering/requests/request_requests.go
services/backend/internal/drivers/http/v1/volunteering/tags/tags_controller.go
services/backend/internal/drivers/http/v1/volunteering/terbank_moderators/terbank_moderator_controller.go
services/backend/internal/drivers/http/v1/volunteering/terbank_moderators/terbank_moderator_requests.go
services/backend/internal/drivers/http/v1/volunteering/terbanks/terbank_controller.go
services/backend/internal/drivers/http/v1/volunteering/volunteer/volunteer_controller.go
services/backend/internal/drivers/http/v1/volunteering/volunteer/volunteer_requests.go
services/backend/internal/drivers/http/v1/volunteering/volunteer_card/volunteer_card_controller.go
services/backend/internal/drivers/http/v1/volunteering/volunteer_tag/volunteer_tag_controller.go
services/backend/internal/drivers/http/v1/volunteering/volunteer_tag/volunteer_tag_requests.go
services/backend/internal/drivers/http/v1/volunteering/volunteering_routes.go
services/backend/internal/drivers/seeders/admin_seeder.go
services/backend/internal/drivers/seeders/gosb_seeder.go
services/backend/internal/drivers/seeders/seeders.go
services/backend/internal/drivers/seeders/tag_seeder.go
services/backend/internal/drivers/seeders/terbank_seeder.go
services/backend/main.go
services/backend/migrations/000001_volunteer_table.down.sql
services/backend/migrations/000001_volunteer_table.up.sql
services/backend/migrations/000002_tag_table.down.sql
services/backend/migrations/000002_tag_table.up.sql
services/backend/migrations/000003_volunteer_tag_table.down.sql
services/backend/migrations/000003_volunteer_tag_table.up.sql
services/backend/migrations/000004_volunteer_role.down.sql
services/backend/migrations/000004_volunteer_role.up.sql
services/backend/migrations/000005_event_table.down.sql
services/backend/migrations/000005_event_table.up.sql
services/backend/migrations/000006_project_table.down.sql
services/backend/migrations/000006_project_table.up.sql
services/backend/migrations/000007_request_table.down.sql
services/backend/migrations/000007_request_table.up.sql
services/backend/migrations/000008_event_tag_table.down.sql
services/backend/migrations/000008_event_tag_table.up.sql
services/backend/migrations/000009_event_creator.down.sql
services/backend/migrations/000009_event_creator.up.sql
services/backend/migrations/000010_terbank_table.down.sql
services/backend/migrations/000010_terbank_table.up.sql
services/backend/migrations/000011_terbank_moderator_table.down.sql
services/backend/migrations/000011_terbank_moderator_table.up.sql
services/backend/migrations/000012_gosb_table.down.sql
services/backend/migrations/000012_gosb_table.up.sql
services/backend/migrations/000013_change_role.down.sql
services/backend/migrations/000013_change_role.up.sql
services/backend/migrations/000014_rename_terbank_id.down.sql
services/backend/migrations/000014_rename_terbank_id.up.sql
services/backend/migrations/000015_organizer_table.down.sql
services/backend/migrations/000015_organizer_table.up.sql
services/backend/migrations/000016_organizer_role.down.sql
services/backend/migrations/000016_organizer_role.up.sql
services/backend/migrations/000017_participation_table.down.sql
services/backend/migrations/000017_participation_table.up.sql
services/backend/migrations/000018_event_hours.down.sql
services/backend/migrations/000018_event_hours.up.sql
services/backend/migrations/000019_report_table.down.sql
services/backend/migrations/000019_report_table.up.sql
services/backend/migrations/000020_many_reports.down.sql
services/backend/migrations/000020_many_reports.up.sql
services/backend/migrations/000021_gosp_to_gosb.down.sql
services/backend/migrations/000021_gosp_to_gosb.up.sql
services/backend/pkg/bearer_parser.go
services/backend/pkg/excel_service.go
services/backend/pkg/gin_binding.go
services/backend/pkg/gorm.go
services/backend/pkg/gorm_logger.go
services/backend/pkg/gorm_paginate_simple.go
services/backend/pkg/http.go
services/backend/pkg/logger.go
services/backend/pkg/migrate_postgres.go
services/backend/pkg/module.go
services/backend/pkg/request_handler.go
services/bot/Dockerfile
services/bot/chain.pem
services/bot/core/__init__.py
services/bot/core/bot_kit/__init__.py
services/bot/core/bot_kit/fsm/__init__.py
services/bot/core/bot_kit/fsm/context.py
services/bot/core/bot_kit/fsm/state.py
services/bot/core/bot_kit/fsm/storage/__init__.py
services/bot/core/bot_kit/fsm/storage/base.py
services/bot/core/bot_kit/fsm/storage/in_memory.py
services/bot/core/bot_kit/router.py
services/bot/core/clients/__init__.py
services/bot/core/clients/ai_assistant.py
services/bot/core/clients/base.py
services/bot/core/clients/event.py
services/bot/core/clients/report.py
services/bot/core/clients/request.py
services/bot/core/clients/tag.py
services/bot/core/clients/terbank.py
services/bot/core/clients/user.py
services/bot/core/config/__init__.py
services/bot/core/config/bootstrap.py
services/bot/core/config/settings.py
services/bot/core/dependencies/__init__.py
services/bot/core/dependencies/base.py
services/bot/core/dependencies/context.py
services/bot/core/dependencies/di.py
services/bot/core/dependencies/event.py
services/bot/core/dependencies/registry.py
services/bot/core/dependencies/report.py
services/bot/core/dependencies/request.py
services/bot/core/dependencies/tag.py
services/bot/core/dependencies/terbank.py
services/bot/core/dependencies/user.py
services/bot/core/handlers/__init__.py
services/bot/core/handlers/admin.py
services/bot/core/handlers/ai.py
services/bot/core/handlers/event.py
services/bot/core/handlers/events_filters.py
services/bot/core/handlers/events_pages.py
services/bot/core/handlers/events_ui.py
services/bot/core/handlers/main.py
services/bot/core/handlers/moderation.py
services/bot/core/handlers/noop.py
services/bot/core/handlers/points.py
services/bot/core/handlers/profile.py
services/bot/core/handlers/report.py
services/bot/core/handlers/request.py
services/bot/core/handlers/view_by_uuid.py
services/bot/core/handlers/volunteer_home.py
services/bot/core/markups/__init__.py
services/bot/core/markups/ai.py
services/bot/core/markups/event.py
services/bot/core/markups/events_filters.py
services/bot/core/markups/gosb.py
services/bot/core/markups/inline.py
services/bot/core/markups/pagination.py
services/bot/core/markups/points.py
services/bot/core/markups/profile.py
services/bot/core/markups/request.py
services/bot/core/markups/tag.py
services/bot/core/markups/tags.py
services/bot/core/markups/terbank.py
services/bot/core/schemas/__init__.py
services/bot/core/schemas/event.py
services/bot/core/schemas/gosb.py
services/bot/core/schemas/participation.py
services/bot/core/schemas/request.py
services/bot/core/schemas/tag.py
services/bot/core/schemas/terbank.py
services/bot/core/schemas/user.py
services/bot/core/services/__init__.py
services/bot/core/services/base.py
services/bot/core/services/event.py
services/bot/core/services/file.py
services/bot/core/services/registry/__init__.py
services/bot/core/services/registry/base.py
services/bot/core/services/registry/registry.py
services/bot/core/services/report.py
services/bot/core/services/requests.py
services/bot/core/services/tag.py
services/bot/core/services/terbank.py
services/bot/core/services/user.py
services/bot/core/states/__init__.py
services/bot/core/utils/__init__.py
services/bot/core/utils/date_time.py
services/bot/core/utils/events_filter.py
services/bot/core/utils/fsm/__init__.py
services/bot/core/utils/fsm/base.py
services/bot/core/utils/fsm/state.py
services/bot/core/utils/fsm/storage/__init__.py
services/bot/core/utils/fsm/storage/base.py
services/bot/core/utils/fsm/storage/memory.py
services/bot/core/utils/logger.py
services/bot/core/utils/messages.py
services/bot/core/utils/profile.py
services/bot/core/utils/tags_toggle_ui.py
services/bot/core/utils/text_wrap.py
services/bot/main.py
services/bot/requirements.txt
templates.yml
utils/format
utils/prettify_logs.py
Editor is loading...
Leave a Comment