Структура ответа#

Детальное описание структуры ответа от /api/avia/ticket_issue.

Основная структура#

Ответ содержит список выписанных билетов.

ПолеТипОписание
ticketsarrayМассив выписанных билетов
ticketingFeeTicketingFeeСбор за оформление билета, загруженный из бронирования

Структура Ticket#

ПолеТипОписание
idstringИдентификатор билета
numberstringНомер билета
vendorstringКод BSP/ARC
passengerobjectИнформация о пассажире

Структура Passenger#

ПолеТипОписание
idstringИдентификатор пассажира из бронирования
firstNamestringИмя пассажира
lastNamestringФамилия пассажира
middleNamestringОтчество пассажира
typePassengerTypeТип пассажира (adt, chd, inf)
titlestringОбращение (MR, MRS, MS и т.д.)

Структура TicketingFee#

Сбор за оформление билета, рассчитанный пресетом Revenue Manager (загружается из данных бронирования).

ПолеТипОписание
amountMonetaryAmountОбщая сумма сбора (baseFee × множитель allocation)
allocationstringРежим распределения сбора: perTicket | perSegment
breakdownPerPtcmap<string, MonetaryAmount>Разбивка сбора по типам пассажиров (только при allocation = perTicket)
presetInfoPresetInfoПресет, по которому рассчитан сбор

Структура PresetInfo#

ПолеТипОписание
presetIdguidИдентификатор пресета
presetNamestringНазвание пресета

Примеры ответов#

Успешная выписка для одного пассажира#

 1{
 2  "tickets": [
 3    {
 4      "number": "2469982473",
 5      "vendor": "555",
 6      "passenger": {
 7        "id": "PAX_12",
 8        "firstName": "IVAN",
 9        "lastName": "EGOROV",
10        "type": "adt"
11      }
12    }
13  ]
14}

Выписка для нескольких пассажиров#

 1{
 2  "tickets": [
 3    {
 4      "number": "2469982473",
 5      "vendor": "555",
 6      "passenger": {
 7        "id": "PAX_1",
 8        "firstName": "IVAN",
 9        "lastName": "EGOROV",
10        "type": "adt"
11      }
12    },
13    {
14      "number": "2469982474",
15      "vendor": "555",
16      "passenger": {
17        "id": "PAX_2",
18        "firstName": "MARIA",
19        "lastName": "EGOROVA",
20        "type": "adt"
21      }
22    },
23    {
24      "number": "2469982475",
25      "vendor": "555",
26      "passenger": {
27        "id": "PAX_3",
28        "firstName": "PETR",
29        "lastName": "EGOROV",
30        "type": "chd"
31      }
32    }
33  ]
34}

Ответ с ошибкой - бронирование не найдено#

1{
2  "error": {
3    "code": "BOOKING_NOT_FOUND",
4    "message": "Бронирование не найдено",
5    "description": "Бронирование с указанным ID не существует",
6    "errorId": "018c5f2e-9b3a-7f4d-8e2c-1a2b3c4d5e6f"
7  }
8}

Ответ с ошибкой — билеты уже выписаны#

1{
2  "error": {
3    "code": "ALREADY_TICKETED",
4    "message": "PNR is already ticketed",
5    "description": null,
6    "errorId": "018c5f2e-9b3a-7f4d-8e2c-1a2b3c4d5e6f"
7  }
8}

Ответ с ошибкой — отказ поставщика#

Причины отказа перевозчика (изменилась цена, истёк тайм-лимит выписки, не хватает средств) отдельных кодов не имеют — они приходят под SUPPLIER_ERROR текстом поставщика в message:

1{
2  "error": {
3    "code": "SUPPLIER_ERROR",
4    "message": "TKT TIME LIMIT EXPIRED",
5    "description": null,
6    "errorId": "018c5f2e-9b3a-7f4d-8e2c-1a2b3c4d5e6f"
7  }
8}

Использование данных#

Получение номеров билетов#

 1var response = await IssueTicket(bookingId, "invoice");
 2
 3Console.WriteLine("Билеты успешно выписаны:");
 4
 5foreach (var ticket in response.Tickets)
 6{
 7    Console.WriteLine($"Билет: {ticket.Number}");
 8    Console.WriteLine($"Пассажир: {ticket.Passenger.LastName} {ticket.Passenger.FirstName}");
 9    Console.WriteLine($"Тип: {ticket.Passenger.Type}");
10    Console.WriteLine($"Vendor: {ticket.Vendor}");
11    Console.WriteLine();
12}

