NurCore API
API Reference

Quote API (Pricing Pipeline)

Единый источник цены для бронирования, обмена и возврата с Decimal-точностью.

Quote API — единая точка расчёта цены для всех потоков (booking creation, exchange, refund). Решает 3 проблемы legacy-расчёта:

  1. Floating-point ошибки — все суммы в Decimal с округлением HALF_UP (ADR 0001)
  2. Currency drift — penalty в exchange всегда в original_currency (Bug #4)
  3. Non-determinism — quote замораживается в Redis на 300s, гарантированная цена

Base URL: https://api.nurcore.kg/api/v1/fares

Endpoints

EndpointНазначениеAuth
POST /quoteСоздать quote для booking creationOptional*
GET /quote/{quote_id}Получить snapshot quote (Redis)Optional
POST /quote/exchangeQuote для обмена билета (M6.A)Optional*
POST /quote/refundQuote для возврата (M6.C)None
GET /audit-logИстория всех изменений ценыStaff JWT

* Auth опционален, но partner_id в request игнорируется без auth (ADR 0003 — anti-spoofing).


POST /quote

Базовый расчёт цены для одного направления / даты / pax mix.

curl -X POST \
  -H "X-API-Key: $PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "route_id": "uuid",
    "departure_date": "2026-05-15",
    "pax": [
      {"type": "adult", "count": 2},
      {"type": "child", "count": 1}
    ],
    "fare_family_id": "uuid-classic",
    "channel": "web",
    "ancillaries": [
      {"code": "BG23", "qty": 2}
    ],
    "promo_code": "SUMMER10",
    "target_currency": "KGS"
  }' \
  "https://api.nurcore.kg/api/v1/fares/quote"

Request body

ПолеТипRequiredОписание
route_idUUIDone_of*ID маршрута
origin + destinationstring(3) + string(3)one_of*IATA коды (альтернатива route_id)
departure_dateISO dateДата вылета
return_dateISO dateДля round-trip
paxarray(1-9)Список pax по типам
fare_classstringRBD (Y/B/M/H/...) для preferred bucket
fare_family_idUUIDСемейство тарифа (LIGHT/CLASSIC/FLEX)
cabinenumeconomy / business / first
channelenumweb / mobile / partner / kiosk (default: web)
partner_idUUID(только с auth) применит partner-discount
flight_idUUIDQuote для конкретного рейса (вместо schedule-based)
promo_codestringПромо-код
ancillariesarraySSR с qty ([{code: "BG23", qty: 2}])
target_currencystring(3)Конвертация в указанную валюту

*one_of(route_id, [origin+destination]) — должно быть указано одно.

Response 200

{
  "quote_id": "abc123XyZ...",
  "valid_until": "2026-05-12T10:30:00Z",
  "currency": "KGS",
  "channel": "web",
  "partner_id": null,
  "gross": "26500.00",
  "channel_discount": "0.00",
  "commission_amount": "0.00",
  "net": "26500.00",
  "round_trip": false,
  "segment_prices": null,
  "line_items": [
    {
      "type": "base_fare",
      "description": "Adult × 2",
      "amount": "7600.00",
      "currency": "KGS"
    },
    {
      "type": "base_fare",
      "description": "Child × 1",
      "amount": "2660.00",
      "currency": "KGS"
    },
    {
      "type": "promo",
      "description": "SUMMER10 — 10% off",
      "amount": "-1000.00",
      "currency": "KGS",
      "rate": "10%"
    },
    {
      "type": "ancillary",
      "description": "Багаж 23 кг x2",
      "amount": "3000.00",
      "currency": "KGS"
    }
  ],
  "applied_rules": [
    {
      "rule_type": "load_factor",
      "name": "Load factor 78% → +5%",
      "factor": "1.05"
    },
    {
      "rule_type": "promo",
      "name": "SUMMER10",
      "delta": "-1000.00"
    }
  ],
  "fare_class": "M",
  "fare_family_id": "uuid",
  "flight_id": "uuid",
  "pax_count": 3
}

Семантика полей

  • gross — цена до channel discount (публичная цена)
  • channel_discount — отрицательное число (0 для web/mobile, отрицательное для partner)
  • commission_amountabs(channel_discount) для отчётности
  • net — что фактически платит клиент
  • valid_until — TTL Redis, после этой даты quote_id invalid
  • round_triptrue если net уже включает оба плеча (см. ниже)
  • segment_prices["<outbound>", "<return>"] per-leg base subtotal (Decimal-as-string), только при round-trip; иначе null

