# План миграции legacy -> `hub.bent.ge`

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

## Цель

Перенести данные из старой системы в новую без копирования legacy-структуры 1 в 1.

Принцип:

- в новую БД переносим не старые таблицы, а бизнес-смысл;
- старые ID, мусорные поля, временные флаги и хаотичные связи не становятся новой моделью;
- все сомнительные вещи идут в staging и manual review, а не ломают новую схему.

## Что переносим

### Переносим автоматически

- клиентов;
- бронирования;
- локации;
- машины;
- модели машин;
- provider/owner данные;
- документы бронирований и клиентов, если они доступны;
- комментарии / заметки / история действий, если она реально есть;
- extras внутри бронирований;
- назначения реальной машины на бронь, если это восстанавливается из legacy.

### Переносим частично / через нормализацию

- статусы бронирований;
- payment statuses;
- extras catalog;
- pricing snapshot внутри старых бронирований;
- promo / discount данные;
- источники бронирований и каналы.

### Не переносим как есть

- старую структуру pricing;
- старые технические конфиги;
- старые UI-данные;
- мусорные и дублирующиеся справочники;
- legacy permissions / pages / ad-hoc flags;
- временные поля, которые не имеют бизнес-смысла в новой модели.

## Базовый подход

Миграция должна идти в 3 слоя:

1. `extract`
   выгружаем данные из старой БД в read-only staging

2. `normalize`
   приводим данные к новой доменной модели и словарям

3. `load`
   загружаем уже нормализованные сущности в `hub`

Нельзя делать прямой `INSERT INTO new_table SELECT ... FROM old_table`.

## Временный staging слой

Я бы добавил отдельный staging-модуль, а не писал миграцию сразу в боевые таблицы.

### Нужны staging-таблицы

- `legacy_import_runs`
- `legacy_customers_raw`
- `legacy_bookings_raw`
- `legacy_vehicles_raw`
- `legacy_locations_raw`
- `legacy_extras_raw`
- `legacy_documents_raw`
- `legacy_notes_raw`
- `legacy_mapping_overrides`
- `legacy_import_errors`

### Нужны mapping-таблицы

- `legacy_customer_map`
- `legacy_booking_map`
- `legacy_vehicle_map`
- `legacy_location_map`
- `legacy_extra_map`
- `legacy_status_map`

Смысл простой:

- raw хранит старые данные почти как есть;
- map хранит соответствие `legacy_id -> new_id`;
- overrides позволяет руками починить спорные случаи без переписывания кода.

## Как переносим сущности

### 1. Locations

Сначала собираем единый справочник новых locations.

Нормализуем:

- аэропорты;
- офисы;
- delivery points;
- city-center точки;
- дубли с разными названиями.

На выходе одна новая таблица `locations` и mapping старых location-полей к новым ID.

### 2. Providers / Owners

Если в legacy есть owner/provider/source машин:

- собираем в новый `providers`;
- чистим дубли названий;
- приводим типы к контролируемому списку.

### 3. Vehicle models / vehicle groups / vehicles

Это важнейший слой.

#### Что делаем

- модели собираем в `vehicle_models`;
- продаваемые сущности собираем в `vehicle_groups`;
- реальные машины идут в `vehicles`.

#### Ключевое правило

Если в legacy бронь была "на модель", это не значит, что в новой системе бронирование должно указывать на конкретную машину.

Новая логика:

- бронь резервирует `vehicle_group`;
- реальная машина назначается отдельно.

#### Что может быть сложно

- одна и та же модель в legacy может быть записана по-разному;
- часть машин может не иметь нормального model/provider linkage;
- по некоторым броням может быть непонятно, какая именно машина была фактически выдана.

Для этого нужен слой ручного маппинга.

### 4. Customers

Клиентов нельзя просто копировать строка в строку.

#### Нормализация

- email -> lower case;
- phone -> единый формат;
- full name -> first / last / full;
- nationality / locale -> коды;
- license/passport -> перенос только если поле реально читаемое.

#### Дедупликация

Нужны правила:

- exact email match;
- exact normalized phone match;
- soft match name + phone;
- soft match name + email.

Дубли нельзя автоматически склеивать без confidence-score.

Лучше так:

- high confidence -> auto merge;
- medium confidence -> review queue;
- low confidence -> отдельный клиент.

### 5. Bookings

Это главный объект миграции.

#### Переносим в новую бронь

- reference;
- source/channel;
- customer;
- vehicle group;
- pickup/dropoff locations;
- pickup/dropoff dates;
- comment;
- flight;
- currency;
- totals;
- deposit;
- prepayment;
- payment status;
- status;
- created/updated timestamps.

#### Что не делаем

Не тащим legacy booking schema целиком.

#### Что делаем обязательно

Для каждой старой брони сохраняем:

- `legacy_booking_id`;
- raw payload;
- исходный status;
- исходный source;
- price snapshot.

### 6. Booking statuses

Старые статусы не должны переехать бездумно.

Нужен controlled map, например:

