From c55dea54d6fd221881c30a6619ebeb140efc572b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 09:35:51 +0000
Subject: [PATCH 1/6] Initial plan
From 1811bebf62ba5aa5618c6cf7bb026ee98fd69014 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 09:45:54 +0000
Subject: [PATCH 2/6] Add parsing of FOP for tickets (Invoice, Exchange, Credit
card, Mixed)
Co-authored-by: Smotrov <7815789+Smotrov@users.noreply.github.com>
---
src/Services/Air/AirParser.js | 20 ++++++-
test/Air/AirParser.test.js | 57 +++++++++++++++++++
test/FakeResponses/Air/getTicket_FOP_CC.xml | 45 +++++++++++++++
.../Air/getTicket_FOP_EXCHANGE.xml | 45 +++++++++++++++
.../Air/getTicket_FOP_INVOICE.xml | 45 +++++++++++++++
.../Air/getTicket_FOP_INVOICE_WITH_NUMBER.xml | 45 +++++++++++++++
.../FakeResponses/Air/getTicket_FOP_MIXED.xml | 46 +++++++++++++++
7 files changed, 300 insertions(+), 3 deletions(-)
create mode 100644 test/FakeResponses/Air/getTicket_FOP_CC.xml
create mode 100644 test/FakeResponses/Air/getTicket_FOP_EXCHANGE.xml
create mode 100644 test/FakeResponses/Air/getTicket_FOP_INVOICE.xml
create mode 100644 test/FakeResponses/Air/getTicket_FOP_INVOICE_WITH_NUMBER.xml
create mode 100644 test/FakeResponses/Air/getTicket_FOP_MIXED.xml
diff --git a/src/Services/Air/AirParser.js b/src/Services/Air/AirParser.js
index 983d3381..aea68dc2 100644
--- a/src/Services/Air/AirParser.js
+++ b/src/Services/Air/AirParser.js
@@ -577,9 +577,23 @@ function getTicketFromEtr(etr, obj, allowNoProviderLocatorCodeRetrieval = false)
? etr[`common_${this.uapi_version}:CreditCardAuth`]
: [];
const formOfPayment = fopData.map((fop) => {
- return fop.Type === 'Credit'
- ? utils.getCreditCardData(fop[`common_${this.uapi_version}:CreditCard`], ccAuthData)
- : fop.Type.toUpperCase();
+ if (fop.Type === 'Credit') {
+ return utils.getCreditCardData(fop[`common_${this.uapi_version}:CreditCard`], ccAuthData);
+ }
+ if (fop.Type === 'MiscFormOfPayment') {
+ const miscFop = fop[`common_${this.uapi_version}:MiscFormOfPayment`];
+ if (miscFop) {
+ const { Category: category, Text: text } = miscFop;
+ if (category === 'Invoice') {
+ return text ? `INVOICE:${text}` : 'INVOICE';
+ }
+ if (category === 'Exchange') {
+ return text ? `EXCHANGE:${text}` : 'EXCHANGE';
+ }
+ return text ? `${category.toUpperCase()}:${text}` : category.toUpperCase();
+ }
+ }
+ return fop.Type.toUpperCase();
});
const ticketsList = Object.values(etr['air:Ticket']);
const exchangedTickets = [];
diff --git a/test/Air/AirParser.test.js b/test/Air/AirParser.test.js
index e655db2c..8cece29f 100644
--- a/test/Air/AirParser.test.js
+++ b/test/Air/AirParser.test.js
@@ -855,6 +855,63 @@ describe('#AirParser', () => {
expect(result.tickets[0].coupons[0].fareBasisCode).to.equal('ACOORP1CH/FS14');
expect(result.tickets[0].coupons[1].fareBasisCode).to.equal('ACOORP1/FS10');
});
+
+ describe('FOP parsing', () => {
+ it('should parse invoice FOP without number', async () => {
+ const uParser = new Parser('air:AirRetrieveDocumentRsp', 'v52_0', {});
+ const parseFunction = airParser.AIR_GET_TICKET;
+ const xml = fs.readFileSync(`${xmlFolder}/getTicket_FOP_INVOICE.xml`).toString();
+
+ const json = await uParser.parse(xml);
+ const result = parseFunction.call(uParser, json);
+
+ expect(result.formOfPayment).to.deep.equal(['INVOICE']);
+ });
+
+ it('should parse invoice FOP with number', async () => {
+ const uParser = new Parser('air:AirRetrieveDocumentRsp', 'v52_0', {});
+ const parseFunction = airParser.AIR_GET_TICKET;
+ const xml = fs.readFileSync(`${xmlFolder}/getTicket_FOP_INVOICE_WITH_NUMBER.xml`).toString();
+
+ const json = await uParser.parse(xml);
+ const result = parseFunction.call(uParser, json);
+
+ expect(result.formOfPayment).to.deep.equal(['INVOICE:AGT']);
+ });
+
+ it('should parse credit card FOP', async () => {
+ const uParser = new Parser('air:AirRetrieveDocumentRsp', 'v52_0', {});
+ const parseFunction = airParser.AIR_GET_TICKET;
+ const xml = fs.readFileSync(`${xmlFolder}/getTicket_FOP_CC.xml`).toString();
+
+ const json = await uParser.parse(xml);
+ const result = parseFunction.call(uParser, json);
+
+ expect(result.formOfPayment).to.deep.equal(['VI4111111111111111']);
+ });
+
+ it('should parse exchange ticket FOP', async () => {
+ const uParser = new Parser('air:AirRetrieveDocumentRsp', 'v52_0', {});
+ const parseFunction = airParser.AIR_GET_TICKET;
+ const xml = fs.readFileSync(`${xmlFolder}/getTicket_FOP_EXCHANGE.xml`).toString();
+
+ const json = await uParser.parse(xml);
+ const result = parseFunction.call(uParser, json);
+
+ expect(result.formOfPayment).to.deep.equal(['EXCHANGE:0649902789371']);
+ });
+
+ it('should parse mixed FOP (cash + invoice with number)', async () => {
+ const uParser = new Parser('air:AirRetrieveDocumentRsp', 'v52_0', {});
+ const parseFunction = airParser.AIR_GET_TICKET;
+ const xml = fs.readFileSync(`${xmlFolder}/getTicket_FOP_MIXED.xml`).toString();
+
+ const json = await uParser.parse(xml);
+ const result = parseFunction.call(uParser, json);
+
+ expect(result.formOfPayment).to.deep.equal(['CASH', 'INVOICE:AGT']);
+ });
+ });
});
describe('AIR_LOW_FARE_SEARCH()', () => {
it('should test parsing of low fare search request', () => {
diff --git a/test/FakeResponses/Air/getTicket_FOP_CC.xml b/test/FakeResponses/Air/getTicket_FOP_CC.xml
new file mode 100644
index 00000000..47e926f5
--- /dev/null
+++ b/test/FakeResponses/Air/getTicket_FOP_CC.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ 0LDQXJ
+
+
+
+
+
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00 NUC501.00END ROE1.0 XT 401YK6ND418XW7BE598YQ
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00Y1SAV0 NUC501.00END ROE1.0
+
+
+
+
+
+
+
+
+
+
diff --git a/test/FakeResponses/Air/getTicket_FOP_EXCHANGE.xml b/test/FakeResponses/Air/getTicket_FOP_EXCHANGE.xml
new file mode 100644
index 00000000..9fe69b84
--- /dev/null
+++ b/test/FakeResponses/Air/getTicket_FOP_EXCHANGE.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ 0LDQXJ
+
+
+
+
+
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00 NUC501.00END ROE1.0 XT 401YK6ND418XW7BE598YQ
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00Y1SAV0 NUC501.00END ROE1.0
+
+
+
+
+
+
+
+
+
+
diff --git a/test/FakeResponses/Air/getTicket_FOP_INVOICE.xml b/test/FakeResponses/Air/getTicket_FOP_INVOICE.xml
new file mode 100644
index 00000000..37c6b9ec
--- /dev/null
+++ b/test/FakeResponses/Air/getTicket_FOP_INVOICE.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ 0LDQXJ
+
+
+
+
+
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00 NUC501.00END ROE1.0 XT 401YK6ND418XW7BE598YQ
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00Y1SAV0 NUC501.00END ROE1.0
+
+
+
+
+
+
+
+
+
+
diff --git a/test/FakeResponses/Air/getTicket_FOP_INVOICE_WITH_NUMBER.xml b/test/FakeResponses/Air/getTicket_FOP_INVOICE_WITH_NUMBER.xml
new file mode 100644
index 00000000..2d7d22d6
--- /dev/null
+++ b/test/FakeResponses/Air/getTicket_FOP_INVOICE_WITH_NUMBER.xml
@@ -0,0 +1,45 @@
+
+
+
+
+ 0LDQXJ
+
+
+
+
+
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00 NUC501.00END ROE1.0 XT 401YK6ND418XW7BE598YQ
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00Y1SAV0 NUC501.00END ROE1.0
+
+
+
+
+
+
+
+
+
+
diff --git a/test/FakeResponses/Air/getTicket_FOP_MIXED.xml b/test/FakeResponses/Air/getTicket_FOP_MIXED.xml
new file mode 100644
index 00000000..2616288e
--- /dev/null
+++ b/test/FakeResponses/Air/getTicket_FOP_MIXED.xml
@@ -0,0 +1,46 @@
+
+
+
+
+ 0LDQXJ
+
+
+
+
+
+
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00 NUC501.00END ROE1.0 XT 401YK6ND418XW7BE598YQ
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+ IEV LO X/WAW LO BRU 501.00Y1SAV0 NUC501.00END ROE1.0
+
+
+
+
+
+
+
+
+
+
From c3ee70eb747e1633c2a46d5e5c876fee17bf7b05 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 10:29:34 +0000
Subject: [PATCH 3/6] Extract parseMiscFormOfPayment to reduce cognitive
complexity in getTicketFromEtr
Co-authored-by: Smotrov <7815789+Smotrov@users.noreply.github.com>
---
src/Services/Air/AirParser.js | 26 +++++++++++++++-----------
1 file changed, 15 insertions(+), 11 deletions(-)
diff --git a/src/Services/Air/AirParser.js b/src/Services/Air/AirParser.js
index aea68dc2..d13215eb 100644
--- a/src/Services/Air/AirParser.js
+++ b/src/Services/Air/AirParser.js
@@ -543,6 +543,20 @@ const AirErrorHandler = function (rsp) {
}
};
+function parseMiscFormOfPayment(miscFop) {
+ if (!miscFop) {
+ return 'MISCFORMOFPAYMENT';
+ }
+ const { Category: category, Text: text } = miscFop;
+ if (category === 'Invoice') {
+ return text ? `INVOICE:${text}` : 'INVOICE';
+ }
+ if (category === 'Exchange') {
+ return text ? `EXCHANGE:${text}` : 'EXCHANGE';
+ }
+ return text ? `${category.toUpperCase()}:${text}` : category.toUpperCase();
+}
+
function getTicketFromEtr(etr, obj, allowNoProviderLocatorCodeRetrieval = false) {
// Checking if pricing info exists
if (!allowNoProviderLocatorCodeRetrieval && !etr.ProviderLocatorCode) {
@@ -581,17 +595,7 @@ function getTicketFromEtr(etr, obj, allowNoProviderLocatorCodeRetrieval = false)
return utils.getCreditCardData(fop[`common_${this.uapi_version}:CreditCard`], ccAuthData);
}
if (fop.Type === 'MiscFormOfPayment') {
- const miscFop = fop[`common_${this.uapi_version}:MiscFormOfPayment`];
- if (miscFop) {
- const { Category: category, Text: text } = miscFop;
- if (category === 'Invoice') {
- return text ? `INVOICE:${text}` : 'INVOICE';
- }
- if (category === 'Exchange') {
- return text ? `EXCHANGE:${text}` : 'EXCHANGE';
- }
- return text ? `${category.toUpperCase()}:${text}` : category.toUpperCase();
- }
+ return parseMiscFormOfPayment(fop[`common_${this.uapi_version}:MiscFormOfPayment`]);
}
return fop.Type.toUpperCase();
});
From 934b662de1160505e5b21fddf67785617802a729 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 10:36:31 +0000
Subject: [PATCH 4/6] Fix all SonarCloud issues: optional chaining, cognitive
complexity, nesting depth
Co-authored-by: Smotrov <7815789+Smotrov@users.noreply.github.com>
---
.eslintrc | 3 +
src/Services/Air/AirParser.js | 8 +--
test/Air/AirParser.test.js | 116 +++++++++++++++++-----------------
3 files changed, 63 insertions(+), 64 deletions(-)
diff --git a/.eslintrc b/.eslintrc
index 0c09c898..5f4240e4 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,5 +1,8 @@
{
"extends": "airbnb-base",
+ "parserOptions": {
+ "ecmaVersion": 2020
+ },
"plugins": [
"import"
],
diff --git a/src/Services/Air/AirParser.js b/src/Services/Air/AirParser.js
index d13215eb..0afb3a0b 100644
--- a/src/Services/Air/AirParser.js
+++ b/src/Services/Air/AirParser.js
@@ -833,10 +833,7 @@ function airGetTickets(obj) {
}
function airCancelTicket(obj) {
- if (
- !obj['air:VoidResultInfo']
- || obj['air:VoidResultInfo'].ResultType !== 'Success'
- ) {
+ if (obj['air:VoidResultInfo']?.ResultType !== 'Success') {
throw new AirRuntimeError.TicketCancelResultUnknown(obj);
}
return true;
@@ -943,8 +940,7 @@ function extractBookings(obj) {
const providerInfoKey = providerInfo.Key;
const resRemarks = remarks[providerInfoKey] || [];
const splitBookings = (
- providerInfo['universal:ProviderReservationDetails']
- && providerInfo['universal:ProviderReservationDetails'].DivideDetails === 'true'
+ providerInfo['universal:ProviderReservationDetails']?.DivideDetails === 'true'
)
? resRemarks.reduce(
(acc, remark) => {
diff --git a/test/Air/AirParser.test.js b/test/Air/AirParser.test.js
index 8cece29f..1657865b 100644
--- a/test/Air/AirParser.test.js
+++ b/test/Air/AirParser.test.js
@@ -1272,6 +1272,63 @@ describe('#AirParser', () => {
});
});
+ function testPricingInfo(pricingInfo) {
+ expect(pricingInfo).to.include.all.keys([
+ 'fareCalculation',
+ 'farePricingMethod',
+ 'farePricingType',
+ 'baggage',
+ 'timeToReprice',
+ 'passengers',
+ 'uapi_pricing_info_ref',
+ 'totalPrice',
+ 'basePrice',
+ 'equivalentBasePrice',
+ 'taxes',
+ 'passengersCount',
+ 'taxesInfo',
+ ]);
+
+ // Passengers
+ pricingInfo.passengers.forEach(
+ (p) => {
+ expect(p).to.be.an('object');
+ expect(p).to.include.all.keys(['uapi_passenger_ref', 'isTicketed']);
+ expect(p.uapi_passenger_ref).to.be.a('string');
+ expect(p.isTicketed).to.be.a('boolean');
+ if (p.isTicketed) {
+ expect(p.ticketNumber).to.be.a('string').and.to.match(ticketRegExp);
+ }
+ }
+ );
+
+ expect(pricingInfo.fareCalculation).to.be.a('string').and.to.have.length.above(0);
+ expect(new Date(pricingInfo.timeToReprice)).to.be.an.instanceof(Date);
+
+ expect(pricingInfo.passengersCount).to.be.an('object');
+ Object.keys(pricingInfo.passengersCount).forEach(
+ (ptc) => expect(pricingInfo.passengersCount[ptc]).to.be.a('number')
+ );
+ expect(pricingInfo.taxesInfo).to.be.an('array');
+ pricingInfo.taxesInfo.forEach(
+ (tax) => {
+ expect(tax).to.be.an('object');
+ expect(tax.value).to.match(/^[A-Z]{3}(\d+\.)?\d+$/);
+ expect(tax.type).to.match(/^[A-Z]{2}$/);
+ }
+ );
+ // Checking baggage
+ expect(pricingInfo.baggage).to.be.an('array');
+ pricingInfo.baggage.forEach(
+ (baggage) => {
+ expect(baggage).to.be.an('object');
+ expect(baggage).to.have.all.keys(['units', 'amount']);
+ expect(baggage.units).to.be.a('string');
+ expect(baggage.amount).to.be.a('number');
+ }
+ );
+ }
+
function testBooking(jsonResult) {
expect(jsonResult).to.be.an('array');
jsonResult.forEach((result) => {
@@ -1353,64 +1410,7 @@ describe('#AirParser', () => {
(reference) => expect(reference).to.be.a('string')
);
- fareQuote.pricingInfos.forEach(
- (pricingInfo) => {
- expect(pricingInfo).to.include.all.keys([
- 'fareCalculation',
- 'farePricingMethod',
- 'farePricingType',
- 'baggage',
- 'timeToReprice',
- 'passengers',
- 'uapi_pricing_info_ref',
- 'totalPrice',
- 'basePrice',
- 'equivalentBasePrice',
- 'taxes',
- 'passengersCount',
- 'taxesInfo',
- ]);
-
- // Passengers
- pricingInfo.passengers.forEach(
- (p) => {
- expect(p).to.be.an('object');
- expect(p).to.include.all.keys(['uapi_passenger_ref', 'isTicketed']);
- expect(p.uapi_passenger_ref).to.be.a('string');
- expect(p.isTicketed).to.be.a('boolean');
- if (p.isTicketed) {
- expect(p.ticketNumber).to.be.a('string').and.to.match(ticketRegExp);
- }
- }
- );
-
- expect(pricingInfo.fareCalculation).to.be.a('string').and.to.have.length.above(0);
- expect(new Date(pricingInfo.timeToReprice)).to.be.an.instanceof(Date);
-
- expect(pricingInfo.passengersCount).to.be.an('object');
- Object.keys(pricingInfo.passengersCount).forEach(
- (ptc) => expect(pricingInfo.passengersCount[ptc]).to.be.a('number')
- );
- expect(pricingInfo.taxesInfo).to.be.an('array');
- pricingInfo.taxesInfo.forEach(
- (tax) => {
- expect(tax).to.be.an('object');
- expect(tax.value).to.match(/^[A-Z]{3}(\d+\.)?\d+$/);
- expect(tax.type).to.match(/^[A-Z]{2}$/);
- }
- );
- // Checking baggage
- expect(pricingInfo.baggage).to.be.an('array');
- pricingInfo.baggage.forEach(
- (baggage) => {
- expect(baggage).to.be.an('object');
- expect(baggage).to.have.all.keys(['units', 'amount']);
- expect(baggage.units).to.be.a('string');
- expect(baggage.amount).to.be.a('number');
- }
- );
- }
- );
+ fareQuote.pricingInfos.forEach(testPricingInfo);
});
// Checking segments format
expect(result.segments).to.be.an('array');
From dd6d5a701539b9ef37eb4b7910b1ad54e029a2d5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 10:51:00 +0000
Subject: [PATCH 5/6] Move testPricingInfo and testBooking to outer scope to
fix SonarCloud S7721
Co-authored-by: Smotrov <7815789+Smotrov@users.noreply.github.com>
---
test/Air/AirParser.test.js | 462 ++++++++++++++++++-------------------
1 file changed, 231 insertions(+), 231 deletions(-)
diff --git a/test/Air/AirParser.test.js b/test/Air/AirParser.test.js
index 1657865b..bf46e62e 100644
--- a/test/Air/AirParser.test.js
+++ b/test/Air/AirParser.test.js
@@ -177,6 +177,237 @@ function checkEMDCoupons(coupons) {
});
}
+function testPricingInfo(pricingInfo) {
+ expect(pricingInfo).to.include.all.keys([
+ 'fareCalculation',
+ 'farePricingMethod',
+ 'farePricingType',
+ 'baggage',
+ 'timeToReprice',
+ 'passengers',
+ 'uapi_pricing_info_ref',
+ 'totalPrice',
+ 'basePrice',
+ 'equivalentBasePrice',
+ 'taxes',
+ 'passengersCount',
+ 'taxesInfo',
+ ]);
+
+ // Passengers
+ pricingInfo.passengers.forEach(
+ (p) => {
+ expect(p).to.be.an('object');
+ expect(p).to.include.all.keys(['uapi_passenger_ref', 'isTicketed']);
+ expect(p.uapi_passenger_ref).to.be.a('string');
+ expect(p.isTicketed).to.be.a('boolean');
+ if (p.isTicketed) {
+ expect(p.ticketNumber).to.be.a('string').and.to.match(ticketRegExp);
+ }
+ }
+ );
+
+ expect(pricingInfo.fareCalculation).to.be.a('string').and.to.have.length.above(0);
+ expect(new Date(pricingInfo.timeToReprice)).to.be.an.instanceof(Date);
+
+ expect(pricingInfo.passengersCount).to.be.an('object');
+ Object.keys(pricingInfo.passengersCount).forEach(
+ (ptc) => expect(pricingInfo.passengersCount[ptc]).to.be.a('number')
+ );
+ expect(pricingInfo.taxesInfo).to.be.an('array');
+ pricingInfo.taxesInfo.forEach(
+ (tax) => {
+ expect(tax).to.be.an('object');
+ expect(tax.value).to.match(/^[A-Z]{3}(\d+\.)?\d+$/);
+ expect(tax.type).to.match(/^[A-Z]{2}$/);
+ }
+ );
+ // Checking baggage
+ expect(pricingInfo.baggage).to.be.an('array');
+ pricingInfo.baggage.forEach(
+ (baggage) => {
+ expect(baggage).to.be.an('object');
+ expect(baggage).to.have.all.keys(['units', 'amount']);
+ expect(baggage.units).to.be.a('string');
+ expect(baggage.amount).to.be.a('number');
+ }
+ );
+}
+
+function testBooking(jsonResult) {
+ expect(jsonResult).to.be.an('array');
+ jsonResult.forEach((result) => {
+ expect(result).to.be.an('object');
+ // Checking object keys
+ expect(result).to.include.all.keys([
+ 'version', 'uapi_ur_locator', 'uapi_reservation_locator',
+ 'airlineLocatorInfo', 'bookingPCC', 'passengers', 'pnr',
+ 'fareQuotes', 'segments', 'serviceSegments', 'hostCreatedAt',
+ 'createdAt', 'modifiedAt', 'type', 'tickets', 'emails',
+ ]);
+ expect(result.version).to.be.at.least(0);
+ expect(result.uapi_ur_locator).to.match(/^[A-Z0-9]{6}$/);
+ expect(result.uapi_reservation_locator).to.match(/^[A-Z0-9]{6}$/);
+ expect(result.airlineLocatorInfo).to.be.an('array');
+ expect(result.emails).to.be.an('array');
+ result.airlineLocatorInfo.forEach((info) => {
+ expect(info).have.all.keys([
+ 'createDate',
+ 'supplierCode',
+ 'locatorCode',
+ ]);
+ expect(new Date(info.createDate)).to.be.instanceof(Date);
+ expect(info.supplierCode).to.match(/^[A-Z0-9]{2}$/);
+ expect(info.locatorCode).to.match(/^[A-Z0-9]{6}$/);
+ });
+ expect(result.bookingPCC).to.match(/^[A-Z0-9]{3,4}$/);
+ expect(result.pnr).to.match(/^[A-Z0-9]{6}$/);
+ expect(new Date(result.hostCreatedAt)).to.be.an.instanceof(Date);
+ expect(new Date(result.createdAt)).to.be.an.instanceof(Date);
+ expect(new Date(result.modifiedAt)).to.be.an.instanceof(Date);
+ expect(result.type).to.equal('uAPI');
+ // Checking passengers format
+ expect(result.passengers).to.be.an('array');
+ expect(result.passengers).to.have.length.above(0);
+ result.passengers.forEach((passenger) => {
+ expect(passenger).to.be.an('object');
+ expect(passenger).to.include.keys([
+ 'lastName', 'firstName', 'uapi_passenger_ref',
+ ]);
+ });
+ // Checking reservations format
+ expect(result.fareQuotes).to.be.an('array');
+ result.fareQuotes.forEach((fareQuote) => {
+ expect(fareQuote).to.be.an('object');
+ expect(fareQuote).to.include.all.keys([
+ 'index',
+ 'pricingInfos',
+ 'uapi_segment_refs',
+ 'uapi_passenger_refs',
+ 'endorsement',
+ 'effectiveDate',
+ ]);
+ expect(fareQuote.index).to.be.a('number');
+ expect(fareQuote.pricingInfos).to.be.an('array').and.to.have.length.above(0);
+
+ if (fareQuote.tourCode) {
+ expect(fareQuote.tourCode).to.match(/^[A-Z0-9]+/);
+ }
+
+ if (fareQuote.endorsement) {
+ expect(fareQuote.endorsement).to.match(/^[A-Z0-9.\-\s/]+$/);
+ }
+
+ if (fareQuote.platingCarrier) {
+ expect(fareQuote.platingCarrier).to.match(/^[A-Z0-9]{2}$/);
+ }
+
+ expect(fareQuote.uapi_passenger_refs).to.be.an('array');
+ expect(fareQuote.uapi_passenger_refs).to.have.length.above(0);
+ fareQuote.uapi_passenger_refs.forEach(
+ (reference) => expect(reference).to.be.a('string')
+ );
+
+ // Segment references
+ expect(fareQuote.uapi_segment_refs).to.be.an('array');
+ expect(fareQuote.uapi_segment_refs).to.have.length.above(0);
+ fareQuote.uapi_segment_refs.forEach(
+ (reference) => expect(reference).to.be.a('string')
+ );
+
+ fareQuote.pricingInfos.forEach(testPricingInfo);
+ });
+ // Checking segments format
+ expect(result.segments).to.be.an('array');
+ result.segments.forEach(
+ (segment) => {
+ expect(segment).to.be.an('object');
+ expect(segment).to.have.include.keys([
+ 'index', 'from', 'to', 'bookingClass', 'departure', 'arrival', 'airline',
+ 'flightNumber', 'serviceClass', 'status', 'plane', 'duration',
+ 'techStops', 'group', 'uapi_segment_ref', 'uapiSegmentReference',
+ ]);
+ expect(segment.index).to.be.a('number');
+ expect(segment.from).to.match(/^[A-Z]{3}$/);
+ expect(segment.to).to.match(/^[A-Z]{3}$/);
+ expect(segment.bookingClass).to.match(/^[A-Z]{1}$/);
+ expect(new Date(segment.departure)).to.be.an.instanceof(Date);
+ expect(new Date(segment.arrival)).to.be.an.instanceof(Date);
+ expect(segment.airline).to.match(/^[A-Z0-9]{2}$/);
+ expect(segment.flightNumber).to.match(/^\d+$/);
+ expect(segment.serviceClass).to.be.oneOf([
+ 'Economy', 'Business', 'First', 'PremiumEconomy',
+ ]);
+ expect(segment.status).to.match(/^[A-Z]{2}$/);
+ // Planes
+ if (segment.plane) {
+ expect(segment.plane).to.be.an('array');
+ segment.plane.forEach((plane) => expect(plane).to.be.a('string'));
+ }
+ // Duration
+ expect(segment.duration).to.be.an('array');
+ segment.duration.forEach((duration) => expect(duration).to.match(/^\d+$/));
+ // Tech stops
+ expect(segment.techStops).to.be.an('array');
+ segment.techStops.forEach((stop) => expect(stop).to.match(/^[A-Z]{3}$/));
+ // Segment reference
+ expect(segment.uapi_segment_ref).to.be.a('string');
+ // Next segment reference
+ if (segment.nextSegmentReference === null) {
+ expect(segment.nextSegmentReference).to.be.a('null');
+ }
+
+ if (segment.nextSegmentReference) {
+ expect(segment.nextSegmentReference).to.be.a('string');
+ }
+ }
+ );
+
+ if (result.serviceSegments) {
+ expect(result.serviceSegments).to.be.an('array');
+ const allSegments = [].concat(result.segments).concat(result.serviceSegments);
+ const maxIndex = allSegments.reduce((acc, x) => {
+ if (x.index > acc) {
+ return x.index;
+ }
+ return acc;
+ }, 0);
+ expect(maxIndex).to.be.equal(allSegments.length);
+ result.serviceSegments.forEach((segment) => {
+ expect(segment).to.include.all.keys([
+ 'index', 'carrier', 'airport', 'date', 'rfiCode',
+ 'rfiSubcode', 'feeDescription', 'name', 'amount', 'currency',
+ ]);
+ expect(segment.carrier).to.match(/^[A-Z0-9]{2}$/);
+ expect(segment.airport).to.match(/^[A-Z]{3}$/);
+ expect(new Date(segment.date)).to.be.instanceof(Date);
+ expect(segment.rfiCode).to.match(/^[A-Z]$/);
+ expect(segment.rfiSubcode).to.match(/^[0-9A-Z]{3}$/);
+ expect(segment.feeDescription).to.be.a('string');
+ expect(segment.name).to.match(/^[A-Z]+\/[A-Z]+$/);
+ expect(segment.amount).to.be.a('number');
+ expect(segment.currency).to.match(/^[A-Z]{3}$/);
+ });
+ }
+
+ // Checking tickets
+ expect(result.tickets).to.be.an('array');
+ if (result.tickets.length > 0) {
+ result.tickets.forEach(
+ (ticket) => {
+ expect(ticket).to.be.an('object').and.to.have.all.keys([
+ 'number', 'uapi_passenger_ref', 'uapi_pricing_info_ref', 'passengers',
+ ]);
+ expect(ticket.passengers.length).to.be.equal(1);
+ expect(ticket.passengers[0]).to.have.all.keys(['firstName', 'lastName']);
+ expect(ticket.number).to.match(/\d{13}/);
+ expect(ticket.uapi_passenger_ref).to.be.a('string');
+ }
+ );
+ }
+ });
+}
+
describe('#AirParser', () => {
describe('AIR_CANCEL_TICKET', () => {
it('should return error when no VoidResultInfo available', () => {
@@ -1272,237 +1503,6 @@ describe('#AirParser', () => {
});
});
- function testPricingInfo(pricingInfo) {
- expect(pricingInfo).to.include.all.keys([
- 'fareCalculation',
- 'farePricingMethod',
- 'farePricingType',
- 'baggage',
- 'timeToReprice',
- 'passengers',
- 'uapi_pricing_info_ref',
- 'totalPrice',
- 'basePrice',
- 'equivalentBasePrice',
- 'taxes',
- 'passengersCount',
- 'taxesInfo',
- ]);
-
- // Passengers
- pricingInfo.passengers.forEach(
- (p) => {
- expect(p).to.be.an('object');
- expect(p).to.include.all.keys(['uapi_passenger_ref', 'isTicketed']);
- expect(p.uapi_passenger_ref).to.be.a('string');
- expect(p.isTicketed).to.be.a('boolean');
- if (p.isTicketed) {
- expect(p.ticketNumber).to.be.a('string').and.to.match(ticketRegExp);
- }
- }
- );
-
- expect(pricingInfo.fareCalculation).to.be.a('string').and.to.have.length.above(0);
- expect(new Date(pricingInfo.timeToReprice)).to.be.an.instanceof(Date);
-
- expect(pricingInfo.passengersCount).to.be.an('object');
- Object.keys(pricingInfo.passengersCount).forEach(
- (ptc) => expect(pricingInfo.passengersCount[ptc]).to.be.a('number')
- );
- expect(pricingInfo.taxesInfo).to.be.an('array');
- pricingInfo.taxesInfo.forEach(
- (tax) => {
- expect(tax).to.be.an('object');
- expect(tax.value).to.match(/^[A-Z]{3}(\d+\.)?\d+$/);
- expect(tax.type).to.match(/^[A-Z]{2}$/);
- }
- );
- // Checking baggage
- expect(pricingInfo.baggage).to.be.an('array');
- pricingInfo.baggage.forEach(
- (baggage) => {
- expect(baggage).to.be.an('object');
- expect(baggage).to.have.all.keys(['units', 'amount']);
- expect(baggage.units).to.be.a('string');
- expect(baggage.amount).to.be.a('number');
- }
- );
- }
-
- function testBooking(jsonResult) {
- expect(jsonResult).to.be.an('array');
- jsonResult.forEach((result) => {
- expect(result).to.be.an('object');
- // Checking object keys
- expect(result).to.include.all.keys([
- 'version', 'uapi_ur_locator', 'uapi_reservation_locator',
- 'airlineLocatorInfo', 'bookingPCC', 'passengers', 'pnr',
- 'fareQuotes', 'segments', 'serviceSegments', 'hostCreatedAt',
- 'createdAt', 'modifiedAt', 'type', 'tickets', 'emails',
- ]);
- expect(result.version).to.be.at.least(0);
- expect(result.uapi_ur_locator).to.match(/^[A-Z0-9]{6}$/);
- expect(result.uapi_reservation_locator).to.match(/^[A-Z0-9]{6}$/);
- expect(result.airlineLocatorInfo).to.be.an('array');
- expect(result.emails).to.be.an('array');
- result.airlineLocatorInfo.forEach((info) => {
- expect(info).have.all.keys([
- 'createDate',
- 'supplierCode',
- 'locatorCode',
- ]);
- expect(new Date(info.createDate)).to.be.instanceof(Date);
- expect(info.supplierCode).to.match(/^[A-Z0-9]{2}$/);
- expect(info.locatorCode).to.match(/^[A-Z0-9]{6}$/);
- });
- expect(result.bookingPCC).to.match(/^[A-Z0-9]{3,4}$/);
- expect(result.pnr).to.match(/^[A-Z0-9]{6}$/);
- expect(new Date(result.hostCreatedAt)).to.be.an.instanceof(Date);
- expect(new Date(result.createdAt)).to.be.an.instanceof(Date);
- expect(new Date(result.modifiedAt)).to.be.an.instanceof(Date);
- expect(result.type).to.equal('uAPI');
- // Checking passengers format
- expect(result.passengers).to.be.an('array');
- expect(result.passengers).to.have.length.above(0);
- result.passengers.forEach((passenger) => {
- expect(passenger).to.be.an('object');
- expect(passenger).to.include.keys([
- 'lastName', 'firstName', 'uapi_passenger_ref',
- ]);
- });
- // Checking reservations format
- expect(result.fareQuotes).to.be.an('array');
- result.fareQuotes.forEach((fareQuote) => {
- expect(fareQuote).to.be.an('object');
- expect(fareQuote).to.include.all.keys([
- 'index',
- 'pricingInfos',
- 'uapi_segment_refs',
- 'uapi_passenger_refs',
- 'endorsement',
- 'effectiveDate',
- ]);
- expect(fareQuote.index).to.be.a('number');
- expect(fareQuote.pricingInfos).to.be.an('array').and.to.have.length.above(0);
-
- if (fareQuote.tourCode) {
- expect(fareQuote.tourCode).to.match(/^[A-Z0-9]+/);
- }
-
- if (fareQuote.endorsement) {
- expect(fareQuote.endorsement).to.match(/^[A-Z0-9.\-\s/]+$/);
- }
-
- if (fareQuote.platingCarrier) {
- expect(fareQuote.platingCarrier).to.match(/^[A-Z0-9]{2}$/);
- }
-
- expect(fareQuote.uapi_passenger_refs).to.be.an('array');
- expect(fareQuote.uapi_passenger_refs).to.have.length.above(0);
- fareQuote.uapi_passenger_refs.forEach(
- (reference) => expect(reference).to.be.a('string')
- );
-
- // Segment references
- expect(fareQuote.uapi_segment_refs).to.be.an('array');
- expect(fareQuote.uapi_segment_refs).to.have.length.above(0);
- fareQuote.uapi_segment_refs.forEach(
- (reference) => expect(reference).to.be.a('string')
- );
-
- fareQuote.pricingInfos.forEach(testPricingInfo);
- });
- // Checking segments format
- expect(result.segments).to.be.an('array');
- result.segments.forEach(
- (segment) => {
- expect(segment).to.be.an('object');
- expect(segment).to.have.include.keys([
- 'index', 'from', 'to', 'bookingClass', 'departure', 'arrival', 'airline',
- 'flightNumber', 'serviceClass', 'status', 'plane', 'duration',
- 'techStops', 'group', 'uapi_segment_ref', 'uapiSegmentReference',
- ]);
- expect(segment.index).to.be.a('number');
- expect(segment.from).to.match(/^[A-Z]{3}$/);
- expect(segment.to).to.match(/^[A-Z]{3}$/);
- expect(segment.bookingClass).to.match(/^[A-Z]{1}$/);
- expect(new Date(segment.departure)).to.be.an.instanceof(Date);
- expect(new Date(segment.arrival)).to.be.an.instanceof(Date);
- expect(segment.airline).to.match(/^[A-Z0-9]{2}$/);
- expect(segment.flightNumber).to.match(/^\d+$/);
- expect(segment.serviceClass).to.be.oneOf([
- 'Economy', 'Business', 'First', 'PremiumEconomy',
- ]);
- expect(segment.status).to.match(/^[A-Z]{2}$/);
- // Planes
- if (segment.plane) {
- expect(segment.plane).to.be.an('array');
- segment.plane.forEach((plane) => expect(plane).to.be.a('string'));
- }
- // Duration
- expect(segment.duration).to.be.an('array');
- segment.duration.forEach((duration) => expect(duration).to.match(/^\d+$/));
- // Tech stops
- expect(segment.techStops).to.be.an('array');
- segment.techStops.forEach((stop) => expect(stop).to.match(/^[A-Z]{3}$/));
- // Segment reference
- expect(segment.uapi_segment_ref).to.be.a('string');
- // Next segment reference
- if (segment.nextSegmentReference === null) {
- expect(segment.nextSegmentReference).to.be.a('null');
- }
-
- if (segment.nextSegmentReference) {
- expect(segment.nextSegmentReference).to.be.a('string');
- }
- }
- );
-
- if (result.serviceSegments) {
- expect(result.serviceSegments).to.be.an('array');
- const allSegments = [].concat(result.segments).concat(result.serviceSegments);
- const maxIndex = allSegments.reduce((acc, x) => {
- if (x.index > acc) {
- return x.index;
- }
- return acc;
- }, 0);
- expect(maxIndex).to.be.equal(allSegments.length);
- result.serviceSegments.forEach((segment) => {
- expect(segment).to.include.all.keys([
- 'index', 'carrier', 'airport', 'date', 'rfiCode',
- 'rfiSubcode', 'feeDescription', 'name', 'amount', 'currency',
- ]);
- expect(segment.carrier).to.match(/^[A-Z0-9]{2}$/);
- expect(segment.airport).to.match(/^[A-Z]{3}$/);
- expect(new Date(segment.date)).to.be.instanceof(Date);
- expect(segment.rfiCode).to.match(/^[A-Z]$/);
- expect(segment.rfiSubcode).to.match(/^[0-9A-Z]{3}$/);
- expect(segment.feeDescription).to.be.a('string');
- expect(segment.name).to.match(/^[A-Z]+\/[A-Z]+$/);
- expect(segment.amount).to.be.a('number');
- expect(segment.currency).to.match(/^[A-Z]{3}$/);
- });
- }
-
- // Checking tickets
- expect(result.tickets).to.be.an('array');
- if (result.tickets.length > 0) {
- result.tickets.forEach(
- (ticket) => {
- expect(ticket).to.be.an('object').and.to.have.all.keys([
- 'number', 'uapi_passenger_ref', 'uapi_pricing_info_ref', 'passengers',
- ]);
- expect(ticket.passengers.length).to.be.equal(1);
- expect(ticket.passengers[0]).to.have.all.keys(['firstName', 'lastName']);
- expect(ticket.number).to.match(/\d{13}/);
- expect(ticket.uapi_passenger_ref).to.be.a('string');
- }
- );
- }
- });
- }
-
describe('AIR_CREATE_RESERVATION()', () => {
it('should parse booking with no itinerary', () => {
const uParser = new Parser('universal:UniversalRecordImportRsp', 'v52_0', { });
From acee2a9c55b51630b9330bc15f8ea5cba8b4c883 Mon Sep 17 00:00:00 2001
From: Smotrov Oleksii
Date: Thu, 12 Mar 2026 13:08:40 +0200
Subject: [PATCH 6/6] Update src/Services/Air/AirParser.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/Services/Air/AirParser.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/Services/Air/AirParser.js b/src/Services/Air/AirParser.js
index 0afb3a0b..8af6e778 100644
--- a/src/Services/Air/AirParser.js
+++ b/src/Services/Air/AirParser.js
@@ -554,7 +554,8 @@ function parseMiscFormOfPayment(miscFop) {
if (category === 'Exchange') {
return text ? `EXCHANGE:${text}` : 'EXCHANGE';
}
- return text ? `${category.toUpperCase()}:${text}` : category.toUpperCase();
+ const normalizedCategory = category ? category.toUpperCase() : 'UNKNOWN';
+ return text ? `${normalizedCategory}:${text}` : normalizedCategory;
}
function getTicketFromEtr(etr, obj, allowNoProviderLocatorCodeRetrieval = false) {