Per-pax-type pricing

Каждый pax.type тарифицируется по своему FareRuleadult и child имеют разные цены (включая discount_percentage на FarePrice). Отдельный line_item на каждый тип/плечо: "Adult × 2", "Child × 1".

  • infant = 0 (политика: lap infant без места; отдельного infant FareRule в системе нет). Всегда "Infant × N (free, lap infant)" → 0.00.
  • Если для типа нет своего FareRule — fallback на adult-тариф.
  • Fixed per-pax сборы (сервисный сбор) считаются за платный pax за плечо — infant исключён, round-trip → ×2.

Правила тарифа: fare_rules (refund/exchange policy)

В ответе QuoteResponse.fare_rules — список per leg (outbound + return при round-trip). Каждая запись описывает, можно ли вернуть и обменять билет, сколько это будет стоить и за какое время до вылета меняется штраф.

{
  "fare_rules": [
    {
      "leg": "outbound",
      "fare_rule_id": "…",
      "fare_family_id": "…",
      "refundable": true,
      "changeable": true,
      "cancellation_fee": "500.00",
      "change_fee": "500.00",
      "currency": "KGS",
      "advance_purchase_days": null,
      "exchange_threshold_hours": 24.0,
      "exchange_penalty_multiplier": 2.0,
      "notes": [
        "Обмен разрешён: штраф 500.00 KGS (до вылета > 24ч). Если < 24ч до вылета — штраф 1000.00 KGS.",
        "Возврат разрешён: штраф 500.00 KGS. Сумма возврата = total − штраф. Возможен до момента вылета."
      ]
    },
    {
      "leg": "return",
      "refundable": false,
      "changeable": false,
      "notes": [
        "Обмен не разрешён правилами тарифа.",
        "Возврат не разрешён правилами тарифа."
      ]
    }
  ]
}

Семантика полей:

ПолеЗначение
refundableТариф разрешает возврат (если false/quote/refund вернёт refund_amount=0)
changeableТариф разрешает обмен (если false/quote/exchange → 422)
cancellation_feeШтраф за возврат per pax, фиксированный из FareRule
change_feeШтраф за обмен per pax, до threshold
exchange_threshold_hoursПорог (часы до вылета), глобальная настройка fare.exchange_threshold_hours
exchange_penalty_multiplierКоэффициент при < threshold (default 2.0) — change_fee × 2
advance_purchase_daysЕсли задано — тариф недоступен ближе к вылету
notesГотовые русские строки для UI — не нужно генерировать самим

Правило обмена:

если время_до_вылета >= exchange_threshold_hours:  штраф = change_fee
иначе:                                              штраф = change_fee × multiplier

Правило возврата (на момент вылета):

если refundable:  refund_amount = total − cancellation_fee (минимум 0)
иначе:            refund_amount = 0, penalty = total (full forfeit)

Round-trip caveat: у направлений «туда» и «обратно» может быть разная политика (разные FareRule). UI должен показать обе записи из fare_rules[], не агрегировать.

Round-trip (return_date)

При указании return_date вместе с origin+destination engine резолвит обратный маршрут (swap O/D), тарифицирует обратное плечо per-pax-type, и net = полная стоимость поездки (оба плеча). round_trip=true, segment_prices=["<out>","<ret>"].

⚠️ Round-trip требует origin+destination. С route_id без origin/destination обратный маршрут не резолвится → quote one-way (round_trip=false); booking-service добьёт return-плечо отдельно.

Пример round-trip line_items (1 adult + 1 child + 1 infant):

Туда:    Adult × 1   3800.00
Туда:    Child × 1   2660.00
Туда:    Infant × 1  0.00  (free, lap infant)
Обратно: Adult × 1   3800.00
Обратно: Child × 1   2660.00
Обратно: Infant × 1  0.00  (free, lap infant)
Сервисный сбор       400.00   (100 × 2 paid pax × 2 legs)
net = 13320.00 ; segment_prices = ["6460.00","6460.00"]

Pricing pipeline order

base × pax → dynamic → ancillaries → taxes → channel discount → promo → net

См. ADR 0001 для всех правил.


GET /quote/{quote_id}

Получить snapshot quote по ID (для booking creation flow).