- `new / created / waiting` -> `pending`
- `confirmed / approved` -> `upcoming`
- `active / handed_over / in_rent` -> `active`
- `done / closed / finished` -> `completed`
- `cancelled / rejected` -> `cancelled`

Если есть статусы, которые не ложатся в новую модель:

- не создаём сразу новые системные статусы;
- сначала складываем их в mapping review.

### 7. Extras

Вот здесь как раз нельзя копировать "как в старой".

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

Сначала создаём новый нормальный каталог `extras`:

- `code`
- `name`
- `pricing_type`
- `base price`
- `sort order`
- `is_active`

Примеры:

- `additional_driver`
- `child_seat`
- `full_insurance`
- `wifi`
- `delivery_fee`
- `airport_service`

#### Потом маппим legacy extras

Например:

- `baby chair`
- `childseat`
- `seat for kid`

все идут в один новый `extra_code`.

#### Важно

В новой системе extras должны быть:

- чистым каталогом;
- с нормальными code;
- с понятным pricing_type.

А в бронях legacy мы переносим уже:

- canonical extra id;
- имя на момент брони;
- quantity;
- unit price;
- total price.

Если старый extra не удаётся уверенно распознать:

- не создаём мусорный новый extra автоматически;
- кладём в review queue.

### 8. Price snapshot

Старую pricing engine переносить в лоб не надо.

Новая pricing-модель уже лучше и чище.

Из legacy нам нужно перенести только:

- финальные суммы по старым броням;
- доступные breakdown lines;
- скидки;
- extras totals;
- fees totals;
- deposit / prepayment.

То есть:

- для истории броней сохраняем snapshot;
- live rate plans в новой системе строим отдельно и руками/через новый UI.

### 9. Documents

Если файлы физически доступны:

- переносим метаданные;
- переносим storage path;
- привязываем к `booking_documents` / будущим customer-doc tables.

Если файлов уже нет, а в БД есть только запись:

- переносим как metadata-only документ с понятным статусом.

### 10. Activities / notes / comments

Если в legacy есть история:

- переносим в `booking_activities`;
- типизируем: `status`, `payment`, `assignment`, `note`, `document`, `system`.

Если история грязная, лучше перенести как текстовые activity, чем потерять совсем.

## Что нельзя мигрировать автоматически в v1

Я бы не тащил автоматически:

- старые тарифные таблицы;
- старые promo logic;
- старые permission matrices;
- старые UI configurations;
- старые deleted/trash entities без явной бизнес-ценности.

Это лучше пересобрать в новой системе заново.

## Технический план работ

### Этап 1. Freeze и аудит

- фиксируем snapshot старой БД;
- описываем legacy tables и связи;
- составляем data inventory;
- определяем обязательные и спорные поля.

### Этап 2. Mapping spec

- status mapping;
- source mapping;
- location mapping;
- provider mapping;
- model/group mapping;
- extra mapping;
- payment status mapping.

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

### Этап 3. Staging importer

- read-only импорт из legacy в `legacy_*_raw`;
- лог import run;
- log ошибок;
- контроль количества записей.

### Этап 4. Normalizers

- customer normalizer;
- vehicle normalizer;
- booking normalizer;
- extras normalizer;
- documents normalizer.

### Этап 5. Dry run

Сначала не вся база.

Порядок:

1. 20 клиентов
2. 20 машин
3. 50 бронирований
4. 50 extras
5. 20 документов

Смотрим:

- дубли;
- кривые статусы;
- missing links;
- unknown extras;
- суммы и расхождения.

### Этап 6. Full import

Порядок загрузки:

1. locations
2. providers
3. models
4. groups
5. vehicles
6. customers
7. extras catalog
8. bookings
9. booking assignments
10. booking extras
11. documents
12. activities

### Этап 7. Reconciliation

Нужны автоматические проверки:

- count bookings old vs new;
- count customers old vs new;
- count vehicles old vs new;
- total amount compare;
- unassigned booking count;
- unknown status count;
- unknown extra count;
- missing customer link count;
- missing vehicle/group link count.

### Этап 8. Manual review queue

Отдельно разбираем:

- дубли клиентов;
- unknown extras;
- спорные locations;
- бронь без понятной машины;
- бронь без понятного vehicle group;
- документы без файла.

## Что я предлагаю делать практически

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

1. Не мигрировать legacy напрямую в боевые таблицы.
2. Сначала сделать staging-слой и mapping-таблицы.
3. Сначала нормализовать `locations`, `providers`, `groups`, `extras`.
4. Потом переносить `customers` и `bookings`.
5. Live pricing в новой системе не импортировать как legacy-мусор, а собирать заново.
6. В старых бронированиях сохранять только snapshot цен и фактические totals.

## Что я могу сделать следующим шагом

Я могу сделать это по-настоящему, а не словами:

1. написать `legacy-audit.md` по старой БД;
2. сделать `mapping-spec.md` для статусов, extras, locations и sources;
3. создать staging migration tables в Laravel;
4. написать первый importer command для клиентов и бронирований;
5. сделать dry-run импорт на тестовом срезе.
