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