User Service — Data Model
База данных: user_db
erDiagram franchises { uuid id PK string name "varchar(255)" string type "corporate | individual, default corporate" timestamp created_at timestamp updated_at } legal_entities { uuid id PK uuid franchise_id string type "franchise | franchisee" string name string inn string kpp string ogrn string legal_address string bank_name string bank_bik string bank_account string bank_corr_account string contact_phone string contact_email boolean is_primary "default false" string status "active | suspended" string contract_number date contract_date uuid owner_user_id FK "nullable" timestamp deleted_at "nullable, soft delete" timestamp created_at timestamp updated_at } employees { uuid id PK uuid franchise_id string first_name "varchar(100)" string last_name "varchar(100)" string email "varchar(255)" string password_hash "varchar(72), bcrypt" string phone "varchar(20), nullable" string pin_hash "varchar(72), nullable, bcrypt" string status "active | inactive, default active" boolean is_courier "default false" string netmonet_profile_id "varchar(100), nullable — BR 3.2" boolean netmonet_enabled "default false — BR 3.2" timestamp created_at timestamp updated_at } employee_stores { uuid id PK uuid employee_id FK uuid store_id } roles { uuid id PK uuid franchise_id uuid owner_legal_entity_id FK "nullable — скрытая роль владельца партнёра (BR 1.4.4)" string name "varchar(100), unique per franchise+deleted_at IS NULL" text description "nullable" boolean is_system "default false" timestamp deleted_at "nullable, soft delete" timestamp created_at timestamp updated_at } role_permissions { uuid role_id PK_FK "ON DELETE CASCADE" string permission_key PK "varchar(100), e.g. menu.edit, pos.cash.withdraw" boolean granted "default true" } employee_roles { uuid employee_id PK_FK "ON DELETE CASCADE" uuid role_id PK_FK "ON DELETE RESTRICT" timestamp created_at } employee_role_stores { uuid employee_id PK uuid role_id PK uuid store_id PK } franchises ||--o{ legal_entities : "owns (multi-tenant)" franchises ||--o{ employees : "owns (multi-tenant)" franchises ||--o{ roles : "owns (multi-tenant)" legal_entities ||--o{ roles : "hidden owner role (BR 1.4.4)" employees ||--o{ employee_stores : "works at" employees ||--o| employee_legal_details : "has" employees ||--o{ shift_schedules : "planned" employees ||--o{ shift_records : "worked" employees ||--o{ salary_formulas : "individual" employees ||--o{ payroll_records : "paid" employees ||--o{ employee_roles : "has roles (BR 1.4.3)" roles ||--o{ role_permissions : "granted" roles ||--o{ employee_roles : "assigned to" employee_roles ||--o{ employee_role_stores : "scoped to stores" roles ||--o{ salary_formulas : "role formula (BR 1.4.3)" shift_records ||--o{ shift_corrections : "corrected" shift_templates ||--o{ shift_schedules : "from template" employee_legal_details { uuid id PK uuid employee_id FK "UNIQUE" uuid franchise_id string inn "nullable" string passport_series "nullable" string passport_number "nullable" string driver_license_number "nullable" date driver_license_expiry "nullable" string snils "nullable" timestamp created_at timestamp updated_at } shift_templates { uuid id PK uuid franchise_id uuid store_id string name "varchar(100)" time start_time integer duration_minutes timestamp created_at timestamp updated_at } shift_schedules { uuid id PK uuid franchise_id uuid store_id uuid employee_id FK date date uuid template_id FK "nullable" time start_time time end_time timestamp created_at timestamp updated_at } shift_records { uuid id PK uuid franchise_id uuid store_id uuid employee_id FK date date "business day" timestamp clock_in timestamp clock_out "nullable" timestamp break_start "nullable" timestamp break_end "nullable" integer break_duration_minutes "default 0" string source "pos | manual" boolean auto_closed "default false" string status "on_schedule | off_schedule | missed | unplanned" timestamp created_at timestamp updated_at } shift_corrections { uuid id PK uuid shift_record_id FK string type "increase | decrease" integer minutes text comment "required" uuid created_by FK timestamp created_at } salary_formulas { uuid id PK uuid franchise_id uuid store_id "nullable (deferred in BR 1.4.3 — always NULL in MVP)" uuid role_id FK "nullable, role-based formula (BR 1.4.3)" uuid employee_id FK "nullable, individual formula" string formula_type "hourly | fixed | mixed" decimal hourly_rate "nullable" decimal monthly_salary "nullable" decimal overtime_rate "nullable" integer norm_hours "default 160" timestamp created_at timestamp updated_at } payroll_records { uuid id PK uuid franchise_id uuid store_id uuid employee_id FK date period_start date period_end decimal planned_hours decimal actual_hours decimal break_hours decimal net_hours jsonb formula_snapshot decimal amount string status "calculated | confirmed | paid" uuid confirmed_by FK "nullable" timestamp confirmed_at "nullable" timestamp created_at timestamp updated_at } import_previews { uuid id PK uuid franchise_id uuid user_id string entity_type "legal_entity" jsonb valid_rows jsonb errors integer total_rows integer valid_count integer error_count timestamp expires_at timestamp created_at }
Таблицы
franchises
(Введено в BR 1.4.4 §3)
Tenant-сущность. Каждая запись — отдельная франшиза (бренд) со своим изолированным набором ЮЛ, ТТ, сотрудников и каталога. На все остальные таблицы ссылается через franchise_id.
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| name | varchar(255) | no | Бренд / название сети | |
| type | varchar(20) | no | corporate | corporate / individual — см. Франшизы |
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Constraints:
CHECK (type IN ('corporate','individual'))
Индексы (franchises)
| Имя | Колонки | Тип |
|---|---|---|
| pk_franchises | (id) | PRIMARY KEY |
Ключевые правила (franchises)
type = 'corporate'(default) — полноценная франшиза с иерархией: одно главное ЮЛ + произвольное число партнёров. Раздел «Юр. лица» виден в UI; черезPOST /legal-entitiesможно создавать ЮЛtype=franchiseetype = 'individual'— ИП-режим: одно главное ЮЛ, партнёров нет. Раздел «Юр. лица» скрыт в UI. На бэкеPOST /legal-entitiesсtype=franchisee→403 FRANCHISE_TYPE_INDIVIDUAL- Изменение
typeпосле старта — вручную SQL (UI-операции в MVP нет) - При миграции существующей единственной франшизы →
corporate
legal_entities
Юридические лица. Два типа: franchise (головное ЮЛ бренда) и franchisee (ЮЛ партнёра).
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| franchise_id | uuid | no | Тенант | |
| type | varchar(20) | no | franchise / franchisee | |
| name | varchar(255) | no | Полное наименование | |
| inn | varchar(12) | no | ИНН (10 или 12 цифр) | |
| kpp | varchar(9) | yes | КПП (у ИП нет) | |
| ogrn | varchar(15) | no | ОГРН (13) или ОГРНИП (15) | |
| legal_address | text | no | Юридический адрес | |
| bank_name | varchar(255) | yes | Название банка | |
| bank_bik | varchar(9) | yes | БИК | |
| bank_account | varchar(20) | yes | Расчётный счёт | |
| bank_corr_account | varchar(20) | yes | Корр. счёт | |
| contact_phone | varchar(20) | yes | Телефон (+7XXXXXXXXXX) | |
| contact_email | varchar(255) | yes | ||
| is_primary | boolean | no | false | Главное ЮЛ (только type=franchise) |
| status | varchar(20) | no | active | active / suspended |
| contract_number | varchar(50) | yes | Номер договора (только type=franchisee) | |
| contract_date | date | yes | Дата договора (только type=franchisee) | |
| owner_user_id | uuid | yes | Владелец-франчайзи (FK → employees.id, только type=franchisee) | |
| deleted_at | timestamp | yes | Soft delete (NULL = активно) | |
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Индексы
| Имя | Колонки | Тип | Условие |
|---|---|---|---|
| uq_legal_entities_inn | (franchise_id, inn) | UNIQUE | WHERE deleted_at IS NULL |
| idx_legal_entities_franchise | (franchise_id) | BTREE | |
| idx_legal_entities_type | (franchise_id, type) | BTREE | WHERE deleted_at IS NULL |
| idx_legal_entities_owner | (owner_user_id) | BTREE | WHERE deleted_at IS NULL |
| idx_legal_entities_status | (franchise_id, status) | BTREE | WHERE deleted_at IS NULL |
employees
Сотрудники франшизы.
(Обновлено в BR 1.4.4)
Колонка
role(enumadmin_franchise/admin_franchisee/manager/cashier) удалена (ALTER TABLE employees DROP COLUMN role). Вместе с ней удаленыCHECK-constraint и индексidx_employees_role. Scope сотрудника определяется через владение ЮЛ +employee_role_stores— см. Ролевая модель.
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| franchise_id | uuid | no | Тенант | |
| first_name | varchar(100) | no | Имя | |
| last_name | varchar(100) | no | Фамилия | |
| varchar(255) | no | Email (уникален в рамках franchise_id) | ||
| password_hash | varchar(72) | no | Хэш пароля (bcrypt, cost 12) | |
| phone | varchar(20) | yes | Телефон (+7XXXXXXXXXX) | |
| pin_hash | varchar(72) | yes | Хэш PIN-кода (bcrypt, 4 цифры) | |
| — | — | (Удалено в BR 1.4.4 — единая permission-based модель) | ||
| status | varchar(20) | no | active | active / inactive |
| is_courier | boolean | no | false | Может доставлять заказы |
| netmonet_profile_id | varchar(100) | yes | — | (BR 3.2) ID сотрудника в системе Нетмонет для сопоставления webhook чаевых (миграция 025) |
| netmonet_enabled | boolean | no | false | (BR 3.2) Согласие сотрудника на получение чаевых через Нетмонет |
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Индексы (employees)
| Имя | Колонки | Тип | Условие |
|---|---|---|---|
| uq_employees_email | (franchise_id, email) | UNIQUE | |
| — | (Удалён в BR 1.4.4 вместе с колонкой role) | ||
| idx_employees_status | (franchise_id, status) | BTREE |
employee_stores
Привязка сотрудников к торговым точкам (many-to-many).
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| employee_id | uuid | no | FK → employees.id | |
| store_id | uuid | no | ID торговой точки |
Индексы (employee_stores)
| Имя | Колонки | Тип | Условие |
|---|---|---|---|
| uq_employee_stores | (employee_id, store_id) | UNIQUE | |
| idx_employee_stores_store | (store_id) | BTREE | Для PIN lookup по ТТ |
import_previews
Временное хранилище preview импорта (вместо Redis на этапе MVP).
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| franchise_id | uuid | no | Тенант | |
| user_id | uuid | no | Кто загрузил | |
| entity_type | varchar(50) | no | Тип сущности (legal_entity) | |
| valid_rows | jsonb | no | Валидные строки (готовые к вставке) | |
| errors | jsonb | no | Ошибки: [{row, field, message}] | |
| total_rows | integer | no | Всего строк | |
| valid_count | integer | no | Валидных | |
| error_count | integer | no | С ошибками | |
| expires_at | timestamp | no | Срок жизни (created_at + 30 min) | |
| created_at | timestamp | no | now() |
Очистка expired preview — scheduled task или проверка при обращении (lazy cleanup).
employee_legal_details (BR 1.4.1)
Юридические детали сотрудника. Связь 1:1 с employees.
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| employee_id | uuid | no | FK → employees.id, UNIQUE | |
| franchise_id | uuid | no | Тенант | |
| inn | varchar(12) | yes | ИНН физлица (12 цифр) | |
| passport_series | varchar(4) | yes | Серия паспорта | |
| passport_number | varchar(6) | yes | Номер паспорта | |
| driver_license_number | varchar(20) | yes | Номер водительского удостоверения | |
| driver_license_expiry | date | yes | Срок действия ВУ | |
| snils | varchar(14) | yes | СНИЛС (формат XXX-XXX-XXX XX) | |
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Индексы (employee_legal_details)
| Имя | Колонки | Тип |
|---|---|---|
| uq_employee_legal_details_employee | (employee_id) | UNIQUE |
| idx_employee_legal_details_franchise | (franchise_id) | BTREE |
shift_templates (BR 1.4.1)
Шаблоны смен для быстрого создания расписания. Макс. 4 на ТТ.
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| franchise_id | uuid | no | Тенант | |
| store_id | uuid | no | FK → Store Service | |
| name | varchar(100) | no | Название (“Утренняя”, “Вечерняя”) | |
| start_time | time | no | Время начала | |
| duration_minutes | integer | no | Продолжительность (минуты) | |
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Индексы (shift_templates)
| Имя | Колонки | Тип | Условие |
|---|---|---|---|
| idx_shift_templates_store | (franchise_id, store_id) | BTREE |
Ограничение макс. 4 шаблона на ТТ — проверяется на уровне приложения.
shift_schedules (BR 1.4.1)
Плановые смены — расписание работы сотрудников.
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| franchise_id | uuid | no | Тенант | |
| store_id | uuid | no | FK → Store Service | |
| employee_id | uuid | no | FK → employees.id | |
| date | date | no | Бизнес-день плановой смены | |
| template_id | uuid | yes | FK → shift_templates.id (если из шаблона) | |
| start_time | time | no | Время начала | |
| end_time | time | no | Время окончания | |
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Индексы (shift_schedules)
| Имя | Колонки | Тип | Условие |
|---|---|---|---|
| uq_shift_schedules_employee_date | (employee_id, store_id, date) | UNIQUE | |
| idx_shift_schedules_store_date | (franchise_id, store_id, date) | BTREE |
shift_records (BR 1.4.1)
Фактически отработанные смены.
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| franchise_id | uuid | no | Тенант | |
| store_id | uuid | no | FK → Store Service | |
| employee_id | uuid | no | FK → employees.id | |
| date | date | no | Бизнес-день (дата clock_in) | |
| clock_in | timestamp | no | Время прихода | |
| clock_out | timestamp | yes | Время ухода (null = смена открыта) | |
| break_start | timestamp | yes | Начало перерыва | |
| break_end | timestamp | yes | Конец перерыва | |
| break_duration_minutes | integer | no | 0 | Вычисляется: break_end − break_start |
| source | varchar(10) | no | pos / manual | |
| auto_closed | boolean | no | false | Автозакрытие (превышение 24ч) |
| status | varchar(20) | no | on_schedule / off_schedule / missed / unplanned | |
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Индексы (shift_records)
| Имя | Колонки | Тип | Условие |
|---|---|---|---|
| idx_shift_records_employee_date | (employee_id, date) | BTREE | |
| idx_shift_records_store_date | (franchise_id, store_id, date) | BTREE | |
| idx_shift_records_status | (franchise_id, store_id, status) | BTREE |
shift_corrections (BR 1.4.1)
Корректировки отработанных часов для прошедших смен.
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| shift_record_id | uuid | no | FK → shift_records.id | |
| type | varchar(10) | no | increase / decrease | |
| minutes | integer | no | Величина корректировки (минуты) | |
| comment | text | no | Причина (обязательно) | |
| created_by | uuid | no | FK → employees.id (кто корректировал) | |
| created_at | timestamp | no | now() |
Индексы (shift_corrections)
| Имя | Колонки | Тип |
|---|---|---|
| idx_shift_corrections_record | (shift_record_id) | BTREE |
salary_formulas (BR 1.4.1 → обновлено в BR 1.4.3)
Формулы начисления зарплаты. Могут быть привязаны к роли-объекту или к конкретному сотруднику.
(Изменено в BR 1.4.3: поле role (varchar enum) заменено на role_id (UUID FK → roles.id). Per-ТТ ставки (store_id) помечены как deferred — всегда NULL в MVP.)
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| franchise_id | uuid | no | Тенант | |
| store_id | uuid | yes | NULL | Deferred BR 1.4.3 — в MVP всегда NULL. В будущем возможна per-ТТ ставка |
| role_id | uuid | yes | FK → roles.id. NULL = индивидуальная формула | |
| employee_id | uuid | yes | FK → employees.id. NULL = формула по роли | |
| formula_type | varchar(10) | no | hourly / fixed / mixed | |
| hourly_rate | decimal(10,2) | yes | Ставка ₽/час (для hourly, mixed) | |
| monthly_salary | decimal(12,2) | yes | Оклад ₽/мес (для fixed, mixed) | |
| overtime_rate | decimal(10,2) | yes | Ставка за переработку ₽/час (для mixed) | |
| norm_hours | integer | yes | 160 | Норма часов/мес (для mixed) |
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Иерархия формул
employee_id != NULL→ индивидуальная. Иначеrole_id→ по роли. Индивидуальная перекрывает ролевую.
Индексы (salary_formulas)
| Имя | Колонки | Тип | Условие |
|---|---|---|---|
| uq_salary_formulas_role | (role_id) | UNIQUE | WHERE employee_id IS NULL (одна формула на роль — обновлено в BR 1.4.3) |
| uq_salary_formulas_employee | (employee_id) | UNIQUE | WHERE employee_id IS NOT NULL |
payroll_records (BR 1.4.1)
Ведомости начислений за период.
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| franchise_id | uuid | no | Тенант | |
| store_id | uuid | no | FK → Store Service | |
| employee_id | uuid | no | FK → employees.id | |
| period_start | date | no | Начало периода (1-е число месяца) | |
| period_end | date | no | Конец периода (последний день месяца) | |
| planned_hours | decimal(6,2) | no | 0 | Плановые часы |
| actual_hours | decimal(6,2) | no | 0 | Фактические часы |
| break_hours | decimal(6,2) | no | 0 | Часы перерывов |
| net_hours | decimal(6,2) | no | 0 | Чистое время (actual − break ± corrections) |
| formula_snapshot | jsonb | no | Снимок формулы на момент расчёта | |
| amount | decimal(12,2) | no | 0 | Рассчитанная сумма |
| status | varchar(20) | no | calculated | calculated / confirmed / paid |
| confirmed_by | uuid | yes | FK → employees.id | |
| confirmed_at | timestamp | yes | ||
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Индексы (payroll_records)
| Имя | Колонки | Тип |
|---|---|---|
| uq_payroll_records_employee_store_period | (employee_id, store_id, period_start) | UNIQUE |
| idx_payroll_store_period | (franchise_id, store_id, period_start) | BTREE |
| idx_payroll_status | (franchise_id, status) | BTREE |
roles (BR 1.4.3, обновлено BR 1.4.4)
Объект-роль с настраиваемыми permissions. После BR 1.4.4 — единственный слой ролевой модели (enum employees.role удалён).
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| id | uuid | no | gen_random_uuid() | PK |
| franchise_id | uuid | no | Тенант | |
| owner_legal_entity_id | uuid | yes | NULL | FK → legal_entities.id ON DELETE CASCADE. Если задано — роль скрытая (персональная для владельца партнёра), не показывается в списках /admin/roles, редактируется только через карточку ЮЛ. При удалении ЮЛ партнёра скрытая роль каскадно удаляется (Введено в BR 1.4.4 §5.2) |
| name | varchar(100) | no | Название роли (для скрытых — техническое, владельцу не показывается) | |
| description | text | yes | Необязательное описание | |
| is_system | boolean | no | false | true для системной роли «Администратор» — нельзя удалить/изменить права |
| deleted_at | timestamp | yes | Soft delete. NULL = активна | |
| created_at | timestamp | no | now() | |
| updated_at | timestamp | no | now() |
Индексы (roles)
| Имя | Колонки | Тип | Условие |
|---|---|---|---|
| uq_roles_name_per_franchise | (franchise_id, LOWER(name)) | UNIQUE | WHERE deleted_at IS NULL AND owner_legal_entity_id IS NULL |
| idx_roles_franchise | (franchise_id) | BTREE | |
| idx_roles_system | (franchise_id, is_system) | BTREE | |
| idx_roles_visible | (franchise_id) | BTREE (partial) | WHERE owner_legal_entity_id IS NULL AND deleted_at IS NULL — для быстрого списка обычных ролей (BR 1.4.4) |
| idx_roles_hidden | (owner_legal_entity_id) | BTREE (partial) | WHERE owner_legal_entity_id IS NOT NULL — для lookup скрытой роли по ЮЛ (BR 1.4.4) |
Уникальность имени и скрытые роли
Партиал-уникальный индекс
uq_roles_name_per_franchiseисключает скрытые роли — их технические имена не участвуют в проверке коллизий с пользовательскими ролями.
role_permissions (BR 1.4.3)
Набор permission-ключей для каждой роли. Ключи перечислены в API.md в разделе «Permission catalog».
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| role_id | uuid | no | FK → roles.id, ON DELETE CASCADE | |
| permission_key | varchar(100) | no | menu.read, pos.cash.withdraw и т.п. | |
| granted | boolean | no | true | true = право выдано. false позволяет явно «выключить» унаследованное (резерв на будущее) |
PRIMARY KEY: (role_id, permission_key).
Системные роли
Для
is_system = trueролей permissions фиксированы (все возможные ключи granted=true). Редактирование этой таблицы для системных ролей запрещено на уровне приложения.
employee_roles (BR 1.4.3)
Связь «сотрудник — роль» (N:M).
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| employee_id | uuid | no | FK → employees.id, ON DELETE CASCADE | |
| role_id | uuid | no | FK → roles.id, ON DELETE RESTRICT (нельзя удалить роль, назначенную сотруднику) | |
| created_at | timestamp | no | now() |
PRIMARY KEY: (employee_id, role_id).
Индексы
| Имя | Колонки | Тип |
|---|---|---|
| idx_employee_roles_role | (role_id) | BTREE |
| idx_employee_roles_employee | (employee_id) | BTREE |
employee_role_stores (BR 1.4.3)
Набор магазинов, в которых действует данная связка (сотрудник, роль). Если сотруднику назначена роль «Курьер» только в ТТ-3, а роль «Бариста» в ТТ-1 и ТТ-2 — это две разные записи employee_roles с разными наборами строк employee_role_stores.
| Поле | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
| employee_id | uuid | no | ||
| role_id | uuid | no | ||
| store_id | uuid | no | FK → Store Service |
PRIMARY KEY: (employee_id, role_id, store_id).
FOREIGN KEY (employee_id, role_id) → employee_roles(employee_id, role_id) ON DELETE CASCADE.
Пустой набор магазинов
Если у связки
employee_rolesвообще нет строк вemployee_role_stores— роль действует без ограничения по магазинам (например, для системной роли «Администратор»).
Индексы
| Имя | Колонки | Тип |
|---|---|---|
| idx_employee_role_stores_store | (store_id) | BTREE |
kds_devices (BR 5.1)
(Добавлено в BR 5.1)
KDS-устройства (Android-планшеты), зарегистрированные в системе. Регистрируются явно через POST /admin/kds/devices/register, привязаны к одной ТТ. Soft-delete (revoked_at) — force-logout админом.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | Суррогатный PK |
device_id | uuid | NOT NULL | — | UUID, генерится на устройстве при первом запуске и хранится в SQLite. Передаётся в каждом запросе как X-Device-Id. |
franchise_id | uuid | NOT NULL | — | Мультитенантность |
store_id | uuid | NOT NULL | — | ТТ устройства (cross-service ref → Store Service) |
name | varchar(100) | NULL | — | Понятное имя (default = 'KDS-{first 6 hex of device_id}') |
last_user_id | uuid | NULL | — | Последний логинившийся (для аудита) |
current_user_id | uuid | NULL | — | Активная сессия (NULL если разлогинен или ещё не логинился) |
app_version | varchar(20) | NULL | — | Установленная версия APK (передаётся при heartbeat) |
last_seen_at | timestamp | NULL | — | Heartbeat. Используется для расчёта is_online (< 2 мин ago) |
revoked_at | timestamp | NULL | — | Soft-удалено админом через DELETE /admin/kds/devices/{id} |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Ограничения:
uq_kds_devices_franchise_device— UNIQUE(franchise_id, device_id)WHERErevoked_at IS NULL— одно активное устройство с этимdevice_idв франшизе. После revoke — можно ре-зарегистрировать с тем жеdevice_id.- Кросс-сервисные ссылки:
store_id→ Store Service,last_user_id/current_user_id→users.id(внутри User Service)
Индексы:
| Имя | Колонки | Тип |
|---|---|---|
idx_kds_devices_franchise_store | (franchise_id, store_id) WHERE revoked_at IS NULL | BTREE |
idx_kds_devices_last_seen | (last_seen_at DESC) WHERE revoked_at IS NULL | BTREE |
Бизнес-правила:
revoked_at != NULL→ 401DEVICE_REVOKEDпри любом use устройства; KDS должен пройти регистрацию заново- Повторная регистрация (после revoke) — это новая запись с тем же
device_id. Старая остаётся в БД для аудита current_user_idсбрасывается в NULL при logout / auto-logout / revokelast_user_idсохраняет последнего успешного юзера (для аудита, не сбрасывается)
Миграция (Liquibase changeset ZZ-add-kds-devices.xml):
- Создаёт таблицу
kds_devices - Добавляет permissions
kds.accessиkds.settings.editв каталог permissions (см. ниже)
pos_desktop_devices (POS Desktop onboarding)
(Добавлено как часть POS Desktop onboarding, миграция
027-pos-desktop-devices.xml)
POS-устройства (Windows-кассы), зарегистрированные в системе. Структура и поведение зеркальные kds_devices — выделено в отдельную таблицу для будущих расхождений (например, разные heartbeat-интервалы).
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | Суррогатный PK |
device_id | uuid | NOT NULL | — | UUID, генерится на ПК при первом запуске и хранится в localStorage. Передаётся в каждом запросе как X-Device-Id. |
franchise_id | uuid | NOT NULL | — | Мультитенантность |
store_id | uuid | NOT NULL | — | ТТ кассы (cross-service ref → Store Service) |
name | varchar(100) | NULL | — | Понятное имя (default = 'POS-{first 6 hex of device_id}') |
last_user_id | uuid | NULL | — | Последний логинившийся кассир (для аудита) |
current_user_id | uuid | NULL | — | Активная сессия (NULL если разлогинен) |
app_version | varchar(20) | NULL | — | Версия инсталлера (передаётся в X-App-Version) |
last_seen_at | timestamp | NULL | — | Heartbeat. Используется для расчёта is_online (< 2 мин ago) |
revoked_at | timestamp | NULL | — | Soft-удалено админом через DELETE /admin/pos/devices/{id} |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Ограничения:
uq_pos_desktop_devices_franchise_device— UNIQUE(franchise_id, device_id)WHERErevoked_at IS NULL— одно активное устройство на франшизу.
Индексы:
| Имя | Колонки | Тип |
|---|---|---|
idx_pos_desktop_devices_franchise_store | (franchise_id, store_id) WHERE revoked_at IS NULL | BTREE |
idx_pos_desktop_devices_last_seen | (last_seen_at DESC) WHERE revoked_at IS NULL | BTREE |
Бизнес-правила:
- Идентичны
kds_devices. Permissionpos.settings.edit(расширенный смысл — покрывает и настройки кассы, и управление устройствами). - Force-logout через 401
DEVICE_REVOKEDна следующем heartbeat’e в pos-bff middleware (см. API ·/internal/pos-devices/{deviceId}/heartbeat).
Permissions catalog (KDS-расширение)
(Добавлено в BR 5.1)
В каталог permissions (см. таблицу roles.permissions и permission-catalog) добавляются:
| Permission | Категория | Описание |
|---|---|---|
kds.access | KDS | Базовый доступ к KDS-приложению (PIN-логин). Без этого — 403 KDS_ACCESS_DENIED |
kds.settings.edit | KDS | Регистрация/правка/удаление KDS-устройств; редактирование kds_franchise_settings (звуки/громкость/auto_logout); правка kitchen_stations пороги |
marketing.read (BR 6.1) | Маркетинг | Просмотр списка слайдов своей ТТ. См. Маркетинговая информация |
marketing.write (BR 6.1) | Маркетинг | Создание, редактирование, удаление, изменение порядка слайдов. Edit implies Read |
Эти permissions выдаются ролям через стандартный механизм role_permissions (см. таблицу выше).
Миграция BR 6.1 (NNN-marketing-permissions.xml):
- INSERT в
permission_catalogключейmarketing.read,marketing.writeс категорией «Маркетинг» - INSERT в
role_permissionsдля системной роли «Администратор» каждой франшизы — оба ключа
Ключевые правила
Юридические лица
inn— уникален в рамкахfranchise_id(partial unique index, WHERE deleted_at IS NULL)inn— неизменяем после созданияis_primary = true— максимум одно ЮЛ наfranchise_id, только дляtype = franchisestatus = suspended— возможен только дляtype = franchisee;type = franchiseвсегдаactiveowner_user_id— nullable, привязка к франчайзи может быть выполнена позжеdeleted_at— soft delete, запись остаётся в БДcontract_number,contract_date— только дляtype = franchisee, nullable
Сотрудники
(Переработано в BR 1.4.4 — enum employees.role удалён, единая permission-модель)
password_hash— bcrypt, cost factor 12pin_hash— bcrypt, 4 цифры, уникален в рамках store_id (проверка на уровне приложения, не DB-constraint, т.к. значение захэшировано)email— уникален в рамкахfranchise_id- Нет поля
role: scope сотрудника определяется отдельным internal endpointGET /internal/users/{id}/scope(см. API) по правилам:- user =
legal_entities.owner_user_idи ЮЛtype=franchise→ вся франшиза - user =
legal_entities.owner_user_idи ЮЛtype=franchisee→ свои ЮЛ + их ТТ - иначе → ТТ из
employee_role_stores(объединение всех permissions-ролей)
- user =
- Привязка к магазинам — только через
employee_role_stores(per-role);employee_storesостаётся как legacy для прямой связи (используется PIN-логином) - Правила создания:
- Владелец франшизы — только при bootstrap (SQL), не через API
- Владелец партнёра — только в одной транзакции с
POST /legal-entities(type=franchisee) - Обычные сотрудники — через
POST /employees(поляroleнет)
- Доступ к юридическим деталям — только владельцы (по scope) для своих сотрудников
- Миграция BR 1.4.4:
ALTER TABLE employees DROP COLUMN role+ удаление CHECK constraint и индексаidx_employees_role
Роли и permissions (BR 1.4.3, обновлено BR 1.4.4)
roles.name— уникально в рамкахfranchise_id(case-insensitive) среди неудалённых обычных ролей (owner_legal_entity_id IS NULL); скрытые роли владельцев партнёров в проверке не участвуютis_system = true— можно переименовать/изменить описание, но нельзя удалить, изменить permissions или снять флаг системной- При удалении роли (soft delete) проверяется: нет ли записей в
employee_rolesс этойrole_idу активных сотрудников →409 ROLE_IN_USE employee_roles— сотрудник может иметь несколько ролей одновременноemployee_role_stores— набор магазинов для каждой связки(employee, role)независим; пустой набор = без ограниченияstore_idвemployee_role_storesдолжен быть подмножеством ТТ, доступных сотруднику по scope (см. правила scope в разделе «Сотрудники») — проверка на уровне приложения- Edit implies Read: при
granted=trueдля<section>.editавтоматически должна быть запись<section>.readсgranted=true— проверка на уровне приложения при сохранении - Скрытые роли владельцев партнёров (BR 1.4.4 §5.2):
owner_legal_entity_id IS NOT NULL→ роль скрыта из всех публичных списков (GET /roles,GET /roles/{id}возвращает 404)- Создаётся автоматически при
POST /legal-entitiesс режимом «Настроенные права» или черезPUT /legal-entities/{id}/owner-permissionsпри переключении full→custom - Удаляется при переключении custom→full или при физическом удалении ЮЛ (
ON DELETE SET NULL→ но в коде роль удаляется вместе с ЮЛ) - Минимум permissions (форсится сервером, нельзя снять):
pos.access,stores.read,employees.read
Юридические детали (BR 1.4.1)
- 1:1 с
employees(уникальныйemployee_id) - Все поля (кроме employee_id и franchise_id) — nullable
- Доступ: только владельцы (по scope — см. правила scope в разделе «Сотрудники»), для своих сотрудников
Шаблоны смен (BR 1.4.1)
- Максимум 4 шаблона на
store_id(проверка на уровне приложения) - При удалении шаблона привязанные
shift_schedulesне удаляются (template_idостаётся для истории)
Расписание смен (BR 1.4.1)
- Уникальность: один сотрудник — одна плановая смена на
(employee_id, store_id, date) - Плановые смены можно создавать только на будущие дни (
date > CURRENT_DATE) - Удалять и редактировать — только будущие
Фактические смены (BR 1.4.1)
date(бизнес-день) = датаclock_in— для ночных смен бизнес-день = день началаstatus— вычисляется автоматически при создании/обновлении записи сопоставлением сshift_schedules:on_schedule— факт в пределах ±30 мин от планаoff_schedule— факт есть, но отклонение >30 минmissed— план есть, факта нет (ставится ретроспективно)unplanned— факт есть, плана нет
auto_closed= true — смена автоматически закрыта через 24ч (scheduled job)break_duration_minutes=break_end - break_start(в минутах, 0 если нет перерыва)
Корректировки смен (BR 1.4.1)
- Можно корректировать смены в любом статусе кроме
missed comment— обязательное поле- Корректировку можно удалить (сброс)
Формулы зарплаты (BR 1.4.1)
employee_id IS NOT NULL→ индивидуальная формула, перекрывает ролевуюemployee_id IS NULL+role+store_id→ формула по роли для ТТ- Иерархия: индивидуальная → по роли+ТТ → нет формулы
formula_type = hourly: обязательноhourly_rateformula_type = fixed: обязательноmonthly_salaryformula_type = mixed: обязательноmonthly_salary,overtime_rate,norm_hours
Ведомости (BR 1.4.1)
- Уникальность:
(employee_id, store_id, period_start) formula_snapshot— JSON-снимок формулы на момент расчёта (для историчности)- Статус-машина:
calculated→confirmed→paid(только вперёд) confirmed— пересчёт невозможен;calculated— можно пересчитать