Сохранение билетов в базу данных#

 1public async Task SaveTicketsToDatabase(string bookingId, TicketIssueResponse response)
 2{
 3    foreach (var ticket in response.Tickets)
 4    {
 5        await _database.Tickets.InsertAsync(new TicketEntity
 6        {
 7            BookingId = bookingId,
 8            TicketNumber = ticket.Number,
 9            Vendor = ticket.Vendor,
10            PassengerId = ticket.Passenger.Id,
11            PassengerFirstName = ticket.Passenger.FirstName,
12            PassengerLastName = ticket.Passenger.LastName,
13            PassengerType = ticket.Passenger.Type,
14            IssuedAt = DateTime.UtcNow
15        });
16    }
17
18    await _database.SaveChangesAsync();
19
20    LogInfo($"Saved {response.Tickets.Count} tickets for booking {bookingId}");
21}

Отправка билетов пассажирам#

 1public async Task SendTicketsToPassengers(
 2    string bookingId,
 3    TicketIssueResponse ticketResponse)
 4{
 5    // Получаем полную информацию о бронировании
 6    var booking = await RetrieveBooking(bookingId);
 7
 8    // Группируем билеты по пассажирам
 9    var ticketsByPassenger = ticketResponse.Tickets
10        .GroupBy(t => t.Passenger.Id)
11        .ToList();
12
13    foreach (var group in ticketsByPassenger)
14    {
15        var passengerId = group.Key;
16        var passengerTickets = group.ToList();
17
18        // Находим контакты пассажира
19        var email = booking.Pnr.Contacts
20            .FirstOrDefault(c => c.PassengerId == passengerId && c.Type == "email")?.Value;
21
22        if (!string.IsNullOrEmpty(email))
23        {
24            await SendTicketEmail(email, booking, passengerTickets);
25            LogInfo($"Tickets sent to {email}");
26        }
27        else
28        {
29            LogWarning($"No email for passenger {passengerId}");
30        }
31    }
32}

Формирование списка для отображения#

 1public class TicketDisplayModel
 2{
 3    public string TicketNumber { get; set; }
 4    public string PassengerName { get; set; }
 5    public string PassengerType { get; set; }
 6    public string Route { get; set; }
 7    public string DepartureDate { get; set; }
 8}
 9
10public async Task<List<TicketDisplayModel>> GetTicketDisplayModels(
11    string bookingId,
12    TicketIssueResponse ticketResponse)
13{
14    var booking = await RetrieveBooking(bookingId);
15    var result = new List<TicketDisplayModel>();
16
17    foreach (var ticket in ticketResponse.Tickets)
18    {
19        var route = GetRouteDescription(booking.Pnr.Itinerary);
20        var departureDate = booking.Pnr.Itinerary[0].Segments[0].Origin.DateTime;
21
22        result.Add(new TicketDisplayModel
23        {
24            TicketNumber = ticket.Number,
25            PassengerName = $"{ticket.Passenger.LastName} {ticket.Passenger.FirstName}",
26            PassengerType = GetPassengerTypeDescription(ticket.Passenger.Type),
27            Route = route,
28            DepartureDate = departureDate
29        });
30    }
31
32    return result;
33}
34
35private string GetPassengerTypeDescription(PassengerType type)
36{
37    return type switch
38    {
39        PassengerType.ADT => "Взрослый",
40        PassengerType.CHD => "Ребенок",
41        PassengerType.INF => "Младенец",
42        _ => type.ToString()
43    };
44}

Проверка выписки#

 1public async Task<bool> VerifyTicketIssue(
 2    string bookingId,
 3    TicketIssueResponse ticketResponse)
 4{
 5    // Получаем обновленное бронирование
 6    var booking = await RetrieveBooking(bookingId);
 7
 8    // Проверяем количество билетов
 9    var expectedTicketCount = booking.Pnr.Passengers.Count;
10    var actualTicketCount = ticketResponse.Tickets.Count;
11
12    if (expectedTicketCount != actualTicketCount)
13    {
14        LogError($"Ticket count mismatch: expected {expectedTicketCount}, got {actualTicketCount}");
15        return false;
16    }
17
18    // Проверяем, что билеты появились в бронировании
19    if (booking.Pnr.Tickets.Count == 0)
20    {
21        LogError("Tickets not found in booking after issue");
22        return false;
23    }
24
25    // Проверяем соответствие номеров
26    var responseNumbers = ticketResponse.Tickets
27        .Select(t => t.Number)
28        .OrderBy(n => n)
29        .ToList();
30
31    var bookingNumbers = booking.Pnr.Tickets
32        .Select(t => t.Number)
33        .OrderBy(n => n)
34        .ToList();
35
36    if (!responseNumbers.SequenceEqual(bookingNumbers))
37    {
38        LogError("Ticket numbers mismatch between response and booking");
39        return false;
40    }
41
42    LogInfo($"Ticket issue verified for booking {bookingId}");
43    return true;
44}

Генерация отчета о выписке#

 1public class TicketIssueReport
 2{
 3    public string BookingId { get; set; }
 4    public string PnrLocator { get; set; }
 5    public DateTime IssuedAt { get; set; }
 6    public List<TicketInfo> Tickets { get; set; }
 7    public decimal TotalAmount { get; set; }
 8    public string PaymentType { get; set; }
 9}
