Exchange (Обмен билета)
4-шаговый flow для обмена билета на другой рейс (даты → рейсы → preview → execute).
Обмен билета на другой рейс (того же направления). Состоит из 4 шагов с Price Lock в Redis для защиты от race conditions.
Когда обмен невозможен
| Условие | Код ошибки |
|---|---|
| Бронь в статусе CANCELLED / COMPLETED / EXPIRED | 400 |
| Хотя бы один пассажир уже прошёл регистрацию (купон в COMPLETED / CLOSED) | 400 |
| Не та авиакомпания (нет прав доступа к брони) | 403 |
| Бронь не найдена | 404 |
| Price Lock истёк (>15 мин с момента preview) | 410 |
Flow
1. GET /bookings/{id}/exchange/dates → массив доступных дат
2. GET /bookings/{id}/exchange/flights → варианты рейсов на дату
3. POST /bookings/{id}/exchange/preview → расчёт цены + Price Lock (Redis 15м)
4. POST /bookings/{id}/exchange/execute → выполнить обмен1. GET /bookings/{booking_id}/exchange/dates
Календарь доступных дат для обмена (то же направление).
curl -H "X-API-Key: $SECRET_KEY" \
"https://api.nurcore.kg/api/v1/bookings/$BOOKING_ID/exchange/dates?\
date_from=2026-05-15&date_to=2026-08-15"Query (optional):
| Param | Default |
|---|---|
date_from | сегодня |
date_to | сегодня + 90 дней |
Response 200:
{
"booking_id": "uuid",
"dates": ["2026-05-15", "2026-05-17", "2026-05-20"]
}2. GET /bookings/{booking_id}/exchange/flights
Варианты рейсов на выбранную дату.
curl -H "X-API-Key: $SECRET_KEY" \
"https://api.nurcore.kg/api/v1/bookings/$BOOKING_ID/exchange/flights?date=2026-05-17"Query:
| Param | Required |
|---|---|
date | ✓ — ISO date |
Response 200:
{
"booking_id": "uuid",
"date": "2026-05-17",
"legs": [
{
"flight_id": "uuid",
"flight_number": "ZM-202",
"origin_iata": "FRU",
"destination_iata": "OSS",
"departure_time": "2026-05-17T07:30:00Z",
"arrival_time": "2026-05-17T08:55:00Z",
"available_seats": 67,
"aircraft_type": "DHC-8-402"
}
]
}legs приходит из Schedule Service. Если пусто — на этой дате рейсов
нет, попросите выбрать другую дату.
3. POST /bookings/{booking_id}/exchange/preview
Рассчитывает разницу в цене + штраф + Price Lock.
curl -X POST \
-H "X-API-Key: $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"new_flight_id": "'$NEW_FLIGHT_ID'"
}' \
"https://api.nurcore.kg/api/v1/bookings/$BOOKING_ID/exchange/preview"Request body:
| Поле | Тип | Required | Описание |
|---|---|---|---|
new_flight_id | UUID | ✓ | ID нового рейса (из /exchange/flights) |
exchange_quote_id | string | — | (M6.B opt-in) Quote ID из POST /fares/quote/exchange для Decimal-precise расчёта |
Response 200:
{
"original_total": 5000.00,
"new_total": 6500.00,
"fare_difference": 1000.00,
"penalty": 500.00,
"tax_difference": 0.00,
"extra_amount": 1500.00,
"refund_amount": 0.00,
"currency": "KGS",
"changeable": true,
"lock_expires_in": 900,
"message": null
}Семантика полей:
| Поле | Знак | Описание |
|---|---|---|
original_total | ≥0 | Сумма, уплаченная за исходный билет |
new_total | ≥0 | Полная стоимость нового билета |
fare_difference | new − old | Разница в тарифе (может быть отрицательная) |
penalty | ≥0 | Штраф за обмен (зависит от Fare Rules) |
extra_amount | ≥0 | К доплате (если new + penalty > old) |
refund_amount | ≥0 | К возврату (если old > new + penalty) |
lock_expires_in | сек | До истечения Price Lock (default 900 = 15 минут) |
Правило взаимоисключения: extra_amount и refund_amount не могут
быть оба > 0 — либо доплата, либо возврат.
Pricing modes
NurCore поддерживает 2 режима расчёта exchange:
Legacy (default): старая формула, штраф = change_fee × 2.0 если
< 24 часов до вылета. Может страдать от floating-point округления.
M6.B Unified (recommended): передайте exchange_quote_id из
POST /api/v1/fares/quote/exchange → расчёт через единый pricing
pipeline с Decimal-точностью.
# Шаг 3a — создать exchange quote (валюта стабильна, Decimal)
curl -X POST \
-H "X-API-Key: $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"booking_id":"'$BOOKING_ID'","new_flight_id":"'$NEW_FLIGHT_ID'"}' \
"https://api.nurcore.kg/api/v1/fares/quote/exchange"Ответ содержит quote_id — передайте его в preview и execute.
4. POST /bookings/{booking_id}/exchange/execute
Выполнить обмен — использует Price Lock из шага 3.
curl -X POST \
-H "X-API-Key: $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"new_flight_id": "'$NEW_FLIGHT_ID'",
"exchange_quote_id": "'$EXCHANGE_QUOTE_ID'"
}' \
"https://api.nurcore.kg/api/v1/bookings/$BOOKING_ID/exchange/execute"Что происходит:
- Acquire row-level lock на бронь (
SELECT ... FOR UPDATE) - Re-check купонов через Check-in Service
- Использование Price Lock из Redis (если ещё жив)
- Создание новой брони на новый рейс (те же пассажиры, контакт, тарифы)
- Отмена старой брони (status=CANCELLED)
- Возврат данных новой брони + старый booking_id
Response 200:
{
"original_booking_id": "uuid-old",
"original_booking_reference": "ABC123",
"new_booking_id": "uuid-new",
"new_booking_reference": "XYZ789",
"new_flight_id": "uuid",
"extra_amount": 1500.00,
"refund_amount": 0,
"currency": "KGS",
"payment_required": true,
"payment_url": "https://api.nurcore.kg/api/v1/bookings/uuid-new/initiate-payment"
}Сценарии после execute:
| Сценарий | Действие mobile/UI |
|---|---|
extra_amount > 0 + payment_required: true | Редирект на initiate-payment для новой брони |
extra_amount > 0 (B2B partner с кошельком) | POST /bookings/$NEW_ID/pay-with-balance |
refund_amount > 0 | Refund инициируется на исходный платёж автоматически |
extra_amount = refund_amount = 0 | Новая бронь сразу в CONFIRMED |
Price Lock semantics
Между preview и execute цена гарантирована на 15 минут. Если
пользователь нажал «Подтвердить» через 16 минут:
executeвернёт 410 Gone- В UI отобразите «Цена изменилась, обновите расчёт» → редирект на
preview
Если за окно Lock'а появились другие изменения (новые promo, изменения RBD), они не применяются — пользователь платит зафиксированную цену.
Concurrent exchanges
Обмен использует SELECT ... FOR UPDATE — если два параллельных
запроса на ту же бронь → второй ждёт первого. После первого commit
второй увидит status=CANCELLED и вернёт 400.
В UI ограничьте обмен одной активной сессией пользователя.
Ошибки
| Code | Сценарий |
|---|---|
| 400 | Бронь в финальном статусе, или купон в COMPLETED/CLOSED |
| 403 | Нет прав на эту бронь |
| 404 | Бронь / рейс не найдены |
| 410 | Price Lock истёк → нужен новый preview |
| 502 | Schedule / Fare / Checkin сервис недоступен — retry с backoff |
Связанные endpoints
- Quote exchange (M6.B) —
POST /fares/quote/exchangeдля Decimal-precise расчёта - Booking lifecycle — статусы CONFIRMED / CANCELLED
- Payments —
initiate-paymentпосле execute