curl -H "X-API-Key: $PUBLISHABLE_KEY" \
     "https://api.nurcore.kg/api/v1/fares/quote/abc123XyZ..."

Response: идентичен POST /quote (тот же QuoteResponse).

Errors:

CodeСценарий
410Quote expired (valid_until < now) или не существует — нужен новый POST

POST /quote/exchange (M6.A)

Расчёт обмена через unified pipeline с Decimal-precision.

curl -X POST \
  -H "X-API-Key: $SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "original_booking_id": "uuid",
    "original_total_amount": "5000.00",
    "original_currency": "KGS",
    "original_fare_rule_id": "uuid",
    "original_scheduled_departure": "2026-05-20T07:30:00Z",
    "new_quote": {
      "route_id": "uuid",
      "departure_date": "2026-05-25",
      "pax": [{"type": "adult", "count": 1}],
      "fare_family_id": "uuid-classic"
    }
  }' \
  "https://api.nurcore.kg/api/v1/fares/quote/exchange"

Request body

new_quote — полностью идентичен QuoteRequest для нового рейса. Все остальные поля описывают исходную бронь.

Response 200

Extends QuoteResponse для new flight плюс exchange fields:

{
  "quote_id": "exchange_abc...",
  "valid_until": "2026-05-12T10:35:00Z",
  "currency": "KGS",
  "gross": "6500.00",
  "net": "6500.00",

  "original_total": "5000.00",
  "fare_difference": "1500.00",
  "penalty": "500.00",
  "refund_amount": "0.00",
  "extra_to_charge": "2000.00",
  "changeable": true
}

Логика exchange:

Сценарийextra_to_chargerefund_amount
new > old, fare_diff + penaltymax(0, diff + penalty)0
new < old, refundable=true0|diff| - penalty (max 0)
new < old, refundable=false00 (no refund)
changeable=false422 в response

Penalty rules (per FareRule):

  • change_fee — стандартный штраф
  • doubled если до original_scheduled_departure < 24 часов

POST /quote/refund (M6.C)

Расчёт возврата (full cancel брони).

curl -X POST \
  -H "X-API-Key: $SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "original_booking_id": "uuid",
    "original_total_amount": "5000.00",
    "original_currency": "KGS",
    "original_fare_rule_id": "uuid",
    "original_scheduled_departure": "2026-05-20T07:30:00Z"
  }' \
  "https://api.nurcore.kg/api/v1/fares/quote/refund"

Response 200

{
  "original_total": "5000.00",
  "refund_amount": "4500.00",
  "penalty": "500.00",
  "refundable": true,
  "currency": "KGS",
  "fare_rule_id": "uuid",
  "notes": "10% cancellation fee per Classic rules"
}

Логика:

FareRule.refundablewithin-24hResult
truefalserefund = total - cancellation_fee
truetruerefund = total - cancellation_fee × 2
falseanyrefund = 0, penalty = total (full forfeit)

Tax / Fees shape (v2, IATA-compliant)

С 2026-06-01 line_items[type=tax] содержит дополнительные поля в metadata для BSP/NDC compatibility:

{
  "type": "tax",
  "description": "VAT — НДС",
  "amount": 912.24,
  "currency": "KGS",
  "rate": "12%",
  "metadata": {
    "code": "VAT",
    "tax_type": "percentage",
    "configured_amount": 12,
    "applies_to": "per_itinerary",
    "refundable": true,
    "jurisdiction": "KG"
  }
}
ПолеЗначениеНазначение
codeIATA tax code (YQ/YR/XF/AY/UB) или custom (VAT/AIRPORT_KG/BOOK_FEE)BSP reporting / interlining
applies_toper_pax / per_segment / per_itineraryMultiplier convention
refundabletrue (VAT/YQ) / false (некоторые airport fees, AY US security)Refund flow knows что возвращать
jurisdictionKG/carrier/US/...Tax reporting per country

Real-world KG breakdown (BSZ→OSS round-trip, 2 ADT):

ComponentCodeTypeapplies_toRefundableCalculation
VAT 12%VATpercentageper_itinerary7602 × 12% = 912.24
Топливный сборYQfixedper_pax500 × 2 × 2 legs = 2000
Аэропортовый сборAIRPORT_KGfixedper_segment200 × 2 legs = 400
Сервисный сборBOOK_FEEfixedper_itinerary100 × 1 = 100

