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) {