renovation-estimator · этап 1 · гейт согласования

Структура базы данных модуля расчёта сметы ремонта

НА СОГЛАСОВАНИИ

20 таблиц в пяти блоках. Схема уже развёрнута в PostgreSQL с демо-данными и проверена живыми запросами — но это черновик: правки на этом этапе дешёвые, дальше разработка не двигается до утверждения.

1Общая карта связей

Как блоки сцеплены: каталог наполняется через импорт; планировка от генератора раскладывается в модель помещения; движок соединяет поверхности с рецептами и каталогом и пишет смету.

erDiagram
    category ||--o{ category : "родитель"
    category ||--o{ item : "содержит"
    category ||--o{ attribute_def : "схема атрибутов"
    item ||--o{ price : "цены (история)"
    supplier ||--o{ price : "предлагает"
    item ||--o{ item_source : "привязка к источнику"
    import_source ||--o{ item_source : "ключ идемпотентности"
    import_source ||--o{ import_batch : "сеансы загрузки"
    import_batch ||--o{ staging_item : "строки"

    finish_system ||--o{ finish_system_component : "состав рецепта"
    finish_system_component }o--o| item : "конкретная позиция"
    finish_system_component }o--o| category : "или подбор из категории"
    room_type_default }o--|| finish_system : "дефолт по типу комнаты"

    project ||--o{ layout : "версии планировки"
    layout ||--o{ room : "комнаты"
    room ||--o{ surface : "пол/стены/потолок"
    surface ||--o{ opening : "проёмы"
    surface }o--o| finish_system : "назначенная отделка"
    room ||--o{ placed_item : "наполнение"
    placed_item }o--o| item : "резолв в каталог"

    project ||--o{ estimate : "версии смет"
    estimate }o--|| layout : "по планировке"
    estimate ||--o{ estimate_line : "строки"
    estimate ||--o{ estimate_issue : "проблемы"
    estimate_line }o--o| item : "позиция"
    estimate_line }o--o| price : "снапшот цены"

2Ключевые решения схемы

3Блок «Каталог»

Что бывает на свете: материалы, наполнение, работы, их свойства и цены.

category

Дерево категорий: материалы / наполнение / работы

  • idPK
  • parent_idFK→category
  • code flooring.laminateUNIQUE
  • name
  • kind material / furnishing / work
  • path материализованный путь
  • sort_order

unit

Единицы измерения

  • idPK
  • code m2, m, pcs, l, kg, packUNIQUE
  • name
  • dimension area / length / count / volume / mass

attribute_def

Схема атрибутов категории (шаблон для attrs)

  • idPK
  • category_idFK→category
  • code width_mm, color, power_w
  • name
  • data_type number / string / bool / enum
  • unit_code nullable
  • enum_values JSONB
  • is_required

Атрибуты наследуются вниз по дереву категорий

item

Позиция каталога — материал, объект наполнения или работа

  • idPK
  • category_idFK→category
  • name, description
  • base_unit_idFK→unit
  • attrs JSONB + GIN-индекс
  • package_qty 2.13 м²/пачка — для округления
  • package_unit_idFK→unit
  • image_url, is_active
  • created_at, updated_at

supplier

Поставщик / источник цен

  • idPK
  • codeUNIQUE
  • name, url, notes
  • priority порядок предпочтения при выборе цены
  • is_service служебный «без источника», с реальными не сливается

price

Цена позиции; история не затирается

  • idPK
  • item_idFK→item
  • supplier_idFK→supplier
  • price, currency RUB
  • unit_id цена может быть за упаковкуFK→unit
  • price_type retail / trade / estimate
  • valid_from, collected_at
  • is_current частичный UNIQUE-индекс

4Блок «Расчётные правила»

Как из квадратных метров получаются литры, пачки и рубли: рецепты отделки с нормами расхода.

finish_system

Рецепт отделки поверхности: «пол ламинат», «стены покраска»

  • idPK
  • code floor_laminateUNIQUE
  • name, description
  • surface_kind floor / wall / ceiling
  • is_active

finish_system_component

Строка рецепта: компонент + норма расхода

  • idPK
  • system_idFK→finish_system
  • role «подложка», «укладка», sort_order
  • component_ref_type item / category
  • item_id конкретная позиция…FK→item
  • category_id …или подбор из категорииFK→category
  • measure_basis area / perimeter / opening_count / room_count / piece
  • consumption_rate 0.25 л/м² + consumption_unit_id
  • waste_factor запас/подрезка, 1.05
  • rounding none / up_unit / up_package
  • params {"layers": 2}, condition JSONB

Плинтус считается от периметра минус двери, краска — от площади, порожек — от числа проёмов

room_type_default

Отделка по умолчанию, если генератор не указал

  • idPK
  • room_type bathroom, kitchen, bedroom…
  • surface_kind
  • finish_system_idFK→finish_system
  • style_profile задел: эконом / комфорт / бизнес

5Блок «Модель помещения»

Планировка от генератора, разложенная в расчётные единицы: комнаты → поверхности → проёмы, плюс расставленное наполнение.

project

Проект (квартира / объект)

  • idPK
  • name, customer, status
  • created_at

layout

Принятая планировка — иммутабельная версия входа

  • idPK
  • project_idFK→project
  • external_layout_id id в генераторе
  • generator_version, schema_version
  • raw_json вход целиком — для переразбора
  • imported_at

Новая версия от генератора = новая строка, старая не меняется

room

Комната

  • idPK
  • layout_idFK→layout
  • external_id, name, room_type
  • height_mm
  • floor_area_m2, perimeter_m
  • skirting_length_m периметр минус двери и проходы
  • geometry полигон, мм

surface

Поверхность — то, к чему применяется рецепт

  • idPK
  • room_idFK→room
  • kind floor / wall / ceiling
  • wall_index, length_mm для стен
  • gross_area_m2 − openings_area_m2 = net_area_m2
  • finish_system_idFK→finish_system
  • finish_source layout / room_default / manual

opening

Проём: вычитается из стены, дверь — и из периметра плинтуса

  • idPK
  • surface_idFK→surface
  • kind door / window / passage
  • width_mm, height_mm, area_m2
  • sill_height_mm, offset_mm откосы, подоконники, раскладка
  • linked_room_external_id межкомнатная дверь не считается дважды

placed_item

Расставленное наполнение

  • idPK
  • room_idFK→room
  • external_id
  • catalog_item_id конкретная позиция…FK→item
  • selection_params …или параметры подбора, JSONB
  • qty, position {x, y, rotation}
  • resolved_item_id что подобралиFK→item
  • resolution_status pending / resolved / ambiguous / not_found

6Блок «Сметы»

Результат: версионные сметы, каждая цифра со следом происхождения.

estimate

Расчёт; пересчёт = новая версия, старые неизменны

  • idPK
  • project_idFK→project
  • layout_idFK→layout
  • version UNIQUE в рамках проекта
  • status draft / final
  • pricing_policy JSONB: приоритет поставщиков, тип цены
  • total_materials / _furnishing / _works
  • grand_total, created_at

estimate_line

Строка сметы

  • idPK
  • estimate_idFK→estimate
  • section floor / walls / ceiling / furnishing / works
  • room_id NULL = итог по объектуFK→room
  • item_idFK→item
  • measured_qty замер: 18.4 м²
  • qty к закупке: 10 пачек + unit_id
  • unit_price, price_id какая цена взятаFK→price
  • total
  • calc_trace JSONB: рецепт, базис, норма, запас, округление

estimate_issue

Проблемы расчёта — не роняют смету

  • idPK
  • estimate_idFK→estimate
  • severity warning / error
  • code NO_PRICE, ITEM_NOT_RESOLVED, NO_FINISH_SYSTEM
  • context JSONB

7Блок «Импорт каталога»

Гибкий вход под будущие источники: сырьё → карантин → сопоставление → каталог. Повторная загрузка того же файла = 0 изменений.

import_source

Зарегистрированный источник + правила преобразования

  • idPK
  • code leroy_csv, manualUNIQUE
  • name
  • source_type csv / xlsx / json / api / manual
  • mapping_profile JSONB: колонки → каноника, трансформации
  • default_supplier_idFK→supplier

item_source

Привязка позиции к артикулу источника — ключ идемпотентности

  • idPK
  • item_idFK→item
  • source_idFK→import_source
  • external_key артикул / SKU / URL
  • raw_snapshot JSONB
  • last_seen_at

UNIQUE (source_id, external_key) — повторный импорт обновляет, а не дублирует

import_batch

Сеанс загрузки

  • idPK
  • source_idFK→import_source
  • file_name
  • status loaded / mapped / applied / failed
  • stats {"rows":120,"created":15,"updated":100}
  • started_at, finished_at

staging_item

Сырая строка в карантине до применения

  • idPK
  • batch_idFK→import_batch
  • row_num
  • raw как пришло, canonical после маппинга
  • external_key
  • match_status new / matched / conflict / error
  • error_text
  • applied_item_idFK→item

8Три сквозных примера на живых данных

Демо-данные уже в базе; расчёты ниже показывают, как схема отработает типовые случаи.

Краска: норма расхода

item «Краска в/д белая матовая», ведро 9 л → рецепт wall_paint, компонент «краска»

стены комнаты (за вычетом проёмов)net_area = 36.8 м²
норма (2 слоя, params {"layers": 2})36.8 × 0.25 л/м² = 9.20 л
запас (waste 1.05)9.20 × 1.05 = 9.66 л
округление up_package (ведро 9 л)⌈9.66 / 9⌉ = 2 ведра (18 л)
цена 5 490 ₽/ведро (Поставщик А)2 × 5 490 = 10 980 ₽

Ламинат: упаковка и две цены

item «Ламинат Дуб Натур 33», package_qty 2.13 м²/пачка, цены: А 1 290 ₽/м² (priority 10), Б 1 245 ₽/м² (priority 20)

пол комнатыnet_area = 18.4 м²
запас на подрезку (waste 1.05)18.4 × 1.05 = 19.32 м²
округление up_package⌈19.32 / 2.13⌉ = 10 пачек = 21.3 м²
политика «мин. цена» → Поставщик Б21.3 × 1 245 = 26 518.50 ₽
в строке сметыmeasured_qty=18.4 м², qty=10 пачек, price_id→снапшот цены Б

Диван: подбор по параметрам

генератор прислал не конкретную модель, а требования → placed_item.selection_params → JSONB-подбор → resolved_item

запрос генератора{"category_code":"furniture.sofa", "attrs":{"width_mm":{"max":2400}, "seats":{"min":3}}}
подбор по каталогу (реально выполнен в БД)→ «Диван 3-местный „Осло“, рогожка» {width_mm: 2200, seats: 3}
результатresolution_status = resolved, resolved_item_id → «Осло»
цена (Поставщик Б)68 900 ₽ → раздел «Наполнение»

Что согласовываем

  1. Механизм гибких атрибутов: JSONB-значения + шаблоны по категориям.
  2. Единый каталог item для материалов, наполнения и работ.
  3. Цены: несколько поставщиков, история, выбор по политике при расчёте.
  4. Рецепты отделки как данные: норма × базис × запас × округление до упаковки.
  5. Наполнение от генератора: конкретной позицией или параметрами подбора.
  6. Импорт через staging с маппинг-профилями — под любые будущие источники.
  7. Сметы: версии, снапшот цен, след расчёта в каждой строке.

Если структура устраивает — по слову «согласовано» продолжаю этапы 2–5 (импорт каталога, контракт с генератором, движок расчёта, экспорт). Любые правки схемы сейчас — самые дешёвые.