# Правильная конвертация legacy в новую БД `hub`

Дата: 19 марта 2026

## 1. Что считаем источником истины

У нас не 3 разные боевые БД.

Правильный источник данных один:

- legacy production DB: `ontripge_midge`

Три старых бэка работают поверх одной и той же модели:

- `app.bent.ge` — внутренний backoffice / structured CRUD layer
- `cd.bent.ge` — основной legacy API layer
- `bent.ge` — public facade, частично поверх `cd.bent.ge`

Для конвертации это значит:

- данные берём из `ontripge_midge`
- бизнес-смысл и edge cases читаем из кода `app.bent.ge` и `cd.bent.ge`
- `bent.ge` используем только чтобы понять public flows

## 2. Главный принцип

Мы не переносим legacy-таблицы в новую БД.

Мы делаем так:

1. читаем legacy-данные
2. приводим их к новой доменной модели
3. загружаем уже нормализованные сущности в `hub`

То есть:

- не `b_clients_booking -> clients_booking_new`
- а `b_clients_booking -> bookings + vehicle_assignments + price_snapshot`

## 3. Как делать правильно архитектурно

Нужны 3 слоя.

### Слой 1. Raw import

Туда складываем legacy как есть.

Таблицы:

- `legacy_import_runs`
- `legacy_raw_records`
- `legacy_import_errors`

Задача слоя:

- сохранить исходные данные без потерь
- дать возможность повторного импорта
- иметь воспроизводимый snapshot

### Слой 2. Mapping / normalization

Здесь мы превращаем legacy в новые бизнес-сущности.

Нужны mapping-таблицы и value maps:

- `legacy_entity_links`
- `legacy_value_mappings`

Задача слоя:

- определить, как legacy ID переходят в новые ID
- хранить и кодовые mapping-правила, и manual overrides без переписывания importer-а

### Слой 3. Final load

Только здесь пишем в боевые таблицы `hub`.

## 4. Какую новую модель считаем правильной

Вот основа правильной новой модели:

- `customers`
- `locations`
- `providers`
- `vehicle_models`
- `vehicle_groups`
- `vehicles`
- `bookings`
- `booking_vehicle_assignments`
- `booking_extras`
- `booking_documents`
- `booking_activities`

### Важное правило

Legacy бронирует **конкретную машину**:

- `b_clients_booking.car`

Новая система должна работать **правильно**, а не по legacy-ошибке:

- бронь хранит `vehicle_group`
- фактическая машина идёт в `booking_vehicle_assignments`

Для исторических бронирований:

- сохраняем фактическую legacy-машину как assignment
- одновременно определяем `vehicle_group` по машине/модели

## 5. Что реально есть в legacy

Ключевые таблицы:

- `b_clients`
- `b_clients_booking`
- `b_clients_price`
- `b_clients_media`
- `b_clients_services`
- `b_cars`
- `b_locations`
- `b_status`
- `b_marketplaces`

Фактическая роль таблиц:

- `b_clients` — клиенты
- `b_clients_booking` — ядро бронирований
- `b_clients_price` — не pricing engine, а extras rows внутри брони
- `b_clients_media` — в основном документы клиента
- `b_clients_payment` — сейчас почти мёртвая таблица, не источник истины

## 6. Правильная стратегия по сущностям

### 6.1. Клиенты

Источник:

- `b_clients`

Правильная конвертация:

- переносим в `customers`
- приводим телефон в единый формат
- email в lowercase
- собираем `full_name`
- отдельно фиксируем:
  - `passport_id`
  - `driving_license_id`
  - `birthday`
  - `locale`
  - `country_residence`

Нельзя делать:

- тупой insert без дедупликации

Правильный путь:

- exact match по email
- exact match по normalized phone
- soft match по name + phone
- review queue для спорных дублей

### 6.2. Бронирования

Источник:

- `b_clients_booking`

Правильная конвертация:

- `ref` -> `bookings.reference`
- `external_id` -> `external_reference`
- `external_marketplace` -> `source/channel`
- `client` -> `customer_id`
- `service` -> `service_type`
- `operator` -> `created_by/owner` или activity meta
- `location_start/end` -> pickup/dropoff locations
- `start/end` -> pickup/dropoff datetime
- `flight` -> `flight_number`
- `comment` -> `comment`
- pricing fields -> snapshot + totals

Критично:

- legacy status переносим не 1 в 1
- новая status-модель должна быть новой

### 6.3. Статусы бронирований

Источник:

- `b_status`

Фактически в legacy статусы такие:

- `pending`
- `confirmed_without_pay`
- `confirmed_with_payment`
- `canceled_client`
- `canceled_us`

Это плохая модель для новой системы.

Правильный mapping:

- `pending` -> `pending`
- `confirmed_without_pay` -> `upcoming`
- `confirmed_with_payment` -> `upcoming`
- `canceled_client` -> `cancelled`
- `canceled_us` -> `cancelled`

Но этого мало.

Потому что legacy не хранит нормально:

- `active`
- `completed`

Значит для исторических данных нужен derive rule:

- если `cancelled` -> `cancelled`
- если не cancelled и `dropoff_at < now/import_cutoff` -> `completed`
- если `pickup_at <= now/import_cutoff < dropoff_at` -> `active`
- если `pickup_at > now/import_cutoff` -> `upcoming`

