Store Service — Data Model
База данных: store_db
erDiagram stores { uuid id PK uuid franchise_id "NOT NULL" uuid legal_entity_id "NOT NULL" varchar name "NOT NULL" text address "NOT NULL" decimal latitude "NOT NULL, (10,7)" decimal longitude "NOT NULL, (10,7)" varchar city "NULL, (100)" varchar phone "NULL, (20)" varchar email "NULL, (255)" varchar status "NOT NULL, default active" boolean is_published "NOT NULL, default false" uuid price_list_id "NULL" varchar timezone "NOT NULL, default Europe/Moscow, (50)" integer standby_idle_minutes "NOT NULL, default 5 — BR 6.1" integer standby_transition_seconds "NOT NULL, default 9 — BR 6.1" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" timestamp deleted_at "NULL" } marketing_slides { uuid id PK uuid store_id FK "NOT NULL" uuid franchise_id "NOT NULL" text image_url "NOT NULL — public URL" varchar image_mime "NOT NULL, (50)" integer image_size_bytes "NOT NULL" varchar title "NOT NULL, (200)" integer order_index "NOT NULL" boolean active "NOT NULL, default true" varchar source "NOT NULL, default manual" jsonb source_ref "NULL" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" timestamp deleted_at "NULL" } store_schedules { uuid id PK uuid store_id FK "NOT NULL" integer day_of_week "NOT NULL, 0-6" time open_time "NULL" time close_time "NULL" boolean is_closed "NOT NULL, default false" } pos_terminals { uuid id PK uuid store_id FK "NOT NULL" uuid franchise_id "NOT NULL" varchar label "NOT NULL, (100)" varchar fs_number "NOT NULL, UNIQUE, (30)" varchar rn_kkt "NULL, (30)" varchar status "NOT NULL, default active" timestamp last_seen_at "NULL" timestamp deleted_at "NULL" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } zal_tables { uuid id PK uuid store_id FK "NOT NULL" uuid franchise_id "NOT NULL" integer number "NOT NULL" varchar label "NULL, (50)" integer capacity "NOT NULL, default 4" integer position_x "NOT NULL, default 0" integer position_y "NOT NULL, default 0" varchar status "NOT NULL, default free" uuid current_order_id "NULL" uuid current_waiter_id "NULL — BR 3.2, для привязки чаевых" varchar reserved_note "NULL, (200)" timestamp reserved_until "NULL" timestamp deleted_at "NULL" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } stores ||--o{ store_schedules : "has schedule" stores ||--o{ pos_terminals : "has terminals" stores ||--o{ zal_tables : "has tables" stores ||--o{ marketing_slides : "has slides"
Таблицы
stores
Торговые точки сети.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | ID франшизы (из JWT) |
legal_entity_id | uuid | NOT NULL | — | FK → User Service legal_entities (логическая связь) |
name | varchar(255) | NOT NULL | — | Название ТТ |
address | text | NOT NULL | — | Полный адрес |
latitude | decimal(10,7) | NOT NULL | — | Широта |
longitude | decimal(10,7) | NOT NULL | — | Долгота |
city | varchar(100) | NULL | — | Город |
phone | varchar(20) | NULL | — | Телефон |
email | varchar(255) | NULL | — | |
status | varchar(20) | NOT NULL | 'active' | active / suspended |
is_published | boolean | NOT NULL | false | Опубликована для клиентов |
price_list_id | uuid | NULL | NULL | ID прейскуранта (Catalog Service). NULL = используется дефолтный. (BR 1.10) |
timezone | varchar(50) | NOT NULL | 'Europe/Moscow' | IANA timezone (e.g. Europe/Moscow) |
standby_idle_minutes | integer | NOT NULL | 5 | Минут неактивности кассира до запуска standby-режима. (BR 6.1) |
standby_transition_seconds | integer | NOT NULL | 9 | Секунд между слайдами в standby-карусели. (BR 6.1) |
created_at | timestamp | NOT NULL | now() | Дата создания |
updated_at | timestamp | NOT NULL | now() | Дата обновления |
deleted_at | timestamp | NULL | — | Soft delete |
Индексы:
idx_stores_franchise_id—franchise_ididx_stores_legal_entity_id—legal_entity_ididx_stores_status_published—status, is_published(для фильтрации по виртуальным статусам)uq_stores_le_name— UNIQUE(legal_entity_id, name)WHEREdeleted_at IS NULL(название ТТ уникально в рамках ЮЛ)
legal_entity_id— логическая FKСсылается на таблицу
legal_entitiesв User Service (database-per-service). Валидация при создании через internal API.
store_schedules
Расписание работы ТТ по дням недели.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
store_id | uuid | NOT NULL | — | FK → stores.id |
day_of_week | integer | NOT NULL | — | 0=Понедельник..6=Воскресенье |
open_time | time | NULL | — | Время открытия (HH:MM) |
close_time | time | NULL | — | Время закрытия (HH:MM) |
is_closed | boolean | NOT NULL | false | Выходной день |
Ограничения:
UNIQUE (store_id, day_of_week)— один день = одна записьFK store_id REFERENCES stores(id) ON DELETE CASCADE
Правила валидации
- Если
is_closed = true, тоopen_timeиclose_timeдолжны бытьNULL- Если
is_closed = false, тоopen_timeиclose_timeобязательныclose_timeможет быть меньшеopen_time(работа через полночь)
pos_terminals
POS-терминалы (физические устройства), привязанные к ТТ по заводскому номеру ФН.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
store_id | uuid | NOT NULL | — | FK → stores.id |
franchise_id | uuid | NOT NULL | — | Франшиза (из JWT) |
label | varchar(100) | NOT NULL | — | Название терминала (например «Касса 1») |
fs_number | varchar(30) | NOT NULL | — | Заводской номер ФН, UNIQUE глобально |
rn_kkt | varchar(30) | NULL | — | Регистрационный номер ККТ |
status | varchar(20) | NOT NULL | 'active' | active / inactive |
last_seen_at | timestamp | NULL | — | Последнее обращение с терминала |
deleted_at | timestamp | NULL | — | Soft delete |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Индексы:
idx_pos_terminals_store—store_ididx_pos_terminals_franchise—franchise_iduq_pos_terminals_fs_number— UNIQUEfs_number
zal_tables
Столы в зале для dine-in: визуальная карта с позициями и статусами.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
store_id | uuid | NOT NULL | — | FK → stores.id |
franchise_id | uuid | NOT NULL | — | Франшиза (из JWT) |
number | integer | NOT NULL | — | Номер стола |
label | varchar(50) | NULL | — | Доп. метка (например «у окна») |
capacity | integer | NOT NULL | 4 | Кол-во посадочных мест |
position_x | integer | NOT NULL | 0 | Координата X на canvas-карте |
position_y | integer | NOT NULL | 0 | Координата Y на canvas-карте |
status | varchar(20) | NOT NULL | 'free' | free / occupied / reserved (CHECK-constraint) |
current_order_id | uuid | NULL | — | ID текущего активного заказа |
current_waiter_id | uuid | NULL | — | Текущий назначенный официант (BR 3.2, для привязки чаевых из Нетмонета, миграция 009) |
reserved_note | varchar(200) | NULL | — | Комментарий к брони |
reserved_until | timestamp | NULL | — | Время до которого стол забронирован |
deleted_at | timestamp | NULL | — | Soft delete |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Индексы:
idx_zal_tables_store—store_ididx_zal_tables_franchise—franchise_ididx_zal_tables_order—current_order_ididx_zal_tables_waiter—current_waiter_iduq_zal_tables_store_number_live— UNIQUE(store_id, number)WHEREdeleted_at IS NULL
CHECK-constraint: status IN ('free','occupied','reserved')
marketing_slides
(BR 6.1 — Маркетинговая информация / standby-карусель POS Desktop)
Слайды для standby-карусели на POS Desktop. Per-store, статичные картинки (jpg/png/webp), управляются маркетологом из админки.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
store_id | uuid | NOT NULL | — | FK → stores.id (ON DELETE CASCADE) |
franchise_id | uuid | NOT NULL | — | Денормализация из stores, для быстрых scope-фильтров (event-публикация по franchise_id) |
image_url | text | NOT NULL | — | Публичный URL картинки в S3-бакете erp-marketing-slides |
image_mime | varchar(50) | NOT NULL | — | image/jpeg / image/png / image/webp (CHECK-constraint) |
image_size_bytes | integer | NOT NULL | — | Размер файла в байтах (для аудита + валидации лимита 10 МБ при апдейтах) |
title | varchar(200) | NOT NULL | — | Заголовок для админки. На POS не отображается |
order_index | integer | NOT NULL | — | Порядок в карусели. order — зарезервированное слово в SQL, поэтому колонка order_index. В API наружу отдаётся как order |
active | boolean | NOT NULL | true | Активность слайда. Только активные крутятся в standby |
source | varchar(20) | NOT NULL | 'manual' | manual / ai_photo_studio (CHECK-constraint). См. BR 6.2 |
source_ref | jsonb | NULL | — | Для source=ai_photo_studio: { "job_id": "uuid", "preset_id": "string" } для трассировки |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() | |
deleted_at | timestamp | NULL | — | Soft delete |
Индексы:
idx_marketing_slides_store—store_ididx_marketing_slides_store_active_order—(store_id, active, order_index)WHEREdeleted_at IS NULL(для быстрого чтения активных в правильном порядке)uq_marketing_slides_store_order— UNIQUE(store_id, order_index)WHEREdeleted_at IS NULL(защита от дублей order при reorder; reorder происходит в транзакции с временным сдвигом если упирается)
CHECK-constraints:
image_mime IN ('image/jpeg', 'image/png', 'image/webp')source IN ('manual', 'ai_photo_studio')order_index >= 0image_size_bytes > 0 AND image_size_bytes <= 10485760(10 МБ)
Бизнес-лимит активных слайдов на ТТ
Лимит 20 активных слайдов на ТТ форсится на уровне сервиса (проверка
COUNT WHERE active = true AND deleted_at IS NULLперед INSERT/UPDATE). На уровне БД не закреплён — было бы дорогой constraint.
S3-хранилище
image_urlхранит публичный URL видаhttps://s3.erp/erp-marketing-slides/{store_id}/{slide_id}.{ext}. Бакетerp-marketing-slides— отдельный отgensvc-outputsAI Photo Studio. При создании слайда сsource=ai_photo_studioфайл копируется из gensvc-бакета вerp-marketing-slides(не ссылается напрямую, т.к. presigned TTL gensvc — 1 час, сломал бы карусель).
Отложенная очистка S3
При soft delete файл в S3 не удаляется. Cron раз в сутки чистит файлы, у которых нет ссылки в активной строке
marketing_slides(включаяdeleted_at IS NULLдля свежих soft-deletes — даём окно отката).
Маппинг виртуальных статусов
Фронтенд оперирует виртуальными статусами, бэкенд хранит два отдельных поля:
| Виртуальный статус | status | is_published |
|---|---|---|
| draft | active | false |
| published | active | true |
| suspended | suspended | false |
Приостановленная ТТ не может быть опубликована
suspended + is_published = true— невалидная комбинация. При приостановке ЮЛ вызываетсяPOST /internal/stores/unpublish-by-legal-entity.