# Legacy -> Hub mapping spec

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

## Источник

- source connection: `legacy_mysql`
- source database: `ontripge_midge`

Основные legacy-таблицы:

- `b_users`
- `b_clients`
- `b_clients_booking`
- `b_clients_price`
- `b_clients_media`
- `b_cars`
- `b_cars_model`
- `b_cars_gear`
- `b_cars_type`
- `b_cars_insurance`
- `b_locations`
- `b_extras`
- `b_marketplaces`
- `b_status`
- `b_clients_services`
- `b_portals_agent`

## Конвертация пользователей

`b_users` -> `users`

- `name` -> `users.name`
- `email` -> `users.email`
- `password` -> `users.password`
- `role` -> `users.role`
- `created` -> `users.created_at`
- `updated` -> `users.updated_at`

Правило:

- legacy password уже в `bcrypt`, поэтому переносится как есть
- если email дублируется, приоритет у уже существующего `users.email`
- связь хранится в `legacy_entity_links`

## Конвертация клиентов

`b_clients` -> `customers`

- `first_name` -> `customers.first_name`
- `last_name` -> `customers.last_name`
- `first_name + last_name` -> `customers.full_name`
- `email` -> `customers.email`
- `phone_code` + `phone` -> `customers.phone_code` / `customers.phone`
- `phone_code2` + `phone2` -> `customers.secondary_phone_code` / `customers.secondary_phone`
- `birthday` -> `customers.birth_date`
- `country_residence` -> `customers.country_residence`
- `city` -> `customers.city`
- `locale` -> `customers.locale`
- `personal_id` -> `customers.personal_id`
- `passport_id` -> `customers.passport_number`
- `issued` -> `customers.passport_issued_at`
- `passport_valid` -> `customers.passport_expires_at`
- `driving_license_id` -> `customers.driver_license_number`
- `place_birth` -> `customers.place_of_birth`
- `signature` -> `customers.signature`

Правила:

- email lowercased
- `unknown@email` и аналогичные placeholder-значения не используются для merge
- merge только при высоком совпадении:
  - exact same normalized email
  - или exact same normalized phone + exact same normalized full name
- любые legacy ids всегда сохраняются в `legacy_entity_links`

## Конвертация локаций

`b_locations` -> `locations`

- `title` -> `locations.name`
- `code` -> `locations.code`
- `type` -> `locations.type`
- `address` -> `locations.address`
- `active` -> `locations.is_active`

Derive rules:

- `type = airport` -> `is_airport = true`
- если `code` пустой, код генерируется из title

## Конвертация providers

`b_portals_agent` -> `providers`

- `title` -> `providers.name`
- `code` -> `providers.code`
- `phone` -> `providers.contact_phone`
- `email` -> `providers.contact_email`
- `director` -> `providers.contact_name`

Derive rules:

- provider type:
  - `internal`, если code/title указывают на Bent
  - иначе `partner`

## Конвертация машин и групп

`b_cars_model` -> `vehicle_models`
`b_cars` -> `vehicles`

### vehicle_models

- `title` делится на `make` + `name`
- `seats` -> `vehicle_models.seats`
- `doors` -> `vehicle_models.doors`
- `year` в model не хранится, год берётся на уровне конкретной машины

### vehicle_groups

Legacy не хранит sellable group отдельно.

Правильное правило миграции:

- создаём `vehicle_group` на каждый legacy model
- `vehicle_group.name = b_cars_model.title`
- `vehicle_group.slug` генерируется из title
- `vehicle_group.category` derive из:
  - `tags`
  - `b_cars_type.title`
  - `premium`
- `vehicle_group.transmission`, `fuel_type`, `is_premium` derive по фактическим `b_cars` этой модели

Это сохраняет новую правильную модель:

- booking -> `vehicle_group`
- exact legacy car -> `booking_vehicle_assignments`

### vehicles

`b_cars` -> `vehicles`