То есть новый статус строится по:

- legacy status
- датам аренды
- моменту миграции

### 6.4. Машины, модели, группы

Источник:

- `b_cars`
- `b_cars_model`
- `b_cars_type`
- `b_cars_gear`
- `b_portals_agent`

Правильная конвертация:

- `b_cars_model` -> `vehicle_models`
- продаваемые группы собираем отдельно в `vehicle_groups`
- `b_cars` -> `vehicles`

Нельзя делать:

- ставить `booking.car` как постоянную структуру в новой схеме

Правильно:

- машина legacy становится historical assignment
- параллельно рассчитываем `vehicle_group` для брони

### 6.5. Extras

Источник:

- `b_clients_price`
- `b_extras`

Ключевой факт:

- `b_clients_price` — это extras on booking, а не мастер-прайс

Правильная конвертация:

1. сначала создаём новый чистый extras catalog
2. потом legacy extra rows маппим в него

В новом каталоге нужны нормальные codes:

- `additional_driver`
- `child_seat`
- `wifi_hotspot`
- `super_cover`
- `winter_tires`
- `theft_protection`
- `unlimited_mileage`
- `collision_damage_waiver`
- `local_fees_vat_parking`

Критичный нюанс:

- `b_clients_price.value` перегружено
- там бывает:
  - `included`
  - `day`
  - numeric ids / numeric values

Особенно важно:

- у `Additional Driver` numeric `value` часто выглядит как ссылка на второго клиента/водителя

Значит правильно так:

- included extras переносим как snapshot line с нулевой ценой и флагом `included`
- платные extras переносим как `booking_extras`
- special-case `Additional Driver` маппим отдельно, возможно в `booking_extra.meta`

Нельзя делать:

- просто копировать `value` как есть в новую колонку

### 6.6. Документы / медиа

Источник:

- `b_clients_media`

Факт:

- это в основном документы клиента
- не booking documents

Правильная конвертация:

- основная часть -> `customer_documents` или customer media module
- те немногие, что привязаны к booking, можно продублировать ссылкой в booking context

То есть в новой схеме тут нужен отдельный customer-documents слой.

### 6.7. Платежи

Источник:

- формально `b_clients_payment`

Факт:

- таблица почти пустая

Правильный вывод:

- она не должна считаться источником истины
- payment history исторически у нас дырявая

Что делаем правильно:

- для старых броней переносим payment state только из booking-level полей
- полноценный ledger в новой системе строим заново

## 7. Какой order миграции правильный

Не надо сразу лить всё.

Правильный порядок:

1. import raw legacy snapshot
2. import lookups
3. normalize locations
4. normalize providers
5. normalize vehicle models
6. build vehicle groups
7. import vehicles
8. import customers
9. build new extras catalog
10. import bookings
11. create booking assignments from legacy `car`
12. import booking extras
13. import customer documents
14. import booking activities / notes if needed

## 8. Как делать import правильно технически

Не через ad-hoc SQL.

Правильный способ:

- Laravel console commands
- на каждый домен свой importer
- всё идемпотентно
- каждая запись имеет:
  - `legacy_source`
  - `legacy_table`
  - `legacy_id`
  - `import_run_id`

### Нужны отдельные команды

- `hub:legacy:import-raw`
- `hub:legacy:map-lookups`
- `hub:legacy:import-customers`
- `hub:legacy:import-fleet`
- `hub:legacy:import-bookings`
- `hub:legacy:import-booking-extras`
- `hub:legacy:import-documents`
- `hub:legacy:reconcile`

### Нужны dry-run режимы

У каждой команды:

- `--dry-run`
- `--limit`
- `--from-id`
- `--only-changed`

## 9. Как контролировать качество

После каждого большого этапа нужны reconciliation checks:

- count legacy bookings vs imported bookings
- count legacy clients vs imported customers
- count legacy cars vs imported vehicles
- count bookings without mapped customer
- count bookings without mapped group
- count extras without mapped canonical extra
- count documents without file
- count unknown statuses
- total pricing compare

## 10. Что нельзя делать

Неправильные варианты:

- копировать legacy schema в новую 1 в 1
- делать import напрямую в боевые таблицы без staging
- смешивать pricing catalog и booking extras
- переносить legacy statuses как есть
- держать в новой броне прямую legacy-логику `booking -> exact car`
- тащить пустой `b_clients_payment` как будто это нормальный ledger

## 11. Правильный минимальный roadmap

### Шаг 1

Сделать staging tables и import-run log.

### Шаг 2

Сделать mapping spec:

- statuses
- marketplaces
- services
- extras
- locations
- car -> model -> group

### Шаг 3

Сделать importer для:

- customers
- vehicles
- bookings
- booking extras

### Шаг 4

Сделать reconciliation report.

### Шаг 5

Сделать manual review queue для спорных cases.

## 12. Что я считаю правильным следующим практическим шагом

Не писать сразу импорт всей базы.

Сначала:

1. зафиксировать exact mapping spec
2. создать staging migrations
3. написать importer только для:
   - `b_clients`
   - `b_cars`
   - `b_clients_booking`
   - `b_clients_price`
4. прогнать dry-run на 50 бронированиях
5. посмотреть расхождения

Это и будет правильный путь без костылей.