applies_to semantics:

  • per_pax — fixed × paid_pax × num_legs (стандарт для security fees)
  • per_segment — fixed × num_legs (airport fee, не зависит от pax)
  • per_itinerary — fixed × 1 (booking fee, single charge)
  • percentage type → всегда rate × base (base уже включает all-pax all-legs)

Backward compat: legacy v1 shape [{name, amount, type}] (без applies_to) продолжает работать — calculator определяет mode automatically.


Per-customer promo limit

С 2026-06-01 промо-коды могут иметь max_uses_per_customer (fare.promo_codes column). Если задан и customer_email передан в quote — проверяется по нормализованному email (lowercase + trim):

{
  "applied_rules": [
    {
      "rule_type": "promo_rejected",
      "name": "Promo SUMMER25 not applied",
      "description": "Reason: per_customer_limit_reached"
    }
  ]
}

Передавайте customer_email в quote для preview-check:

curl -X POST -H "X-API-Key: $KEY" -d '{
  "origin": "BSZ", "destination": "OSS",
  "departure_date": "2026-08-16",
  "pax": [{"type": "ADT", "count": 1}],
  "promo_code": "SUMMER25",
  "customer_email": "user@example.com"
}' "https://api.nurcore.kg/api/v1/fares/quote"

Реальное списание usage происходит на стороне booking-service после POST /bookings/{id}/confirm — quote-time check только preview.


PADIS passenger codes (IATA standard)

QuoteRequest.pax[].type принимает PADIS-стандарт codes (helpful для NDC / GDS интеграций):

PADISNurCore internalОписание
ADTadult12+
CHD / CHLDchild2-11
INF / INFTinfant0-2, lap (free)
INS / INFSinfant_with_seat0-2, with paid seat
YTH / STUyouth12-25 age-based discount
SRC / SRsenior65+ age-based discount
MIL / ITXadultMapped to adult (no separate fare)
{
  "pax": [
    {"type": "ADT", "count": 2},
    {"type": "INF", "count": 1}
  ]
}

И paxType / passenger_type / quantity aliases работают (B2B compat).


Использование в booking flow

Quote API — рекомендуемый путь для создания брони. Альтернативный legacy-путь (без quote_id) сохранён для backward-compat, но имеет floating-point ошибки.

// 1. Получить quote
const quoteResp = await fetch("/api/v1/fares/quote", {
  method: "POST",
  body: JSON.stringify({route_id, departure_date, pax, fare_family_id})
});
const quote = await quoteResp.json();

// 2. Показать total user'у, дать подтвердить (≤300 секунд!)
showPrice(quote.net, quote.currency);

// 3. Создать бронь с quote_id
const bookingResp = await fetch("/api/v1/bookings/", {
  method: "POST",
  body: JSON.stringify({
    flight_id: quote.flight_id,
    fare_price_id: quote.fare_resolution.fare_id,
    quote_id: quote.quote_id,  // ← цена берётся из quote, не пересчитывается
    passengers,
    contact_email,
    contact_name
  })
});

Channel discount (B2B)

Если auth + partner_id указан в request — pipeline применит channel discount из partnerships (например 10%). Партнёр видит net-цену (уже со скидкой), а commission_amount показывает его выгоду.

{
  "gross": "10000.00",
  "channel_discount": "-1000.00",
  "commission_amount": "1000.00",
  "net": "9000.00"
}

См. ADR 0002 — Channel Discount.


Dynamic pricing (M3)

Pipeline применяет dynamic multipliers если DYNAMIC_PRICING_ENABLED=true в deployment env:

  • Load factor — заполнение >70% → +5%, >85% → +10%
  • DOW (day of week) — пн-чт vs выходные
  • Season — high/low season multipliers
  • Booking curve — DTD (days to departure)

Применённые multipliers видны в applied_rules[] для transparency.


Audit log

Все изменения цены (booking total_amount, applied promo, exchange penalty) автоматически логируются в fare.price_audit_log через SQLAlchemy event listeners.

curl -H "Authorization: Bearer $JWT" \
     "https://api.nurcore.kg/api/v1/fares/audit-log?\
booking_id=$BOOKING_ID&from=2026-05-01"

См. Audit Log endpoint для всех фильтров.


Errors

CodeСценарий
400No matching fare для route+date / FareRule не найден / Validation
410quote_id expired (>300s)
422Invalid request body (Pydantic validation)
503Inventory service unavailable

Связанные документы

On this page