- `provider` -> `provider_id`
- `model` -> `vehicle_model_id`
- derived group by model -> `vehicle_group_id`
- `number` -> `plate`
- `vehicle_id` -> `vin`, если значение похоже на VIN
- fallback VIN -> deterministic `LEGACY-{car_id}`
- `title` -> `internal_code` fallback source
- `year` -> `year`
- `fuel` -> `powertrain`
- `gear` -> `transmission`
- `color` -> `color`
- insurance title/id -> `insurance_policy_number` snapshot string
- `issued` -> `purchase_date`

## Конвертация marketplace / source

`b_marketplaces`

- `direct` -> `direct`
- `rentalcars` -> `rentalcars`
- `localrent` -> `localrent`

## Конвертация service

`b_clients_services`

- `car_rental` -> `rental`
- `transfer` -> `transfer`

## Конвертация booking status

`b_status`

Legacy codes:

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

Hub mapping:

- cancelled:
  - `canceled_client`
  - `canceled_us`
- pending:
  - `pending`
- completed:
  - non-cancelled booking with `dropoff_at < now`
- active:
  - non-cancelled booking with `pickup_at <= now < dropoff_at`
- upcoming:
  - all other confirmed bookings

Payment status:

- `confirmed_with_payment` or `prepaid = Y` -> `paid`
- all other legacy bookings -> `unpaid`

## Конвертация booking totals

`b_clients_booking` -> `bookings`

- `ref` -> `bookings.reference`
- `external_id` -> `bookings.external_reference`
- marketplace mapping -> `bookings.source`
- service mapping -> `bookings.service_type`
- `client` -> `bookings.customer_id`
- `operator` -> `bookings.created_by_user_id`
- group derived from legacy car -> `bookings.vehicle_group_id`
- `location_start` -> `pickup_location_id`
- `location_end` -> `dropoff_location_id`
- `start` -> `pickup_at`
- `end` -> `dropoff_at`
- `flight` -> `flight_number`
- `comment` -> `comment`
- `rental_sale_price` fallback `rental_price` -> `base_total`
- `location_price` -> `fees_total`
- `extra_price` -> `extras_total`
- `total_sale_price` fallback `total_price` -> `final_total`
- `rental_deposit` -> `deposit_amount`
- `prepayment` -> `prepayment_amount` as derived amount from percent
- `created` -> `created_at`

`price_snapshot` сохраняет:

- все legacy pricing fields
- legacy status code/title
- legacy marketplace id/code
- legacy service id/code
- legacy operator id
- legacy car id
- legacy signature presence

## Конвертация extras

### extra catalog

`b_extras` -> `extras`

- `title` -> `extras.name`
- `code` -> `extras.code`
- pricing type derive:
  - if usage value = `day` -> `per_day`
  - if usage value = `included` and price = 0 -> `included`
  - otherwise -> `per_rental`
- catalog price default `0`, потому что legacy хранит реальные цены на booking row, а не в master catalog

### booking extras

`b_clients_price` -> `booking_extras`

- `booking` -> `booking_id`
- `extra` -> `extra_id`
- legacy title -> `name`
- derived pricing type -> `pricing_type`
- `price` -> `unit_price`
- `price` -> `total_price`
- quantity default `1`
- overloaded legacy `value` -> `booking_extras.meta.legacy_value`

Особый случай:

- `Additional Driver` / `driver`
  - numeric `value` часто legacy client id
  - это не quantity
  - сохраняется только в `meta.legacy_value`

## Конвертация документов клиента

`b_clients_media` -> `customer_documents`

- `client` -> `customer_id`
- `title` -> `name`
- `file` -> `file_path`
- extension(file) -> `file_type`
- status default `valid`

Важно:

- на первом шаге переносим ссылку на файл
- физический copy файлов в новое storage делается отдельным этапом

## Что не переносим как отдельный источник истины

- `b_clients_payment`
  - таблица пустая
  - не используется как нормальный ledger

- `b_clients_manage`
  - не считать основным источником

## Staging / audit tables

Используем:

- `legacy_import_runs`
- `legacy_raw_records`
- `legacy_entity_links`
- `legacy_value_mappings`
- `legacy_import_errors`

Это даёт:

- повторяемый dry-run
- ручные overrides без переписывания кода
- reconcile между legacy counts и hub counts
