Skip to content

Commit e8078c6

Browse files
authored
Feature: Implementation of hotel booking logic (#13)
* Storage of hotel offers in redis * Rework preparation for OTA order creation * Working Soap Template for HoteResNotifRQ * Request echoes the Soap message * Getting answer from simulator * Implemented the answer * Update with Now secrets * Lgacy Simard deployment in schema
1 parent b1a6de4 commit e8078c6

23 files changed

+1005
-64
lines changed

api/v1/orders/[orderId]/fulfill.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
const axios = require('axios');
22
const { transform } = require('camaro');
3-
const { airFranceConfig, JWT } = require('../../../../config');
3+
//const { airFranceConfig, JWT } = require('../../../../config');
4+
const config = require('../../../../config');
45
const { basicDecorator } = require('../../../../decorators/basic');
56
const { mapNdcRequestData} = require('../../../../helpers/transformInputData/fulfillOrder');
67
const { fulfillOrderTemplate } = require('../../../../helpers/soapTemplates/fulfillOrder');
78
const { ErrorsTransformTemplate, fulfillOrderTransformTemplate } = require('../../../../helpers/camaroTemplates/fulfillOrder');
89
const { reduceToObjectByKey, reduceToProperty } = require('../../../../helpers/parsers');
910

1011
const simardHeaders = {
11-
Authorization: JWT,
12+
Authorization: config.JWT,
1213
}
1314

1415
module.exports = basicDecorator(async (req, res) => {
@@ -28,7 +29,7 @@ module.exports = basicDecorator(async (req, res) => {
2829
'Content-Type': 'text/xml;charset=UTF-8',
2930
'Accept-Encoding': 'gzip,deflate',
3031
SOAPAction: '"http://www.af-klm.com/services/passenger/AirDocIssue/airDocIssue"',
31-
api_key: airFranceConfig.apiKey,
32+
api_key: config.airFranceConfig.apiKey,
3233
},
3334
});
3435

api/v1/orders/createWithOffer.js

+105-31
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
const axios = require('axios');
22
const { transform } = require('camaro');
33

4-
const { airFranceConfig } = require('../../../config');
4+
//const { airFranceConfig } = require('../../../config');
5+
const config = require('../../../config');
56

67
const { basicDecorator } = require('../../../decorators/basic');
78
const { mapNdcRequestData } = require('../../../helpers/transformInputData/createOrder');
@@ -12,46 +13,119 @@ const {
1213
mergeHourAndDate, reduceToObjectByKey, useDictionary, reduceContactInformation, splitPropertyBySpace
1314
} = require('../../../helpers/parsers');
1415

16+
const offer = require('../../../helpers/models/offer');
17+
const hotelResolver = require('../../../helpers/resolvers/hotel/orderCreateWithOffer');
18+
19+
20+
function returnCleanError(err, res) {
21+
if(err.code && err.message) {
22+
res.status(err.code).json({message: err.message});
23+
}
24+
25+
// Default Error
26+
else {
27+
res.status(500).json({message: 'A server error occured ', details: err});
28+
}
29+
}
30+
1531
module.exports = basicDecorator( async (req, res) => {
1632
const requestBody = req.body;
1733

18-
const ndcRequestData = mapNdcRequestData(requestBody);
19-
const ndcBody = orderCreateRequestTemplate(ndcRequestData);
20-
const response = await axios.post('https://ndc-rct.airfranceklm.com/passenger/distribmgmt/001451v01/EXT',
21-
ndcBody,
22-
{
23-
headers: {
24-
'Content-Type': 'text/xml;charset=UTF-8',
25-
'Accept-Encoding': 'gzip,deflate',
26-
SOAPAction: '"http://www.af-klm.com/services/passenger/ProvideOrderCreate/provideOrderCreate"',
27-
api_key: airFranceConfig.apiKey,
28-
},
29-
});
34+
// Retrieve the offer
35+
if(requestBody.offerId) {
36+
offer.offerManager.getOffer(requestBody.offerId)
37+
38+
.then(storedOffer => {
39+
// Check if there is no offer returned
40+
if(storedOffer == null) {
41+
res.status(404).json({message: 'Offer expired or not found'});
42+
}
3043

31-
const { errors } = await transform(response.data, ErrorsTransformTemplate);
32-
if (errors.length) throw new Error(`${errors[0].message}`);
44+
// Handle an Accomodation offer
45+
if(storedOffer instanceof offer.AccommodationOffer) {
46+
// Resolve this query for an hotel offer
47+
hotelResolver(storedOffer, requestBody.passengers)
48+
.then(orderCreationResults => {
49+
res.send(orderCreationResults);
50+
//res.status(200).json(orderCreationResults);
51+
})
52+
.catch(err => {
53+
returnCleanError(err, res);
54+
});
55+
}
3356

34-
const createResults = await transform(response.data, provideOrderCreateTransformTemplate);
57+
// Handle a flight offer
58+
else if(storedOffer instanceof offer.FlightOffer) {
3559

36-
createResults.order.itinerary.segments =
37-
mergeHourAndDate(createResults.order.itinerary.segments, 'splittedDepartureDate', 'splittedDepartureTime', 'departureTime');
60+
// TODO: Move this to dedicated module
61+
const ndcRequestData = mapNdcRequestData(requestBody);
62+
const ndcBody = orderCreateRequestTemplate(ndcRequestData);
63+
axios.post(
64+
'https://ndc-rct.airfranceklm.com/passenger/distribmgmt/001451v01/EXT',
65+
ndcBody,
66+
{
67+
headers: {
68+
'Content-Type': 'text/xml;charset=UTF-8',
69+
'Accept-Encoding': 'gzip,deflate',
70+
SOAPAction: '"http://www.af-klm.com/services/passenger/ProvideOrderCreate/provideOrderCreate"',
71+
api_key: config.airFranceConfig.apiKey,
72+
},
73+
}
74+
)
75+
.then(response => {
76+
// Attempt to parse as a an error
77+
transform(response.data, ErrorsTransformTemplate)
78+
.then(errors => {
79+
// If an error is found, stop here
80+
if (errors.length) throw new Error(`${errors[0].message}`);
3881

39-
createResults.order.itinerary.segments =
40-
mergeHourAndDate(createResults.order.itinerary.segments, 'splittedArrivalDate', 'splittedArrivalTime', 'arrivalTime');
82+
// Otherwise parse as a result
83+
transform(response.data, provideOrderCreateTransformTemplate)
84+
.then(createResults => {
85+
createResults.order.itinerary.segments =
86+
mergeHourAndDate(createResults.order.itinerary.segments, 'splittedDepartureDate', 'splittedDepartureTime', 'departureTime');
87+
88+
createResults.order.itinerary.segments =
89+
mergeHourAndDate(createResults.order.itinerary.segments, 'splittedArrivalDate', 'splittedArrivalTime', 'arrivalTime');
90+
91+
createResults.order.itinerary.segments = reduceToObjectByKey(createResults.order.itinerary.segments);
92+
93+
createResults.order.price.commission = createResults.order.price.commission.reduce((total, {value}) => total + parseFloat(value), 0).toString();
94+
createResults.order.price.taxes = createResults.order.price.taxes.reduce((total, {value}) => total + parseFloat(value), 0).toString();
95+
96+
createResults.order.contactList = reduceToObjectByKey(createResults.order.contactList);
97+
createResults.order.passengers = useDictionary(createResults.order.passengers, createResults.order.contactList, 'contactInformation');
98+
createResults.order.passengers = splitPropertyBySpace(createResults.order.passengers, 'firstnames');
99+
createResults.order.passengers = splitPropertyBySpace(createResults.order.passengers, 'lastnames');
100+
createResults.order.passengers = reduceContactInformation(createResults.order.passengers);
101+
createResults.order.passengers = reduceToObjectByKey(createResults.order.passengers);
102+
103+
delete createResults.order.contactList;
104+
105+
res.status(200).json(createResults);
106+
})
41107

42-
createResults.order.itinerary.segments = reduceToObjectByKey(createResults.order.itinerary.segments);
108+
})
109+
})
43110

44-
createResults.order.price.commission = createResults.order.price.commission.reduce((total, {value}) => total + parseFloat(value), 0).toString();
45-
createResults.order.price.taxes = createResults.order.price.taxes.reduce((total, {value}) => total + parseFloat(value), 0).toString();
111+
}
46112

47-
createResults.order.contactList = reduceToObjectByKey(createResults.order.contactList);
48-
createResults.order.passengers = useDictionary(createResults.order.passengers, createResults.order.contactList, 'contactInformation');
49-
createResults.order.passengers = splitPropertyBySpace(createResults.order.passengers, 'firstnames');
50-
createResults.order.passengers = splitPropertyBySpace(createResults.order.passengers, 'lastnames');
51-
createResults.order.passengers = reduceContactInformation(createResults.order.passengers);
52-
createResults.order.passengers = reduceToObjectByKey(createResults.order.passengers);
113+
// Handle other types of offer
114+
else {
115+
res.status(500).json({message: 'Unable to understand the offer type'});
116+
}
117+
118+
})
119+
120+
.catch(err => {
121+
// Handle Errors with a defined code and message
122+
returnCleanError(err, res);
123+
});
124+
}
53125

54-
delete createResults.order.contactList;
126+
// Unable to find the offerId
127+
else {
128+
res.status(400).json({message: 'Missing mandatory field: offerId'});
55129

56-
res.status(200).json(createResults);
130+
}
57131
});

config.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ const airFranceConfig = {
3434

3535
const JWT = process.env.JWT;
3636

37-
module.exports = {
38-
airFranceConfig,
39-
JWT,
40-
}
37+
const redisUrl = process.env.REDIS_URL;
38+
39+
const erevmax = {
40+
reservationUrl: process.env.EREVMAX_RESERVATION_URL,
41+
};
42+
43+
44+
exports.airFranceConfig = airFranceConfig;
45+
exports.JWT = JWT;
46+
exports.redisUrl = redisUrl;
47+
exports.erevmax = erevmax;

helpers/camaroTemplates/fulfillOrder.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { airFranceConfig } from '../../config';
21

32
const fulfillOrderTransformTemplate = {
43
travelDocuments: {

helpers/camaroTemplates/hotelAvail.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,22 @@ const hotelAvailTransformTemplate = {
3838
_roomRates_: ['RoomRates/RoomRate',{
3939
ratePlanReference: '@RatePlanCode',
4040
roomTypeReference: '@RoomTypeCode',
41+
effectiveDate: '@EffectiveDate',
42+
expireDate: '@ExpireDate',
43+
expireDateExclusiveInd: 'boolean(@ExpireDateExclusiveInd="true")',
4144
price: {
4245
currency: 'Total/@CurrencyCode',
4346
_afterTax_: 'Total/@AmountAfterTax',
4447
_beforeTax_: 'Total/@AmountBeforeTax',
45-
}
48+
},
49+
rates: ['Rates/Rate', {
50+
rateTimeUnit: '@RateTimeUnit',
51+
effectiveDate: '@EffectiveDate',
52+
expireDate: '@ExpireDate',
53+
amountBeforeTax: 'Base/@AmountBeforeTax',
54+
amountAfterTax: 'Base/@AmountAfterTax',
55+
currencyCode: 'Base/@CurrencyCode'
56+
}],
4657
}],
4758
_roomTypes_: ['RoomTypes/RoomType', {
4859
_id_: '@RoomTypeCode',
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
const template = {
3+
response: '/SOAP-ENV:Envelope/SOAP-ENV:Body/OTA_HotelResNotifRS/@ResResponseType',
4+
reservationNumber: '/SOAP-ENV:Envelope/SOAP-ENV:Body/OTA_HotelResNotifRS/HotelReservations/HotelReservation/ResGlobalInfo/HotelReservationIDs/HotelReservationID/@ResID_Value',
5+
success: '/SOAP-ENV:Envelope/SOAP-ENV:Body/OTA_HotelResNotifRS/Success',
6+
errors: ['/SOAP-ENV:Envelope/SOAP-ENV:Body/OTA_HotelResNotifRS/Errors/Error', {
7+
message: '@ShortText',
8+
code: '@Code',
9+
}],
10+
};
11+
12+
module.exports.otaHotelResNotifRSTemplate = template;

helpers/camaroTemplates/provideAirShopping.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { airFranceConfig } from '../../config';
1+
//import { airFranceConfig } from '../../config';
2+
const config = require('../../config');
23

34
const provideAirShoppingTransformTemplate = {
45
offers: ['/S:Envelope/S:Body/AirShoppingRS/OffersGroup/AirlineOffers/Offer', {
@@ -7,7 +8,7 @@ const provideAirShoppingTransformTemplate = {
78
price: {
89
currency: 'TotalPrice/DetailCurrencyPrice/Total/@Code',
910
public: 'OfferItem/TotalPriceDetail/TotalAmount/DetailCurrencyPrice/Total',
10-
commission: `number(OfferItem/TotalPriceDetail/BaseAmount) * ${airFranceConfig.commission}`,
11+
commission: `number(OfferItem/TotalPriceDetail/BaseAmount) * ${config.airFranceConfig.commission}`,
1112
taxes: 'OfferItem/TotalPriceDetail/Taxes/Total',
1213
},
1314
offerItems: ['OfferItem', {

helpers/camaroTemplates/provideOrderCreate.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { airFranceConfig } from '../../config';
1+
//import { airFranceConfig } from '../../config';
22

33
const provideOrderCreateTransformTemplate = {
44
orderId: '/S:Envelope/S:Body/ns2:OrderViewRS/ns2:Response/ns2:Order/@OrderID',

0 commit comments

Comments
 (0)