User Service — API Contract
Этот документ является единственным источником правды для API User Service.
Бэкенд реализует контракт. Фронтенд потребляет контракт. Отклонения запрещены.
Содержание
Public API (Bearer JWT)
Франшизы (BR 1.4.4)
Юридические лица
- GET /legal-entities
- GET /legal-entities/{id}
- POST /legal-entities
- PATCH /legal-entities/{id}
- DELETE /legal-entities/{id}
- POST /legal-entities/{id}/suspend
- POST /legal-entities/{id}/resume
- GET /legal-entities/{id}/owner-permissions
- PUT /legal-entities/{id}/owner-permissions
- POST /legal-entities/import/preview
- POST /legal-entities/import/{preview_id}/apply
- GET /legal-entities/import/template
Сотрудники (BR 1.4)
- GET /employees
- GET /employees/{id}
- POST /employees
- PATCH /employees/{id}
- POST /employees/{id}/deactivate
- POST /employees/{id}/reactivate
Роли и permissions (BR 1.4.3)
- GET /roles
- GET /roles/{id}
- POST /roles
- PATCH /roles/{id}
- DELETE /roles/{id}
- POST /roles/{id}/restore
- GET /roles/permission-catalog
Юридические детали сотрудника (BR 1.4.1)
Шаблоны смен (BR 1.4.1)
Расписание смен (BR 1.4.1)
Фактические смены (BR 1.4.1)
Корректировки смен (BR 1.4.1)
Дашборд активности (BR 1.4.1)
Отчёт по смене (BR 2.2)
Зарплата — формулы (BR 1.4.1)
Зарплата — ведомости (BR 1.4.1)
- GET /payroll
- POST /payroll/calculate
- POST /payroll/{id}/confirm
- POST /payroll/{id}/mark-paid
- GET /payroll/{id}/export
Internal API (Service Token)
- GET /internal/legal-entities/{id}
- GET /internal/users/{id}/scope
- POST /internal/users/validate-credentials
- GET /internal/users/by-email
- POST /internal/users/validate-pin
- PATCH /internal/users/{id}/password
Фактические смены — POS (BR 1.4.1)
- POST /internal/shift-records/clock-in
- POST /internal/shift-records/clock-out
- POST /internal/shift-records/break-start
- POST /internal/shift-records/break-end
GET /franchises/{id}
(Введено в BR 1.4.4)
Получить данные франшизы (tenant). Используется фронтом для отображения franchise.type (управляет видимостью раздела «Юр. лица») и брендового имени.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (только своя франшиза — id сравнивается с franchise_id из JWT) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID франшизы |
Response 200
{
"data": {
"id": "uuid",
"name": "string",
"type": "corporate | individual",
"created_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FRANCHISE_NOT_FOUND | 404 | Запрашиваемая id не принадлежит JWT (чужая франшиза) или не существует |
GET /legal-entities
Получить список юридических лиц с пагинацией, фильтрами и поиском.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — все; Franchisee — только свои) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
page | integer | no | Номер страницы (default: 1) |
per_page | integer | no | Записей на страницу (default: 20, max: 100) |
search | string | no | Поиск по наименованию и ИНН |
status | string | no | Фильтр: active / suspended |
type | string | no | Фильтр: franchise / franchisee |
sort | string | no | Сортировка: name_asc (default) / name_desc / created_at_desc |
Response 200
{
"data": [
{
"id": "uuid",
"type": "franchise | franchisee",
"name": "string",
"inn": "string",
"status": "active | suspended",
"is_primary": "boolean",
"store_count": "integer",
"created_at": "datetime"
}
],
"meta": {
"page": "integer",
"per_page": "integer",
"total": "integer"
}
}
store_countполучается batch-запросом к Store Service (POST /internal/stores/count-by-legal-entities)
Ролевой доступ
- Franchise: все ЮЛ франшизы (
WHERE franchise_id = :jwt_franchise_id) - Franchisee: только свои (
WHERE owner_user_id = :jwt_user_id) - Manager, Cashier:
403 FORBIDDEN
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Роль не имеет доступа (Manager, Cashier) |
GET /legal-entities/{id}
Получить детали юридического лица.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любое; Franchisee — только своё) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID юридического лица |
Response 200
{
"data": {
"id": "uuid",
"franchise_id": "uuid",
"type": "franchise | franchisee",
"name": "string",
"inn": "string",
"kpp": "string | null",
"ogrn": "string",
"legal_address": "string",
"bank_name": "string | null",
"bank_bik": "string | null",
"bank_account": "string | null",
"bank_corr_account": "string | null",
"contact_phone": "string | null",
"contact_email": "string | null",
"is_primary": "boolean",
"status": "active | suspended",
"contract_number": "string | null",
"contract_date": "date | null",
"owner_user_id": "uuid | null",
"store_count": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Franchisee пытается посмотреть чужое ЮЛ |
LEGAL_ENTITY_NOT_FOUND | 404 | ЮЛ не найдено или удалено |
POST /legal-entities
Создать юридическое лицо.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (только Franchise) |
| Content-Type | application/json |
Request Body
{
"type": "string, required — franchise | franchisee",
"name": "string, required — полное наименование",
"inn": "string, required — 10 или 12 цифр, проверка контрольной суммы",
"kpp": "string, optional — 9 цифр",
"ogrn": "string, required — 13 или 15 цифр",
"legal_address": "string, required",
"bank_name": "string, optional",
"bank_bik": "string, optional — 9 цифр",
"bank_account": "string, optional — 20 цифр",
"bank_corr_account": "string, optional — 20 цифр",
"contact_phone": "string, optional — формат +7XXXXXXXXXX",
"contact_email": "string, optional — email",
"contract_number": "string, optional — только для type=franchisee",
"contract_date": "date, optional — только для type=franchisee",
"owner": {
"first_name": "string, required",
"last_name": "string, required",
"email": "string, required — уникален в рамках franchise_id",
"phone": "string, optional",
"password": "string, optional — min 6 chars, генерируется если не задан"
},
"owner_permissions": {
"mode": "string, optional — full | custom (default full)",
"permissions": "string[], optional — массив permission-ключей; используется только при mode=custom, при mode=full игнорируется"
}
}Блок
ownerдля type=franchisee (Введено в BR 1.4.2)При
type=franchiseeблокownerобязателен. Backend в одной транзакции:
- Проверяет
franchises.type = corporate(иначе403 FRANCHISE_TYPE_INDIVIDUAL)- Создаёт
legal_entities- Создаёт
employeesизowner.*(в BR 1.4.4 полеroleenum удалено — больше не передаётся)- В зависимости от
owner_permissions.mode:
full(default) — назначает системную роль «Администратор» вemployee_rolescustom— создаёт скрытую роль вroles(сowner_legal_entity_id = {новый_id_ЮЛ}, permissions из запроса + автоматически форсятсяpos.access,stores.read,employees.read); назначает её владельцу- Обновляет
legal_entities.owner_user_id→ id созданного employeeПри
type=franchiseблокиownerиowner_permissionsигнорируются (если переданы).Поле
owner_user_idв request body больше не принимается (заменено на блокowner).
Response 201
{
"data": {
"id": "uuid",
"franchise_id": "uuid",
"type": "franchise | franchisee",
"name": "string",
"inn": "string",
"kpp": "string | null",
"ogrn": "string",
"legal_address": "string",
"bank_name": "string | null",
"bank_bik": "string | null",
"bank_account": "string | null",
"bank_corr_account": "string | null",
"contact_phone": "string | null",
"contact_email": "string | null",
"is_primary": "boolean",
"status": "active",
"contract_number": "string | null",
"contract_date": "date | null",
"owner_user_id": "uuid | null",
"owner": {
"id": "uuid — id созданного employee (только для type=franchisee, иначе null)",
"email": "string",
"temporary_password": "string | null — возвращается ТОЛЬКО если пароль был сгенерирован автоматически"
},
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Роль не admin_franchise |
VALIDATION_ERROR | 400 | Невалидные данные (ИНН, ОГРН, и т.д.) |
OWNER_FIELDS_REQUIRED | 400 | type=franchisee, но блок owner отсутствует или неполный (см. BR 1.4.2) |
OWNER_EMAIL_DUPLICATE | 409 | owner.email уже существует среди сотрудников в рамках franchise_id |
INN_DUPLICATE | 409 | ИНН уже существует в рамках franchise_id |
FRANCHISE_LE_ALREADY_EXISTS | 409 | ЮЛ типа franchise уже существует (допускается только одно) |
INVALID_INN_CHECKSUM | 422 | ИНН не проходит проверку контрольной суммы |
FRANCHISE_TYPE_INDIVIDUAL | 403 | Попытка создать ЮЛ type=franchisee при franchises.type=individual (BR 1.4.4 §3, §7) |
UNKNOWN_PERMISSION_KEY | 400 | В owner_permissions.permissions[] есть ключ вне каталога (BR 1.4.4 §5.2) |
PATCH /legal-entities/{id}
Обновить юридическое лицо.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любое; Franchisee — только своё, ограниченный набор полей) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID юридического лица |
Request Body
Partial update — отправляются только изменяемые поля.
{
"name": "string, optional — ТОЛЬКО Franchise",
"kpp": "string, optional — ТОЛЬКО Franchise",
"ogrn": "string, optional — ТОЛЬКО Franchise",
"legal_address": "string, optional",
"bank_name": "string, optional",
"bank_bik": "string, optional",
"bank_account": "string, optional",
"bank_corr_account": "string, optional",
"contact_phone": "string, optional",
"contact_email": "string, optional",
"contract_number": "string, optional — ТОЛЬКО Franchise, только для type=franchisee",
"contract_date": "date, optional — ТОЛЬКО Franchise, только для type=franchisee",
"owner_user_id": "uuid, optional — ТОЛЬКО Franchise, только для type=franchisee"
}ИНН нельзя менять после создания
Поле
innне принимается в PATCH. Если передано —422 INN_IMMUTABLE.
Franchisee может менять только:
legal_address,bank_name,bank_bik,bank_account,bank_corr_account,contact_phone,contact_emailПопытка изменить другие поля →
403 FORBIDDEN.
Response 200
Полный объект ЮЛ (аналогично GET /legal-entities/{id}).
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на редактирование или попытка изменить запрещённое поле |
LEGAL_ENTITY_NOT_FOUND | 404 | ЮЛ не найдено или удалено |
VALIDATION_ERROR | 400 | Невалидные данные |
INN_IMMUTABLE | 422 | Попытка изменить ИНН |
DELETE /legal-entities/{id}
Удалить юридическое лицо (soft delete).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (только Franchise) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID юридического лица |
Перед удалением проверяется наличие привязанных ТТ
Синхронный вызов Store Service
GET /internal/stores?legal_entity_id={id}. Если есть хотя бы одна ТТ (любой статус) — удаление запрещено, в ответе возвращается список ТТ.
Response 204
Нет тела.
Error Response 422 (есть привязанные ТТ)
{
"error": {
"code": "HAS_ATTACHED_STORES",
"message": "Нельзя удалить ЮЛ с привязанными торговыми точками",
"details": [
{
"store_id": "uuid",
"store_name": "string"
}
]
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Роль не Franchise |
LEGAL_ENTITY_NOT_FOUND | 404 | ЮЛ не найдено или уже удалено |
HAS_ATTACHED_STORES | 422 | Есть привязанные ТТ |
FRANCHISE_LE_CANNOT_BE_DELETED | 422 | Нельзя удалить ЮЛ Франшизы (оно единственное и главное) |
POST /legal-entities/{id}/suspend
Приостановить ЮЛ Франчайзи. Все ТТ этого ЮЛ снимаются с публикации.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (только Franchise) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID юридического лица |
Синхронный вызов Store Service
POST /internal/stores/unpublish-by-legal-entity— снимает все ТТ этого ЮЛ с публикации.
Response 200
{
"data": {
"id": "uuid",
"status": "suspended",
"unpublished_stores_count": "integer"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Роль не Franchise |
LEGAL_ENTITY_NOT_FOUND | 404 | ЮЛ не найдено |
NOT_FRANCHISEE_TYPE | 422 | Нельзя приостановить ЮЛ Франшизы |
ALREADY_SUSPENDED | 422 | ЮЛ уже приостановлено |
POST /legal-entities/{id}/resume
Возобновить ЮЛ Франчайзи. ТТ НЕ публикуются автоматически.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (только Franchise) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID юридического лица |
Response 200
{
"data": {
"id": "uuid",
"status": "active"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Роль не Franchise |
LEGAL_ENTITY_NOT_FOUND | 404 | ЮЛ не найдено |
NOT_SUSPENDED | 422 | ЮЛ не приостановлено |
GET /legal-entities/{id}/owner-permissions
(Введено в BR 1.4.4 §5.2)
Получить текущий режим прав владельца партнёра и список permissions. Используется вкладкой «Права» в карточке ЮЛ партнёра.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (владелец франшизы — для любого ЮЛ-партнёра франшизы; владелец партнёра — для своего ЮЛ, только чтение собственных прав) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID ЮЛ партнёра (type=franchisee) |
Response 200
{
"data": {
"mode": "full | custom",
"permissions": ["string — permission keys; пустой при mode=full (значит системная Администратор)"]
}
}mode=full— владельцу назначена системная роль «Администратор»;permissionsвозвращает её полный набор для отображенияmode=custom— владельцу назначена скрытая роль (roles.owner_legal_entity_id = id);permissions— её набор (включая принудительный минимумpos.access,stores.read,employees.read)
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Доступ к чужому ЮЛ |
LEGAL_ENTITY_NOT_FOUND | 404 | ЮЛ не найдено или удалено |
NOT_FRANCHISEE_TYPE | 422 | Запрошен owner-permissions для ЮЛ type=franchise (бессмысленно) |
PUT /legal-entities/{id}/owner-permissions
(Введено в BR 1.4.4 §5.2)
Изменить режим/набор прав владельца партнёра. Применяется немедленно — Auth Service инвалидирует кэш user_permissions владельца на следующем запросе.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (только владелец франшизы) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID ЮЛ партнёра |
Request Body
{
"mode": "string, required — full | custom",
"permissions": "string[], required if mode=custom — массив permission-ключей"
}Серверная логика
- full: если у владельца сейчас скрытая роль — отвязать (
employee_rolesудаляется), скрытая роль удаляется (или soft-delete); назначить системную «Администратор»- custom: если сейчас «Администратор» — отвязать; если скрытая роль уже есть — обновить набор permissions; иначе создать новую скрытую роль (
owner_legal_entity_id = id, техническое имя) и назначить владельцу- Минимум (
pos.access,stores.read,employees.read) форсится сервером всегда приcustom— даже если клиент их не передал- Edit implies Read — серверная валидация при сохранении
Response 200
{
"data": {
"mode": "full | custom",
"permissions": ["string — итоговый набор после применения принудительного минимума"]
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Не владелец франшизы |
LEGAL_ENTITY_NOT_FOUND | 404 | ЮЛ не найдено |
NOT_FRANCHISEE_TYPE | 422 | ЮЛ имеет type=franchise |
OWNER_NOT_ASSIGNED | 422 | У ЮЛ owner_user_id IS NULL — некому давать права |
UNKNOWN_PERMISSION_KEY | 400 | В permissions[] есть ключ вне каталога |
VALIDATION_ERROR | 400 | mode=custom без permissions[], или edit без read |
POST /legal-entities/import/preview
Загрузить xlsx-файл, валидировать строки, вернуть preview.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (только Franchise) |
| Content-Type | multipart/form-data |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
file | file | yes | xlsx-файл (max 10 000 строк) |
Response 200
{
"data": {
"preview_id": "uuid",
"total_rows": "integer",
"valid_rows": "integer",
"error_rows": "integer",
"errors": [
{
"row": "integer",
"field": "string",
"message": "string"
}
],
"expires_at": "datetime"
}
}Preview хранится в PostgreSQL (таблица
import_previews), TTL = 30 минут. Импортируются только ЮЛ Франчайзи.
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Роль не Franchise |
VALIDATION_ERROR | 400 | Неверный формат файла (не xlsx) |
TOO_MANY_ROWS | 422 | Больше 10 000 строк |
POST /legal-entities/import/{preview_id}/apply
Применить импорт — создать ЮЛ из валидных строк preview.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (только Franchise) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
preview_id | uuid | yes | ID preview из предыдущего шага |
Response 200
{
"data": {
"imported": "integer",
"skipped": "integer"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Роль не Franchise |
PREVIEW_NOT_FOUND | 404 | Preview не найден или истёк |
PREVIEW_EXPIRED | 422 | TTL preview истёк (30 минут) |
GET /legal-entities/import/template
Скачать xlsx-шаблон для импорта.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (только Franchise) |
Response 200
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Файл xlsx с заголовками колонок: Наименование, ИНН, КПП, ОГРН, Юр. адрес, Банк, БИК, Расч. счёт, Корр. счёт, Телефон, Email, Номер договора, Дата договора.
GET /internal/legal-entities/{id}
Internal endpoint — только для service-to-service вызовов
Получить ЮЛ по ID (для других сервисов).
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID юридического лица |
Response 200
{
"data": {
"id": "uuid",
"franchise_id": "uuid",
"type": "franchise | franchisee",
"name": "string",
"inn": "string",
"status": "active | suspended",
"is_primary": "boolean",
"owner_user_id": "uuid | null"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
LEGAL_ENTITY_NOT_FOUND | 404 | ЮЛ не найдено |
GET /internal/users/{id}/scope
(Введено в BR 1.4.4 §2)
Internal endpoint — для Auth Service (кэш
user_scope:{user_id}) и downstream-сервисов
Возвращает вычисленный scope сотрудника по правилам Ролевая модель.
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | employees.id |
Response 200
{
"data": {
"user_id": "uuid",
"franchise_id": "uuid",
"scope": {
"type": "all_franchise | legal_entity_ids | store_ids",
"legal_entity_ids": ["uuid"],
"store_ids": ["uuid"]
}
}
}Логика вычисления scope
Сервер проверяет в следующем порядке:
- Если существует
legal_entitiesзапись гдеowner_user_id = {id}Иtype = 'franchise'(главное ЮЛ) →{ type: "all_franchise" }(поляlegal_entity_ids/store_idsотсутствуют) - Иначе если существуют записи
legal_entitiesгдеowner_user_id = {id}Иtype = 'franchisee'(один или несколько ЮЛ) →{ type: "legal_entity_ids", legal_entity_ids: [...все_id_этих_ЮЛ...] } - Иначе →
{ type: "store_ids", store_ids: [...UNIQUE store_id из employee_role_stores для этого employee...] }(может быть пустым массивом, если у сотрудника нет permissions-ролей)
Errors
| Code | HTTP | Когда |
|---|---|---|
USER_NOT_FOUND | 404 | Сотрудник не найден |
POST /internal/users/validate-credentials
Internal endpoint — для Auth Service
Реализация ожидает BR по сотрудникам (ADR-004)
Проверить email + пароль сотрудника. Возвращает данные для формирования JWT.
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
| Content-Type | application/json |
Request Body
{
"email": "string, required",
"password": "string, required"
}Response 200
{
"data": {
"id": "uuid",
"email": "string",
"first_name": "string",
"last_name": "string",
"franchise_id": "uuid",
"status": "active | inactive",
"role_ids": ["uuid"],
"permissions": ["string — permission keys, агрегат с granted=true"],
"scope": {
"type": "all_franchise | legal_entity_ids | store_ids",
"legal_entity_ids": ["uuid"],
"store_ids": ["uuid"]
}
}
}(Расширено в BR 1.4.3: поля role_ids и permissions.)
(Обновлено в BR 1.4.4: удалены role, store_ids, legal_entity_id. Добавлен scope — вычисляется по правилам Ролевая модель.)
Errors
| Code | HTTP | Когда |
|---|---|---|
INVALID_CREDENTIALS | 401 | Неверный email или пароль |
ACCOUNT_DISABLED | 403 | Сотрудник деактивирован (status=inactive) |
GET /internal/users/by-email
Internal endpoint — для Auth Service
Реализация ожидает BR по сотрудникам (ADR-004)
Получить сотрудника по email (без проверки пароля).
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
email | string | yes | Email сотрудника |
Response 200
{
"data": {
"id": "uuid",
"email": "string",
"first_name": "string",
"last_name": "string",
"franchise_id": "uuid",
"status": "active | inactive",
"role_ids": ["uuid"],
"permissions": ["string"],
"scope": {
"type": "all_franchise | legal_entity_ids | store_ids",
"legal_entity_ids": ["uuid"],
"store_ids": ["uuid"]
}
}
}(Расширено в BR 1.4.3: поля role_ids и permissions.)
(Обновлено в BR 1.4.4: удалены role, store_ids, legal_entity_id. Добавлен scope.)
Errors
| Code | HTTP | Когда |
|---|---|---|
USER_NOT_FOUND | 404 | Сотрудник с таким email не найден |
GET /employees
Получить список сотрудников с пагинацией, поиском и фильтрами.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — все по franchise_id; Franchisee — только свои ТТ; Manager — только своя ТТ; Cashier — 403) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
page | integer | no | Номер страницы (default: 1) |
per_page | integer | no | Записей на страницу (default: 20, max: 100) |
search | string | no | Поиск по имени, email, телефону |
role_id | uuid | no | Фильтр по назначенной permissions-роли (BR 1.4.4 — заменил enum-фильтр role) |
store_id | uuid | no | Фильтр по привязке к ТТ (через employee_role_stores) |
status | string | no | Фильтр: active / inactive |
sort | string | no | Сортировка: name_asc (default) / name_desc / created_at_desc |
Response 200
{
"data": [
{
"id": "uuid",
"first_name": "string",
"last_name": "string",
"email": "string",
"phone": "string | null",
"roles": [
{ "id": "uuid", "name": "string", "is_hidden": "boolean" }
],
"stores": [
{ "id": "uuid", "name": "string" }
],
"status": "active | inactive",
"is_courier": "boolean"
}
],
"meta": {
"page": "integer",
"per_page": "integer",
"total": "integer"
}
}(Обновлено в BR 1.4.4: удалены role, store_ids (как старое поле). stores — агрегат уникальных ТТ по всем ролям сотрудника. roles[].is_hidden = true для скрытой роли владельца партнёра.)
Доступ (по scope, BR 1.4.4)
- Владелец франшизы (
scope.type = all_franchise): все сотрудники вfranchise_id - Владелец партнёра (
scope.type = legal_entity_ids): сотрудники, у которых хотя бы одна ТТ принадлежит ЮЛ изscope.legal_entity_ids - Обычный сотрудник с
employees.readpermission (scope.type = store_ids): сотрудники, чьи ТТ пересекаются сscope.store_ids - Без
employees.readpermission →403 FORBIDDEN
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет permission employees.read |
GET /employees/{id}
Получить детали сотрудника.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любой; Franchisee — только свои ТТ; Manager — только своя ТТ; Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID сотрудника |
Response 200
{
"data": {
"id": "uuid",
"franchise_id": "uuid",
"first_name": "string",
"last_name": "string",
"email": "string",
"phone": "string | null",
"status": "active | inactive",
"is_courier": "boolean",
"roles": [
{
"id": "uuid",
"name": "string",
"is_system": "boolean",
"is_hidden": "boolean — true для скрытой роли владельца партнёра (отображается как 'Собственные права')",
"store_ids": ["uuid"]
}
],
"created_at": "datetime",
"updated_at": "datetime"
}
}(Обновлено в BR 1.4.4: удалены поля role, store_ids. Добавлен массив roles[] с флагом is_hidden для скрытых ролей владельцев партнёров.)
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Сотрудник вне scope |
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
POST /employees
Создать сотрудника.
(Переработано в BR 1.4.4)
Поля
role(enum) и отдельноеstore_idsудалены. Магазины — только вroles[].store_ids(per-role). Создание admin-ролей через этот endpoint больше не имеет смысла (нет enum) — владельцы франшизы и партнёров создаются bootstrap-ом иPOST /legal-entitiesсоответственно.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (employees.edit permission + scope: владелец франшизы — любая ТТ; владелец партнёра — только свои ТТ) |
| Content-Type | application/json |
Request Body
{
"first_name": "string, required",
"last_name": "string, required",
"email": "string, required — email, уникален в рамках franchise_id",
"password": "string, required — минимум 6 символов",
"phone": "string, optional — формат +7XXXXXXXXXX",
"pin": "string, optional — 4 цифры, уникален в рамках store_id",
"is_courier": "boolean, optional — default false",
"roles": [
{
"role_id": "uuid, required",
"store_ids": "uuid[], optional — магазины где эта роль действует; [] = все доступные сотруднику в рамках scope"
}
]
}
roles— массив permissions-ролей (объектов из GET /roles). Каждая роль может быть привязана к своему набору магазинов. Если не передано или пустой массив — сотрудник создаётся без permissions-ролей (например, ещё не назначен — позже добавят через PATCH). Валидация:
role_idпринадлежит той же франшизе, не удалён, не скрытый (owner_legal_entity_id IS NULL)store_ids— подмножество ТТ, доступных создателю по его scope (владелец франшизы — любые ТТ; владелец партнёра — только свои)- Пустой
store_ids([]) допустим — роль действует во всех ТТ scope сотрудника
Response 201
{
"data": {
"id": "uuid",
"franchise_id": "uuid",
"first_name": "string",
"last_name": "string",
"email": "string",
"phone": "string | null",
"status": "active",
"is_courier": "boolean",
"roles": [
{
"id": "uuid",
"name": "string",
"store_ids": ["uuid"]
}
],
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет employees.edit permission или указанная ТТ вне scope |
VALIDATION_ERROR | 400 | Невалидные данные (email, phone, pin формат) |
EMAIL_DUPLICATE | 409 | Email уже существует в рамках franchise_id |
PIN_DUPLICATE | 409 | PIN уже занят в данной ТТ |
STORE_NOT_FOUND | 404 | store_id не существует |
ROLE_NOT_FOUND | 404 | role_id из roles[] не существует, удалён или скрытый (BR 1.4.3, BR 1.4.4) |
ROLE_STORE_OUT_OF_SCOPE | 400 | store_id в roles[].store_ids вне scope создателя (BR 1.4.3) |
PATCH /employees/{id}
Обновить сотрудника.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любой; Franchisee — только свои сотрудники) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID сотрудника |
Request Body
Partial update — отправляются только изменяемые поля.
{
"first_name": "string, optional",
"last_name": "string, optional",
"email": "string, optional",
"phone": "string, optional",
"password": "string, optional",
"pin": "string, optional — 4 цифры",
"is_courier": "boolean, optional",
"roles": [
{
"role_id": "uuid",
"store_ids": "uuid[]"
}
]
}(Обновлено в BR 1.4.4)
Поля
role(enum) и отдельноеstore_idsудалены. Магазины — только вroles[].store_ids. Если в запросе пришли legacy-поляroleилиstore_ids(на уровне employee), они игнорируются (или возвращается400 VALIDATION_ERROR— на усмотрение реализации).
Permissions-роли — полная замена (Добавлено в BR 1.4.3)
Поле
roles(массив permissions-ролей) — идемпотентная замена: переданный массив полностью заменяет текущий набор ролей сотрудника. Чтобы добавить/убрать роль — отправить обновлённый полный массив. Валидация та же что и в POST: только обычные (не скрытые) роли,store_idsв рамках scope.
Response 200
Полный объект сотрудника (аналогично GET /employees/{id}).
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на редактирование |
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
VALIDATION_ERROR | 400 | Невалидные данные |
EMAIL_DUPLICATE | 409 | Email уже существует в рамках franchise_id |
PIN_DUPLICATE | 409 | PIN уже занят в данной ТТ |
ROLE_NOT_FOUND | 404 | role_id из roles[] не существует, удалён или скрытый (BR 1.4.3, BR 1.4.4) |
ROLE_STORE_OUT_OF_SCOPE | 400 | store_id в roles[].store_ids вне scope (BR 1.4.3) |
POST /employees/{id}/deactivate
Деактивировать сотрудника (status → inactive). Все активные сессии сотрудника завершаются (через Auth Service, когда будет реализован).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любой; Franchisee — только свои сотрудники) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID сотрудника |
Response 200
{
"data": {
"id": "uuid",
"status": "inactive"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на деактивацию |
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
ALREADY_INACTIVE | 422 | Сотрудник уже деактивирован |
POST /employees/{id}/reactivate
Реактивировать сотрудника (status → active).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любой; Franchisee — только свои сотрудники) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID сотрудника |
Response 200
{
"data": {
"id": "uuid",
"status": "active"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на реактивацию |
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
ALREADY_ACTIVE | 422 | Сотрудник уже активен |
POST /internal/users/validate-pin
Internal endpoint — для Auth Service (POS PIN-авторизация)
Проверить PIN сотрудника по store_id. Используется для авторизации кассира на POS-терминале.
(Переработано в BR 1.4.4 §4)
Логика: PIN совпадает с
pin_hashсотрудника ТТ И в агрегате permissions-ролей сотрудника естьpos.access. Если PIN корректен, ноpos.accessнет →403 POS_ACCESS_DENIED. Enumcashierбольше не используется.
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
| Content-Type | application/json |
Request Body
{
"pin": "string, required — 4 цифры",
"store_id": "uuid, required"
}Response 200
{
"data": {
"id": "uuid",
"email": "string",
"first_name": "string",
"last_name": "string",
"franchise_id": "uuid",
"status": "active | inactive",
"role_ids": ["uuid"],
"permissions": ["string"],
"scope": {
"type": "all_franchise | legal_entity_ids | store_ids",
"legal_entity_ids": ["uuid"],
"store_ids": ["uuid"]
}
}
}(Расширено в BR 1.4.3: поля role_ids и permissions.)
(Обновлено в BR 1.4.4: удалены role, store_ids, legal_entity_id. Добавлен scope. Добавлена ошибка POS_ACCESS_DENIED.)
Errors
| Code | HTTP | Когда |
|---|---|---|
INVALID_CREDENTIALS | 401 | Неверный PIN для данной ТТ |
ACCOUNT_DISABLED | 403 | Сотрудник деактивирован (status=inactive) |
POS_ACCESS_DENIED | 403 | PIN корректен, но в permissions сотрудника нет pos.access (BR 1.4.4 §4) |
PATCH /internal/users/{id}/password
Internal endpoint — для Auth Service
Используется для сброса/смены пароля сотрудника из Auth Service.
Обновить пароль сотрудника по ID. Пароль хэшируется через BCrypt.
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID сотрудника |
Request Body
{
"password": "string, required — новый пароль"
}Response 200
{
"data": {
"status": "updated"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
GET /employees/{id}/legal-details
Получить юридические детали сотрудника (ИНН, паспорт, СНИЛС, водительское удостоверение).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любой; Franchisee — только свои сотрудники; Manager, Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID сотрудника |
Response 200
{
"data": {
"employee_id": "uuid",
"inn": "string | null",
"passport_series": "string | null",
"passport_number": "string | null",
"driver_license_number": "string | null",
"driver_license_expiry": "date | null",
"snils": "string | null",
"updated_at": "datetime | null"
}
}Ролевой доступ
- Franchise: юридические детали любого сотрудника франшизы
- Franchisee: только сотрудники своих ТТ
- Manager, Cashier:
403 FORBIDDEN
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Роль не имеет доступа или попытка доступа к чужому сотруднику |
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
LEGAL_DETAILS_NOT_FOUND | 404 | Юридические детали не заполнены |
PUT /employees/{id}/legal-details
Создать или обновить юридические детали сотрудника (upsert).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любой; Franchisee — только свои сотрудники; Manager, Cashier — 403) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID сотрудника |
Request Body
Все поля опциональны. Отправляются только заполняемые.
{
"inn": "string, optional — 12 цифр (ИНН физлица)",
"passport_series": "string, optional — 4 цифры",
"passport_number": "string, optional — 6 цифр",
"driver_license_number": "string, optional",
"driver_license_expiry": "date, optional — формат YYYY-MM-DD",
"snils": "string, optional — 11 цифр"
}Response 200
{
"data": {
"employee_id": "uuid",
"inn": "string | null",
"passport_series": "string | null",
"passport_number": "string | null",
"driver_license_number": "string | null",
"driver_license_expiry": "date | null",
"snils": "string | null",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Роль не имеет доступа или попытка доступа к чужому сотруднику |
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
VALIDATION_ERROR | 400 | Невалидные данные (формат ИНН, СНИЛС и т.д.) |
GET /shift-templates
Получить список шаблонов смен для торговой точки.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager — только своя ТТ; Cashier — 403) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
store_id | uuid | yes | ID торговой точки |
Response 200
{
"data": [
{
"id": "uuid",
"store_id": "uuid",
"name": "string",
"start_time": "string — формат HH:mm",
"duration_minutes": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
]
}Ролевой доступ
- Franchise: шаблоны любой ТТ франшизы
- Franchisee: только свои ТТ (
store_id IN :jwt_store_ids) - Manager: только своя ТТ (
store_id = :jwt_store_id) - Cashier:
403 FORBIDDEN
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет доступа к данной ТТ или роль Cashier |
STORE_NOT_FOUND | 404 | ТТ не найдена |
POST /shift-templates
Создать шаблон смены для торговой точки.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Request Body
{
"store_id": "uuid, required",
"name": "string, required — название шаблона",
"start_time": "string, required — формат HH:mm",
"duration_minutes": "integer, required — длительность в минутах"
}Response 201
{
"data": {
"id": "uuid",
"store_id": "uuid",
"name": "string",
"start_time": "string",
"duration_minutes": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
}Максимум 4 шаблона на ТТ
При попытке создать пятый шаблон —
422 MAX_TEMPLATES_REACHED.
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на создание шаблонов для данной ТТ |
VALIDATION_ERROR | 400 | Невалидные данные |
STORE_NOT_FOUND | 404 | ТТ не найдена |
MAX_TEMPLATES_REACHED | 422 | Достигнут лимит в 4 шаблона для данной ТТ |
PATCH /shift-templates/{id}
Обновить шаблон смены.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любой; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID шаблона смены |
Request Body
Partial update — отправляются только изменяемые поля.
{
"name": "string, optional",
"start_time": "string, optional — формат HH:mm",
"duration_minutes": "integer, optional"
}Response 200
{
"data": {
"id": "uuid",
"store_id": "uuid",
"name": "string",
"start_time": "string",
"duration_minutes": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на редактирование |
SHIFT_TEMPLATE_NOT_FOUND | 404 | Шаблон не найден |
VALIDATION_ERROR | 400 | Невалидные данные |
DELETE /shift-templates/{id}
Удалить шаблон смены.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любой; Franchisee — только свои ТТ; Manager, Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID шаблона смены |
Response 204
Нет тела.
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на удаление |
SHIFT_TEMPLATE_NOT_FOUND | 404 | Шаблон не найден |
GET /schedules
Получить расписание смен (план) с фактическими данными и вычисленным статусом.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager — только своя ТТ; Cashier — 403) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
store_id | uuid | yes | ID торговой точки |
date_from | date | yes | Начало периода (формат YYYY-MM-DD) |
date_to | date | yes | Конец периода (формат YYYY-MM-DD) |
Фильтры
employee_id/role_id/status— планируютсяСейчас
ScheduleControllerвозвращает все записи в заданном диапазоне для ТТ. Дополнительные фильтры пока не реализованы. Клиент (admin/web) может передавать их в query string — параметры игнорируются (backward-compatible).
Response 200
{
"data": [
{
"id": "uuid",
"employee_id": "uuid",
"employee_name": "string",
"employee_role": "string",
"store_id": "uuid",
"date": "date",
"planned": {
"start_time": "string — HH:mm",
"end_time": "string — HH:mm",
"duration_minutes": "integer",
"template_id": "uuid | null",
"template_name": "string | null"
},
"actual": {
"shift_record_id": "uuid | null",
"clock_in": "datetime | null",
"clock_out": "datetime | null",
"break_minutes": "integer | null",
"worked_minutes": "integer | null",
"corrections_minutes": "integer"
},
"status": "scheduled | in_progress | completed | missed",
"created_at": "datetime"
}
],
"meta": {
"page": "integer",
"per_page": "integer",
"total": "integer"
}
}Статус вычисляется автоматически
scheduled— дата в будущем или сегодня, нет clock_inin_progress— есть clock_in, нет clock_outcompleted— есть clock_in и clock_outmissed— дата в прошлом, нет clock_in
Ролевой доступ
- Franchise: расписание любой ТТ
- Franchisee: только свои ТТ
- Manager: только своя ТТ (read-only)
- Cashier:
403 FORBIDDEN
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет доступа к данной ТТ или роль Cashier |
VALIDATION_ERROR | 400 | Невалидные параметры (date_from > date_to и т.д.) |
POST /schedules
Создать записи расписания (batch). Одним запросом можно запланировать смены для нескольких сотрудников.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Request Body
{
"items": [
{
"employee_id": "uuid, required",
"store_id": "uuid, required",
"date": "date, required — формат YYYY-MM-DD, только будущие даты",
"template_id": "uuid, optional — если указан, start_time и duration_minutes берутся из шаблона",
"start_time": "string, optional — формат HH:mm, обязателен если нет template_id",
"duration_minutes": "integer, optional — обязателен если нет template_id"
}
]
}Дата должна быть в будущем
Нельзя создать запись расписания на прошедшую дату. Сегодняшняя дата допускается.
Response 201
{
"data": [
{
"id": "uuid",
"employee_id": "uuid",
"store_id": "uuid",
"date": "date",
"start_time": "string",
"end_time": "string",
"duration_minutes": "integer",
"template_id": "uuid | null",
"created_at": "datetime"
}
]
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на создание расписания для данной ТТ |
VALIDATION_ERROR | 400 | Невалидные данные (формат даты, времени) |
DATE_IN_PAST | 422 | Дата в прошлом |
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
STORE_NOT_FOUND | 404 | ТТ не найдена |
SHIFT_TEMPLATE_NOT_FOUND | 404 | Шаблон смены не найден |
PATCH /schedules/{id}
Обновить запись расписания. Только для будущих дат.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID записи расписания |
Request Body
Partial update — отправляются только изменяемые поля.
{
"start_time": "string, optional — формат HH:mm",
"end_time": "string, optional — формат HH:mm",
"template_id": "uuid, optional — если указан, перезаписывает start_time и duration"
}Response 200
{
"data": {
"id": "uuid",
"employee_id": "uuid",
"store_id": "uuid",
"date": "date",
"start_time": "string",
"end_time": "string",
"duration_minutes": "integer",
"template_id": "uuid | null",
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на редактирование |
SCHEDULE_NOT_FOUND | 404 | Запись расписания не найдена |
DATE_IN_PAST | 422 | Нельзя редактировать запись на прошедшую дату |
VALIDATION_ERROR | 400 | Невалидные данные |
DELETE /schedules/{id}
Удалить запись расписания. Только для будущих дат.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager, Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID записи расписания |
Response 204
Нет тела.
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на удаление |
SCHEDULE_NOT_FOUND | 404 | Запись расписания не найдена |
DATE_IN_PAST | 422 | Нельзя удалить запись на прошедшую дату |
GET /shift-records
Получить список фактических смен с корректировками.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager — только своя ТТ; Cashier — 403) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
store_id | uuid | no | Фильтр по ТТ |
employee_id | uuid | no | Фильтр по сотруднику |
date_from | date | no | Начало периода (формат YYYY-MM-DD) |
date_to | date | no | Конец периода (формат YYYY-MM-DD) |
page | integer | no | Номер страницы (default: 1) |
per_page | integer | no | Записей на страницу (default: 20, max: 100) |
Response 200
{
"data": [
{
"id": "uuid",
"employee_id": "uuid",
"employee_name": "string",
"store_id": "uuid",
"date": "date",
"clock_in": "datetime | null",
"clock_out": "datetime | null",
"break_start": "datetime | null",
"break_end": "datetime | null",
"break_minutes": "integer",
"worked_minutes": "integer",
"source": "pos | manual",
"corrections": [
{
"id": "uuid",
"type": "increase | decrease",
"minutes": "integer",
"comment": "string",
"created_at": "datetime"
}
],
"total_corrections_minutes": "integer",
"final_worked_minutes": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
],
"meta": {
"page": "integer",
"per_page": "integer",
"total": "integer"
}
}Ролевой доступ
- Franchise: фактические смены любой ТТ
- Franchisee: только свои ТТ
- Manager: только своя ТТ (read-only)
- Cashier:
403 FORBIDDEN
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет доступа к данной ТТ или роль Cashier |
POST /shift-records
Создать фактическую смену вручную (source = manual).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Request Body
{
"employee_id": "uuid, required",
"store_id": "uuid, required",
"date": "date, required — формат YYYY-MM-DD",
"clock_in": "datetime, required",
"clock_out": "datetime, optional",
"break_start": "datetime, optional",
"break_end": "datetime, optional"
}Response 201
{
"data": {
"id": "uuid",
"employee_id": "uuid",
"store_id": "uuid",
"date": "date",
"clock_in": "datetime",
"clock_out": "datetime | null",
"break_start": "datetime | null",
"break_end": "datetime | null",
"break_minutes": "integer",
"worked_minutes": "integer",
"source": "manual",
"corrections": [],
"total_corrections_minutes": 0,
"final_worked_minutes": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на создание |
VALIDATION_ERROR | 400 | Невалидные данные (clock_out < clock_in и т.д.) |
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
STORE_NOT_FOUND | 404 | ТТ не найдена |
PATCH /shift-records/{id}
Обновить фактическую смену. Только для записей с source = manual.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID записи фактической смены |
Request Body
Partial update — отправляются только изменяемые поля.
{
"clock_in": "datetime, optional",
"clock_out": "datetime, optional",
"break_start": "datetime, optional",
"break_end": "datetime, optional"
}Response 200
{
"data": {
"id": "uuid",
"employee_id": "uuid",
"store_id": "uuid",
"date": "date",
"clock_in": "datetime",
"clock_out": "datetime | null",
"break_start": "datetime | null",
"break_end": "datetime | null",
"break_minutes": "integer",
"worked_minutes": "integer",
"source": "manual",
"corrections": [],
"total_corrections_minutes": "integer",
"final_worked_minutes": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на редактирование |
SHIFT_RECORD_NOT_FOUND | 404 | Запись не найдена |
POS_RECORD_IMMUTABLE | 422 | Нельзя редактировать запись с source = pos |
VALIDATION_ERROR | 400 | Невалидные данные |
POST /shift-records/{id}/corrections
Добавить корректировку к фактической смене (увеличить или уменьшить отработанное время).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID записи фактической смены |
Request Body
{
"type": "string, required — increase | decrease",
"minutes": "integer, required — количество минут",
"comment": "string, required — причина корректировки"
}Response 201
{
"data": {
"id": "uuid",
"shift_record_id": "uuid",
"type": "increase | decrease",
"minutes": "integer",
"comment": "string",
"created_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на корректировку |
SHIFT_RECORD_NOT_FOUND | 404 | Запись фактической смены не найдена |
VALIDATION_ERROR | 400 | Невалидные данные (minutes ⇐ 0, пустой comment) |
DELETE /shift-records/{id}/corrections/{correction_id}
Удалить корректировку фактической смены.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая; Franchisee — только свои ТТ; Manager, Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID записи фактической смены |
correction_id | uuid | yes | ID корректировки |
Response 204
Нет тела.
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на удаление |
SHIFT_RECORD_NOT_FOUND | 404 | Запись фактической смены не найдена |
CORRECTION_NOT_FOUND | 404 | Корректировка не найдена |
GET /dashboard/activity
Получить дашборд активности сотрудников: сводка по ТТ и детализация по каждому сотруднику.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager — только своя ТТ; Cashier — 403) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
store_id | uuid | no | Фильтр по ТТ (Franchise может не указывать — сводка по всем) |
date | date | no | Дата (формат YYYY-MM-DD, default: сегодня) |
Response 200
{
"data": {
"summary": {
"revenue": "number",
"total_orders": "integer",
"total_employees": "integer",
"top_seller": {
"employee_id": "uuid | null",
"name": "string | null",
"sales_amount": "number | null"
},
"total_stores": "integer"
},
"employees": [
{
"employee_id": "uuid",
"name": "string",
"clock_in": "datetime | null",
"clock_out": "datetime | null",
"break_duration": "integer — минуты",
"worked_hours": "number — часы с десятичными",
"orders_created": "integer | null",
"orders_completed": "integer | null",
"orders_delivered": "integer | null",
"sales_amount": "number | null",
"last_sale_at": "datetime | null"
}
]
}
}Поля заказов — заглушки до Order Service
Поля
orders_created,orders_completed,orders_delivered,sales_amount,last_sale_at,revenue,total_orders,top_sellerвозвращаютnull/0до тех пор, пока не будет реализован Order Service.
Ролевой доступ
- Franchise: дашборд по любой ТТ или сводка по всем
- Franchisee: только свои ТТ
- Manager: только своя ТТ (read-only)
- Cashier:
403 FORBIDDEN
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет доступа к данной ТТ или роль Cashier |
GET /salary-formulas
Получить список формул расчёта зарплаты (ролевые и индивидуальные).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — все; Franchisee — только свои ТТ; Manager, Cashier — 403) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
store_id | uuid | no | Фильтр по ТТ |
Response 200
{
"data": [
{
"id": "uuid",
"store_id": "uuid | null",
"role": "string | null — admin_franchise | admin_franchisee | manager | cashier",
"employee_id": "uuid | null",
"employee_name": "string | null",
"formula_type": "hourly | fixed | mixed",
"hourly_rate": "number | null",
"monthly_salary": "number | null",
"overtime_rate": "number | null — множитель (например 1.5)",
"norm_hours": "integer | null — норма часов в месяц",
"created_at": "datetime",
"updated_at": "datetime"
}
]
}Приоритет формул
Индивидуальная формула (
employee_idзадан) имеет приоритет над ролевой (roleзадан). Ролевая формула может быть привязана к конкретной ТТ (store_idзадан) или быть общей (store_id = null).
Ролевой доступ
- Franchise: все формулы франшизы
- Franchisee: только формулы своих ТТ
- Manager, Cashier:
403 FORBIDDEN
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет доступа или роль Manager/Cashier |
POST /salary-formulas
Создать формулу расчёта зарплаты.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Request Body
{
"store_id": "uuid, optional — если не указан, формула общая для франшизы",
"role": "string, optional — admin_franchise | admin_franchisee | manager | cashier",
"employee_id": "uuid, optional — индивидуальная формула для сотрудника",
"formula_type": "string, required — hourly | fixed | mixed",
"hourly_rate": "number, optional — ставка за час (для formula_type=hourly)",
"monthly_salary": "number, optional — фиксированный оклад (для formula_type=monthly)",
"overtime_rate": "number, optional — множитель переработки (default: 1.5)",
"norm_hours": "integer, optional — норма часов в месяц (default: 160)"
}Одно из role или employee_id обязательно
Формула должна быть привязана либо к роли (ролевая), либо к конкретному сотруднику (индивидуальная).
Response 201
{
"data": {
"id": "uuid",
"store_id": "uuid | null",
"role": "string | null",
"employee_id": "uuid | null",
"formula_type": "hourly | fixed | mixed",
"hourly_rate": "number | null",
"monthly_salary": "number | null",
"overtime_rate": "number",
"norm_hours": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на создание |
VALIDATION_ERROR | 400 | Невалидные данные (не указан role и employee_id, некорректный formula_type) |
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
STORE_NOT_FOUND | 404 | ТТ не найдена |
PATCH /salary-formulas/{id}
Обновить формулу расчёта зарплаты.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID формулы |
Request Body
Partial update — отправляются только изменяемые поля.
{
"formula_type": "string, optional — hourly | fixed | mixed",
"hourly_rate": "number, optional",
"monthly_salary": "number, optional",
"overtime_rate": "number, optional",
"norm_hours": "integer, optional"
}Response 200
{
"data": {
"id": "uuid",
"store_id": "uuid | null",
"role": "string | null",
"employee_id": "uuid | null",
"formula_type": "hourly | fixed | mixed",
"hourly_rate": "number | null",
"monthly_salary": "number | null",
"overtime_rate": "number",
"norm_hours": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на редактирование |
SALARY_FORMULA_NOT_FOUND | 404 | Формула не найдена |
VALIDATION_ERROR | 400 | Невалидные данные |
DELETE /salary-formulas/{id}
Удалить формулу расчёта зарплаты. Можно удалить только индивидуальные формулы (с employee_id).
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая; Franchisee — только свои ТТ; Manager, Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID формулы |
Response 204
Нет тела.
Только индивидуальные формулы
Ролевые формулы (role задан, employee_id = null) нельзя удалить —
422 ROLE_FORMULA_CANNOT_BE_DELETED. Их можно только обновить через PATCH.
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на удаление |
SALARY_FORMULA_NOT_FOUND | 404 | Формула не найдена |
ROLE_FORMULA_CANNOT_BE_DELETED | 422 | Нельзя удалить ролевую формулу |
GET /payroll
Получить ведомости по зарплате за период.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — все; Franchisee — только свои ТТ; Manager, Cashier — 403) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
store_id | uuid | no | Фильтр по ТТ |
period | string | yes | Период в формате YYYY-MM |
page | integer | no | Номер страницы (default: 1) |
per_page | integer | no | Записей на страницу (default: 20, max: 100) |
Response 200
{
"data": [
{
"id": "uuid",
"employee_id": "uuid",
"employee_name": "string",
"employee_role": "string",
"store_id": "uuid",
"store_name": "string",
"period_start": "date",
"period_end": "date",
"planned_hours": "number",
"actual_hours": "number",
"break_hours": "number",
"net_hours": "number",
"formula_type": "hourly | fixed | mixed",
"formula_snapshot": "object — снимок формулы на момент расчёта",
"amount": "number — рассчитанная сумма",
"status": "calculated | confirmed | paid",
"confirmed_by": "uuid | null",
"confirmed_at": "datetime | null",
"created_at": "datetime"
}
],
"meta": {
"page": "integer",
"per_page": "integer",
"total": "integer"
}
}Ролевой доступ
- Franchise: все ведомости
- Franchisee: только свои ТТ
- Manager, Cashier:
403 FORBIDDEN
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет доступа или роль Manager/Cashier |
VALIDATION_ERROR | 400 | Невалидный формат периода |
POST /payroll/calculate
Рассчитать зарплату за период для всех сотрудников ТТ. Создаёт или пересчитывает записи ведомости.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая ТТ; Franchisee — только свои ТТ; Manager, Cashier — 403) |
| Content-Type | application/json |
Request Body
{
"store_id": "uuid, required",
"period": "string, required — формат YYYY-MM"
}Response 200
{
"data": [
{
"id": "uuid",
"employee_id": "uuid",
"employee_name": "string",
"employee_role": "string",
"store_id": "uuid",
"period_start": "date",
"period_end": "date",
"planned_hours": "number",
"actual_hours": "number",
"break_hours": "number",
"net_hours": "number",
"formula_type": "hourly | fixed | mixed",
"formula_snapshot": "object",
"amount": "number",
"status": "calculated",
"created_at": "datetime"
}
]
}Пересчёт допускается
Если ведомость за период уже существует со статусом
calculated— она будет пересчитана. Ведомости со статусомconfirmedилиpaidне пересчитываются.
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на расчёт |
STORE_NOT_FOUND | 404 | ТТ не найдена |
VALIDATION_ERROR | 400 | Невалидный формат периода |
POST /payroll/{id}/confirm
Подтвердить ведомость. Переводит статус из calculated в confirmed.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая; Franchisee — только свои ТТ; Manager, Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID записи ведомости |
Response 200
{
"data": {
"id": "uuid",
"status": "confirmed",
"confirmed_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на подтверждение |
PAYROLL_NOT_FOUND | 404 | Запись ведомости не найдена |
INVALID_STATUS_TRANSITION | 422 | Статус не calculated (уже подтверждена или оплачена) |
POST /payroll/{id}/mark-paid
Отметить ведомость как оплаченную. Переводит статус из confirmed в paid.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая; Franchisee — только свои ТТ; Manager, Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID записи ведомости |
Response 200
{
"data": {
"id": "uuid",
"status": "paid",
"paid_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на отметку оплаты |
PAYROLL_NOT_FOUND | 404 | Запись ведомости не найдена |
INVALID_STATUS_TRANSITION | 422 | Статус не confirmed (не подтверждена или уже оплачена) |
GET /payroll/{id}/export
Экспортировать ведомость в CSV-файл.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — любая; Franchisee — только свои ТТ; Manager, Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID записи ведомости |
Response 200
Content-Type: text/csv; charset=utf-8
Формат CSV
Файл в кодировке UTF-8 с BOM для корректного открытия в Excel. Колонки: Сотрудник, Роль, Отработано часов, Норма часов, Переработка часов, Базовая сумма, Переработка сумма, Корректировки сумма, Итого, Статус.
Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный JWT |
FORBIDDEN | 403 | Нет прав на экспорт |
PAYROLL_NOT_FOUND | 404 | Запись ведомости не найдена |
POST /internal/shift-records/clock-in
Internal endpoint — для POS BFF (через service token)
Зафиксировать начало смены сотрудника на POS-терминале. Создаёт запись фактической смены с source = pos.
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
| Content-Type | application/json |
Request Body
{
"employee_id": "uuid, required",
"store_id": "uuid, required"
}Response 201
{
"data": {
"id": "uuid",
"employee_id": "uuid",
"store_id": "uuid",
"date": "date",
"clock_in": "datetime",
"source": "pos",
"created_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
STORE_NOT_FOUND | 404 | ТТ не найдена |
SHIFT_ALREADY_OPEN | 422 | У сотрудника уже есть открытая смена в данной ТТ |
POST /internal/shift-records/clock-out
Internal endpoint — для POS BFF (через service token)
Зафиксировать окончание смены сотрудника на POS-терминале.
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
| Content-Type | application/json |
Request Body
{
"employee_id": "uuid, required",
"store_id": "uuid, required"
}Response 200
{
"data": {
"id": "uuid",
"employee_id": "uuid",
"store_id": "uuid",
"date": "date",
"clock_in": "datetime",
"clock_out": "datetime",
"break_minutes": "integer",
"worked_minutes": "integer",
"source": "pos",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
NO_OPEN_SHIFT | 422 | Нет открытой смены для данного сотрудника в данной ТТ |
POST /internal/shift-records/break-start
Internal endpoint — для POS BFF (через service token)
Зафиксировать начало перерыва сотрудника.
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
| Content-Type | application/json |
Request Body
{
"employee_id": "uuid, required",
"store_id": "uuid, required"
}Response 200
{
"data": {
"id": "uuid",
"employee_id": "uuid",
"store_id": "uuid",
"break_start": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
NO_OPEN_SHIFT | 422 | Нет открытой смены |
BREAK_ALREADY_STARTED | 422 | Перерыв уже начат |
POST /internal/shift-records/break-end
Internal endpoint — для POS BFF (через service token)
Зафиксировать окончание перерыва сотрудника.
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
| Content-Type | application/json |
Request Body
{
"employee_id": "uuid, required",
"store_id": "uuid, required"
}Response 200
{
"data": {
"id": "uuid",
"employee_id": "uuid",
"store_id": "uuid",
"break_start": "datetime",
"break_end": "datetime",
"break_minutes": "integer",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
EMPLOYEE_NOT_FOUND | 404 | Сотрудник не найден |
NO_OPEN_SHIFT | 422 | Нет открытой смены |
BREAK_NOT_STARTED | 422 | Перерыв не был начат |
GET /shift-records/{id}/report
Полный X/Z отчёт по конкретной смене. Собирает данные смены из shift_records + вызывает Order Service за финансовой агрегацией.
(Добавлено в BR 2.2)
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (Franchise — все; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — 403) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID записи shift_record |
Response 200
{
"data": {
"shift": {
"id": "uuid",
"employee_id": "uuid",
"employee_name": "Иванов Иван Иванович",
"store_id": "uuid",
"store_name": "Кофейня на Арбате",
"date": "2026-04-14",
"clock_in": "2026-04-14T08:00:00Z",
"clock_out": "2026-04-14T16:30:00Z",
"break_duration_minutes": 30,
"status": "completed"
},
"report_type": "z_report",
"financials": {
"total_revenue": 15420.00,
"cash_amount": 5200.00,
"card_amount": 8720.00,
"qr_amount": 1500.00,
"refund_amount": 0.00,
"discount_amount": 0.00,
"total_orders": 23,
"completed_orders": 21,
"cancelled_orders": 2,
"average_check": 734.29
},
"top_items": [
{
"product_id": "uuid",
"name": "Латте 0.4",
"quantity": 15,
"amount": 4500.00
}
]
}
}| Field | Type | Description |
|---|---|---|
shift.id | uuid | ID записи смены |
shift.employee_id | uuid | ID сотрудника |
shift.employee_name | string | ФИО сотрудника |
shift.store_id | uuid | ID торговой точки |
shift.store_name | string | Название ТТ |
shift.date | date | Дата смены |
shift.clock_in | datetime | Начало смены |
shift.clock_out | datetime | null | Конец смены (null = смена открыта) |
shift.break_duration_minutes | integer | Длительность перерыва в минутах |
shift.status | string | Статус смены |
report_type | string | x_report (смена открыта) | z_report (смена закрыта) |
financials.total_revenue | decimal | Выручка |
financials.cash_amount | decimal | Наличные |
financials.card_amount | decimal | Карта |
financials.qr_amount | decimal | QR |
financials.refund_amount | decimal | Возвраты (0 в Phase 1) |
financials.discount_amount | decimal | Скидки (0 в Phase 1) |
financials.total_orders | integer | Всего заказов |
financials.completed_orders | integer | Завершённых |
financials.cancelled_orders | integer | Отменённых |
financials.average_check | decimal | Средний чек |
top_items | array | Топ-10 товаров по сумме |
top_items[].product_id | uuid | ID товара |
top_items[].name | string | Название товара |
top_items[].quantity | integer | Суммарное количество |
top_items[].amount | decimal | Суммарная стоимость |
X vs Z
Если
clock_outIS NULL (смена открыта) →report_type = "x_report", период = clock_in..now. Еслиclock_outIS NOT NULL (смена закрыта) →report_type = "z_report", период = clock_in..clock_out.
Errors
| Code | HTTP | Когда |
|---|---|---|
SHIFT_RECORD_NOT_FOUND | 404 | Запись смены не найдена |
FORBIDDEN | 403 | Нет доступа к этой ТТ или роль Cashier |
UNAUTHORIZED | 401 | Нет токена или невалидный |
Роли и permissions (BR 1.4.3)
Источник требований
GET /roles
Список ролей в текущей франшизе.
(Обновлено в BR 1.4.4 §5.2)
По умолчанию возвращаются только обычные роли (
owner_legal_entity_id IS NULL). Скрытые роли владельцев партнёров исключены — они доступны только черезGET /legal-entities/{id}/owner-permissions.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (roles.read; владелец франшизы — все; владелец партнёра — те что назначены сотрудникам в своих ТТ) |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
status | string | no | active (default) / deleted / all |
is_system | boolean | no | Фильтр по флагу системной |
include_hidden | boolean | no | default false. Если true — также возвращает скрытые роли владельцев партнёров (owner_legal_entity_id IS NOT NULL). Используется только для админских целей; в обычном UI всегда false (BR 1.4.4) |
search | string | no | Поиск по name (LIKE, case-insensitive) |
page | integer | no | default 1 |
per_page | integer | no | default 20, max 100 |
Response 200
{
"data": [
{
"id": "uuid",
"name": "string",
"description": "string | null",
"is_system": "boolean",
"employee_count": "integer — число сотрудников с этой ролью",
"status": "active | deleted",
"created_at": "datetime"
}
],
"meta": { "page": 1, "per_page": 20, "total": 15 }
}Errors
| Code | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | |
FORBIDDEN | 403 | Нет roles.read |
GET /roles/{id}
Детали роли с полным набором permissions и формулой зарплаты.
(Обновлено в BR 1.4.4 §5.2)
Если запрашиваемая роль — скрытая (
owner_legal_entity_id IS NOT NULL), endpoint возвращает404 ROLE_NOT_FOUND(даже если роль существует). Для управления скрытыми ролями владельцев партнёров используетсяGET /legal-entities/{id}/owner-permissions.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (roles.read) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID роли |
Response 200
{
"data": {
"id": "uuid",
"franchise_id": "uuid",
"name": "string",
"description": "string | null",
"is_system": "boolean",
"owner_legal_entity_id": "uuid | null — если задан, это скрытая роль владельца партнёра (BR 1.4.4 §5.2); при include_hidden=false сюда не попадают",
"status": "active | deleted",
"permissions": [
"menu.read",
"menu.edit",
"pos.access",
"pos.shift.open"
],
"salary_formula": {
"formula_type": "hourly | fixed | mixed",
"hourly_rate": "number | null",
"monthly_salary": "number | null",
"overtime_rate": "number | null",
"norm_hours": "integer | null"
},
"employee_count": "integer",
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Когда |
|---|---|---|
ROLE_NOT_FOUND | 404 | |
FORBIDDEN | 403 |
POST /roles
Создать новую роль.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (roles.edit; фактически только admin_franchise) |
| Content-Type | application/json |
Request Body
{
"name": "string, required — уникально в рамках франшизы",
"description": "string, optional",
"permissions": ["string, required — массив permission-ключей из каталога"],
"salary_formula": {
"formula_type": "hourly | fixed | mixed, optional",
"hourly_rate": "number, required if hourly/mixed",
"monthly_salary": "number, required if fixed/mixed",
"overtime_rate": "number, required if mixed",
"norm_hours": "integer, default 160"
}
}Response 201
Полный объект роли (см. GET /roles/{id}).
Errors
| Code | HTTP | Когда |
|---|---|---|
VALIDATION_ERROR | 400 | Невалидные поля (в т.ч. edit без read) |
NAME_DUPLICATE | 409 | Роль с таким name уже есть (case-insensitive) |
UNKNOWN_PERMISSION_KEY | 400 | В permissions[] есть ключ вне каталога |
FORBIDDEN | 403 | Нет roles.edit |
PATCH /roles/{id}
Частичное редактирование роли.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (roles.edit) |
| Content-Type | application/json |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID роли |
Request Body
{
"name": "string, optional",
"description": "string, optional",
"permissions": "string[], optional — полная замена набора",
"salary_formula": "object | null, optional — null удаляет формулу"
}Системная роль
Для
is_system=trueразрешается менять толькоnameиdescription. Попытка передатьpermissionsилиsalary_formula→422 SYSTEM_ROLE_PROTECTED.
Response 200
Полный объект роли.
Errors
| Code | HTTP | Когда |
|---|---|---|
ROLE_NOT_FOUND | 404 | |
SYSTEM_ROLE_PROTECTED | 422 | Попытка изменить запрещённые поля системной роли |
NAME_DUPLICATE | 409 | |
UNKNOWN_PERMISSION_KEY | 400 | |
VALIDATION_ERROR | 400 | (в т.ч. edit без read) |
DELETE /roles/{id}
Soft delete роли.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (roles.edit) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | ID роли |
Response 204
Нет тела ответа.
Errors
| Code | HTTP | Когда |
|---|---|---|
ROLE_NOT_FOUND | 404 | |
ROLE_IN_USE | 409 | Роль назначена хоть одному активному сотруднику — возвращается в details список с employee_id |
SYSTEM_ROLE_PROTECTED | 422 | Попытка удалить системную роль |
POST /roles/{id}/restore
Восстановить удалённую роль.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (roles.edit) |
Response 200
Полный объект роли (со статусом active).
Errors
| Code | HTTP | Когда |
|---|---|---|
ROLE_NOT_FOUND | 404 | Или status != deleted |
NAME_DUPLICATE | 409 | За время отсутствия создана активная роль с таким же именем |
GET /roles/permission-catalog
Справочник доступных permission-ключей с человекочитаемыми лейблами для фронта.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT (roles.read) |
Response 200
{
"data": {
"backoffice": [
{
"section": "menu",
"label": "Меню",
"permissions": [
{ "key": "menu.read", "label": "Чтение" },
{ "key": "menu.edit", "label": "Редактирование" }
]
}
],
"pos": [
{ "key": "pos.access", "label": "Доступ к POS" },
{ "key": "pos.shift.open", "label": "Открытие смены" }
]
}
}Permission catalog (полный)
Back-office (19 разделов, 36 ключей — 17 с read+edit, 2 только read)
(Обновлено в BR 3.1 — добавлены разделы «Клиенты» и «Группы клиентов» с
.read/.edit, плюс у «Клиенты» есть.delete)
| Раздел | Read | Edit | Доп. |
|---|---|---|---|
| Меню | menu.read | menu.edit | |
| Прейскуранты | price_list.read | price_list.edit | |
| Техкарты | recipes.read | recipes.edit | |
| Ингредиенты | ingredients.read | ingredients.edit | |
| Стоп-листы | stoplists.read | stoplists.edit | |
| Склад | warehouse.read | warehouse.edit | |
| Торговые точки | stores.read | stores.edit | |
| Юридические лица | legal_entities.read | legal_entities.edit | |
| Сотрудники | employees.read | employees.edit | |
| Роли | roles.read | roles.edit | |
| Расписание смен | schedule.read | schedule.edit | |
| Учёт рабочего времени | time_tracking.read | time_tracking.edit | |
| Зарплата | payroll.read | payroll.edit | |
| Дашборд | dashboard.read | — (нет редактирования) | |
| Отчёты | reports.read | — | |
| Заказы | orders.read | orders.edit | |
| Настройки | settings.read | settings.edit | |
| Клиенты | customers.read | customers.edit | customers.delete — только для Owner франшизы (BR 3.1) |
| Группы клиентов | customer_groups.read | customer_groups.edit | (BR 3.1) |
POS (15 ключей, без min/max — deferred)
(Обновлено в BR 3.1 — добавлен
customers.create_quick)
| Ключ | Операция |
|---|---|
pos.access | Базовый доступ к POS-приложению |
pos.shift.open | Открытие смены |
pos.shift.close | Закрытие смены (Z-отчёт) |
pos.orders.create | Создание заказов |
pos.orders.edit | Редактирование незакрытого заказа |
pos.discount.apply | Применение скидок |
pos.cash.deposit | Внесение наличных |
pos.cash.withdraw | Изъятие наличных |
pos.cash.collection | Инкассация |
pos.refund | Возврат средств |
pos.order.cancel | Аннулирование заказа |
pos.item.cancel | Аннулирование позиции |
pos.price.change | Изменение цены для товаров с открытой ценой |
pos.settings.edit | Изменение настроек кассы |
customers.create_quick | Поиск клиента по телефону + quick-create + прикрепление к заказу (BR 3.1) |
Deferred: лимиты min/max
Yuma позволяет задать минимум/максимум на некоторые POS-операции (например, «не более 5000₽ за изъятие»). В MVP — только флажки. См. Роли § Deferred.
GET /internal/users/{id}/permissions
Internal endpoint — для Auth Service
Используется для рефреша кэша permissions в Auth Service при TTL-истечении или явной инвалидации.
Возвращает актуальный агрегированный список permissions сотрудника.
| Параметр | Значение |
|---|---|
| Auth | Service token (X-Service-Token header) |
Path Parameters
| Param | Type | Required | Description |
|---|---|---|---|
id | uuid | yes | employees.id |
Response 200
{
"data": {
"user_id": "uuid",
"role_ids": ["uuid"],
"permissions": ["string — agg granted=true ключей со всех ролей"],
"scope": {
"type": "all_franchise | legal_entity_ids | store_ids",
"legal_entity_ids": ["uuid"],
"store_ids": ["uuid"]
}
}
}(Обновлено в BR 1.4.4: дополнительно возвращается scope для удобства консьюмеров — не нужно делать второй запрос в /internal/users/{id}/scope.)
Errors
| Code | HTTP | Когда |
|---|---|---|
USER_NOT_FOUND | 404 |
POST /admin/kds/devices/register
(BR 5.1 — KDS)
Регистрация KDS-устройства (Android-планшет на кухне). Вызывается при первом запуске KDS-приложения: владелец/менеджер логинится по email+password (получает обычный JWT), вводит UUID устройства и выбирает ТТ из доступных в его scope.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT с permission kds.settings.edit |
| Content-Type | application/json |
Request Body
{
"device_id": "uuid",
"store_id": "uuid",
"kitchen_station_id": "uuid",
"name": "Кухня — bedc86",
"app_version": "0.1.0"
}| Field | Type | Required | Description |
|---|---|---|---|
device_id | uuid | yes | Генерируется на устройстве при первом запуске, сохраняется в SQLite. Уникален в рамках франшизы. |
store_id | uuid | yes | ТТ из scope.store_ids юзера-регистратора |
kitchen_station_id | uuid | no (yes для wizard v2) | Цех (kitchen_stations.id), который обслуживает устройство. BR 5.1 v2 — wizard всегда передаёт это поле; legacy-клиенты без поля → запись создаётся с kitchen_station_id=NULL (старая логика multi-station через PIN). |
name | string | no | Понятное имя. Default = "KDS-{первые 6 hex device_id}" |
app_version | string | no | Версия установленного APK |
Response 201
{
"data": {
"id": "uuid",
"device_id": "uuid",
"franchise_id": "uuid",
"store_id": "uuid",
"kitchen_station_id": "uuid | null",
"name": "Кухня — bedc86",
"app_version": "0.1.0",
"last_user_id": null,
"current_user_id": null,
"last_seen_at": null,
"revoked_at": null,
"created_at": "datetime",
"updated_at": "datetime"
}
}Errors
| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED | 401 | |
FORBIDDEN | 403 | Нет permission kds.settings.edit |
STORE_NOT_FOUND | 404 | store_id не существует |
STORE_NOT_IN_USER_SCOPE | 403 | Юзер не имеет доступа к этой ТТ |
VALIDATION_ERROR | 400 |
Idempotent register (BR 5.1 v2)
Эндпоинт идемпотентен в рамках
(franchise_id, device_id):
- Если active запись (
revoked_at IS NULL) с такой парой уже есть → обновитьstore_id,kitchen_station_id,name,app_versionи вернуть 200 OK с обновлённой записью.- Если active записи нет (новый device_id или старая revoked) → создать новую запись и вернуть 201 Created.
409
DEVICE_ALREADY_REGISTEREDбольше не возвращается — это покрывает кейсы переустановки exe, переноса терминала на другую ТТ/цех, повторного wizard’а послеclearRegistration().
Side-effects
- Создаётся или обновляется запись в
kds_devices. Если запись была ранее сrevoked_at != NULL(после force-logout) — создаётся новая запись с тем жеdevice_id, старая остаётся в БД для аудита.
POST /internal/kds-devices/{deviceId}/heartbeat
(BR 5.1 — KDS)
Heartbeat от KDS-устройства. Вызывается pos-bff на каждом запросе KDS, обновляет last_seen_at, current_user_id, app_version в kds_devices.
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Path Parameters
| Param | Type | Description |
|---|---|---|
deviceId | uuid | device_id (не суррогатный id) |
Request Body
{
"user_id": "uuid",
"app_version": "0.1.0"
}user_id — текущий залогиненный сотрудник или null если на экране PIN.
Response 204 — успех
Errors
| Code | HTTP | Description |
|---|---|---|
DEVICE_NOT_FOUND | 404 | |
DEVICE_REVOKED | 401 | revoked_at != NULL — устройство force-logout, должно ре-регистрироваться |
GET /internal/kds-devices/validate
(BR 5.1 — KDS device validation на PIN-логине)
Проверка что KDS-устройство зарегистрировано и привязано к указанной ТТ. Используется pos-bff перед PIN-логином: если valid=false, возвращает клиенту 403 DEVICE_NOT_REGISTERED → клиент чистит localStorage и показывает wizard заново.
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
device_id | uuid | yes | device_id (не суррогатный id) |
store_id | uuid | yes | ТТ к которой привязано устройство |
Response 200
{ "data": { "valid": true, "kitchen_station_id": "uuid | null" } }valid=true — устройство существует, привязано именно к этой ТТ, не отозвано (revoked_at IS NULL).
valid=false — нет соответствия (удалено / другая ТТ / revoked). Поле kitchen_station_id отсутствует.
kitchen_station_id (BR 5.1 v2) — цех привязки этого устройства. Используется pos-bff для аугментации PIN-response: клиент сразу узнаёт к какому цеху привязан, skip экран выбора станций. null для legacy-устройств (зарегистрированных до v2).
Всегда 200, не 404
Для удобства вызывающего возвращаем 200 с
{valid: bool}. pos-bff проще читатьbody.data.validчем различать 200/404.
Errors
| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED | 401 | Невалидный/отсутствующий X-Service-Token |
GET /admin/kds/devices
(BR 5.1 — KDS)
Список зарегистрированных KDS-устройств франшизы.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT с permission kds.settings.edit |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
store_id | uuid | no | Фильтр по ТТ |
online | bool | no | Только online (last_seen_at < 2 min ago) |
include_revoked | bool | no | Default false. True — включить отозванные |
franchise_id берётся из JWT.
Response 200
{
"data": [
{
"id": "uuid",
"device_id": "uuid",
"store_id": "uuid",
"store_name": "Кофейня на Ленина",
"name": "Планшет горячего цеха",
"last_user": {
"id": "uuid",
"first_name": "Иван",
"last_name": "Петров"
},
"current_user": null,
"app_version": "0.1.0",
"last_seen_at": "datetime | null",
"is_online": true,
"revoked_at": null,
"created_at": "datetime"
}
],
"meta": { "total": 5 }
}is_online рассчитывается на сервере: last_seen_at != NULL AND last_seen_at > NOW() - 2 min.
store_name денормализуется через JOIN/lookup в Store Service (или денормализован в kds_devices.store_name для упрощения).
Errors
| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED | 401 | |
FORBIDDEN | 403 |
PATCH /admin/kds/devices/{id}
(BR 5.1 — KDS)
Переименовать KDS-устройство.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT с kds.settings.edit |
| Content-Type | application/json |
Request Body
{ "name": "Бар-1" }Response 200
Обновлённая запись (формат как в GET).
Errors
| Code | HTTP | Description |
|---|---|---|
DEVICE_NOT_FOUND | 404 | |
VALIDATION_ERROR | 400 |
DELETE /admin/kds/devices/{id}
(BR 5.1 — KDS)
Force-logout: soft-delete KDS-устройства. Активные сессии закрываются, JWT отзывается. Устройство при следующей попытке работы получает 401 DEVICE_REVOKED и переходит в режим «Регистрация требуется».
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT с kds.settings.edit (по бизнес-правилу — только владелец франшизы/партнёра) |
Response 204 — успех
Side-effects
- Проставляется
revoked_at = NOW() - Публикуется событие
user.kds_device.revoked(см. Events) pos-bff(consumer) разрывает WebSocket-сессии этогоdevice_id
Errors
| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED | 401 | |
FORBIDDEN | 403 | Не владелец франшизы |
DEVICE_NOT_FOUND | 404 | |
ALREADY_REVOKED | 409 | Устройство уже отозвано |
POST /admin/pos/devices/register
(POS Desktop onboarding — структура зеркало
POST /admin/kds/devices/register)
Регистрация Windows-кассы. Вызывается с устройства из RegistrationScreen после admin-логина.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT с pos.settings.edit |
| Content-Type | application/json |
Request Body
{
"device_id": "uuid",
"store_id": "uuid",
"name": "Касса бар Тверская",
"app_version": "0.1.1"
}Response 201
Полная запись pos_desktop_devices в формате PosDeviceResponse (см. ниже GET /admin/pos/devices).
Errors
| Code | HTTP | Description |
|---|---|---|
STORE_NOT_IN_USER_SCOPE | 403 | ТТ вне scope админа-регистрирующего |
DEVICE_ALREADY_REGISTERED | 409 | (franchise_id, device_id) уже зарегистрирована |
VALIDATION_ERROR | 400 |
POST /internal/pos-devices/{deviceId}/heartbeat
(POS Desktop onboarding)
Heartbeat от Windows-кассы. Вызывается pos-bff/middleware/auth.ts на каждом аутентифицированном запросе. Обновляет last_seen_at, current_user_id, app_version в pos_desktop_devices.
| Параметр | Значение |
|---|---|
| Auth | X-Service-Token |
Path Parameters
| Param | Type | Description |
|---|---|---|
deviceId | uuid | device_id из X-Device-Id header клиента |
Request Body
{
"user_id": "uuid",
"app_version": "0.1.1"
}Response 204 — успех
Errors
| Code | HTTP | Description |
|---|---|---|
DEVICE_NOT_FOUND | 404 | Устройство не зарегистрировано или revoked. Pos-bff перехватывает 404 и отдаёт клиенту 401 DEVICE_REVOKED. |
GET /admin/pos/devices
(POS Desktop onboarding)
Список зарегистрированных POS-касс франшизы.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT с pos.settings.edit |
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
store_id | uuid | no | Фильтр по ТТ |
online | bool | no | Только online (last_seen_at < 2 min ago) |
include_revoked | bool | no | Default false |
Response 200
{
"data": [
{
"id": "uuid",
"device_id": "uuid",
"store_id": "uuid",
"store_name": null,
"name": "Касса бар Тверская",
"last_user": { "id": "uuid", "first_name": "Иван", "last_name": "Петров" },
"current_user": null,
"app_version": "0.1.1",
"last_seen_at": "datetime | null",
"is_online": true,
"revoked_at": null,
"created_at": "datetime"
}
]
}is_online = (last_seen_at != NULL AND last_seen_at > NOW() - 2 min). store_name в P0 отдаётся null (cross-service lookup отложен).
PATCH /admin/pos/devices/{id}
(POS Desktop onboarding)
Переименовать POS-кассу.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT с pos.settings.edit |
| Content-Type | application/json |
Request Body
{ "name": "Касса бар-2" }Response 200
Обновлённая запись.
Errors
| Code | HTTP | Description |
|---|---|---|
DEVICE_NOT_FOUND | 404 | |
VALIDATION_ERROR | 400 | name пустой или > 100 символов |
DELETE /admin/pos/devices/{id}
(POS Desktop onboarding)
Force-logout: soft-delete POS-кассы. На следующем запросе через pos-bff клиент получает 401 DEVICE_REVOKED, чистит локальную регистрацию и показывает RegistrationScreen.
| Параметр | Значение |
|---|---|
| Auth | Bearer JWT с pos.settings.edit |
Response 204 — успех
Side-effects
revoked_at = NOW(),current_user_id = NULL- В P0 — log-only (см.
PosDeviceEventPublisher). В P1 — Kafka событиеuser.pos_device.revoked.
Errors
| Code | HTTP | Description |
|---|---|---|
DEVICE_NOT_FOUND | 404 | |
ALREADY_REVOKED | 409 |
Общий формат ошибок
{
"error": {
"code": "string — машиночитаемый код (UPPER_SNAKE_CASE)",
"message": "string — человекочитаемое описание",
"details": [
{
"field": "string — путь к полю",
"message": "string — описание ошибки"
}
]
}
}