10
11public async Task<TicketIssueReport> GenerateTicketIssueReport(
12    string bookingId,
13    TicketIssueResponse ticketResponse,
14    string paymentType)
15{
16    var booking = await RetrieveBooking(bookingId);
17
18    var report = new TicketIssueReport
19    {
20        BookingId = bookingId,
21        PnrLocator = booking.Pnr.PnrLocator,
22        IssuedAt = DateTime.UtcNow,
23        PaymentType = paymentType,
24        Tickets = new List<TicketInfo>()
25    };
26
27    foreach (var ticket in ticketResponse.Tickets)
28    {
29        var fare = booking.Pnr.FareInfo.Fares
30            .FirstOrDefault(f => f.PaxType == ticket.Passenger.Type);
31
32        report.Tickets.Add(new TicketInfo
33        {
34            Number = ticket.Number,
35            Vendor = ticket.Vendor,
36            PassengerName = $"{ticket.Passenger.LastName} {ticket.Passenger.FirstName}",
37            PassengerType = ticket.Passenger.Type,
38            Amount = fare?.Price.Total.Value ?? 0
39        });
40
41        report.TotalAmount += fare?.Price.Total.Value ?? 0;
42    }
43
44    return report;
45}

Типичные ошибки#

Код ошибкиПричинаРешение
BOOKING_NOT_FOUNDБронирование не найдено или не принадлежит клиентуПроверьте bookingId
ALREADY_TICKETEDПо этому PNR билеты уже выписаныПроверьте статус через ticket_info
TICKETING_PRESET_ERRORОфис из пресета выписки не найден, неактивен, или для него не удалось определить поставщикаПроверьте офис в пресете выписки в личном кабинете
VALIDATION_ERRORОшибка валидации запроса (HTTP 400)Проверьте обязательные поля и форматы значений
NO_TARGETSПод запрос не нашлось ни одного поставщика: ни один не настроен либо все отсеяны на этапе подготовки запросаПроверьте настройки офисов и пресетов в личном кабинете; при сохранении ошибки обратитесь в поддержку
SUPPLIER_ERRORПоставщик вернул ошибку. Причина — текстом поставщика в поле messageРазберите message; при необходимости повторите запрос позже
SYSTEM_ERRORВнутренняя ошибка Travel APIПовторите запрос позже; при повторении обратитесь в поддержку с errorId

Ошибки, пришедшие от GDS/NDC-поставщика, возвращаются под кодом SUPPLIER_ERROR: отдельного кода на каждую причину отказа Travel API не формирует, причина передаётся текстом поставщика в поле message. Не завязывайте логику на разбор message — его формат зависит от поставщика и не фиксирован.


Обработка ответа#

Успешная выписка#

 1var response = await httpClient.PostAsJsonAsync(
 2    "https://test.travelplatform.ru/api/avia/ticket_issue",
 3    new { bookingId, payment = new { type = "invoice" } }
 4);
 5
 6if (response.IsSuccessStatusCode)
 7{
 8    var result = await response.Content
 9        .ReadFromJsonAsync<TicketIssueResponse>();
10
11    Console.WriteLine($"Выписано билетов: {result.Tickets.Count}");
12
13    // Сохраняем билеты
14    await SaveTickets(bookingId, result.Tickets);
15
16    // Отправляем пассажирам
17    await SendTicketsToPassengers(bookingId, result);
18
19    // Обновляем статус
20    await UpdateBookingStatus(bookingId, "ticketed");
21}

Обработка ошибок с повторной попыткой#

 1public async Task<TicketIssueResponse> IssueTicketWithRetry(
 2    string bookingId,
 3    string paymentType,
 4    int maxRetries = 3)
 5{
 6    for (int attempt = 1; attempt <= maxRetries; attempt++)
 7    {
 8        try
 9        {
10            var response = await IssueTicket(bookingId, paymentType);
11            return response;
12        }
13        catch (ApiException ex) when (ex.Code == "SUPPLIER_ERROR")
14        {
15            // Отказ перевозчика: причина — только текстом в ex.Message.
16            // Отдельных кодов на смену цены или истёкший тайм-лимит нет,
17            // поэтому повторяем ограниченное число раз, переспросив цену.
18            if (attempt == maxRetries)
19            {
20                LogError($"Supplier rejected ticketing: {ex.Message}");
21                throw;
22            }
23
24            LogInfo($"Supplier rejected ticketing on attempt {attempt}: {ex.Message}");
25
26            var repricing = await RepriceBooking(bookingId);
27            var confirmed = await ConfirmNewPrice(repricing);
28
29            if (!confirmed)
30            {
31                throw new OperationCanceledException("Price change not confirmed");
32            }
33        }
34    }
35
36    throw new Exception("Failed to issue ticket after retries");
37}

Связанные операции#