// Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", "contact": {}, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { "/auth/forgot-password": { "post": { "description": "Отправляет на email письмо со ссылкой формы сброса (токен живёт 24 часа, одноразовый). Ответ всегда одинаковый — 200 с ok:true, независимо от того, существует ящик, подтверждён он и активен: эндпоинт не перечисляет аккаунты. Письмо уходит только подтверждённым активным аккаунтам. Лимиты: 10 запросов в минуту с одного IP; 3 письма в сутки на один ящик (счётчик тикает при любом запросе — и для несуществующих ящиков тоже), при превышении — 429.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Запрос письма для сброса пароля", "parameters": [ { "description": "Email для восстановления", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.forgotPasswordReq" } } ], "responses": { "200": { "description": "всегда ok:true, даже если ящик не найден", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректное тело запроса", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "429": { "description": "слишком много попыток", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/auth/login": { "post": { "description": "Возвращает access-токен; refresh-токен — только в httpOnly-куке (Path=/auth, SameSite=Lax, Secure — через COOKIE_SECURE), в JSON его нет. Email ищется без учёта регистра, пароль — bcrypt. Неподтверждённый аккаунт — 403 account not confirmed (отличайте от account disabled). Лимит: 10 попыток в минуту с одного IP.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Вход", "parameters": [ { "description": "Учётные данные", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.loginReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.TokenResponse" } }, "400": { "description": "некорректное тело запроса", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "неверный логин или пароль", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "аккаунт отключён либо не подтверждён", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "429": { "description": "слишком много попыток", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/auth/logout": { "post": { "description": "Гасит refresh-сессию из куки refresh_token и очищает куку. Тело запроса не нужно. Auth-middleware намеренно не используется: выйти должен пользователь и с протухшим access-токеном.", "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Выйти (одно устройство)", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "429": { "description": "слишком много попыток", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/auth/logout-all": { "post": { "security": [ { "Bearer": [] } ], "description": "Закрывает все сессии текущего пользователя.", "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Выйти со всех устройств", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/auth/me": { "get": { "security": [ { "Bearer": [] } ], "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Текущий пользователь", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Profile" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/auth/refresh": { "post": { "description": "Refresh-токен читается из httpOnly-куки refresh_token (Path=/auth), тело запроса не требуется. Ротация: на выходе новый access-токен и новая кука, старая сессия гасится. Повторное использование отозванного токена закрывает все сессии пользователя — признак того, что секретом владеет кто-то второй.", "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Обновить пару токенов", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.TokenResponse" } }, "401": { "description": "куки нет, токен неизвестен, отозван или истёк", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "аккаунт отключён", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "429": { "description": "слишком много попыток", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/auth/reset-password": { "post": { "description": "Токен одноразовый и живёт 24 часа: приходит письмом после POST /auth/forgot-password либо выдаётся админом через POST /users/{id}/password-reset. Новый пароль — по политике (10..72 символа, буквы и цифры). Успех гасит токен и все refresh-сессии пользователя. Лимит: 30 попыток в минуту с одного IP.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Сброс пароля по токену", "parameters": [ { "description": "Токен и новый пароль", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.resetPasswordReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректное тело или токен неизвестен, использован либо истёк; слабый пароль", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "429": { "description": "слишком много попыток", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/auth/telegram": { "get": { "description": "Куда сейчас приходят уведомления о заявках: цель (чат или группа), имя бота (свой или глобальный), подключён ли собственный токен бота (has_bot_token) и можно ли его ввести (token_configurable — зависит от DATA_ENCRYPTION_KEY на сервере). Сам токен наружу не отдаётся.", "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Статус уведомлений в Telegram", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.TelegramStatusResponse" } }, "401": { "description": "нет входа", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "профиль не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "put": { "description": "Принимает @имя группы/канала (бот обязан быть в нём) или числовой chat_id; пустое значение отключает уведомления. Сначала проверяется формат, затем бот проверяет чат через getChat — чужой или недоступный чат не сохраняется.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Указать цель уведомлений вручную", "parameters": [ { "description": "Цель: @группа, chat_id или пусто", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.setTelegramReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "invalid telegram target", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет входа", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "delete": { "description": "Очищает цель уведомлений текущего пользователя (то же, что PUT с пустым значением). Токен бота при этом не трогается — он живёт на своём эндпоинте.", "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Отключить уведомления в Telegram", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "401": { "description": "нет входа", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/auth/telegram/bot": { "post": { "description": "Принимает токен Bot API (формат «id:secret» от @BotFather), проверяет его через getMe, шифрует и сохраняет. Дальше уведомления пользователя идут через этого бота, его поллер поднимается сразу. Лимит: 5 попыток за 5 минут. Без DATA_ENCRYPTION_KEY на сервере — 400.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Подключить своего бота", "parameters": [ { "description": "Токен бота", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.setBotReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "токен не сохранён (формат, getMe или фича выключена)", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет входа", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "профиль не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "429": { "description": "слишком много попыток", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "delete": { "description": "Стирает сохранённый токен текущего пользователя и останавливает его поллер. Повторный вызов безопасен — ответ тот же 200.", "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Отключить своего бота", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "401": { "description": "нет входа", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "профиль не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/auth/telegram/code": { "post": { "description": "Одноразовый код и deep-link t.me/{bot}?start={code}: откройте ссылку в Telegram и отправьте боту код — этот чат начнёт получать уведомления о заявках. Бот — ваш собственный токен, а если его нет — глобальный бот сервера. Код живёт 15 минут, прежние коды гаснут. Лимит: 5 выпусков за 5 минут на пользователя.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "auth" ], "summary": "Выдать код привязки Telegram", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.BindCodeResponse" } }, "400": { "description": "ни свой бот, ни глобальный не настроены", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "профиль не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "429": { "description": "слишком много кодов", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/invitations": { "get": { "security": [ { "Bearer": [] } ], "description": "Список приглашений для текущего мастера: по каждому — проект, кто пригласил и до какого срока. Просроченные сюда не попадают — принять их уже нельзя.", "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Мои приглашения в проекты", "parameters": [ { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "invited_at", "expires_at", "project_name" ], "type": "string", "default": "invited_at", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.InvitationListResponse" } }, "400": { "description": "некорректные параметры страницы", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль worker", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/objects/{id}": { "patch": { "security": [ { "Bearer": [] } ], "description": "Поля опциональны. У уже созданных заявок адрес остаётся тем, что был на момент создания — это осознанно.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Изменить объект", "parameters": [ { "type": "integer", "description": "ID объекта", "name": "id", "in": "path", "required": true }, { "description": "Что изменить", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.updateObjectReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Object" } }, "400": { "description": "некорректный id или пустое тело", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "объект не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "имя уже занято в проекте", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/projects": { "get": { "security": [ { "Bearer": [] } ], "description": "Клиент видит только свои проекты, админ — все (можно сузить через client_id). Мастерам проекты не отдаются. Ответ — страница: items плюс total/limit/offset.", "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Список проектов", "parameters": [ { "type": "string", "description": "Фильтр по клиенту (только admin)", "name": "client_id", "in": "query" }, { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "created_at", "name", "id" ], "type": "string", "default": "created_at", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.ProjectListResponse" } }, "400": { "description": "некорректные параметры страницы", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "post": { "security": [ { "Bearer": [] } ], "description": "Клиент создаёт проект себе; админ — для любого клиента (в этом случае обязателен client_id). Проект — контур, внутри которого живут объекты и мастера.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Создать проект", "parameters": [ { "description": "Данные проекта", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.createProjectReq" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/models.Project" } }, "400": { "description": "некорректное тело; client_id обязателен для admin; владелец не клиент", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "у клиента уже есть проект с таким именем", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/projects/{id}": { "get": { "security": [ { "Bearer": [] } ], "description": "Админ — любой проект, клиент — только свой; чужой проект неотличим от несуществующего (404).", "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Детали проекта", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Project" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "проект не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "delete": { "security": [ { "Bearer": [] } ], "description": "Действие необратимо: вместе с проектом удаляются его объекты, участия мастеров и все заявки (вместе с событиями и фото). Объекты фото в хранилище убираются после успешного удаления в БД. Клиент своих проектов не удаляет — только админ.", "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Удалить проект", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "проект не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "patch": { "security": [ { "Bearer": [] } ], "description": "Поля опциональны, меняется только переданное. Выключенный проект не принимает новые заявки и объекты.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Изменить проект", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true }, { "description": "Что изменить", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.updateProjectReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Project" } }, "400": { "description": "некорректный id или пустое тело", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "проект не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "имя уже занято у этого клиента", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/projects/{id}/invitation/accept": { "post": { "security": [ { "Bearer": [] } ], "description": "invited → active: с этого момента мастер состоит в проекте и ему можно назначать заявки. Действует только на своё непросроченное приглашение (срок — 7 дней, см. repository.InvitationTTL).", "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Принять приглашение в проект", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль worker", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "приглашения нет", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "срок вышел или приглашение уже закрыто", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/projects/{id}/invitation/decline": { "post": { "security": [ { "Bearer": [] } ], "description": "invited → declined: заявок мастер не получает, клиент видит отказ в составе проекта и может пригласить повторно (строка та же, история ответа обновляется).", "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Отклонить приглашение в проект", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль worker", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "приглашения нет", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "приглашение уже закрыто", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/projects/{id}/objects": { "get": { "security": [ { "Bearer": [] } ], "description": "Здания/корпуса, входящие в проект. Адрес заявки берётся из объекта.", "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Объекты проекта", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true }, { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "name", "created_at", "id" ], "type": "string", "default": "name", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.ObjectListResponse" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "проект не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "post": { "security": [ { "Bearer": [] } ], "description": "Объект — здание/корпус с адресом; заявки создаются именно на объект. Выключенный проект объектов не принимает.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Создать объект", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true }, { "description": "Данные объекта", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.createObjectReq" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/models.Object" } }, "400": { "description": "некорректный id или тело", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "проект не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "проект выключен или имя объекта занято", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/projects/{id}/workers": { "get": { "security": [ { "Bearer": [] } ], "description": "Кто приглашён в проект и кем принят: статус invited — приглашение ждёт решения, active — мастер работает с заявками, declined — отказал. Хеши паролей в ответ не попадают.", "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Мастера проекта", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true }, { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "name", "created_at", "email", "id" ], "type": "string", "default": "name", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.ProjectWorkerListResponse" } }, "400": { "description": "некорректный id или параметры страницы", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "проект не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "post": { "security": [ { "Bearer": [] } ], "description": "С 0005 это приглашение, а не готовое членство: мастер становится участником только сам, приняв приглашение (POST /projects/{id}/invitation/accept) — до этого заявки проекта ему не назначаются. Приглашение живёт 7 дней. Мастер может состоять в нескольких проектах; аккаунт должен быть активным, с ролью worker и подтверждённым (0010) — неподтверждённого сначала подтверждает тот, кто его создал.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Пригласить мастера в проект", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true }, { "description": "Мастер", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.addWorkerReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.InvitationResponse" } }, "400": { "description": "некорректный id/тело; не мастер, неактивен или не подтверждён", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "проект или мастер не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "мастер уже состоит в проекте / приглашение уже ждёт ответа", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/projects/{id}/workers/{worker_id}": { "delete": { "security": [ { "Bearer": [] } ], "description": "Нельзя убрать, пока за ним закреплены незакрытые заявки проекта (409) — иначе работа осталась бы без исполнителя. Заявки из истории при этом не теряются.", "produces": [ "application/json" ], "tags": [ "projects" ], "summary": "Убрать мастера из проекта", "parameters": [ { "type": "integer", "description": "ID проекта", "name": "id", "in": "path", "required": true }, { "type": "string", "description": "ID мастера (uuid)", "name": "worker_id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "проект не найден или мастер не состоит в проекте", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "за мастером есть незакрытые заявки", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/recurring": { "get": { "security": [ { "Bearer": [] } ], "description": "Клиент видит только свои правила, админ — все. Ответ — страница: items плюс total/limit/offset.", "produces": [ "application/json" ], "tags": [ "recurring" ], "summary": "Список правил повторения", "parameters": [ { "type": "integer", "description": "Сузить до одного проекта", "name": "project_id", "in": "query" }, { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "created_at", "id", "date" ], "type": "string", "default": "created_at", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.RecurringRuleListResponse" } }, "400": { "description": "некорректные параметры страницы", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "post": { "security": [ { "Bearer": [] } ], "description": "Правило — шаблон заявки (объект, категория, описание, приоритет) плюс расписание: anchor_at (дата-время первого запуска) и period_days — «каждые N дней/недель». Фоновый планировщик сам создаёт заявку, когда срок наступил, — но только если прошлая по правилу уже закрыта (без дублей). Клиент создаёт правило для своего проекта, админ — для любого. Обычно правило заводится чекбоксом в POST /tickets; этот эндпоинт — для управления без заявки.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "recurring" ], "summary": "Создать правило повторяющейся заявки", "parameters": [ { "description": "Данные правила", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.createRecurringReq" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/models.RecurringRule" } }, "400": { "description": "некорректное тело запроса", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "объект не найден (или чужой — неотличим)", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "проект или объект выключен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/recurring/{id}": { "get": { "security": [ { "Bearer": [] } ], "description": "Админ — любое правило, клиент — только своё; чужое неотличимо от несуществующего (404).", "produces": [ "application/json" ], "tags": [ "recurring" ], "summary": "Детали правила повторения", "parameters": [ { "type": "integer", "description": "ID правила", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.RecurringRule" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "правило не найдено", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "delete": { "security": [ { "Bearer": [] } ], "description": "Удаляется само правило, его история срабатываний (какая заявка создавалась и когда) и — каскадом — будущие заявки ещё не созданы. Созданные ранее заявки не трогаются.", "produces": [ "application/json" ], "tags": [ "recurring" ], "summary": "Удалить правило повторения", "parameters": [ { "type": "integer", "description": "ID правила", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "правило не найдено", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "patch": { "security": [ { "Bearer": [] } ], "description": "Частичное обновление: применяются только присланные поля, рамки те же, что при создании (anchor_at — любая дата-время, period_days 1..3650). object_id не меняется (перенос — через удаление и создание). Срок не сдвигается вручную: он пересчитывается от последнего срабатывания с новым расписанием.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "recurring" ], "summary": "Изменить правило повторения", "parameters": [ { "type": "integer", "description": "ID правила", "name": "id", "in": "path", "required": true }, { "description": "Что изменить (хотя бы одно поле)", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.updateRecurringReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.RecurringRule" } }, "400": { "description": "некорректный id, пустое тело или значение вне рамок", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "правило не найдено", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/recurring/{id}/run": { "post": { "security": [ { "Bearer": [] } ], "description": "Заявка создаётся сразу, без ожидания интервала. Остальные рубежи как у планировщика: правило, проект и объект включены, прошлая заявка по правилу закрыта — иначе 409, дубль не создаём. Срок сдвигается на момент запуска (следующий фоновый тик отсчитает интервал от него), уведомление уходит тем же текстом, что при фоновом срабатывании.", "produces": [ "application/json" ], "tags": [ "recurring" ], "summary": "Запустить правило сейчас", "parameters": [ { "type": "integer", "description": "ID правила", "name": "id", "in": "path", "required": true } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/handlers.RecurringRunResponse" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "правило не найдено (или чужое — неотличим)", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "правило, проект или объект выключены, либо есть открытая заявка", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/recurring/{id}/runs": { "get": { "security": [ { "Bearer": [] } ], "description": "Какие заявки создавались по правилу и когда (аудит). Сначала свежие. Строка живёт вместе со своей заявкой: когда заявку удаляет автоочистка (TICKET_RETENTION_DAYS), строка истории уходит следом.", "produces": [ "application/json" ], "tags": [ "recurring" ], "summary": "История срабатываний правила", "parameters": [ { "type": "integer", "description": "ID правила", "name": "id", "in": "path", "required": true }, { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "run_at", "id" ], "type": "string", "default": "run_at", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.RecurringRunListResponse" } }, "400": { "description": "некорректный id или параметры страницы", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client или admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "правило не найдено", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets": { "get": { "security": [ { "Bearer": [] } ], "description": "Выборка режется по роли: клиент видит свои, мастер — назначенные ему, админ — всё. Клиенту и мастеру параметры client_id/worker_id игнорируются: подсмотреть чужое нельзя. Фильтры накладываются поверх выборки по роли. Ответ — страница: items плюс total/limit/offset.", "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Список заявок", "parameters": [ { "enum": [ "new", "assigned", "in_progress", "done", "confirmed", "rejected", "closed" ], "type": "string", "description": "Фильтр по статусу", "name": "status", "in": "query" }, { "enum": [ "electric", "plumbing", "climate", "elevator", "other" ], "type": "string", "description": "Фильтр по категории", "name": "category", "in": "query" }, { "enum": [ "low", "normal", "high" ], "type": "string", "description": "Фильтр по приоритету", "name": "priority", "in": "query" }, { "type": "string", "description": "Фильтр по клиенту (только admin)", "name": "client_id", "in": "query" }, { "type": "string", "description": "Фильтр по мастеру (только admin)", "name": "worker_id", "in": "query" }, { "type": "integer", "description": "Фильтр по проекту", "name": "project_id", "in": "query" }, { "type": "integer", "description": "Фильтр по объекту", "name": "object_id", "in": "query" }, { "type": "string", "description": "created_at с, YYYY-MM-DD (включительно, с полуночи UTC)", "name": "date_from", "in": "query" }, { "type": "string", "description": "created_at по, YYYY-MM-DD (включительно, до полуночи UTC следующего дня)", "name": "date_to", "in": "query" }, { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "created_at", "priority", "id" ], "type": "string", "default": "created_at", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.TicketListResponse" } }, "400": { "description": "некорректный фильтр или параметры страницы", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "неизвестная роль", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "post": { "security": [ { "Bearer": [] } ], "description": "Клиент создаёт заявку на объект своего проекта. Адрес берётся из объекта на момент создания, статус всегда new, мастер не назначен. Если передан recurring — той же операцией создаётся правило повторения (anchor_at — первый запуск, period_days — период в днях), связанное с заявкой: оно видно в GET /tickets/{id} в поле recurring, управление — через PATCH /recurring/{id}.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Создать заявку", "parameters": [ { "description": "Данные заявки", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.createTicketReq" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/models.Ticket" } }, "400": { "description": "некорректное тело запроса", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль client", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "объект не найден (или чужой — неотличим)", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "проект или объект выключен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}": { "get": { "security": [ { "Bearer": [] } ], "description": "Админ — любая заявка, клиент — только своя, мастер — назначенная ему либо свободная (new) из его проектов.", "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Детали заявки", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Ticket" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "заявка не принадлежит вам", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/assign": { "patch": { "security": [ { "Bearer": [] } ], "description": "new → assigned. Доступно админу и владельцу-клиенту. Мастер должен состоять в проекте заявки и быть активным — иначе 400/409.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Назначить мастера", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true }, { "description": "Мастер", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.assignReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Ticket" } }, "400": { "description": "некорректный id/тело, не-мастер, неактивен, приглашение не принято или не состоит в проекте заявки", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль admin или client; чужая заявка", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "заявка уже назначена или закрыта", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/cancel": { "patch": { "security": [ { "Bearer": [] } ], "description": "Закрывает заявку с cancel_reason и closed_by. Клиент — только свои в new/assigned/done/rejected; админ — любую кроме уже закрытой, включая in_progress. Причина обязательна.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Отменить заявку (финал)", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true }, { "description": "Причина отмены, минимум 5 символов", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.reasonReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Ticket" } }, "400": { "description": "некорректный id или тело: причина обязательна", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "чужая заявка или роль не client/admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "нельзя закрыть из текущего статуса", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/confirm": { "patch": { "security": [ { "Bearer": [] } ], "description": "done → closed с confirmed_at. Доступно только владельцу-клиенту.", "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Подтвердить работу (финал)", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Ticket" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "заявка не принадлежит вам", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "заявка не в статусе done", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/done": { "patch": { "security": [ { "Bearer": [] } ], "description": "in_progress → done. Дальше клиент подтверждает или отклоняет.", "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Завершить работу", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Ticket" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль worker", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "не тот статус или заявка назначена другому", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/events": { "get": { "security": [ { "Bearer": [] } ], "description": "Хронология переходов заявки с авторами: created, assigned, started, done, confirmed, rejected, canceled. from_status у события «создана» нет. События пишутся той же SQL-операцией, что и переход, поэтому история не расходится со статусом.", "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Журнал событий заявки", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true }, { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "id", "created_at" ], "type": "string", "default": "id", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.TicketEventListResponse" } }, "400": { "description": "некорректный id или параметры страницы", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "чужая заявка", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/photos": { "get": { "security": [ { "Bearer": [] } ], "description": "Файлы заявки в хронологии (id ASC по умолчанию). В каждом элементе url — временная подписанная ссылка на чтение: бакет приватный, сам ключ наружу не уходит.", "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Список фото отчёта", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true }, { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "id", "created_at" ], "type": "string", "default": "id", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.PhotoListResponse" } }, "400": { "description": "некорректный id или параметры страницы", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "чужая заявка", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "post": { "security": [ { "Bearer": [] } ], "description": "multipart/form-data, поле file: только фото (jpeg/png/webp, определяется по сигнатуре байтов), не больше PHOTO_MAX_BYTES на файл и PHOTO_MAX_PER_TICKET файлов на заявку. Доступно мастеру (заявка in_progress — после «Работа выполнена» отчёт заморожен, мастер — активный участник проекта) или админу. Каждая загрузка оставляет событие photo_added в журнале заявки.", "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Загрузить фото отчёта", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true }, { "type": "file", "description": "Файл фотографии (jpeg/png/webp)", "name": "file", "in": "formData", "required": true } ], "responses": { "201": { "description": "фото загружено, url — временная ссылка на чтение", "schema": { "$ref": "#/definitions/models.TicketPhoto" } }, "400": { "description": "некорректный id, файл отсутствует, не фото или больше лимита", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль worker или admin; чужая заявка", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "лимит файлов исчерпан или заявка не в статусе in_progress", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "500": { "description": "хранилище недоступно", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/photos/{photo_id}": { "delete": { "security": [ { "Bearer": [] } ], "description": "Своё фото удаляет автор, пока заявка в работе (in_progress): после «Работа выполнена» отчёт заморожен; админу — любое фото. Событие photo_removed пишется той же операцией, что и удаление.", "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Удалить фото", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true }, { "type": "integer", "description": "ID фото", "name": "photo_id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректный id или photo_id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль worker или admin; чужая заявка", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка или фото не найдены", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "чужое фото или заявка уже не в работе", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/reject": { "patch": { "security": [ { "Bearer": [] } ], "description": "done → rejected. Не финал: мастер берёт заявку в работу повторно, rework_count увеличивается. Причина обязательна — без неё мастер не поймёт, что переделывать.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Отклонить работу (на доработку)", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true }, { "description": "Причина отклонения, минимум 5 символов", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.reasonReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Ticket" } }, "400": { "description": "некорректный id или тело: причина обязательна", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "заявка не принадлежит вам", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "заявка не в статусе done", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/start": { "patch": { "security": [ { "Bearer": [] } ], "description": "assigned → in_progress, а после отклонения (rejected) — повторный запуск. started_at первого круга сохраняется.", "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Взять в работу", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Ticket" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль worker", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "не тот статус или заявка назначена другому", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/tickets/{id}/take": { "patch": { "security": [ { "Bearer": [] } ], "description": "new → assigned: мастер назначает себя на свободную заявку своего проекта. Дальше — «Взять в работу» (start) как обычно.", "produces": [ "application/json" ], "tags": [ "tickets" ], "summary": "Взять заявку самому", "parameters": [ { "type": "integer", "description": "ID заявки", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Ticket" } }, "400": { "description": "некорректный id; приглашение не принято или отклонено; не состоит в проекте заявки", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль worker", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "заявка не найдена", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "заявка уже взята или не в статусе new", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/users": { "get": { "security": [ { "Bearer": [] } ], "description": "Админ видит любую роль и всех; клиенту доступна только роль worker и только его собственные мастера (чужие организации не видны). С параметром q (минимум 2 символа) возвращается поиск по имени/email среди активных — так клиент пригласит в проект существующего мастера, не видя при этом полного списка. include_inactive=1 дополняет список отключёнными аккаунтами — только для админа.", "produces": [ "application/json" ], "tags": [ "users" ], "summary": "Список пользователей по роли", "parameters": [ { "enum": [ "worker", "admin", "client" ], "type": "string", "default": "worker", "description": "Роль", "name": "role", "in": "query" }, { "type": "string", "description": "Поиск по имени или email, минимум 2 символа", "name": "q", "in": "query" }, { "enum": [ "0", "1" ], "type": "string", "default": "0", "description": "Показать и отключённых (только для админа)", "name": "include_inactive", "in": "query" }, { "type": "integer", "default": 20, "description": "Строк на странице, 1..100", "name": "limit", "in": "query" }, { "type": "integer", "default": 0, "description": "Сдвиг от начала выборки", "name": "offset", "in": "query" }, { "enum": [ "name", "created_at", "email", "id" ], "type": "string", "default": "name", "description": "Поле сортировки", "name": "sort", "in": "query" }, { "enum": [ "asc", "desc" ], "type": "string", "description": "Направление сортировки", "name": "order", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.ProfileListResponse" } }, "400": { "description": "неизвестная роль, параметры страницы или короткий q", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "клиент запрашивает не мастеров", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "post": { "security": [ { "Bearer": [] } ], "description": "Админ создаёт мастеров, админов и клиентов; клиент — только мастеров (для себя, далее мастер добавляется в проект через POST /projects/{id}/workers). Роли admin и client клиентом недоступны. Пароль — по политике (10..72 символа, буквы и цифры). Подтверждение: аккаунт, созданный админом, подтверждён сразу; созданный клиентом — ждёт подтверждения этим же клиентом, после чего мастеру уходит письмо.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "users" ], "summary": "Создать пользователя", "parameters": [ { "description": "Данные пользователя", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.createUserReq" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/models.Profile" } }, "400": { "description": "некорректное тело запроса или слабый пароль", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "клиент пытается создать админа или клиента", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "email уже занят", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/users/{id}": { "delete": { "security": [ { "Bearer": [] } ], "description": "Админ удаляет любой «чистый» аккаунт: refresh-сессии и токены сброса удаляются каскадом, но наличие заявок, проектов или участий блокирует удаление (409 user has related records) — в этом случае отключите аккаунт через PATCH /users/{id}, историю не стираем. Клиент удаляет только мастера, состоящего в одном из его проектов: членства клиента в проектах удаляются в той же транзакции, но если на мастера ссылаются заявки либо чужие проекты — тоже 409, ничего не меняется. Чужой мастер и несуществующий id неразличимы (404). Свой id удалить нельзя.", "produces": [ "application/json" ], "tags": [ "users" ], "summary": "Удалить пользователя", "parameters": [ { "type": "string", "description": "ID пользователя (uuid)", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректный id; нельзя удалить себя", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль admin или client", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "пользователь не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "у пользователя есть связанные записи", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } }, "patch": { "security": [ { "Bearer": [] } ], "description": "Админ меняет любой аккаунт; клиент — только мастера, который активный участник одного из его проектов (иного клиента или чужого мастера не видит — 404). При деактивации все refresh-сессии пользователя закрываются сразу, мастер перестаёт работать во всех проектах. Выключить самого себя нельзя — вы остались бы без входа.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "users" ], "summary": "Включить/выключить аккаунт", "parameters": [ { "type": "string", "description": "ID пользователя (uuid)", "name": "id", "in": "path", "required": true }, { "description": "Новый флаг", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.setUserReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/models.Profile" } }, "400": { "description": "некорректный id или тело; попытка выключить себя", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль admin или client", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "пользователь не найден (для клиента — не его мастер)", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/users/{id}/confirm": { "post": { "security": [ { "Bearer": [] } ], "description": "Подтверждает неподтверждённый аккаунт и шлёт пользователю письмо «аккаунт подтверждён» со ссылкой на вход. Право — в SQL: админ подтверждает любой аккаунт, клиент — только созданного им мастера (иной и несуществующий не видны — 404). Повторное подтверждение — 409. Письмо отправляется после подтверждения; недоступный SMTP не откатывает подтверждение (письмо просто не уйдёт).", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "users" ], "summary": "Подтвердить аккаунт", "parameters": [ { "type": "string", "description": "ID пользователя (uuid)", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль admin или client", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "пользователь не найден (для клиента — не созданный им мастер)", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "409": { "description": "аккаунт уже подтверждён", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/users/{id}/password-reset": { "post": { "security": [ { "Bearer": [] } ], "description": "Токен возвращается в ответе — админ передаёт его пользователю сам, любым каналом. Для пользователя есть почта: POST /auth/forgot-password шлёт письмо со ссылкой. Токен одноразовый, живёт 24 часа (repository.ResetTTL); выпуск нового гасит прежние неиспользованные токены пользователя (включая свежие из писем). В БД хранится только sha256 токена.", "produces": [ "application/json" ], "tags": [ "users" ], "summary": "Выдать токен сброса пароля", "parameters": [ { "type": "string", "description": "ID пользователя (uuid)", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.PasswordResetResponse" } }, "400": { "description": "некорректный id", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "пользователь не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } }, "/users/{id}/password-set": { "post": { "security": [ { "Bearer": [] } ], "description": "Админ ставит пароль пользователю сам, без ссылки. Политика та же, что при создании и при сбросе по токену (10..72 символа, буквы и цифры). Успех закрывает все refresh-сессии пользователя и гасит неиспользованные токены сброса — со старым паролем в системе остаться нельзя, старые ссылки перестают работать. Альтернатива — одноразовая ссылка через POST /users/{id}/password-reset.", "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "users" ], "summary": "Задать пароль напрямую", "parameters": [ { "type": "string", "description": "ID пользователя (uuid)", "name": "id", "in": "path", "required": true }, { "description": "Новый пароль", "name": "body", "in": "body", "required": true, "schema": { "$ref": "#/definitions/handlers.setPasswordReq" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/handlers.OKResponse" } }, "400": { "description": "некорректный id, тело или слабый пароль", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "401": { "description": "нет или неверный токен", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "403": { "description": "нужна роль admin", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } }, "404": { "description": "пользователь не найден", "schema": { "$ref": "#/definitions/handlers.ErrorResponse" } } } } } }, "definitions": { "handlers.BindCodeResponse": { "type": "object", "properties": { "code": { "type": "string" }, "expires_at": { "type": "string" }, "link": { "type": "string" } } }, "handlers.ErrorResponse": { "type": "object", "properties": { "error": { "type": "string" } } }, "handlers.InvitationListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.ProjectInvitation" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.InvitationResponse": { "type": "object", "properties": { "expires_at": { "description": "До какого срока мастер может ответить (см. repository.InvitationTTL).", "type": "string" }, "ok": { "type": "boolean" }, "status": { "description": "Всегда \"invited\": стать участником мастер может только сам, через\nPOST /projects/{id}/invitation/accept.", "type": "string" } } }, "handlers.OKResponse": { "type": "object", "properties": { "ok": { "type": "boolean" }, "revoked": { "description": "Revoked — сколько сессий закрыто; заполняется только в logout-all.", "type": "integer" } } }, "handlers.ObjectListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.Object" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.PasswordResetResponse": { "type": "object", "properties": { "expires_at": { "description": "См. repository.ResetTTL (24 часа).", "type": "string" }, "ok": { "type": "boolean" }, "token": { "type": "string" } } }, "handlers.PhotoListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.TicketPhoto" } }, "limit": { "type": "integer" }, "max_per_ticket": { "description": "MaxPerTicket — лимит файлов на заявку (PHOTO_MAX_PER_TICKET):\nUI рисует «n / N», не зная настройки окружения.", "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.ProfileListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.Profile" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.ProjectListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.Project" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.ProjectWorkerListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.ProjectWorker" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.RecurringRuleListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.RecurringRule" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.RecurringRunListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.RecurringRun" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.RecurringRunResponse": { "type": "object", "properties": { "ok": { "type": "boolean" }, "ticket_id": { "type": "integer" } } }, "handlers.TelegramStatusResponse": { "type": "object", "properties": { "bot_username": { "description": "BotUsername — @имя бота, через который придут уведомления и\nссылка привязки; пусто, пока getMe не удался.", "type": "string" }, "bound": { "description": "Bound — есть ли уже цель уведомлений.", "type": "boolean" }, "enabled": { "description": "Enabled — может ли этот пользователь получать уведомления вообще:\nесть свой токен бота либо настроен глобальный бот сервера.", "type": "boolean" }, "has_bot_token": { "description": "HasBotToken — подключён ли пользователь свой бот (токен в БД).", "type": "boolean" }, "target": { "description": "Target — текущая цель: числовой chat_id либо @группа.", "type": "string" }, "token_configurable": { "description": "TokenConfigurable — можно ли ввести токен прямо сейчас: false,\nесли на сервере не задан DATA_ENCRYPTION_KEY.", "type": "boolean" } } }, "handlers.TicketEventListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.TicketEvent" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.TicketListResponse": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/definitions/models.Ticket" } }, "limit": { "type": "integer" }, "offset": { "type": "integer" }, "total": { "type": "integer" } } }, "handlers.TokenResponse": { "type": "object", "properties": { "expires_in": { "description": "Срок жизни access-токена в секундах.", "type": "integer" }, "token": { "description": "Access-токен: JWT на auth.AccessTTL.", "type": "string" }, "user": { "$ref": "#/definitions/models.Profile" } } }, "handlers.addWorkerReq": { "type": "object", "required": [ "worker_id" ], "properties": { "worker_id": { "type": "string" } } }, "handlers.assignReq": { "type": "object", "required": [ "worker_id" ], "properties": { "worker_id": { "type": "string" } } }, "handlers.createObjectReq": { "type": "object", "required": [ "address", "name" ], "properties": { "address": { "type": "string", "minLength": 5 }, "name": { "type": "string", "minLength": 2 } } }, "handlers.createProjectReq": { "type": "object", "required": [ "name" ], "properties": { "client_id": { "description": "ClientID — только для админа: для какого клиента создаётся проект.\nУ клиента владелец берётся из токена, поле игнорируется.", "type": "string" }, "name": { "type": "string", "minLength": 2 } } }, "handlers.createRecurringReq": { "type": "object", "required": [ "anchor_at", "category", "description", "object_id", "period_days" ], "properties": { "anchor_at": { "description": "Расписание: «каждые PeriodDays дней/недель от AnchorAt» —\nAnchorAt (RFC3339) — дата-время первого запуска, PeriodDays —\nпериод в днях (1..3650; недели кратны 7).", "type": "string" }, "category": { "type": "string", "enum": [ "electric", "plumbing", "climate", "elevator", "other" ] }, "description": { "type": "string", "minLength": 5 }, "floor": { "type": "string" }, "object_id": { "type": "integer" }, "period_days": { "type": "integer", "maximum": 3650, "minimum": 1 }, "priority": { "type": "string", "enum": [ "low", "normal", "high" ] } } }, "handlers.createTicketReq": { "type": "object", "required": [ "category", "description", "object_id" ], "properties": { "category": { "type": "string", "enum": [ "electric", "plumbing", "climate", "elevator", "other" ] }, "description": { "type": "string", "minLength": 5 }, "floor": { "type": "string" }, "object_id": { "description": "Объект, на который создаётся заявка: адрес копируется из объекта.", "type": "integer" }, "priority": { "type": "string", "enum": [ "low", "normal", "high" ] }, "recurring": { "description": "Recurring — задан, если заявку делают повторяющейся.", "allOf": [ { "$ref": "#/definitions/handlers.recurringSchedule" } ] } } }, "handlers.createUserReq": { "type": "object", "required": [ "email", "name", "password", "role" ], "properties": { "email": { "type": "string" }, "name": { "type": "string" }, "password": { "type": "string" }, "phone": { "type": "string" }, "role": { "type": "string", "enum": [ "worker", "admin", "client" ] } } }, "handlers.forgotPasswordReq": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string" } } }, "handlers.loginReq": { "type": "object", "required": [ "email", "password" ], "properties": { "email": { "type": "string" }, "password": { "type": "string" } } }, "handlers.reasonReq": { "type": "object", "required": [ "reason" ], "properties": { "reason": { "type": "string", "minLength": 5 } } }, "handlers.recurringSchedule": { "type": "object", "required": [ "anchor_at", "period_days" ], "properties": { "anchor_at": { "description": "AnchorAt — дата-время первого запуска (RFC3339): точка отсчёта\nсетки сроков, от неё идут «каждые period_days».", "type": "string" }, "period_days": { "description": "PeriodDays — период в днях (1..3650; недели кратны 7).", "type": "integer", "maximum": 3650, "minimum": 1 } } }, "handlers.resetPasswordReq": { "type": "object", "required": [ "password", "token" ], "properties": { "password": { "type": "string" }, "token": { "type": "string" } } }, "handlers.setBotReq": { "type": "object", "properties": { "token": { "description": "Token — токен бота от @BotFather; пусто невалидно (нужен явный ввод).", "type": "string" } } }, "handlers.setPasswordReq": { "type": "object", "required": [ "password" ], "properties": { "password": { "type": "string" } } }, "handlers.setTelegramReq": { "type": "object", "properties": { "target": { "description": "Target — @группа/@канал или числовой chat_id; пусто — отключить.", "type": "string" } } }, "handlers.setUserReq": { "type": "object", "required": [ "is_active" ], "properties": { "is_active": { "description": "*bool обязателен: на bool с binding:\"required\" запрос\n«деактивировать» (is_active=false) не прошёл бы валидацию.", "type": "boolean" } } }, "handlers.updateObjectReq": { "type": "object", "properties": { "address": { "type": "string", "minLength": 5 }, "is_active": { "type": "boolean" }, "name": { "type": "string", "minLength": 2 } } }, "handlers.updateProjectReq": { "type": "object", "properties": { "is_active": { "type": "boolean" }, "name": { "type": "string", "minLength": 2 } } }, "handlers.updateRecurringReq": { "type": "object", "properties": { "anchor_at": { "type": "string" }, "category": { "type": "string" }, "description": { "type": "string" }, "floor": { "type": "string" }, "is_active": { "type": "boolean" }, "period_days": { "type": "integer" }, "priority": { "type": "string" } } }, "models.Object": { "type": "object", "properties": { "address": { "type": "string" }, "created_at": { "type": "string" }, "id": { "type": "integer" }, "is_active": { "type": "boolean" }, "name": { "type": "string" }, "project_id": { "type": "integer" }, "project_name": { "description": "Денормализации для UI.", "type": "string" } } }, "models.Profile": { "type": "object", "properties": { "confirmed_at": { "description": "ConfirmedAt — момент подтверждения аккаунта; nil (null в JSON)\n— ожидает подтверждения, вход до этого запрещён (403\naccount not confirmed). Существующие пользователи подтверждены\nбэкфиллом миграции 0010.", "type": "string" }, "created_at": { "type": "string" }, "email": { "type": "string" }, "id": { "type": "string" }, "is_active": { "type": "boolean" }, "name": { "type": "string" }, "phone": { "type": "string" }, "role": { "type": "string" } } }, "models.Project": { "type": "object", "properties": { "client_id": { "type": "string" }, "client_name": { "description": "Денормализация для UI: чей проект (админ видит все сразу).", "type": "string" }, "created_at": { "type": "string" }, "id": { "type": "integer" }, "is_active": { "type": "boolean" }, "name": { "type": "string" } } }, "models.ProjectInvitation": { "type": "object", "properties": { "client_name": { "type": "string" }, "expires_at": { "type": "string" }, "invited_at": { "type": "string" }, "invited_by": { "description": "id пригласившего", "type": "string" }, "invited_by_name": { "description": "его имя для UI", "type": "string" }, "project_id": { "type": "integer" }, "project_name": { "type": "string" } } }, "models.ProjectWorker": { "type": "object", "properties": { "confirmed_at": { "description": "ConfirmedAt — момент подтверждения аккаунта; nil (null в JSON)\n— ожидает подтверждения, вход до этого запрещён (403\naccount not confirmed). Существующие пользователи подтверждены\nбэкфиллом миграции 0010.", "type": "string" }, "created_at": { "type": "string" }, "email": { "type": "string" }, "expires_at": { "description": "ExpiresAt — крайний срок ответа; заполнен у приглашений.", "type": "string" }, "id": { "type": "string" }, "is_active": { "type": "boolean" }, "name": { "type": "string" }, "phone": { "type": "string" }, "responded_at": { "description": "RespondedAt — когда мастер принял/отклонил; пусто, пока не ответил.", "type": "string" }, "role": { "type": "string" }, "status": { "description": "invited | active | declined (см. 0005_project_invitations.sql).", "type": "string" } } }, "models.RecurringRule": { "type": "object", "properties": { "address": { "description": "Адрес объекта — снимок, как у заявки: правило показывает, куда едут.", "type": "string" }, "anchor_at": { "description": "Расписание: «каждые PeriodDays дней/недель от AnchorAt» —\nAnchorAt — дата-время первого запуска (точка отсчёта сетки\nсроков), PeriodDays — период в днях (1..3650, недели кратны 7).", "type": "string" }, "category": { "description": "Шаблон заявки — те же поля, что у tickets.", "type": "string" }, "client_id": { "description": "ClientID — владелец будущих заявок (клиент проекта).", "type": "string" }, "client_name": { "type": "string" }, "created_at": { "type": "string" }, "created_by": { "description": "CreatedBy — кто завёл правило (админ или клиент); в журнале\nсозданной заявки это же лицо будет автором события «created».", "type": "string" }, "created_by_name": { "description": "Кто завёл правило (админ или сам клиент) — для аудита, в JSON идёт имя.", "type": "string" }, "description": { "type": "string" }, "floor": { "type": "string" }, "id": { "type": "integer" }, "is_active": { "type": "boolean" }, "last_run_at": { "description": "LastRunAt — когда последний раз срабатывали; null (в JSON — null)\nозначает «ещё ни разу», следующий запуск считается от created_at.", "type": "string" }, "next_run_at": { "description": "NextRunAt — ближайший запуск после последнего срабатывания\n(вычисляется при чтении; у выключенного правила — когда включат).", "type": "string" }, "object_id": { "type": "integer" }, "object_name": { "type": "string" }, "period_days": { "type": "integer" }, "priority": { "type": "string" }, "project_id": { "type": "integer" }, "project_name": { "description": "Денормализации для UI.", "type": "string" } } }, "models.RecurringRun": { "type": "object", "properties": { "address": { "description": "Адрес и категория — из заявки на момент создания (для списка истории).", "type": "string" }, "category": { "type": "string" }, "id": { "type": "integer" }, "rule_id": { "type": "integer" }, "run_at": { "type": "string" }, "ticket_id": { "description": "TicketID — созданная заявка (её номер = id); null, если её удалили\nкаскадом вместе с проектом.", "type": "integer" } } }, "models.Ticket": { "type": "object", "properties": { "address": { "type": "string" }, "assigned_at": { "type": "string" }, "assigned_by": { "type": "string" }, "cancel_reason": { "type": "string" }, "category": { "type": "string" }, "client_id": { "type": "string" }, "client_name": { "description": "Денормализованные поля для UI — подтягиваются JOIN'ом", "type": "string" }, "closed_at": { "type": "string" }, "closed_by": { "type": "string" }, "confirmed_at": { "type": "string" }, "created_at": { "type": "string" }, "description": { "type": "string" }, "done_at": { "type": "string" }, "floor": { "type": "string" }, "id": { "type": "integer" }, "object_id": { "type": "integer" }, "object_name": { "type": "string" }, "priority": { "type": "string" }, "project_id": { "description": "Принадлежность: заявка живёт в объекте проекта клиента.\nПроверяется составными FK в БД (миграция 0004).", "type": "integer" }, "project_name": { "type": "string" }, "recurring": { "description": "Recurring — правило повторения, если заявка связана с ним:\nсоздана вместе с правилом (чекбокс «Новая заявка») или самим\nпланировщиком. Заполняется только в детали (GetByID) — спискам\nблок на странице заявки не нужен.", "allOf": [ { "$ref": "#/definitions/models.TicketRecurring" } ] }, "reject_reason": { "type": "string" }, "rejected_at": { "description": "Закрытие цикла: rejected — это доработка, closed — финал.", "type": "string" }, "rework_count": { "description": "Сколько раз клиент отклонял работу — чтобы доработки не бесконечны\nбыли незаметны.", "type": "integer" }, "started_at": { "type": "string" }, "status": { "type": "string" }, "worker_id": { "type": "string" }, "worker_name": { "type": "string" } } }, "models.TicketEvent": { "type": "object", "properties": { "action": { "description": "created | assigned | started | done | confirmed | rejected |\ncanceled | photo_added | photo_removed.", "type": "string" }, "actor_id": { "description": "\"\" — автор удалён (событие сохранится, имя нет).", "type": "string" }, "actor_name": { "type": "string" }, "comment": { "description": "Причина для rejected/canceled, имя файла для photo_*; иначе пусто.", "type": "string" }, "created_at": { "type": "string" }, "from_status": { "description": "\"\" — у события «создана» не было предыдущего статуса.\nУ photo_added/photo_removed статус не менялся: оба равны текущему.", "type": "string" }, "id": { "type": "integer" }, "ticket_id": { "type": "integer" }, "to_status": { "type": "string" } } }, "models.TicketPhoto": { "type": "object", "properties": { "content_type": { "type": "string" }, "created_at": { "type": "string" }, "id": { "type": "integer" }, "original_name": { "description": "Имя, которое дал файл при загрузке: лежит и в журнале (comment).", "type": "string" }, "size_bytes": { "type": "integer" }, "ticket_id": { "type": "integer" }, "uploader_id": { "description": "\"\" — аккаунт загрузившего удалён, файл остаётся.", "type": "string" }, "uploader_name": { "type": "string" }, "url": { "description": "URL — presigned GET (срок жизни ограничен); заполняется только\nв ответах API. ObjectKey — внутренняя деталь, в JSON не уходит.", "type": "string" } } }, "models.TicketRecurring": { "type": "object", "properties": { "anchor_at": { "description": "Расписание: «каждые PeriodDays дней/недель от AnchorAt» —\nточка отсчёта плюс период в днях.", "type": "string" }, "is_active": { "type": "boolean" }, "next_run_at": { "description": "NextRunAt — ближайший запуск, вычисляется при чтении\n(см. repository.nextDue); у выключенного правила — когда включат.", "type": "string" }, "period_days": { "type": "integer" }, "rule_id": { "type": "integer" } } } }, "securityDefinitions": { "Bearer": { "description": "JWT формата Bearer, получить через POST /auth/login. Срок жизни — 1 час, дальше POST /auth/refresh.", "type": "apiKey", "name": "Authorization", "in": "header" } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0", Host: "localhost:8080", BasePath: "/", Schemes: []string{"http"}, Title: "Building Management API", Description: "Управление заявками на обслуживание: админ создаёт клиента, клиент заводит проект, объекты в нём и мастеров, назначает заявку мастеру проекта, мастер выполняет, клиент подтверждает работу или отклоняет её на доработку. Финальный статус — closed.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", RightDelim: "}}", } func init() { swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) }