Структура ответа#
Детальное описание структуры ответа от /api/avia/ticket_issue.
Основная структура#
Ответ содержит список выписанных билетов.
| Поле | Тип | Описание |
|---|
tickets | array | Массив выписанных билетов |
ticketingFee | TicketingFee | Сбор за оформление билета, загруженный из бронирования |
Структура Ticket#
| Поле | Тип | Описание |
|---|
id | string | Идентификатор билета |
number | string | Номер билета |
vendor | string | Код BSP/ARC |
passenger | object | Информация о пассажире |
Структура Passenger#
| Поле | Тип | Описание |
|---|
id | string | Идентификатор пассажира из бронирования |
firstName | string | Имя пассажира |
lastName | string | Фамилия пассажира |
middleName | string | Отчество пассажира |
type | PassengerType | Тип пассажира (adt, chd, inf) |
title | string | Обращение (MR, MRS, MS и т.д.) |
Структура TicketingFee#
Сбор за оформление билета, рассчитанный пресетом Revenue Manager (загружается из данных бронирования).
| Поле | Тип | Описание |
|---|
amount | MonetaryAmount | Общая сумма сбора (baseFee × множитель allocation) |
allocation | string | Режим распределения сбора: perTicket | perSegment |
breakdownPerPtc | map<string, MonetaryAmount> | Разбивка сбора по типам пассажиров (только при allocation = perTicket) |
presetInfo | PresetInfo | Пресет, по которому рассчитан сбор |
Структура PresetInfo#
| Поле | Тип | Описание |
|---|
presetId | guid | Идентификатор пресета |
presetName | string | Название пресета |
Примеры ответов#
Успешная выписка для одного пассажира#
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}
Связанные операции#