diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
new file mode 100644
index 00000000..ace9a2e5
--- /dev/null
+++ b/.github/workflows/node.js.yml
@@ -0,0 +1,28 @@
+# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
+
+name: unit tests
+
+on:
+ push:
+ branches: [ develop ]
+ pull_request:
+ branches: [ develop ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [12.x]
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: npm ci
+ - run: npm run test:unit
diff --git a/README.md b/README.md
index f9516c9a..d77b1a02 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,156 @@
-# ota
-OTA monorepo
+# Glider OTA
+**Open source Online Travel Agency backed up by Winding Tree ecosystem**
-## Start
-* npm install
-* npm start
-* now dev
+## Setting up local development environment
+#### Prerequisites
+Before you start, make sure you have the following prerequisites completed:
+* create an account in [Winding Tree marketplace](https://marketplace.windingtree.com/join) for your travel agency - this step is needed to get ORGiD of your organization
+* you need an account on [vercel.com](https://vercel.com/)
+ * download and install [vercel CLI](https://vercel.com/download)
+* have an instance of [Redis](https://redis.io/) - either local or cloud. It will be used as a temporary storage/cache.
+* have an instance of [Mongo](https://www.mongodb.com/) - either local or cloud. It will be used as a permanent storage.
+* have an instance of [Elasticsearch](https://www.elastic.co/) - either local or cloud. It will be used to store logs and analyze app usage/troubleshoot.
+* create an account in [Stripe](https://stripe.com/). Stripe is used as a payment gateway to allow acceptance of card payments.
-#### List of airports
-List of airports that is used in origin/destination lookup fields is taken from IATA list
-* https://github.com/opentraveldata/opentraveldata/tree/master/data/IATA
+#### Initial setup
+In order to start your local environment, you will need to:
+* clone this repository to a local folder
+* initiate project and install all required packages
-It's curated with a script that transforms data to JSON format and filters out unwanted types
+To achieve this, execute below commands
+```bash
+git clone https://github.com/windingtree/glider-ota.git
+cd glider-ota
+npm install
+```
+Now you have to connect your local project with Vercel project.
+This will be needed only once and vercel CLI will walk you through the process. Just execute the following:
+```bash
+vercel
+```
+Congratulations.
+Now the last step is to provide some configuration details for your own setup (things like database access details, Redis, Elastisearch ...)
+Skip now for details to the next [chapter](#configuration) which explains in details what you need and how configuration is done.
+To provide all configuration details, you will need to add few 'secrets' and you can do this using the following Vercel CLI command:
+```bash
+vercel secrets add secret_name "secret_value"
+```
+After configuration is completed, you are ready to start your project in local environment with this Vercel CLI command:
```bash
- /tools/locations/airports.js
+vercel dev
```
-Original list contains not only airports but also heliports, railstations, ferry ports.
-Unwanted locations can be filtered out
+
+Above command starts backend services as well as React frontend.
+Just open your browser and point to: http://localhost:3000
+
+
+
+#### Configuration
+Most important configuration entries are provided as environment variables and are initiated by as stored in Vercel secrets.
+Read more about:
+* [environment variables](https://vercel.com/docs/v2/build-step?query=secrets#environment-variables)
+* [secrets](https://vercel.com/docs/cli#commands/secrets)
+
+You will need to define the below secrets in Vercel:
+* {staging||production}.glider-ota.mongo.uri2 - Mongo instance URL
+* {staging||production}.glider-ota.mongo.dbname - Mongo database name
+* {staging||production}.glider-ota.redis.host - Redis hostname
+* {staging||production}.glider-ota.redis.password - Redis password
+* {staging||production}.glider-ota.stripe.publishable_key - Stripe publishable key
+* {staging||production}.glider-ota.stripe.secret_key - Stripe secret key
+* {staging||production}.glider-ota.stripe.webhook_secret - Stripe webhook secret key
+* {staging||production}.glider-ota.elastic.url - Elasticsearch URL
+* {staging||production}.glider-ota.glider_jwt - Glider JWT
+* {staging||production}.glider-ota.simard_jwt - Simard JWT
+* {staging||production}.glider-ota.glider-b2b_orgid - Your travel agency ORGiD [Winding Tree marketplace](https://marketplace.windingtree.com)
+Depending on an environment, either 'staging' or 'production' variables will be used.
+More details [here](./api/_lib/config.js)
-#### List of locations
-List of locations that is used in hotels lookup fields is taken from geonames.org list of cities
-* https://download.geonames.org/export/dump/
-It's also curated with a script that transforms data to JSON format and filters out unwanted cities (currently cities with population over 300000 are taken into account)
+## Documentation
+Code is documented using JSDoc comments which allows automatic API docs creation.
+Backend and frontend code is separated, thus API documentation can be generated separately for backend and frontend
+
+Use jsdoc to generate documentation.
+
+
+#### Backend end API documentation
+Execute the following command from the main project folder
```bash
- /tools/locations/process_dictionary_data.js
+jsdoc -c jsdoc_frontend_conf.json
```
+Documentation will be generated into `./docs/frontend` folder
+#### Front end API documentation
+Execute the following command from the main project folder
+```bash
+jsdoc -c jsdoc_backend_conf.json
+```
+
+Documentation will be generated into `./docs/backend` folder
+
+
+## Static/dictionary data
+There are multiple types of external data sources needed by Glider OTA, for example:
+* list of airports (e.g. to let users search the right departure&arrival airport using airport, city name)
+* list of airlines (e.g. to display full airline name, not only 2-letter carrier code)
+* list of cities
+
+This data needs to be sourced and maintained.
+Please refer to documentation [here](./docs/data.md) for more information on this topic
+
+
+## Development
+### System architecture
+
+##### Frontend
+Frontend is a React web app.
+
+
+##### Backend
+Backend is developed as serverless nodejs functions and running on Vercel cloud hosting platform.
+More info:
+* [Vercel](https://vercel.com/)
+* [serverless functions](https://vercel.com/docs/v2/serverless-functions/introduction)
+
+
+##### Integration
+Glider OTA integrates with:
+* Glider B2B to
+ * search for flight offers from airlines connected to Glider B2B
+ * retrieve flight seatmaps
+ * search for hotel offers from hotels connected to Glider B2B
+ * creating flight & hotel reservations
+* Simard
+ * which handles settlement between customers & suppliers (hotels, airlines)
+* Stripe
+ * for online payments processing
+
+
+#### Running application locally
+In order to develop application locally, you need to run backend and frontend.
+This is done with one command.
+```bash
+vercel dev
+```
+
+Changes in the code will be automatically hot deployed.
+
+#### React components & testing
+We use [storybook](https://storybook.js.org/) to document and test UI components.
+To start storybook dashboard, simply run:
+```bash
+npm run storybook
+```
+and open http://localhost:9009/ to see all components used by Glider OTA
+
+
+#### Unit Tests
+Apart from storybook, there are multiple unit tests that can be executed by:
+
+
diff --git a/api/_data/README.md b/api/_data/README.md
new file mode 100644
index 00000000..bfc48c5a
--- /dev/null
+++ b/api/_data/README.md
@@ -0,0 +1,25 @@
+# Introduction
+Files in this folder are considered a static data.
+This data is required at multiple stages and are crucial for Glider OTA.
+
+##List of files
+
+* airlines.json
+ * list of all available airlines
+ * It is used to resolve airline name based on IATA code (and display airline name in search results))
+* airports.json
+ * list of all airports and metropolitan areas
+ * It's used in airports name lookup field(flights search), when user enters airport name in the UI
+ * It's also used to resolve full airport name based on IATA code (and display full airport name in search results)
+
+* cities.json
+ * list of cities and locations
+ * It's used in city name lookup (hotel search), when user enters city name in the UI)
+
+* countries.json
+ * list of countries ('country code' to 'country name' map)
+
+* currencies.json
+ * list of currency codes, names and it's precision digits (aka 'minor units')
+ * it's used in payment (stripe) to calculate amount that needs to be charged in minor units (e.g. 10,65EUR = 1065 in minor units)
+
diff --git a/api/_lib/config.js b/api/_lib/config.js
index a8ecd592..a8b3afc5 100644
--- a/api/_lib/config.js
+++ b/api/_lib/config.js
@@ -1,10 +1,19 @@
-// Define the current enviroment
+/**
+ * Main configuration is maintained here
+ * @module _lib/config
+ */
+
+/**
+ * Detect what is the current environment
+ * @returns {string} 'production' || 'staging' || 'develop'
+ */
+
const determineEnviroment = () => {
// If defined, use the Glider environment variable
if(process.env.GLIDER_ENV) {
return process.env.GLIDER_ENV;
}
-
+
// Otherwise use the Github branch provided by Vercel
switch(process.env.VERCEL_GITHUB_COMMIT_REF || process.env.NOW_GITHUB_COMMIT_REF) {
case 'master':
@@ -14,30 +23,40 @@ const determineEnviroment = () => {
return 'staging';
}
}
-const enviroment = determineEnviroment();
+const environment = determineEnviroment();
-// Get an an environment variable
+/**
+ * Get the value of an environment variable
+ * @param key
+ * @returns {string|undefined}
+ */
const getConfigKey = (key) => {
// Return environment specific variable if any
- const envKey = `${enviroment.toUpperCase()}_${key}`;
+ const envKey = `${environment.toUpperCase()}_${key}`;
if(process.env.hasOwnProperty(envKey)) {
return process.env[envKey];
}
-
+
// Return variable key
if(process.env.hasOwnProperty(key)) {
return process.env[key];
}
-
+
// Config key does not exist
return undefined;
};
-
-const GLIDER_BASEURL = getConfigKey('GLIDER_BASEURL') || `https://${enviroment}.b2b.glider.travel/api/v1`;
+
+const GLIDER_BASEURL = getConfigKey('GLIDER_BASEURL') || `https://${environment}.b2b.glider.travel/api/v1`;
+
+/**
+ * Glider related configuration
+ *
+ * @type {{ORGID: (string|undefined), SEARCH_OFFERS_URL: string, FULFILL_URL: string, SEATMAP_URL: string, CREATE_WITH_OFFER_URL: string, REPRICE_OFFER_URL: string, GLIDER_TOKEN: (string|undefined)}}
+ */
const GLIDER_CONFIG =
{
- GLIDER_TOKEN: getConfigKey('GLIDER_JWT'),
+ GLIDER_TOKEN: getConfigKey('GLIDER_JWT'), //JWT Token needed for Glider API (offers search, creating offers, fulfilment)
SEARCH_OFFERS_URL: GLIDER_BASEURL + "/offers/search",
CREATE_WITH_OFFER_URL: GLIDER_BASEURL + "/orders/createWithOffer",
SEATMAP_URL: GLIDER_BASEURL + "/offers/{offerId}/seatmap",
@@ -46,15 +65,21 @@ const GLIDER_CONFIG =
ORGID: getConfigKey('GLIDER_ORGID'),
};
+
+//Glider OTA OrgID
const ORGID = {
OTA_ORGID: getConfigKey('OTA_ORGID'),
}
-const SIMARD_BASEURL = getConfigKey('SIMARD_BASEURL') || `https://${enviroment}.api.simard.io/api/v1`;
+const SIMARD_BASEURL = getConfigKey('SIMARD_BASEURL') || `https://${environment}.api.simard.io/api/v1`;
+/**
+ * Simard related configuration
+ * @type {{ORGID: (string|string), SIMARD_TOKEN: (string|undefined), GUARANTEES_URL: string, SIMULATE_DEPOSIT_URL: string, CREATE_WITH_OFFER_URL: string, DEPOSIT_EXPIRY_DAYS: number}}
+ */
const SIMARD_CONFIG =
{
- SIMARD_TOKEN: getConfigKey('SIMARD_JWT'),
+ SIMARD_TOKEN: getConfigKey('SIMARD_JWT'), //JWT Token needed for Simard API
GUARANTEES_URL: SIMARD_BASEURL + "/balances/guarantees",
CREATE_WITH_OFFER_URL: SIMARD_BASEURL + "/orders/createWithOffer",
SIMULATE_DEPOSIT_URL: SIMARD_BASEURL + "/balances/simulateDeposit",
@@ -62,21 +87,32 @@ const SIMARD_CONFIG =
DEPOSIT_EXPIRY_DAYS:14,
};
-
+/**
+ * Redis related configuration
+ * @type {{REDIS_HOST: (string|undefined), REDIS_PORT: number, REDIS_PASSWORD: (string|undefined), SESSION_TTL_IN_SECS: number}}
+ */
const REDIS_CONFIG =
{
REDIS_PORT: (getConfigKey('REDIS_PORT') && parseInt(getConfigKey('REDIS_PORT'))) || 14563,
REDIS_HOST: getConfigKey('REDIS_HOST'),
REDIS_PASSWORD: getConfigKey('REDIS_PASSWORD'),
- SESSION_TTL_IN_SECS: 60 * 60,
+ SESSION_TTL_IN_SECS: 60 * 60, //how long session data is stored in redis
};
+/**
+ * Mongo configuration
+ * @type {{DBNAME: (string|undefined), URL: (string|undefined)}}
+ */
const MONGO_CONFIG =
{
URL: getConfigKey('MONGO_URL'),
DBNAME: getConfigKey('MONGO_DBNAME'),
};
+/**
+ * Stripe related config
+ * @type {{BYPASS_WEBHOOK_SIGNATURE_CHECK: boolean, WEBHOOK_SECRET: (string|undefined), PUBLISHABLE_KEY: (string|undefined), SECRET_KEY: (string|undefined)}}
+ */
const STRIPE_CONFIG =
{
PUBLISHABLE_KEY: getConfigKey('STRIPE_PUBLISHABLE_KEY'),
@@ -84,11 +120,21 @@ const STRIPE_CONFIG =
WEBHOOK_SECRET: getConfigKey('STRIPE_WEBHOOK_SECRET'),
BYPASS_WEBHOOK_SIGNATURE_CHECK: (getConfigKey('STRIPE_BYPASS_WEBHOOK_SIGNATURE_CHECK') === "yes"),
};
+
+/**
+ * Elastic access details
+ * @type {{URL: (string|undefined)}}
+ */
const ELASTIC_CONFIG =
{
URL: getConfigKey('ELASTIC_URL'),
};
+
+/**
+ * Generic configuration is here
+ * @type {{ENABLE_HEALHCHECK: boolean, ENVIRONMENT: string}}
+ */
const GENERIC_CONFIG =
{
ENVIRONMENT: determineEnviroment(),
@@ -106,4 +152,4 @@ module.exports = {
ELASTIC_CONFIG,
ORGID,
GENERIC_CONFIG
-};
\ No newline at end of file
+};
diff --git a/api/_lib/cookie-manager.js b/api/_lib/cookie-manager.js
index d08dd8cd..8d97c868 100644
--- a/api/_lib/cookie-manager.js
+++ b/api/_lib/cookie-manager.js
@@ -4,9 +4,16 @@ const {createLogger} = require('./logger');
const logger = createLogger('session-storage');
const SESSION_ID_COOKIE = "wt-ota-session-id";
+/**
+ * Glider OTA is using cookie to maintain session/state between API calls
+ *
This module contains helper methods to generate, read and update session cookie
+ * @module _lib/cookie-manager
+ */
+
+
/**
* Returns sessionID (from request cookie) if it already exists or generates new sessionID in case it did not exist
- * This ensures that with the first time request, we will also have sessionID and send that to the client.
+ *
This ensures that with the first time request, we will also have sessionID and send that to the client.
* @param req
* @param res
* @returns sessionID
@@ -60,4 +67,4 @@ function generateSessionId() {
module.exports = {
getCookie, setCookie, ensureSessionIdCookie, getSessionID
-}
\ No newline at end of file
+}
diff --git a/api/_lib/decorators.js b/api/_lib/decorators.js
index cac7d865..cec75154 100644
--- a/api/_lib/decorators.js
+++ b/api/_lib/decorators.js
@@ -4,6 +4,16 @@ const {v4} = require('uuid');
const createNamespace = require('continuation-local-storage').createNamespace;
const session = createNamespace('ota');
+/**
+ * This module contains decorators wrapping API calls, such as:
+ * - HTTP request/response logging
+ * - session management (adding sessionID cookie if it does not exist)
+ * - unhandled exceptions logging
+ * @module _lib/decorators
+ */
+
+
+
// log REST calls to a separate logger
const restlogger = createLogger('rest-logger');
@@ -86,4 +96,4 @@ function decorate(fn){
}
module.exports={
decorate
-}
\ No newline at end of file
+}
diff --git a/api/_lib/dictionary-data-cache.js b/api/_lib/dictionary-data-cache.js
index e7ec5697..ff404daa 100644
--- a/api/_lib/dictionary-data-cache.js
+++ b/api/_lib/dictionary-data-cache.js
@@ -3,14 +3,24 @@ const _ = require('lodash');
const DB_LOCATION="api/_data/";
const {createLogger} = require('./logger');
const logger = createLogger('dictionary-data-cache')
+
+/**
+ * This module contains functions that operate on static data (aka dictionary data), such as: list of airports, airlines, currencies.
+ *
In general, static data is read into memory once and stored for later usage by API calls (such as airport lookup or decorating search results by additional information)
+ * @module _lib/dictionary-data-cache
+ */
+
+//types of dictionary data being used
+//each entry corresponds with JSON file which will be lazy-loaded
const TABLES={
AIRLINES:'airlines',
AIRPORTS:'airports',
CITIES:'cities',
CURRENCIES:'currencies',
- COUNTRIES:'COUNTRIES'
+ COUNTRIES:'countries'
}
+//cache holder
const CACHE={
};
@@ -134,7 +144,6 @@ function loadTableIntoCache(tableName) {
}
function loadAirlines(){
- console.log("Loading airlines into memory")
let path = `${DB_LOCATION}${TABLES.AIRLINES}.json`;
let data = JSON.parse(fs.readFileSync(path));
let airlineMap = {};
@@ -144,7 +153,6 @@ function loadAirlines(){
return airlineMap;
}
function loadAirports(){
- console.log("Loading airports into memory")
let path = `${DB_LOCATION}${TABLES.AIRPORTS}.json`;
let data = JSON.parse(fs.readFileSync(path));
let airportsMap = {};
@@ -235,7 +243,6 @@ function findCity(query,maxResults = DEFAULT_MAX_LOOKUP_RESULTS){
}
-
module.exports = {
findTableRecords,getTableRecordByKey,TABLES, getCurrencyByCode,getAirportByIataCode,getCountryByCountryCode,findAirport,findCity, getAirlineByIataCode
}
diff --git a/api/_lib/glider-api.js b/api/_lib/glider-api.js
index 3ef6a28c..656ae09c 100644
--- a/api/_lib/glider-api.js
+++ b/api/_lib/glider-api.js
@@ -6,6 +6,17 @@ const logger = createLogger('aggregator-api');
const {enrichResponseWithDictionaryData, setDepartureDatesToNoonUTC, increaseConfirmedPriceWithStripeCommission} = require('./response-decorator');
const {createErrorResponse,ERRORS} = require ('./rest-utils');
+/**
+ * This module is used to interact with Glider (searching for flights/hotels, retrieving seatmaps, creating orders).
+ * @module _lib/glider-api
+ */
+
+
+/**
+ * Create headers needed to make a request to Glider
+ * @param token
+ * @returns {{Authorization: string, accept: string, "Content-Type": string}}
+ */
function createHeaders(token) {
return {
'Authorization': 'Bearer ' + token,
@@ -33,7 +44,6 @@ async function searchOffers(criteria) {
let response;
if(criteria.itinerary)
setDepartureDatesToNoonUTC(criteria)
- console.debug("Criteria:",JSON.stringify(criteria))
try {
response = await axios({
method: 'post',
@@ -124,7 +134,7 @@ async function reprice(offerId, options) {
* @returns {Promise} - booking confirmation, response from Glider /orders/.../fulfill API
*/
async function fulfill(orderId,orderItems,passengers, guaranteeId) {
- let request = createFulfilmentRequest(orderItems,passengers,guaranteeId)
+ let request = _createFulfilmentRequest(orderItems,passengers,guaranteeId)
let urlTemplate=GLIDER_CONFIG.FULFILL_URL;
let urlWithOrderId = urlTemplate.replace("{orderId}",orderId);
logger.debug("Fulfillment URL:[%s]",urlWithOrderId);
@@ -138,7 +148,7 @@ async function fulfill(orderId,orderItems,passengers, guaranteeId) {
}
-function createFulfilmentRequest(orderItems,passengers,guaranteeId){
+function _createFulfilmentRequest(orderItems,passengers,guaranteeId){
let passengerReferences=[];
_.each(passengers, (rec,key)=>{
passengerReferences.push(key)
@@ -152,9 +162,6 @@ function createFulfilmentRequest(orderItems,passengers,guaranteeId){
}
-
-function addAirports(){}
-
module.exports = {
createWithOffer,
searchOffers,
diff --git a/api/_lib/logger.js b/api/_lib/logger.js
index 339a5857..c1337464 100644
--- a/api/_lib/logger.js
+++ b/api/_lib/logger.js
@@ -7,12 +7,25 @@ var Elasticsearch = require('winston-elasticsearch');
name: 'glider-ota',
index: 'ota-default'
});*/
+
+/**
+ * Module used for logging.
+ * It uses 'Winston' logger which can be further customized to add additional transports (e.g. Elastic)
+ * @module _lib/logger
+ */
+
+
let esTransportOpts = {
level: 'debug',
indexPrefix:'ota-log',
clientOpts: { node: ELASTIC_CONFIG.URL }
};
+/**
+ * Create new instance of a logger.
+ * @param loggerName name of the logger (usually unique name per module/functionality)
+ * @returns {Logger}
+ */
function createLogger(loggerName) {
const logger = winston.createLogger({
level: 'debug',
@@ -55,4 +68,4 @@ function createLogger(loggerName) {
-module.exports = {createLogger}
\ No newline at end of file
+module.exports = {createLogger}
diff --git a/api/_lib/mongo-dao.js b/api/_lib/mongo-dao.js
index a513913d..3236d938 100644
--- a/api/_lib/mongo-dao.js
+++ b/api/_lib/mongo-dao.js
@@ -4,6 +4,11 @@ const {createLogger} = require('./logger');
const logger = createLogger('dao');
const url = require('url');
+/**
+ * DAO module to support mongo operations.
+ * @module _lib/mongo-dao
+ */
+
const ORDER_STATUSES={
NEW:'NEW',
FULFILLING:'FULFILLING',
@@ -19,7 +24,12 @@ const PAYMENT_STATUSES={
// Create cached connection variable
let _db;
-// Get the connection
+/**
+ * Return connection to mongo instance.
+ *
If connection was previously initialized, it will return that instance.
+ *
Otherwise connection is initialized before returning.
+ * @returns {Promise}
+ */
function getConnection() {
return new Promise(function(resolve, reject) {
// Get the cached connection if exists
@@ -158,7 +168,7 @@ function createTransactionEntry(comment, details){
/**
* Update order status in database (.order.order_status)
- * Operation also updates .order.lastModifyDateTime and adds record to .order.transactions to log a change
+ *
Operation also updates .order.lastModifyDateTime and adds record to .order.transactions to log a change
* @param offerId
* @param order_status
@@ -188,7 +198,7 @@ function updateOrderStatus(offerId, order_status, comment, transactionDetails){
/**
* Update order payment status in database (.order.payment_status)
- * Operation also updates .order.lastModifyDateTime and adds record to .order.transactions to log a change
+ *
Operation also updates .order.lastModifyDateTime and adds record to .order.transactions to log a change
* @param offerId
* @param payment_status
* @param comment
@@ -215,7 +225,7 @@ function updatePaymentStatus(offerId, payment_status, payment_details, comment,
/**
* Update passengers of an offer
- * Operation also updates .order.lastModifyDateTime and adds record to .order.transactions to log a change
+ *
Operation also updates .order.lastModifyDateTime and adds record to .order.transactions to log a change
* @param offerId
* @param passengers passengers details
diff --git a/api/_lib/response-decorator.js b/api/_lib/response-decorator.js
index ff87df3b..55d07e55 100644
--- a/api/_lib/response-decorator.js
+++ b/api/_lib/response-decorator.js
@@ -1,3 +1,10 @@
+/**
+ * Module contains various method which are used to enrich response from Glider Aggregator before it's returned to UI.
+ *
For example, search results don't contain full airport names (IATA codes instead), for the users however we need to display full airport name.
+ *
Therefore we cannot simply return 'raw' results, we need to 'enrich' them before.
+ * @module _lib/response-decorator
+ */
+
const {createLogger} = require('./logger');
const {getAirportByIataCode,getCountryByCountryCode, getAirlineByIataCode} = require ('./dictionary-data-cache')
const _ = require('lodash');
@@ -7,10 +14,21 @@ const { parseISO } = require('date-fns');
const logger = createLogger('response-decorator-logger');
+
+/**
+ * Main function which takes care of enriching search results with additional information (e.g. full airport name, airline name)
+ *
+ * @param results
+ */
function enrichResponseWithDictionaryData(results){
+
+ //add origin & destination airport details (city_name and airport_name)
enrichAirportCodesWithAirportDetails(results);
+ //add airline details for each flight segment(airline_name)
enrichOperatingCarrierWithAirlineNames(results);
+ //add local time to each flight segment departure and arrival (dates returned by Glider are always UTC)
convertUTCtoLocalAirportTime(results);
+ //add small commission to the price to cover credit card transaction fee
increaseOfferPriceWithStripeCommission(results);
results['metadata']={
uuid:v4(),
@@ -64,7 +82,7 @@ function enrichOperatingCarrierWithAirlineNames(results){
/**
* Departure date from UI may come in a local timezone and hour may be random.
- * We should search with UTC and with hour = 12
+ *
We should search with UTC and with hour = 12
* @param criteria
*/
function setDepartureDatesToNoonUTC(criteria){
@@ -123,7 +141,7 @@ function increaseConfirmedPriceWithStripeCommission(repriceResponse){
}
//add 5% on top of the total price to cover for OPC fee
-//FIXME - replace hardcoded commision with configurable value
+//FIXME - replace hardcoded commission with configurable value
function _addOPCFee(price){
return Number(price)*1.05;
}
diff --git a/api/_lib/rest-utils.js b/api/_lib/rest-utils.js
index a2eb6ad5..35227055 100644
--- a/api/_lib/rest-utils.js
+++ b/api/_lib/rest-utils.js
@@ -1,3 +1,9 @@
+/**
+ * This module contains helper method used with REST api
+ * @module _lib/rest-utils
+ *
+ */
+
const ERRORS={
REQUEST_TIMEOUT:500,
INVALID_METHOD:'INVALID_METHOD',
@@ -71,4 +77,4 @@ function getRawBodyFromRequest(request) {
}
-module.exports={ERRORS,createErrorResponse,sendErrorResponse,getRawBodyFromRequest}
\ No newline at end of file
+module.exports={ERRORS,createErrorResponse,sendErrorResponse,getRawBodyFromRequest}
diff --git a/api/_lib/session-storage.js b/api/_lib/session-storage.js
index 115a191d..67cb4993 100644
--- a/api/_lib/session-storage.js
+++ b/api/_lib/session-storage.js
@@ -1,3 +1,8 @@
+/**
+ * Module contains helper functions to store/retrieve data in/from temporary session storage (Redis)
+ * @module _lib/session-storage
+ */
+
const redis = require('async-redis');
const {createLogger} = require('./logger');
const {REDIS_CONFIG} = require('./config');
@@ -17,6 +22,7 @@ const getClient = () => {
// Lazy load the client
if(!_client) {
+ console.warn("Redis client not initialized - connecting")
_client = redis.createClient({
port: REDIS_CONFIG.REDIS_PORT,
host: REDIS_CONFIG.REDIS_HOST,
@@ -48,7 +54,6 @@ const getClient = () => {
return Math.min(options.attempt * 100, 3000);
}
});
-
// Close connection to the Redis on exit
process.on('exit', function () {
logger.info("Shutting down redis connections gracefully");
@@ -60,16 +65,19 @@ const getClient = () => {
_client.on('end', function () {
logger.info("Redis client event=end");
+ if(_client) {
+ _client = undefined;
+ }
});
-
+
_client.on('error', function (err) {
logger.error("Redis client event=error, message=%s", err);
});
-
+
_client.on('ready', function (param) {
logger.info("Redis client event=ready");
});
-
+
_client.on('connect', function (param) {
logger.info("Redis client event=connect");
});
@@ -83,8 +91,8 @@ const getClient = () => {
/**
* Helper class to deal with storing session data on a server side.
- * Data is stored in Redis database, using temporary keys (short TTL, configured with REDIS_CONFIG.SESSION_TTL_IN_SECS)
- * Session is maintained with the client using cookie
+ *
Data is stored in Redis database, using temporary keys (short TTL, configured with REDIS_CONFIG.SESSION_TTL_IN_SECS)
+ *
Session is maintained with the client using cookie
*/
class SessionStorage {
constructor(sessionID) {
diff --git a/api/_lib/shopping-cart.js b/api/_lib/shopping-cart.js
index edf8eb4f..c71b4823 100644
--- a/api/_lib/shopping-cart.js
+++ b/api/_lib/shopping-cart.js
@@ -1,3 +1,7 @@
+/**
+ * Shopping cart module
+ * @module _lib/shopping-cart
+ */
const {createLogger} = require('./logger');
const logger = createLogger('session-storage');
const {SessionStorage} = require('./session-storage');
@@ -8,7 +12,7 @@ const CART_ITEMKEYS = {
OFFER : 'offer',
PASSENGERS : 'passengers',
SEATS : 'seats',
- ANCILLARIES : 'ancillaries',
+ ANCILLARIES : 'ancillaries',
CONFIRMED_OFFER : 'confirmed-offer',
};
@@ -32,10 +36,10 @@ class ShoppingCart {
/**
* Upsert (add or replace) offer in the shopping cart.
- * Multiple items can be added to the shopping cart (e.g. flights, hotelresults, passenger details, ancillaries).
- * cartKey parameter is supposed to identify what type of element is added to the cart.
- * If item with a given key is added for the first time, it will be normally added and can be later retrieved.
- * If item with a same key is added again, previously stored item is replaced with a new one.
+ *
Multiple items can be added to the shopping cart (e.g. flights, hotelresults, passenger details, ancillaries).
+ *
cartKey parameter is supposed to identify what type of element is added to the cart.
+ *
If item with a given key is added for the first time, it will be normally added and can be later retrieved.
+ *
If item with a same key is added again, previously stored item is replaced with a new one.
*
* @param cartKey identifies what type of item is added to the cart (e.g. flight, seat selection, hotel)
* @param item value which will be associated in the cart with a cartKey
diff --git a/api/_lib/simard-api.js b/api/_lib/simard-api.js
index 764adbd5..32873ba0 100644
--- a/api/_lib/simard-api.js
+++ b/api/_lib/simard-api.js
@@ -1,3 +1,9 @@
+/**
+ * This module contains functions to interact with Simard (payments)
+ *
+ * @module _lib/simard-api
+ */
+
const {createLogger} = require('./logger');
const addDays = require("date-fns/addDays")
const axios = require('axios').default;
@@ -65,7 +71,7 @@ function simulateDeposit(amount, currency) {
logger.error("Guarantee creation failed", error);
reject(error);
});
-
+
});
}
diff --git a/api/_lib/stripe-api.js b/api/_lib/stripe-api.js
index 1b8f7568..b120caf8 100644
--- a/api/_lib/stripe-api.js
+++ b/api/_lib/stripe-api.js
@@ -1,3 +1,8 @@
+/**
+ * Module contains functions used to integrate with Stripe payment API
+ *
+ * @module _lib/stripe-api
+ */
const {STRIPE_CONFIG} = require('./config');
const {createLogger} = require('./logger');
const {getCurrencyByCode} = require('./dictionary-data-cache')
@@ -13,8 +18,8 @@ const PAYMENT_TYPES={CARD:'card'}
/**
* Creates payment intent - that's required before authorizing payment by stripe.
- * For security reasons, server side must future transaction details, before card can be authorized.
- * confirmedOfferId is passed to stripe intent as metadata, so that it can be later on retrieved in a webhook call (after successful payment)
+ *
For security reasons, server side must future transaction details, before card can be authorized.
+ *
confirmedOfferId is passed to stripe intent as metadata, so that it can be later on retrieved in a webhook call (after successful payment)
* @param payment_method_type
* @param amount
* @param currency
diff --git a/api/cart/accommodation.js b/api/cart/accommodation.js
index 0e57af7e..3972553d 100644
--- a/api/cart/accommodation.js
+++ b/api/cart/accommodation.js
@@ -3,12 +3,25 @@ const {sendErrorResponse,ERRORS} = require("../_lib/rest-utils")
const logger = require('../_lib/logger').createLogger('/cart1')
const {decorate} = require('../_lib/decorators');
-const shoppingCartController = async (req, res) => {
+
+/**
+ * @module endpoint /cart/accommodation
+ */
+
+
+/**
+ * /cart/accommodation endpoint handler
+ * This endpoint is used to add/remove/retrieve hotel accommodation to/from the shopping cart
+ * @async
+ */
+
+const cartAccommodationController = async (req, res) => {
let sessionID = req.sessionID;
let shoppingCart = new ShoppingCart(sessionID);
let method = req.method;
let cartItemKey=CART_ITEMKEYS.CONFIRMED_OFFER;
+ //store item in cart
if(method === 'POST') {
let offer = req.body.offer;
if(!validateOffer(res,offer))
@@ -16,10 +29,12 @@ const shoppingCartController = async (req, res) => {
let cart = await shoppingCart.addItemToCart(cartItemKey,offer,0);
res.json({result:"OK"})
}
+ //retrieve contents of cart
else if(method === 'GET') {
let offer = await shoppingCart.getItemFromCart(cartItemKey);
res.json(offer);
}
+ //delete item from cart
else if(method === 'DELETE') {
await shoppingCart.removeItemFromCart(cartItemKey);
res.json({result:"OK"})
@@ -51,5 +66,5 @@ function sendValidationErrorResponse(res, fieldName, validationMessage){
return false;
}
-module.exports = decorate(shoppingCartController);
+module.exports = decorate(cartAccommodationController);
diff --git a/api/cart/cart.js b/api/cart/cart.js
index ec523f4d..6b1945e1 100644
--- a/api/cart/cart.js
+++ b/api/cart/cart.js
@@ -4,7 +4,26 @@ const { sendErrorResponse, ERRORS } = require("../_lib/rest-utils");
const logger = require('../_lib/logger').createLogger('/cart1');
const { decorate } = require('../_lib/decorators');
-const shoppingCartController = async (req, res) => {
+/**
+ * @module endpoint /cart/cart
+ */
+
+
+/**
+ * /cart/cart endpoint handler
+ *
+ * This is a generic endpoint to add/remove/retrieve items from shopping cart.
+ *
More specialized endpoints exist to store specific type of items in cart:
+ *
+ * - /cart/accommodation to add/remove/retrieve hotel stays to/from cart
+ *
- /cart/offer to add/remove/retrieve flight offers to/from cart
+ *
- /cart/passengers to add/remove/retrieve passengers to/from cart
+ *
- /cart/seats to add/remove/retrieve seats to/from cart
+ *
+ *
+ * @async
+ */
+const cartController = async (req, res) => {
let sessionID = req.sessionID;
let shoppingCart = new ShoppingCart(sessionID);
let method = req.method;
@@ -37,5 +56,5 @@ const shoppingCartController = async (req, res) => {
}
-module.exports = decorate(shoppingCartController);
+module.exports = decorate(cartController);
diff --git a/api/cart/offer.js b/api/cart/offer.js
index bc846f77..7a544448 100644
--- a/api/cart/offer.js
+++ b/api/cart/offer.js
@@ -4,7 +4,19 @@ const {sendErrorResponse,ERRORS} = require("../_lib/rest-utils")
const logger = require('../_lib/logger').createLogger('/cart1')
const {decorate} = require('../_lib/decorators');
-const shoppingCartController = async (req, res) => {
+
+/**
+ * @module endpoint /cart/offer
+ */
+
+
+/**
+ * /cart/offer endpoint handler
+ * This endpoint is used to add/remove/retrieve flight offer to/from the shopping cart
+ * @async
+ */
+
+const cartOfferController = async (req, res) => {
let sessionID = req.sessionID;
let shoppingCart = new ShoppingCart(sessionID);
let method = req.method;
@@ -62,5 +74,5 @@ function sendValidationErrorResponse(res, fieldName, validationMessage){
return false;
}
-module.exports = decorate(shoppingCartController);
+module.exports = decorate(cartOfferController);
diff --git a/api/cart/passengers.js b/api/cart/passengers.js
index 9c59689f..44603924 100644
--- a/api/cart/passengers.js
+++ b/api/cart/passengers.js
@@ -4,7 +4,18 @@ const {sendErrorResponse,ERRORS} = require("../_lib/rest-utils")
const logger = require('../_lib/logger').createLogger('/cart1')
const {decorate} = require('../_lib/decorators');
-const shoppingCartController = async (req, res) => {
+
+/**
+ * @module endpoint /cart/passengers
+ */
+
+
+/**
+ * /cart/passengers endpoint handler
+ * This endpoint is used to add/remove/retrieve passenger details to/from the shopping cart
+ * @async
+ */
+const cartPassengersController = async (req, res) => {
let sessionID = req.sessionID;
let shoppingCart = new ShoppingCart(sessionID);
let method = req.method;
@@ -71,5 +82,5 @@ function sendValidationErrorResponse(res, fieldName, validationMessage, paxId){
};
-module.exports = decorate(shoppingCartController);
+module.exports = decorate(cartPassengersController);
diff --git a/api/cart/reprice.js b/api/cart/reprice.js
index 1b2a3d46..6a349df9 100644
--- a/api/cart/reprice.js
+++ b/api/cart/reprice.js
@@ -5,14 +5,18 @@ const {ShoppingCart,CART_ITEMKEYS} = require('../_lib/shopping-cart');
const {sendErrorResponse,ERRORS} = require("../_lib/rest-utils")
const logger = createLogger('/offerSummary')
+/**
+ * @module endpoint /cart/reprice
+ */
+
+
/**
* /cart/reprice controller
* This call should be made to re-price all items from the cart and get a final, binding price
- * @param req
- * @param res
- * @returns {Promise}
+ * @async
*/
-const offerRepriceController = async (req, res) => {
+
+const cartRepriceController = async (req, res) => {
let sessionID=req.sessionID;
let shoppingCart = new ShoppingCart(sessionID);
let offer = await shoppingCart.getItemFromCart(CART_ITEMKEYS.OFFER);
@@ -34,5 +38,5 @@ const offerRepriceController = async (req, res) => {
}
-module.exports = decorate(offerRepriceController);
+module.exports = decorate(cartRepriceController);
diff --git a/api/cart/seats.js b/api/cart/seats.js
index f79e1537..8f001b3b 100644
--- a/api/cart/seats.js
+++ b/api/cart/seats.js
@@ -4,11 +4,23 @@ const { sendErrorResponse, ERRORS } = require("../_lib/rest-utils");
const logger = require('../_lib/logger').createLogger('/cart1');
const { decorate } = require('../_lib/decorators');
-const seatRequestHandler = async (req, res) => {
+
+/**
+ * @module endpoint /cart/seats
+ */
+
+
+
+/**
+ * /cart/seats endpoint handler
+ * This endpoint is used to add/remove/retrieve selected flight seats to/from the shopping cart
+ * @async
+ */
+const cartSeatsController = async (req, res) => {
const shoppingCart = new ShoppingCart(req.sessionID);
switch(req.method) {
- case 'POST':
+ case 'POST':
// Retrieve and check the seats
let seats = req.body;
if(!seats && !Array.isArray(seats)) {
@@ -34,7 +46,7 @@ const seatRequestHandler = async (req, res) => {
res.status(500).json({message:"Failed to add seats to shopping cart"});
});
break;
-
+
default:
logger.warn("Unsupported method:%s",req.method);
res.status(405).json({message:"Method Not Supported"});
@@ -42,4 +54,4 @@ const seatRequestHandler = async (req, res) => {
}
-module.exports = decorate(seatRequestHandler);
\ No newline at end of file
+module.exports = decorate(cartSeatsController);
diff --git a/api/hCheck.js b/api/hCheck.js
index e5320a0c..0f1c5e29 100644
--- a/api/hCheck.js
+++ b/api/hCheck.js
@@ -6,9 +6,16 @@ const {v4} = require('uuid');
const {createLogger} = require('./_lib/logger')
const logger = createLogger('/debug')
+
+/**
+ * @module endpoint /hCheck
+ */
+
+
var getNamespace = require('continuation-local-storage').getNamespace;
var session = getNamespace('ota');
+
const healthCheckController = async (req, res) => {
if(!GENERIC_CONFIG.ENABLE_HEALHCHECK){
return res.status(404).send('');
diff --git a/api/lookup/airportByIata.js b/api/lookup/airportByIata.js
index e70af75b..2ae81ab5 100644
--- a/api/lookup/airportByIata.js
+++ b/api/lookup/airportByIata.js
@@ -4,7 +4,20 @@ const {sendErrorResponse,ERRORS} = require("../_lib/rest-utils")
const dictionary = require("../_lib/dictionary-data-cache")
const logger = createLogger('/lookup/airportByIata')
-const lookupController = async (req, res) => {
+
+/**
+ * @module endpoint /lookup/airportByIata
+ */
+
+
+/**
+ * /lookup/airportByIata endpoint handler
+ * This endpoint is used to search for airports using airport IATA code (e.g. LHR = London Heathrow)
+ * @async
+ */
+
+
+const lookupAirportByIataController = async (req, res) => {
let iataCode = req.query.iata;
if(!validateRequest(iataCode)){
sendErrorResponse(res,400,ERRORS.INVALID_INPUT,"Invalid request parameter, iata="+iataCode,req.body);
@@ -24,4 +37,4 @@ function validateRequest(iataCode){
return false;
return true;
}
-module.exports = decorate(lookupController);
\ No newline at end of file
+module.exports = decorate(lookupAirportByIataController);
diff --git a/api/lookup/airportSearch.js b/api/lookup/airportSearch.js
index a88b7937..eb274dec 100644
--- a/api/lookup/airportSearch.js
+++ b/api/lookup/airportSearch.js
@@ -4,7 +4,21 @@ const {sendErrorResponse,ERRORS} = require("../_lib/rest-utils")
const dictionary = require("../_lib/dictionary-data-cache")
const MAX_RESULTS=30;
const logger = createLogger('/lookup/airportSearch')
-const lookupController = async (req, res) => {
+
+
+/**
+ * @module endpoint /lookup/airportSearch
+ */
+
+
+/**
+ * /lookup/airportSearch endpoint handler
+ * This endpoint is used to search for airports by it's name (or city, metropolitan area, airport name).
+ * @async
+ */
+
+
+const lookupAirportSearchController = async (req, res) => {
let searchquery = req.query.searchquery;
if(!validateRequest(searchquery)){
sendErrorResponse(res,400,ERRORS.INVALID_INPUT,"Invalid request parameter, searchquery="+searchquery,req.body);
@@ -24,4 +38,4 @@ function validateRequest(query){
return false;
return true;
}
-module.exports = decorate(lookupController);
\ No newline at end of file
+module.exports = decorate(lookupAirportSearchController);
diff --git a/api/lookup/citySearch.js b/api/lookup/citySearch.js
index d6d3ca6e..f5b53a24 100644
--- a/api/lookup/citySearch.js
+++ b/api/lookup/citySearch.js
@@ -4,7 +4,21 @@ const {sendErrorResponse,ERRORS} = require("../_lib/rest-utils")
const dictionary = require("../_lib/dictionary-data-cache")
const MAX_RESULTS=30;
const logger = createLogger('/lookup/airportSearch')
-const lookupController = async (req, res) => {
+
+
+/**
+ * @module endpoint /lookup/citySearch
+ */
+
+
+/**
+ * /lookup/citySearch endpoint handler
+ * This endpoint is used to search for locations (cities, places) for hotel search purposes
+ * @async
+ */
+
+
+const lookupCitySearchController = async (req, res) => {
let searchquery = req.query.searchquery;
if(!validateRequest(searchquery)){
sendErrorResponse(res,400,ERRORS.INVALID_INPUT,"Invalid request parameter, searchquery="+searchquery,req.body);
@@ -24,4 +38,4 @@ function validateRequest(query){
return false;
return true;
}
-module.exports = decorate(lookupController);
\ No newline at end of file
+module.exports = decorate(lookupCitySearchController);
diff --git a/api/order/checkout.js b/api/order/checkout.js
index 02ee502b..c20f23e6 100644
--- a/api/order/checkout.js
+++ b/api/order/checkout.js
@@ -6,25 +6,31 @@ const {storeConfirmedOffer} = require('../_lib/mongo-dao');
const {createGuarantee} = require('../_lib/simard-api');
const logger = createLogger('/checkout')
const {sendErrorResponse,ERRORS} = require("../_lib/rest-utils")
-const DEV_MODE=false;
+
+/**
+ * @module endpoint /order/checkout
+ */
+
+
/**
- * /checkoutUrl call handler
- * This creates payment intent to be later paid with a given form of payment
- * Expected request:
- * {
- * type: