const fs = require('fs');
const fse = require('fs-extra');
const path = require('path');
const os = require('os');
const { execSync,spawnSync } = require('child_process');
const xml2js = require('xml2js');
const AdmZip = require('adm-zip');
const moment = require('moment-timezone');
const sqlite3 = require('sqlite3');
const _ = require('lodash'); // TODO
const envfile = require('envfile');
const log = require('loglevel');

const constants = require('../models/constants');
const constantsXml = require('../models/constants-xml');
const enums = require('../models/enums');
const enumsXml = require('../models/enums-xml');
const errorCodes = require('../files/error-codes.json');
const HelpersCtrl = require('./helpers');

const Pos = require('../models/pos');
const PosServ = require('../services/pos');

const PosTransactionCtrl = require('./pos-transaction');
const Employee = require('../models/employee');
const FinancialTransaction = require('../models/financial-transaction');
const FareDistanceConfiguration = require('../models/fare-distance-configuration');
const FareEnvironmentConfiguration = require('../models/fare-environment-configuration');
const FareTable = require('../models/fare-table');
const Translations = require('../models/translations');
const SmartCardActions = require('../models/smart-card');

class PosCtrl {
    constructor () {
        this.model = Pos;
        this.service = new PosServ();
        this.agenda = global.agendaInstance;

        this.posTransactionCtrl = new PosTransactionCtrl();
        this.employeeRoleModel = Employee.EmployeeRole;
        this.employeeWorkTimeModel = Employee.EmployeeWorkTime;
        this.financialTransactionModel = FinancialTransaction.FinancialTransaction;
        this.organizationsModel = FinancialTransaction.Organizations;
        this.fareDistanceModel = FareDistanceConfiguration;
        this.fareEnvironmentModel = FareEnvironmentConfiguration;
        this.fareTableModel = FareTable;
        this.smartCardActionsModel = SmartCardActions;
        this.translationsModel = Translations;
        this.helpersCtrl = new HelpersCtrl();
        this.timeFormat = 'HH:mm';
    }

    currentPosData = async () => {
        try {
            let pos = await this.model.findOne({});
            if (!pos) {
                let posData = {
                    deviceId: process.env.POS_DEVICE_ID,
                    distributionVersion: '',
                    distributionVersionDownloaded: process.env.CLIENT_APIURL,
                    faretableTimestamp: moment.utc(0).toISOString(),
                    alarmStates: [],
                    componentCounter: {
                        smartCardReader: {
                            ok: 0,
                            err: 0
                        },
                        ticketPrinter: {
                            ok: 0
                        },
                        documentScanner: {
                            ok: 0,
                            err: 0
                        }
                    },
                    blockStatus: '',
                    actionListVersion: '',
                    transactionCounter: 0,
                    securityElementId: '',
                    securityElementTransactionCounter: 0,
                    invoiceCounter: 0,
                    invoiceDate: '',
                // device
                    terminalId: 0,
                    deviceSharedId: '',
                    deviceVersion: 'POS',
                    locationExternalId: 0,
                    location: '',
                    locationTranslations: '',
                    locationDescription: '',
                    locationLongitude: 0,
                    locationLatitude: 0,
                    organization: '',
                    organizationDescription: '',
                    organizationExternalId: 0,
                // software
                    appVersion: '26.01.01',
                    projectName: process.env.CLIENT_NAME,
                    projectHomeUrl: process.env.CLIENT_HOME_URL,
                // modules
                    hasTickets: true,
                    hasTicketGroups: false,
                    hasTicketLuggage: false,
                    hasProducts: true,
                    hasPenalties: true,
                    hasAccounts: true,
                    hasCardHolders: false,
                    hasReports: true,
                    hasPosUsers: true,
                    hasDriverCards: false,
                    hasSmartCards: true,
                    hasChangeRiderType: false,
                    hasSmartCardRecreate: false,
                    hasReservation: false,
                    hasSalesOrder: false,
                // account modules
                    hasAccountTopUp: true,
                    hasAccountTickets: true,
                    hasAccountTicketGroups: true,
                    hasAccountTicketLuggage: false,
                    hasAccountProducts: true,
                    hasAccountVerify: true,
                    hasAccountBlock: true,
                    hasAccountBlockDevice: true,
                    hasAccountManage: true,
                    hasAccountLink: true,
                    hasAccountPenalties: true,
                    hasAccountHistory: true,
                    hasAccountSmartcard: true,
                // settings
                    cashFloat: true,
                    virtualKeyboard: false,
                    autoLockMinutes: 10,
                    minAutoLockMinutes: 1,
                    maxAutoLockMinutes: 15,
                    endOfWorkHour: '03:00',
                    employeeRoles: {},
                    employeeLanguage: 'en',
                    firstLanguage: 'en-GB',
                    secondLanguage: 'ar-BH',
                    minPassengers: 1,
                    maxPassengers: 12,
                    minNoOfPassengers: 1,
                    maxNoOfPassengers: 12,
                    maxTickets: 50,
                    minNoOfTickets: 1,
                    maxNoOfTickets: 50,
                    phone: '+000 0000 0000',
                    website: 'website.com',
                    invoiceUpload: false,
                    hasDiscount: false,
                    transitTypeOrder: {},
                    utcOffset: this.helpersCtrl.getHHmm(moment().utcOffset()),
                    displayName: 'station',
                    bilingualPrint: true,
                    riderTypeIcon: {1: 'adult', 2: 'athlete', 3: 'child1', 4: 'child2', 5: 'companion', 6: 'corporate', 7: 'disabled', 8: 'group1', 9: 'group2', 10: 'groupdiscount', 11: 'senior', 12: 'student', 13: 'verifiedadult'},
                // currency
                    currencyPrefix: false,
                    currencyCode: 'EUR',
                    currencyDecimal: 2,
                    showCurrencySymbol: false,
                    currencyDecimalSeparator: ',',
                    currencyThousandsSeparator: '',
                    currencyConvertRate: 1.17,
                    secondCurrencyCode: 'USD',
                    secondCurrencyDecimal: 2,
                    showSecondCurrency: false
                };

                if (['oman'].includes(process.env.CLIENT_NAME)) {
                // device
                    posData.deviceVersion = 'E-PoS XtremeII';
                // modules
                    posData.hasTicketGroups = false;
                    posData.hasTicketLuggage = true;
                    posData.hasDriverCards = true;
                    posData.hasSmartCards = false;
                // account modules
                    posData.hasAccountTicketGroups = true;
                    posData.hasAccountTicketLuggage = true;
                    posData.hasAccountSmartcard = false;
                // settings
                    posData.minPassengers = 2;
                    posData.minNoOfPassengers = 2;
                    posData.phone = '+968 2412 1500';
                    posData.website = 'mwasalat.om';
                    posData.transitTypeOrder = {100: 1, 200: 2, 300: 3, 500: 4, 700: 5, 900: 6};
                    posData.displayName = 'stop';
                    posData.riderTypeIcon = {1: 'adult', 2: 'child1', 3: 'student', 4: 'groupdiscount', 5: 'groupdiscount', 6: 'groupdiscount', 7: 'senior', 8: 'disabled', 9: 'verifiedadult', 10: 'group1', 11: 'group2', 12: 'verifiedadult'};
                // currency
                    posData.currencyCode = 'OMR';
                    posData.currencyDecimal = 3;
                    posData.currencyDecimalSeparator = '.';
                    posData.currencyConvertRate = 9.55;
                    posData.secondCurrencyCode = 'AED';
                    posData.secondCurrencyDecimal = 2;
                }

                if (['kuwait'].includes(process.env.CLIENT_NAME)) {
                // device
                    posData.deviceVersion = 'Wintec AnyPOS600';
                // modules
                    posData.hasTickets = true;
                    posData.hasTicketLuggage = true;
                    posData.hasPenalties = true;
                    posData.hasAccounts = true;
                    posData.hasCardHolders = true;
                    posData.hasDriverCards = false;
                    posData.hasSmartCards = true;
                    posData.hasChangeRiderType = true;
                    posData.hasSmartCardRecreate = true;
                // account modules
                    posData.hasAccountTicketLuggage = true;
                    posData.hasAccountLink = false;
                    posData.hasAccountSmartcard = true;
                // settings
                    posData.cashFloat = false;
                    // posData.secondLanguage = 'ar-KW';
                    posData.maxPassengers = 8;
                    posData.maxNoOfPassengers = 8;
                    posData.maxTickets = 20;
                    posData.maxNoOfTickets = 20;
                    posData.phone = '+965 1882 211';
                    posData.website = 'citybuskw.com';
                    posData.transitTypeOrder = {100: 1, 200: 2, 300: 3, 500: 4, 501: 5};
                    posData.displayName = 'stop';
                    posData.riderTypeIcon = {1: 'adult', 2: 'student', 3: 'verifiedadult', 4: 'corporate', 5: 'disabled'};
                // currency
                    posData.currencyCode = 'KWD';
                    posData.currencyDecimal = 3;
                    posData.currencyDecimalSeparator = '.';
                    posData.currencyConvertRate = 2.79;
                    posData.secondCurrencyCode = 'EUR';
                    posData.secondCurrencyDecimal = 2;
                }

                if (['hafil'].includes(process.env.CLIENT_NAME)) {
                // device
                    posData.deviceVersion = 'Wintec AnyPOS600';
                // modules
                    posData.hasTickets = false;
                    posData.hasTicketGroups = false;
                    posData.hasTicketLuggage = false;
                    posData.hasProducts = false;
                    posData.hasPenalties = false;
                    posData.hasPosUsers = false;
                    posData.hasSmartCards = false;
                    posData.hasReservation = true;
                // account modules
                    posData.hasAccountTopUp = false;
                    posData.hasAccountTickets = false;
                    posData.hasAccountTicketGroups = false;
                    posData.hasAccountTicketLuggage = false;
                    posData.hasAccountProducts = false;
                    posData.hasAccountLink = false;
                    posData.hasAccountPenalties = false;
                    posData.hasAccountSmartcard = false;
                // settings
                    posData.phone = '+966 80030 40155';
                    posData.website = 'darbalwatan.com';
                    posData.transitTypeOrder = {1: 1, 2: 2, 3: 3};
                    posData.displayName = 'stop';
                    // posData.invoiceUpload = true;
                    posData.riderTypeIcon = {1: 'adult', 2: 'child2', 3: 'student', 4: 'senior', 5: 'disabled', 6: 'disabled', 7: 'child1', 8: 'companion', 9: 'verifiedadult', 10: 'verifiedadult', 11: 'verifiedadult'};
                // currency
                    posData.currencyCode = 'SAR';
                    posData.currencyDecimal = 2;
                    posData.currencyDecimalSeparator = '.';
                    posData.currencyConvertRate = 0.23;
                    posData.secondCurrencyCode = 'EUR';
                    posData.secondCurrencyDecimal = 2;
                }

                if (['sharjah'].includes(process.env.CLIENT_NAME)) {
                // device
                    posData.deviceVersion = 'Wintec AnyPOS600';
                // modules
                    posData.hasTickets = true;
                    posData.hasTicketLuggage = true;
                    posData.hasProducts = false;
                    posData.hasPenalties = false;
                    posData.hasAccounts = false;
                    posData.hasCardHolders = true;
                    posData.hasDriverCards = false;
                    posData.hasSmartCards = true;
                // account modules
                    posData.hasAccountTicketLuggage = true;
                    posData.hasAccountLink = false;
                    posData.hasAccountSmartcard = true;
                // settings
                    // posData.secondLanguage = 'ar-KW';
                    posData.maxPassengers = 8;
                    posData.maxNoOfPassengers = 8;
                    posData.maxTickets = 20;
                    posData.maxNoOfTickets = 20;
                    posData.phone = '+971 60052 5252';
                    posData.website = 'srta.gov.ae';
                    posData.transitTypeOrder = {1: 1, 2: 2};
                    posData.displayName = 'stop';
                    posData.riderTypeIcon = {1: 'adult', 2: 'student'};
                // currency
                    posData.currencyPrefix = true;
                    posData.currencyCode = 'AED';
                    posData.currencyDecimal = 2;
                    posData.showCurrencySymbol = true;
                    posData.currencyDecimalSeparator = '.';
                    posData.currencyConvertRate = 0.23;
                    posData.secondCurrencyCode = 'EUR';
                    posData.secondCurrencyDecimal = 2;
                }

                if (['mok'].includes(process.env.CLIENT_NAME)) {
                // device
                    posData.deviceVersion = 'Wintec AnyPOS600';
                // modules
                    posData.hasTickets = true;
                    posData.hasPenalties = false;
                    posData.hasPosUsers = false;
                    posData.hasSmartCards = false;
                // account modules
                    posData.hasAccountLink = false;
                    posData.hasAccountSmartcard = false;
                // settings
                    posData.employeeLanguage = 'sl';
                    posData.firstLanguage = 'sl-SI';
                    posData.secondLanguage = 'en-GB';
                    posData.maxPassengers = 8;
                    posData.maxNoOfPassengers = 8;
                    posData.maxTickets = 20;
                    posData.maxNoOfTickets = 20;
                    posData.phone = '+386 4237 3000';
                    posData.website = 'kranj.si';
                    posData.transitTypeOrder = {1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6};
                    posData.displayName = 'stop';
                    posData.bilingualPrint = false;
                    posData.riderTypeIcon = {1: 'adult', 2: 'child2', 3: 'student', 4: 'adult', 5: 'adult', 6: 'verifiedadult', 7: 'verifiedadult', 8: 'adult', 9: 'adult', 10: 'athlete', 11: 'child2', 12: 'senior', 13: 'adult', 14: 'child2', 15: 'adult'};
                // currency
                    posData.currencyCode = 'EUR';
                    posData.currencyDecimal = 2;
                    posData.currencyConvertRate = 1;
                    posData.secondCurrencyCode = 'EUR';
                    posData.secondCurrencyDecimal = 2;
                }

                pos = await this.model.create(posData);
            }

            pos.developmentMode = process.env.POS_DEVELOPMENT_MODE;

            return global.currentPosData = pos;
        } catch (err) {
            log.error(err);
        }
    }

    currentEmployeeData = async () => {
        try {
            let employee = await this.employeeWorkTimeModel.findOne({logoutTime: moment.utc(0).toISOString()}).sort({loginTime: -1});
            if (!employee) {
                return global.currentEmployeeData = {};
            }
            return global.currentEmployeeData = {
                employeeId: employee.employeeId,
                employeeName: `${employee.firstName} ${employee.lastName}`,
                employeeRoleId: employee.employeeRoleId,
                employeeRoleName: employee.employeeRoleName,
                employeeLoginTime: moment.utc(employee.loginTime).toISOString(),
                employeeStartBalanceAmount: employee.startBalanceAmount,
                employeeEndBalanceAmount: employee.endBalanceAmount,
                employeeSessionId: employee.sessionId
            };
        } catch (err) {
            log.error(err);
        }
    }

    cashAmount = async (req, res) => {
        try {
            const id = req.body.id;
            const amount = req.body.amount * Math.pow(10, global.currentPosData.currencyDecimal);
            const cashOut = req.body.cashOut;

            if (typeof global.currentEmployeeData !== undefined && typeof global.currentEmployeeData === 'object' && Object.keys(global.currentEmployeeData).length === 0 && !global.currentEmployeeData.hasOwnProperty('employeeId')) {
                return true;
            }

            if (cashOut && global.currentEmployeeData.hasOwnProperty('employeeLogoutTime')) {
                return true;
            }

            let cashAmount = {startBalanceAmount: 0};

            if (!cashOut) {
                global.currentEmployeeData.employeeStartBalanceAmount = amount;
                cashAmount = {startBalanceAmount: global.currentEmployeeData.employeeStartBalanceAmount};

                let summaryCashCollected = 0;
                let summaryRevenueCollected = 0;
                let summaryCashCanceled = 0;
                let summaryRevenueCanceled = 0;
                let startBalanceAmount = global.currentEmployeeData.employeeStartBalanceAmount;
                let endBalanceAmount = 0;

                await this.posTransactionCtrl.create({
                    function: 'summaryOpenClose',
                    component: 'ticketPrinter',
                    parameters: {
                        summary: {
                            summaryActivityType: constantsXml.summaryActivityType.openSummary,
                            summaryCashCollected: summaryCashCollected,
                            summaryRevenueCollected: summaryRevenueCollected,
                            summaryCashCanceled: summaryCashCanceled,
                            summaryRevenueCanceled: summaryRevenueCanceled,
                            cashShiftOpen: (global.currentPosData.cashFloat) ? startBalanceAmount : 0,
                            cashShiftClose: (global.currentPosData.cashFloat) ? endBalanceAmount : 0
                        }
                    }
                });

                await this.posTransactionCtrl.create({
                    function: 'cashInOut',
                    component: {
                        deviceType: constantsXml.deviceType.officePos,
                        deviceVersion: global.currentPosData.deviceVersion,
                        deviceSoftwareVersion: global.currentPosData.appVersion,
                        deviceId: global.currentPosData.deviceId,
                        deviceSharedId: global.currentPosData.deviceSharedId,
                        deviceDutyCode: global.currentEmployeeData.employeeSessionId,
                        deviceComponentType: constantsXml.componentType.banknoteCashbox,
                        deviceComponentId: global.currentPosData.deviceId,
                        terminalId: global.currentPosData.terminalId,
                        deviceTransactionCounter: global.currentPosData.transactionCounter,
                        faretableTimestamp: global.currentPosData.faretableTimestamp,
                        operatorRef: global.currentEmployeeData.employeeId,
                        operatorTypeExternalId: global.currentEmployeeData.employeeRoleId,
                        operatorTypeName: global.currentEmployeeData.employeeRoleName,
                        operationMode: constantsXml.operationMode.issuing
                    },
                    parameters: {
                        cash: {
                            cashOperationType: constantsXml.cashOperationType.cashContainerInsert,
                            cashActivityType: constantsXml.cashActivityType.cashCollection,
                            previousBalance: 0,
                            newBalance: 0 + (global.currentPosData.cashFloat) ? startBalanceAmount : 0
                        },
                        collection: {
                            cashCollectionSessionId: global.currentPosData.transactionCounter,
                            cashCollectionEventId: global.currentPosData.transactionCounter,
                            collectedByUserType: constantsXml.collectedByUserType.afcDeviceUser,
                            collectedByEmployeeRef: global.currentEmployeeData.employeeId,
                            collectedByEmployeeRoleNumber: global.currentEmployeeData.employeeRoleId,
                            collectedByEmployeeRoleName: global.currentEmployeeData.employeeRoleName
                        },
                        container: {
                            previousOperationalStatus: constantsXml.cashContainerOperationalStatus.inActive,
                            newOperationalStatus: constantsXml.cashContainerOperationalStatus.active,
                            previousLevelStatusStatus: constantsXml.cashContainerLevelStatus.empty,
                            newLevelStatus: constantsXml.cashContainerLevelStatus.empty,
                            previousBlockStatusStatus: constantsXml.cashContainerBlockStatus.notBlocked,
                            newBlockStatus: constantsXml.cashContainerBlockStatus.notBlocked
                        }
                    }
                });
            } else {
                global.currentEmployeeData.employeeLogoutTime = moment.utc().toISOString();
                global.currentEmployeeData.employeeEndBalanceAmount = amount;
                cashAmount = {endBalanceAmount: global.currentEmployeeData.employeeEndBalanceAmount, diffBalanceAmount: (global.currentEmployeeData.employeeEndBalanceAmount - global.currentEmployeeData.employeeStartBalanceAmount)};

                const employeeFinancialTransactions = await this.financialTransactionModel.find({transactionTime: {$gte: moment.utc(global.currentEmployeeData.employeeLoginTime).toISOString()}});
                const employeeCanceledTransactions = employeeFinancialTransactions.filter((f) => f.activityType == constantsXml.activityType.issueReversal);
                const employeeCanceledTickets = employeeCanceledTransactions.filter((f) => f.ticketId);
                const employeeSaleTransactions = employeeFinancialTransactions.filter((f) => f.activityType != constantsXml.activityType.issueReversal && !employeeCanceledTickets.includes(f.ticketId));

                let summaryCashCollected = employeeSaleTransactions.filter((f) => !f.cardTransactionNo).map((m) => m.farePrice).reduce((p, n) => p + n, 0);
                let summaryRevenueCollected = employeeSaleTransactions.map((m) => m.farePrice).reduce((p, n) => p + n, 0);
                let summaryCashCanceled = employeeCanceledTransactions.filter((f) => !f.cardTransactionNo).map((m) => m.farePrice).reduce((p, n) => p + n, 0);
                let summaryRevenueCanceled = employeeCanceledTransactions.map((m) => m.farePrice).reduce((p, n) => p + n, 0);

                let startBalanceAmount = global.currentEmployeeData.employeeStartBalanceAmount;
                let endBalanceAmount = global.currentEmployeeData.employeeEndBalanceAmount;

                let saleFareItemDetails = {saleFareItemDetail: []};
                let distinctFareItems = employeeSaleTransactions.map((m) => m.fareItemExternalId).filter((v, i, s) => s.indexOf(v) === i);
                for (let i = 0; i < distinctFareItems.length; i++) {

                    let transactionsForProduct = employeeSaleTransactions.filter((f) => f.fareItemExternalId == distinctFareItems[i]);
                    let distinctRiders = transactionsForProduct.map((m) => m.riderExternalId).filter((v, i, s) => s.indexOf(v) === i);
                    for (let j = 0; j < distinctRiders.length; j++) {

                        let transactionsForRider = transactionsForProduct.filter((f) => f.riderExternalId == distinctRiders[j]);
                        let item = transactionsForRider[0];
                        saleFareItemDetails.saleFareItemDetail.push({
                            activityType: constantsXml.activityType[item.activityType],
                            fareItemCategory: constantsXml.fareItemCategory[item.fareItemCategory],
                            fareItemType: constantsXml.fareItemType[item.fareItemType],
                            fareItemExternalID: item.fareItemExternalId,
                            fareItemName: item.fareItemName,
                            passengerRiderTypeExternalID: item.riderExternalId,
                            passengerRiderTypeName: item.riderName,
                            noOfFareItems: transactionsForRider.length,
                            fareItemCashCollected: transactionsForRider.filter((f) => f.cardTransactionNo == null).map((m) => m.farePrice).reduce((p, n) => p + n, 0),
                            fareItemRevenueCollected: transactionsForRider.map((m) => m.farePrice).reduce((p, n) => p + n, 0),
                            currencyCode: global.currentPosData.currencyCode
                        });
                    }
                }

                if (saleFareItemDetails.saleFareItemDetail.length) {

                }

                let summaryPaymentInstrumentItemDetails = {summaryPaymentInstrumentItemDetail: []};
                const prepayCashDetails = employeeSaleTransactions.filter((f) => f.cardTransactionNo == null);
                const prepayDebitCardDetails = employeeSaleTransactions.filter((f) => f.cardTransactionNo != null);

                if (prepayCashDetails.length) {
                    summaryPaymentInstrumentItemDetails.summaryPaymentInstrumentItemDetail.push({
                        paymentInstrument: constantsXml.paymentInstrument.prepayCash,
                        paymentInstrumentTransactions: prepayCashDetails.length,
                        paymentInstrumentRevenueCollected: prepayCashDetails.map((m) => m.farePrice).reduce((p, n) => p + n, 0),
                        currencyCode: global.currentPosData.currencyCode
                    });
                }

                if (prepayDebitCardDetails.length) {
                    summaryPaymentInstrumentItemDetails.summaryPaymentInstrumentItemDetail.push({
                        paymentInstrument: constantsXml.paymentInstrument.prepayDebitCard,
                        paymentInstrumentTransactions: prepayDebitCardDetails.length,
                        paymentInstrumentRevenueCollected: prepayDebitCardDetails.map((m) => m.farePrice).reduce((p, n) => p + n, 0),
                        currencyCode: global.currentPosData.currencyCode
                    });
                }

                await this.posTransactionCtrl.create({
                    function: 'summaryOpenClose',
                    component: 'ticketPrinter',
                    parameters: {
                        summary: {
                            summaryActivityType: constantsXml.summaryActivityType.closeSummary,
                            summaryCashCollected: summaryCashCollected,
                            summaryRevenueCollected: summaryRevenueCollected,
                            summaryCashCanceled: summaryCashCanceled,
                            summaryRevenueCanceled: summaryRevenueCanceled,
                            cashShiftOpen: (global.currentPosData.cashFloat) ? startBalanceAmount : 0,
                            cashShiftClose: (global.currentPosData.cashFloat) ? endBalanceAmount : 0,
                            // summaryPaymentInstrumentItemDetails: summaryPaymentInstrumentItemDetails
                            // saleFareItemDetails: saleFareItemDetails
                        }
                    }
                });

                await this.posTransactionCtrl.create({
                    function: 'cashInOut',
                    component: {
                        deviceType: constantsXml.deviceType.officePos,
                        deviceVersion: global.currentPosData.deviceVersion,
                        deviceSoftwareVersion: global.currentPosData.appVersion,
                        deviceId: global.currentPosData.deviceId,
                        deviceSharedId: global.currentPosData.deviceSharedId,
                        deviceDutyCode: global.currentEmployeeData.employeeSessionId,
                        deviceComponentType: constantsXml.componentType.banknoteCashbox,
                        deviceComponentId: global.currentPosData.deviceId,
                        terminalId: global.currentPosData.terminalId,
                        deviceTransactionCounter: global.currentPosData.transactionCounter,
                        faretableTimestamp: global.currentPosData.faretableTimestamp,
                        operatorRef: global.currentEmployeeData.employeeId,
                        operatorTypeExternalId: global.currentEmployeeData.employeeRoleId,
                        operatorTypeName: global.currentEmployeeData.employeeRoleName,
                        operationMode: constantsXml.operationMode.issuing
                    },
                    parameters: {
                        cash: {
                            cashOperationType: constantsXml.cashOperationType.cashContainerRemoval,
                            cashActivityType: constantsXml.cashActivityType.cashCollection,
                            previousBalance: 0,
                            newBalance: summaryCashCollected - summaryCashCanceled
                        },
                        collection: {
                            cashCollectionSessionId: global.currentEmployeeData.employeeSessionId,
                            cashCollectionEventId: global.currentEmployeeData.employeeSessionId,
                            collectedByUserType: constantsXml.collectedByUserType.afcDeviceUser,
                            collectedByEmployeeRef: global.currentEmployeeData.employeeId,
                            collectedByEmployeeRoleNumber: global.currentEmployeeData.employeeRoleId,
                            collectedByEmployeeRoleName: global.currentEmployeeData.employeeRoleName
                        },
                        container: {
                            previousOperationalStatus: constantsXml.cashContainerOperationalStatus.active,
                            newOperationalStatus: constantsXml.cashContainerOperationalStatus.transportInProgress,
                            previousLevelStatusStatus: constantsXml.cashContainerLevelStatus.full,
                            newLevelStatus: constantsXml.cashContainerLevelStatus.empty,
                            previousBlockStatusStatus: constantsXml.cashContainerBlockStatus.notBlocked,
                            newBlockStatus: constantsXml.cashContainerBlockStatus.notBlocked
                        }
                    }
                });
            }

            const employeeWorkTime = await this.employeeWorkTimeModel.findOneAndUpdate({employeeId: global.currentEmployeeData.employeeId, logoutTime: moment.utc(0).toISOString()},
                cashAmount
            ).sort({loginTime: -1});

            res.status(200).json(employeeWorkTime);
        } catch (err) {
            log.error(err);
            res.status(400).json({ error: err.message });
        }
    }

    updateSettings = async (req, res) => {
        try {
            const settings = req.body;
            const routes = settings.routes;

            await this.model.findOneAndUpdate({ deviceId: global.currentPosData.deviceId }, {
                employeeLanguage: settings.employeeLanguage,
                showSecondCurrency: settings.showSecondCurrency,
                virtualKeyboard: settings.virtualKeyboard,
                autoLockMinutes: settings.autoLockMinutes,
                minPassengers: settings.minPassengers,
                maxPassengers: settings.maxPassengers,
                maxTickets: settings.maxTickets,
                transitTypeOrder: Object.fromEntries(routes.map((k, i) => [[k.externalId], i+1]))
            });

            const posSettings = await this.currentPosData();

            res.status(200).json(posSettings);
        } catch (err) {
            log.error(err);
            res.status(400).json({ error: err.message });
        }
    }

    sendError = async (req, res) => {
        try {
            const peripheralsStatusList = req.body;
            const errorCodesList = errorCodes.errorCodes.filter((f) => f.type == 'peripheral');

            for (let i=0; i<peripheralsStatusList.length; i++) {
                let component = 'component';
                let errorcode = 'errorcode';
                let status = '';
                switch (peripheralsStatusList[i].alarmType) {
                    case 1:
                        component = constantsXml.componentType.camera;
                        errorcode = errorCodesList.find((f) => f.peripheral == 'camera');
                    break;
                    case 2:
                        component = constantsXml.componentType.smartCardReader;
                        errorcode = errorCodesList.find((f) => f.peripheral == 'cardreader');
                    break;
                    case 3:
                        component = constantsXml.componentType.ticketPrinter;
                        errorcode = errorCodesList.find((f) => f.peripheral == 'receiptprinter');
                    break;
                    case 4:
                        component = constantsXml.componentType.qrCodeReader;
                        errorcode = errorCodesList.find((f) => f.peripheral == 'codescanner');
                    break;
                    case 5:
                        component = constantsXml.componentType.documentScanner;
                        errorcode = errorCodesList.find((f) => f.peripheral == 'documentscanner');
                    break;
                    case 6:
                        component = constantsXml.componentType.smartCardPrinter;
                        errorcode = errorCodesList.find((f) => f.peripheral == 'cardprinter');
                    break;
                    case 7:
                        component = constantsXml.componentType.customerDisplay;
                        errorcode = errorCodesList.find((f) => f.peripheral == 'customerdisplay');
                    break;
                    case 8:
                        component = constantsXml.componentType.creditCardReader;
                        errorcode = errorCodesList.find((f) => f.peripheral == 'edcterminal');
                    break;
                    case 9:
                        component = constantsXml.componentType.ticketPrinter;
                        errorcode = errorCodesList.find((f) => f.peripheral == 'luggageprinter');
                    break;
                }

                if (peripheralsStatusList[i].status == 'connected') {
                    const deleted = await this.delAlarmType(errorcode.code);
                    if (deleted) {
                        status = constantsXml.errorStatusType.errorClosed;
                    }
                } else {
                    const raised = await this.addAlarmType(errorcode.code);
                    if (raised) {
                        status = constantsXml.errorStatusType.errorRaised;
                    }
                }

                if (status) {
                    await this.posTransactionCtrl.create({
                        function: 'error',
                        component: component,
                        parameters: {
                            error: {
                                errorCode: errorcode.code,
                                errorStatus: status,
                                errorDetails: errorcode.description
                            }
                        }
                    });
                }
            }

            res.status(200).json();
        } catch (err) {
            log.error(err);
            res.status(400).json({ error: err.message });
        }
    }

    errorCodes = async (req, res) => {
        try {
            const generateErrorCodes = (typeof req.query.generate != 'undefined') ? req.query.generate : false;
            const errorCodesList = errorCodes.errorCodes;

            // http://localhost:1000/api/device/errorCodes?generate=30
            if (generateErrorCodes) {
                await this.generateErrorCodes(generateErrorCodes, errorCodesList);
            }

            res.status(200).json(errorCodesList);
        } catch (err) {
            log.error(err);
            res.status(400).json({ error: err.message });
        }
    }

    generateErrorCodes = async (typeid, errorCodesList) => {
        try {
            log.info('generate-error-codes-start');
            // generate error codes csv import file from file error-codes.json
            const fields = ['Code', 'Type ID', 'Summary', 'Description', 'Severity'];
            let csv = fields.join('\t') + '\n';

            const errorCodesListLen = errorCodesList.length;
            for (let i=0; i<errorCodesListLen; i++) {
                let code = errorCodesList[i][fields[0].toLowerCase()];
                let summary = errorCodesList[i][fields[2].toLowerCase()];
                let description = errorCodesList[i][fields[3].toLowerCase()];
                let severity = errorCodesList[i][fields[4].toLowerCase()];
                let severityCapitalize = severity[0].toUpperCase()+severity.slice(1);

                csv += [code, typeid, summary, description, severityCapitalize].join('\t') + '\n';
            }

            fs.writeFileSync(path.join(process.cwd(), 'files', 'ImportErrorCodes'+typeid+'.csv'), csv, 'utf8');

            log.info('generate-error-codes-end');
        } catch (err) {
            log.error(err);
            res.status(400).json({ error: err.message });
        }
    }

    reboot = async (req, res) => {
        log.info('reboot API');
        spawnSync('shutdown', ['-r', '-f', '-t', 0]);
    }

    shutdown = async (req, res) => {
        log.info('shutdown API');
        spawnSync('shutdown', ['-s', '-f', '-t', 0]);
    }

    restart = async (req, res) => {
        log.info('restart API');
        spawnSync('npm.cmd', ['run', 'service']);
    }

    updateCounter = async (req, res) => {
        const counterId = req.query.counterId;
        const device = await this.model.findOne({});

        let component = 'component';
        let status = 'ok';
        switch (parseInt(counterId)) {
            case 1:
                component = constantsXml.componentType.smartCardReader;
            case 2:
                component = constantsXml.componentType.smartCardReader;
                status = 'err';
            break;
            case 3:
                component = constantsXml.componentType.ticketPrinter;
            break;
            case 4:
                component = constantsXml.componentType.documentScanner;
            case 5:
                component = constantsXml.componentType.documentScanner;
                status = 'err';
            break;
        }

        await this.model.findOneAndUpdate({ deviceId: global.currentPosData.deviceId }, {
            ['componentCounter.'+component+'.'+status]: (device.componentCounter[component][status] == 0) ? 1 : device.componentCounter[component][status]+1
        }, { upsert: true });

        res.status(200).json(true);
    }

    device = async (req, res) => {
        try {
            const currentPosData = global.currentPosData;
            res.status(200).json(currentPosData);
        } catch (err) {
            log.error(err);
            res.status(400).json({ error: err.message });
        }
    }

    pulse = async () => {
        try {
            log.info('pos-pulse-start');
            await this.currentPosData();
            await this.currentEmployeeData();
            // heartbeat
            const newPosDataHeartbeat = await this.heartbeat(global.currentPosData);
            if (!newPosDataHeartbeat) {
                log.info('pos-pulse-failed');
                return;
            }
            // check transactions folders
            await this.checkTransactionsFolders();
            // download distribution if changed
            log.info('distribution-active', global.currentPosData.distributionVersion);
            if (global.currentPosData.distributionVersion != newPosDataHeartbeat.distributionVersion) {
                log.info('pos-pulse-distribution');
                await this.distributionDownload(global.currentPosData, newPosDataHeartbeat);
                await this.distributionStatus(global.currentPosData, newPosDataHeartbeat);
                await this.distributionClean();
                await this.distributionSwitch(false);
                global.io.emit('notify frontend', 'distributionPending');
            }
            let newPosData = {};
            // download and update action list if changed
            log.info('actionList-active', global.currentPosData.actionListVersion);
            if (global.currentPosData.actionListVersion != newPosDataHeartbeat.actionListVersion) {
                log.info('pos-pulse-actionList');
                await this.actionListDownload(global.currentPosData, newPosDataHeartbeat);
                newPosData.actionListVersion = newPosDataHeartbeat.actionListVersion;
            }
            // update block status if changed
            if (global.currentPosData.blockStatus != newPosDataHeartbeat.blockStatus) {
                log.info('pos-pulse-blockStatus');
                newPosData.blockStatus = newPosDataHeartbeat.blockStatus;
            }
            // update device assignments if changed
            const newPosDataAssignments = await this.assignments(global.currentPosData);
            if (global.currentPosData.terminalId != newPosDataAssignments.terminalId && !!newPosDataAssignments.terminalId) {
                newPosData.terminalId = newPosDataAssignments.terminalId;
            }
            if (global.currentPosData.deviceSharedId != newPosDataAssignments.deviceSharedId && !!newPosDataAssignments.deviceSharedId) {
                newPosData.deviceSharedId = newPosDataAssignments.deviceSharedId;
            }
            if (global.currentPosData.locationExternalId != newPosDataAssignments.locationReferenceNumber && !!newPosDataAssignments.locationReferenceNumber) {
                newPosData.locationExternalId = (!Number.isNaN(newPosDataAssignments.locationReferenceNumber)) ? newPosDataAssignments.locationReferenceNumber : 0;
            }
            if (global.currentPosData.location != newPosDataAssignments.locationName && !!newPosDataAssignments.locationName) {
                newPosData.location = newPosDataAssignments.locationName;
            }
            if (newPosDataAssignments.locationNameTranslations && global.currentPosData.locationTranslations != newPosDataAssignments.locationNameTranslations.translation._) {
                newPosData.locationTranslations = (newPosDataAssignments.locationNameTranslations.translation._ != '') ? newPosDataAssignments.locationNameTranslations.translation._ : newPosDataAssignments.locationName;
            }
            if (global.currentPosData.locationDescription != newPosDataAssignments.locationDescription && global.currentPosData.locationDescription != newPosDataAssignments.locationName && !!newPosDataAssignments.locationDescription && !!newPosDataAssignments.locationName) {
                newPosData.locationDescription = (newPosDataAssignments.locationDescription != '') ? newPosDataAssignments.locationDescription : newPosDataAssignments.locationName;
            }
            if (global.currentPosData.locationLongitude != newPosDataAssignments.locationLongitude && !!newPosDataAssignments.locationLongitude) {
                newPosData.locationLongitude = newPosDataAssignments.locationLongitude;
            }
            if (global.currentPosData.locationLatitude != newPosDataAssignments.locationLatitude && !!newPosDataAssignments.locationLatitude) {
                newPosData.locationLatitude = newPosDataAssignments.locationLatitude;
            }
            if (global.currentPosData.organization != newPosDataAssignments.organizationName && !!newPosDataAssignments.organizationName) {
                newPosData.organization = newPosDataAssignments.organizationName;
            }
            if (global.currentPosData.organizationDescription != newPosDataAssignments.organizationDescription && !!newPosDataAssignments.organizationDescription) {
                newPosData.organizationDescription = newPosDataAssignments.organizationDescription;
            }
            if (global.currentPosData.organizationExternalId != newPosDataAssignments.organizationId && 'default' != newPosDataAssignments.organizationId) {
                newPosData.organizationExternalId = (!Number.isNaN(newPosDataAssignments.organizationId)) ? newPosDataAssignments.organizationId : 0;
            }
            // organizations
            const organizations = await this.organizationsModel.find({}, {_id: 0, __v: 0}).lean();
            const organizationsList = await this.service.getOrganizations();
            if (organizationsList && !_.isEqual(organizations, organizationsList)) {
                log.info('pos-pulse-organizations');
                await this.organizationsModel.deleteMany({});
                await this.organizationsModel.insertMany(organizationsList);
            }
            // update deviceId if changed
            if (global.currentPosData.deviceId != process.env.POS_DEVICE_ID) {
                log.info('pos-pulse-deviceId');
                newPosData.deviceId = process.env.POS_DEVICE_ID;
            }
            // update appVersion if changed
            log.info('applicationVersion-active', global.currentPosData.appVersion);
            if (global.currentPosData.appVersion != process.env.POS_APP_VERSION && process.env.hasOwnProperty('POS_APP_VERSION')) {
                log.info('pos-pulse-appVersion');
                newPosData.appVersion = process.env.POS_APP_VERSION;
            }
            // update projectName if changed
            if (global.currentPosData.projectName != process.env.CLIENT_NAME) {
                log.info('pos-pulse-projectName');
                newPosData.projectName = process.env.CLIENT_NAME;
            }
            // update distributionVersionDownloaded if changed
            if (global.currentPosData.distributionVersionDownloaded != process.env.CLIENT_APIURL) {
                log.info('pos-pulse-distributionVersionDownloaded');
                newPosData.distributionVersionDownloaded = process.env.CLIENT_APIURL;
            }
            // update autoLockMinutes for development mode
            if (process.env.POS_DEVELOPMENT_MODE == 'true') {
                if (global.currentPosData.autoLockMinutes != 100) {
                    log.info('pos-pulse-autoLockMinutesDevelopmentMode');
                    newPosData.autoLockMinutes = 100;
                }
            }
            // update endOfWorkHour if changed
            const employeeAutoLogout = await this.agenda.jobs({name: 'employee-logout'});
            const endOfWorkTimeRun = moment.utc(employeeAutoLogout[0].attrs.nextRunAt).format(this.timeFormat);
            const endOfWorkTimeNow = moment(global.currentPosData.endOfWorkHour, this.timeFormat).utc().format(this.timeFormat);
            if (endOfWorkTimeRun != endOfWorkTimeNow) {
                log.info('pos-pulse-endOfWorkHour');
                await this.agenda.cancel({name: 'employee-logout'});
                await this.agenda.schedule('at '+global.currentPosData.endOfWorkHour, 'employee-logout');
            }
            if (['kuwait','hafil'].includes(process.env.CLIENT_NAME)) {
                if (!employeeAutoLogout[0].attrs.disabled) {
                    log.info('pos-pulse-endOfWorkHour-cancel');
                    await this.agenda.disable({name: 'employee-logout'});
                }
            }
            // if something changed update database
            if (Object.keys(newPosData).length !== 0) {
                log.info('pos-pulse-posData', newPosData);
                await this.model.findOneAndUpdate({deviceId: global.currentPosData.deviceId}, newPosData);
                if (Object.keys(newPosData).find((k) => ['blockStatus'].includes(k))) {
                    global.io.emit('notify frontend', newPosData.blockStatus);
                }
                if (Object.keys(newPosData).find((k) => ['terminalId', 'deviceSharedId', 'location', 'locationExternalId', 'locationLongitude', 'locationLatitude', 'organization', 'organizationDescription', 'organizationExternalId', 'appVersion'].includes(k))) {
                    global.io.emit('notify frontend', 'location');
                }
            }
            // check if commands were sent to device
            const newPosCommands = await this.getCommands(global.currentPosData);
            if (newPosCommands.length !== 0) {
                log.info('pos-pulse-commands');
                await this.executeCommands(newPosCommands);
            }
            // if nobody logged in switch pending config
            if (typeof global.currentEmployeeData !== undefined && typeof global.currentEmployeeData === 'object' && Object.keys(global.currentEmployeeData).length === 0 && !global.currentEmployeeData.hasOwnProperty('employeeId')) {
                await this.switchConfig();
            } else {
                await this.currentPosData();
            }
            log.info('pos-pulse-end');
        } catch (err) {
            log.error(err);
        }
    }

    distributionSwitch = async (shouldSwitch) => {
        try {
            const ddc = await this.fareEnvironmentModel.Configuration.countDocuments({});
            if (ddc == 1 || shouldSwitch) {
                // delete old distribution data if exists
                await this.distributionRemove();
                // activate pending distribution
                const activeDistribution = await this.fareEnvironmentModel.Configuration.findOneAndUpdate({isActive: false, isPending: true, activationDate: {$lte: moment.utc().toISOString()}}, {
                    isActive: true,
                    isPending: false
                }, {new: true}).sort({activationDate: -1});

                if (activeDistribution.isActive == true) {
                    // deactivate previous distribution
                    const previousDistribution = await this.fareEnvironmentModel.Configuration.findOneAndUpdate({isActive: true, isPending: false, _id: {$ne: activeDistribution._id}}, {
                        isActive: false,
                        isPending: false
                    }, {new: true});

                    await this.model.findOneAndUpdate({deviceId: global.currentPosData.deviceId}, {
                        distributionVersion: activeDistribution.distributionVersion,
                        faretableTimestamp: moment.utc(activeDistribution.faretableTimestamp).toISOString()
                    });
                    global.io.emit('notify frontend', 'location');

                    // software update from distribution
                    await this.softwarePkgSwitch(activeDistribution.softwareVersion);
                }
            }
        } catch (err) {
            log.error(err);
        }
    }

    switchConfig = async () => {
        try {
            const pendingConfig = await this.fareEnvironmentModel.Configuration.findOne({isActive: false, isPending: true, activationDate: {$lte: moment.utc().toISOString()}});
            if (pendingConfig) {
                log.info('distribution-switch', pendingConfig.distributionVersion);
                await this.distributionSwitch(true);
                await this.currentPosData();
            }
        } catch (err) {
            log.error(err);
        }
    }

    heartbeat = async (current) => {
        try {
            const heartbeat = {
                heartbeat: {
                    deviceId: current.deviceId,
                    distributionVersion: current.distributionVersion,
                    alarmStates: current.alarmStates.map(i => {return {alarmState: i.alarmState}}),
                    blockStatus: current.blockStatus
                }
            };

            const heartbeatResponse = await this.service.heartbeat(heartbeat);

            return (heartbeatResponse && !heartbeatResponse.hasOwnProperty('head')) ? heartbeatResponse : false;
        } catch (err) {
            log.error(err);
        }
    }

    distribution = async (heartbeat) => {
        try {
            const distributionDownloadResponse = await this.service.distributionDownload(heartbeat.distributionVersion);

            return (distributionDownloadResponse && !distributionDownloadResponse.hasOwnProperty('head')) ? distributionDownloadResponse : false;
        } catch (err) {
            log.error(err);
        }
    }

    actionList = async (current, heartbeat) => {
        try {
            const device = {
                device: {
                    deviceId: current.deviceId
                }
            };

            return await this.service.actionList(device, heartbeat.actionListVersion);
        } catch (err) {
            log.error(err);
        }
    }

    distributionStatus = async (current, heartbeat) => {
        try {
            const reportDistributionStatus = {
                reportDistributionStatus: {
                    deviceId: current.deviceId,
                    distributionVersion: heartbeat.distributionVersion,
                    upgradeState: constants.distributionStatus.finishedDownload
                }
            };

            return await this.service.distributionStatus(reportDistributionStatus);
        } catch (err) {
            log.error(err);
        }
    }

    assignments = async (current) => {
        try {
            const deviceAssignments = {
                deviceAssignments: {
                    deviceId: current.deviceId
                }
            };

            return await this.service.assignments(deviceAssignments);
        } catch (err) {
            log.error(err);
        }
    }

    checkTransactionsFolders = async () => {
        try {
            const errorCodesList = errorCodes.errorCodes.filter(i => i.type == 'transaction');
            const folders = [
                { path: path.join(process.cwd(), process.env.FOLDER_TRANSACTIONS_UNSENT), alarmType: errorCodesList.find((f) => f.transaction == 'unsent').code },
                { path: path.join(process.cwd(), process.env.FOLDER_TRANSACTIONS_CORRUPTED), alarmType: errorCodesList.find((f) => f.transaction == 'corrupted').code },
                { path: path.join(process.cwd(), process.env.FOLDER_TRANSACTIONS_UNKNOWN), alarmType: errorCodesList.find((f) => f.transaction == 'unknown').code }
            ];
            for (let i=0; i<folders.length; i++) {
                if (fs.readdirSync(folders[i].path).length == 0) {
                    await this.delAlarmType(folders[i].alarmType);
                } else {
                    await this.addAlarmType(folders[i].alarmType);
                }
            }
        } catch (err) {
            log.error(err);
        }
    }

    addAlarmType = async (alarmType) => {
        try {
            if (typeof global.currentPosData == undefined) {
                await this.currentPosData();
            }

            return await this.model.findOneAndUpdate({deviceId: (global.currentPosData) ? global.currentPosData.deviceId : process.env.POS_DEVICE_ID, 'alarmStates.alarmState': {$nin: [alarmType]}}, {
                $push: {
                    alarmStates: {
                        alarmState: alarmType
                    }
                }
            }, {new: true});
        } catch (err) {
            log.error(err);
        }
    }

    delAlarmType = async (alarmType) => {
        try {
            if (typeof global.currentPosData == undefined) {
                await this.currentPosData();
            }

            return await this.model.findOneAndUpdate({deviceId: (global.currentPosData) ? global.currentPosData.deviceId : process.env.POS_DEVICE_ID, 'alarmStates.alarmState': {$in: [alarmType]}}, {
                $pull: {
                    alarmStates: {
                        alarmState: alarmType
                    }
                }
            }, {new: false});
        } catch (err) {
            log.error(err);
        }
    }

    parseXmltoJson = async (xmlPath) => {
        try {
            return await xml2js.parseStringPromise(fs.readFileSync(xmlPath, 'utf8'), { attrkey: 'att', charkey: 'text', explicitRoot: false, explicitArray: false, mergeAttrs: true });
        } catch (err) {
            log.error(err);
        }
    }

    distributionDownload = async (current, heartbeat) => {
        try {
            ////////////////////////////////////////// DISTRIBUTION ALREADY SAVED //////////////////////////////////////////
            const ddc = await this.fareEnvironmentModel.Configuration.findOne({distributionVersion: heartbeat.distributionVersion});
            if (ddc) {
                log.info('distribution-skiped', heartbeat.distributionVersion);
                if (ddc.isActive != true) {
                    await this.fareEnvironmentModel.Configuration.findOneAndUpdate({distributionVersion: heartbeat.distributionVersion}, {
                        isPending: true
                    });
                }
                return;
            }

            ////////////////////////////////////////// DISTRIBUTION FOLDER //////////////////////////////////////////
            let deviceMnemonic = process.env.CLIENT_DEVICE_MNEMONIC;
            let distributionFilesPath = path.join(process.cwd(), process.env.FOLDER_DISTRIBUTION);
            let distributionZipFile;
            const distributionLocalZipFile = fs.readdirSync(distributionFilesPath).filter((file) => file.startsWith('DISTRIBUTION_'+deviceMnemonic) && file.endsWith('.zip'))[0];

            if (distributionLocalZipFile) {
                log.info('distribution-zip-file', 'Local');
                distributionZipFile = fs.readFileSync(path.join(distributionFilesPath, distributionLocalZipFile));
            } else {
                log.info('distribution-zip-file', 'Remote', heartbeat.distributionVersion);
                distributionZipFile = await this.distribution(heartbeat);
                if (!distributionZipFile) {
                    log.info('distribution-zip-file-error');
                    return;
                }
            }

            ////////////////////////////////////////// DISTRIBUTION FILE //////////////////////////////////////////
            let distributionFile = new AdmZip(distributionZipFile);
            distributionFile.extractAllTo(distributionFilesPath, true);

            ////////////////////////////////////////// POS FOLDER //////////////////////////////////////////

            ////////////////// software package unzip //////////////////
            let softwarePkgFile = fs.readdirSync(path.join(distributionFilesPath, deviceMnemonic)).find((file) => file.endsWith('.zip'));
            if (softwarePkgFile) {
                let softwarePkgZip = new AdmZip(path.join(distributionFilesPath, deviceMnemonic, softwarePkgFile));
                softwarePkgZip.extractAllTo(path.join(distributionFilesPath), true);
            }

            let softwarePkgFolder = fs.readdirSync(path.join(distributionFilesPath)).find((folder) => folder.toLowerCase().startsWith('pos_software_pkg_'));
            let softwarePkgVersion = (softwarePkgFolder) ? softwarePkgFolder.toLowerCase().split('pos_software_pkg_')[1] : global.currentPosData.appVersion;

            ////////////////// device settings //////////////////
            let deviceSettingsFile = fs.readdirSync(path.join(distributionFilesPath, deviceMnemonic)).filter((file) => file.toLowerCase().startsWith(deviceMnemonic.toLowerCase()+'_config-') && !file.includes('v2') && file.endsWith('.xml'))[0].toString();
            let deviceSettingsFileDate = (deviceSettingsFile.toLowerCase().split(deviceMnemonic.toLowerCase()+'_config-')[1]).split('_')[0];
            let deviceSettings = await this.parseXmltoJson(path.join(distributionFilesPath, deviceMnemonic, deviceSettingsFile));

            ////////////////// employee roles permissions //////////////////
            let employeeRolePremissionsFile = fs.readdirSync(path.join(distributionFilesPath, deviceMnemonic)).filter((file) => file.toLowerCase().startsWith(deviceMnemonic.toLowerCase()+'_employee_role_permissions-') && file.endsWith('.xml'))[0].toString();
            let employeeRolePremissionsDate = (employeeRolePremissionsFile.toLowerCase().split(deviceMnemonic.toLowerCase()+'_employee_role_permissions-')[1]).split('_')[0];
            let employeeRolePremissions = await this.parseXmltoJson(path.join(distributionFilesPath, deviceMnemonic, employeeRolePremissionsFile));

            let employeeRoles = [];
            let employeeRolesSettings = {maintenance: [], supervisor: [], csa: []};
            for (const [role, roleId] of Object.entries(deviceSettings)) {
                if (role.toLowerCase().includes('role')) {
                    let roleName = role.split('_')[1];
                    let roleLongName = constants.employeeRolesLong[roleName.toLowerCase()];
                    employeeRoles.push({
                        fareEnvironmentConfigurationId: 0,
                        employeeRoleId: roleId,
                        employeeRoleName: roleName,
                        employeeRoleLongName: roleLongName
                    });
                    employeeRolesSettings[roleName.toLowerCase()] = parseInt(roleId);
                }
            }

            if (employeeRolePremissions.hasOwnProperty('permissionsWithRolesGranted') && employeeRolePremissions.permissionsWithRolesGranted.hasOwnProperty('permissionWithRolesGranted')) {
                let employees = employeeRolePremissions.permissionsWithRolesGranted.permissionWithRolesGranted.filter(f => f.employeeRoles).map(p => p.employeeRoles.employeeRole).flat(2);
                let employeeR = [...new Map(employees.map(item => [item.externalId, item])).values()];

                for (let i=0; i<employeeR.length; i++) {
                    let roleName = (employeeR[i].name).match(/\b\w/g);
                    employeeRoles.push({
                        fareEnvironmentConfigurationId: 0,
                        employeeRoleId: employeeR[i].externalId,
                        employeeRoleName: employeeR[i].name,
                        employeeRoleLongName: employeeR[i].name + ' Employee Role'
                    });
                    employeeRolesSettings[((employeeR[i].name).match(/maintenance|supervisor/i)) ? (employeeR[i].name).toLowerCase().match(/maintenance|supervisor/i).shift() : 'csa'].push(parseInt(employeeR[i].externalId));
                }
            }

            await this.model.findOneAndUpdate({deviceId: current.deviceId}, {
                autoLockMinutes: (deviceSettings.AutoLockMaxMinutes < global.currentPosData.autoLockMinutes) ? deviceSettings.AutoLockMaxMinutes : global.currentPosData.autoLockMinutes,
                maxAutoLockMinutes: (deviceSettings.AutoLockMaxMinutes) ? deviceSettings.AutoLockMaxMinutes : global.currentPosData.maxAutoLockMinutes,
                endOfWorkHour: (deviceSettings.EndOfWorkTimeHHMM) ? deviceSettings.EndOfWorkTimeHHMM : global.currentPosData.endOfWorkHour,
                currencyConvertRate: (deviceSettings.OMRtoAED) ? deviceSettings.OMRtoAED : global.currentPosData.currencyConvertRate,
                phone: (deviceSettings.Phone) ? deviceSettings.Phone : global.currentPosData.phone,
                website: (deviceSettings.Website) ? deviceSettings.Website : global.currentPosData.website,
                minPassengers: (deviceSettings.MinNumberOfPassangers) ? deviceSettings.MinNumberOfPassangers : global.currentPosData.minNoOfPassengers,
                maxPassengers: (deviceSettings.MaxNumberOfPassangers) ? deviceSettings.MaxNumberOfPassangers : global.currentPosData.maxNoOfPassengers,
                minNoOfPassengers: (deviceSettings.MinNumberOfPassangers) ? deviceSettings.MinNumberOfPassangers : global.currentPosData.minNoOfPassengers,
                maxNoOfPassengers: (deviceSettings.MaxNumberOfPassangers) ? deviceSettings.MaxNumberOfPassangers : global.currentPosData.maxNoOfPassengers,
                maxTickets: (deviceSettings.MaxNumberOfTickets) ? deviceSettings.MaxNumberOfTickets : global.currentPosData.maxNoOfTickets,
                maxNoOfTickets: (deviceSettings.MaxNumberOfTickets) ? deviceSettings.MaxNumberOfTickets : global.currentPosData.maxNoOfTickets,
                invoiceUpload: (deviceSettings.InvoiceUpload) ? (String(deviceSettings.InvoiceUpload).toLowerCase() === 'true') : global.currentPosData.invoiceUpload,
                employeeRoles: employeeRolesSettings
            });

            if (global.currentPosData.endOfWorkHour != deviceSettings.EndOfWorkTimeHHMM) {
                await this.agenda.cancel({name: 'employee-logout'});
                await this.agenda.schedule('at '+deviceSettings.EndOfWorkTimeHHMM, 'employee-logout');
            }

            ////////////////////////////////////////// FARE FOLDER //////////////////////////////////////////

            ////////////////// fare table unzip //////////////////
            let fareTableFile = fs.readdirSync(path.join(distributionFilesPath, 'fare')).filter((file) => file.endsWith('.zip'))[0].toString();
            let fareTableZip = new AdmZip(path.join(distributionFilesPath, 'fare', fareTableFile));
            fareTableZip.extractAllTo(path.join(distributionFilesPath, 'fare'), true);

            ////////////////// bmps folder //////////////////
            let fareTableBmpsFolder = fs.readdirSync(path.join(distributionFilesPath, 'fare')).find((folder) => folder.startsWith('bmps_'));
            if (fareTableBmpsFolder) {
                let fareTableBmps = fs.readdirSync(path.join(distributionFilesPath, 'fare', fareTableBmpsFolder));
                if (!softwarePkgFolder) {
                    fs.mkdirSync(path.join(distributionFilesPath, 'pos_software_pkg_'+softwarePkgVersion));
                    softwarePkgFolder = 'pos_software_pkg_'+softwarePkgVersion;
                }
                for (let i=0; i<fareTableBmps.length; i++) {
                    fs.renameSync(path.join(distributionFilesPath, 'fare', fareTableBmpsFolder, fareTableBmps[i]), path.join(distributionFilesPath, softwarePkgFolder, fareTableBmps[i]));
                }
            }

            ////////////////// fare environment configuration //////////////////
            let fareEnvironmentConfigurations = await this.parseXmltoJson(path.join(distributionFilesPath, 'fare', 'fareEnvironmentConfiguration.xml'));
            let fareEnvironmentConfigurationsData = fareEnvironmentConfigurations['fareEnvironmentConfigurations']['fareEnvironmentConfiguration'];
            let fareEnvironmentConfigurationsDataLen = (Array.isArray(fareEnvironmentConfigurationsData)) ? fareEnvironmentConfigurationsData.length : 1;
            if (fareEnvironmentConfigurationsDataLen == 1) { fareEnvironmentConfigurationsData[0] = fareEnvironmentConfigurationsData; }
            for (let e=0; e<fareEnvironmentConfigurationsDataLen; e++) {
                let fareEnvironmentConfigurationData = fareEnvironmentConfigurationsData[e];
                log.info('fareEnvironment activationDate', fareEnvironmentConfigurationData.activationDate);

                const fareEnvironmentConfigurationInserted = await this.fareEnvironmentModel.Configuration.insertMany({
                    fareEnvironmentConfigurationId: fareEnvironmentConfigurationData.id,
                    name: fareEnvironmentConfigurationData.name,
                    activationDate: moment.utc(fareEnvironmentConfigurationData.activationDate).toISOString(),
                    creationDate: moment.utc(fareEnvironmentConfigurations.dateCreated).toISOString(),
                    faretableTimestamp: moment.utc(0).toISOString(),
                    distributionVersion: heartbeat.distributionVersion,
                    businessDayStart: fareEnvironmentConfigurations.businessDayStart,
                    softwareVersion: softwarePkgVersion,
                    isActive: false,
                    isPending: true
                }, {new: true});

                await this.model.findOneAndUpdate({deviceId: current.deviceId}, {
                    utcOffset: moment().tz(fareEnvironmentConfigurations.applicationTimezone).format('Z'), // "+03:00"
                    currencyCode: fareEnvironmentConfigurations.currencyCodeType,
                    currencyDecimal: fareEnvironmentConfigurations.centsDecimalPlaces
                });

                await this.employeeRoleModel.insertMany(employeeRoles);

                await this.employeeRoleModel.updateMany({fareEnvironmentConfigurationId: 0}, {
                    fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id
                });

                //log.info('distribution-insert', heartbeat.distributionVersion, fareEnvironmentConfigurationInserted[0]._id);

                // fare environment configuration - transit types
                let eTransitTypes = [];
                if (fareEnvironmentConfigurationData['transitTypes'] && fareEnvironmentConfigurationData['transitTypes'].hasOwnProperty('transitType')) {
                    let eTransitTypesData = fareEnvironmentConfigurationData['transitTypes']['transitType'];
                    let eTransitTypesLen = (Array.isArray(eTransitTypesData)) ? eTransitTypesData.length : 1;
                    if (eTransitTypesLen == 1) { eTransitTypesData[0] = eTransitTypesData; }
                    log.info('e_TransitTypes', eTransitTypesLen);
                    for (let i=0; i<eTransitTypesLen; i++) {
                        eTransitTypes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: eTransitTypesData[i].externalId,
                            shortNumber: eTransitTypesData[i].shortNumber,
                            name: eTransitTypesData[i].name,
                            description: eTransitTypesData[i].description,
                            fareSystemModel: eTransitTypesData[i].fareSystemModel,
                            routeSystemType: eTransitTypesData[i].routeSystemType,
                            nameTranslations: (eTransitTypesData[i].hasOwnProperty('nameTranslations') && eTransitTypesData[i].nameTranslations != '') ? eTransitTypesData[i].nameTranslations.nameTranslation.text : '',
                            routes: (eTransitTypesData[i].hasOwnProperty('routes')) ? eTransitTypesData[i].routes.route : [],
                            zones: (eTransitTypesData[i].hasOwnProperty('zones')) ? eTransitTypesData[i].zones.zone : []
                        });
                    }
                }
                // fare environment configuration - zone types
                let eZoneTypes = [];
                if (fareEnvironmentConfigurationData['zoneTypes'] && fareEnvironmentConfigurationData['zoneTypes'].hasOwnProperty('zoneType')) {
                    let eZoneTypesData = fareEnvironmentConfigurationData['zoneTypes']['zoneType'];
                    let eZoneTypesLen = (Array.isArray(eZoneTypesData)) ? eZoneTypesData.length : 1;
                    if (eZoneTypesLen == 1) { eZoneTypesData[0] = eZoneTypesData; }
                    log.info('e_ZoneTypes', eZoneTypesLen);
                    for (let i=0; i<eZoneTypesLen; i++) {
                        eZoneTypes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            name: eZoneTypesData[i].name,
                            externalId: eZoneTypesData[i].externalId,
                            usedForPeriodPassRestriction: (eZoneTypesData[i].usedForPeriodPassRestriction != '') ? eZoneTypesData[i].usedForPeriodPassRestriction : false,
                            usedForStoredValueInterchange: (eZoneTypesData[i].usedForStoredValueInterchange != '') ? eZoneTypesData[i].usedForStoredValueInterchange : false,
                            nameTranslations: (eZoneTypesData[i].hasOwnProperty('nameTranslations') && eZoneTypesData[i].nameTranslations != '') ? eZoneTypesData[i].nameTranslations.nameTranslation.text : '',
                            zones: (eZoneTypesData[i].hasOwnProperty('zones')) ? eZoneTypesData[i].zones.zone : []
                        });
                    }
                }
                // fare environment configuration - zones
                let eZones = [];
                if (fareEnvironmentConfigurationData['zones'] && fareEnvironmentConfigurationData['zones'].hasOwnProperty('zone')) {
                    let eZonesData = fareEnvironmentConfigurationData['zones']['zone'];
                    let eZonesLen = (Array.isArray(eZonesData)) ? eZonesData.length : 1;
                    if (eZonesLen == 1) { eZonesData[0] = eZonesData; }
                    log.info('e_Zones', eZonesLen);
                    for (let i=0; i<eZonesLen; i++) {
                        eZones.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            name: eZonesData[i].name,
                            externalId: eZonesData[i].externalId,
                            sharedCode: eZonesData[i].sharedCode,
                            shortNumber: eZonesData[i].shortNumber,
                            nameTranslations: (eZonesData[i].hasOwnProperty('nameTranslations') && eZonesData[i].nameTranslations != '') ? eZonesData[i].nameTranslations.nameTranslation.text : '',
                            stopsInZone: (eZonesData[i].hasOwnProperty('stopsInZone')) ? eZonesData[i].stopsInZone.stopInZone : []
                        });
                    }
                }
                // fare environment configuration - routes
                let eRoutes = [];
                if (fareEnvironmentConfigurationData['routes'] && fareEnvironmentConfigurationData['routes'].hasOwnProperty('route')) {
                    let eRoutesData = fareEnvironmentConfigurationData['routes']['route'];
                    let eRoutesLen = (Array.isArray(eRoutesData)) ? eRoutesData.length : 1;
                    if (eRoutesLen == 1) { eRoutesData[0] = eRoutesData; }
                    log.info('e_Routes', eRoutesLen);
                    for (let i=0; i<eRoutesLen; i++) {
                        eRoutes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            avlId: eRoutesData[i].avlId,
                            externalId: eRoutesData[i].externalId,
                            name: eRoutesData[i].name,
                            destination: eRoutesData[i].destination,
                            variant: eRoutesData[i].variant,
                            direction: eRoutesData[i].direction,
                            version: eRoutesData[i].version,
                            code: eRoutesData[i].code,
                            codeCustomer: eRoutesData[i].codeCustomer,
                            validFrom: moment.utc(eRoutesData[i].validFrom).toISOString(),
                            validTo: moment.utc(eRoutesData[i].validTo).toISOString(),
                            length: eRoutesData[i].length,
                            routeType: eRoutesData[i].routeType,
                            machineId: eRoutesData[i].machineId,
                            salesEnabled: (eRoutesData[i].salesEnabled != '') ? eRoutesData[i].salesEnabled : false,
                            backDoorValidationEnabled: (eRoutesData[i].backDoorValidationEnabled != '') ? eRoutesData[i].backDoorValidationEnabled : false,
                            stopsOnRoute: (eRoutesData[i].hasOwnProperty('stopsOnRoute')) ? eRoutesData[i].stopsOnRoute.stopOnRoute : []
                        });
                    }
                }
                // fare environment configuration - stops
                let eStops = [];
                if (fareEnvironmentConfigurationData['stops'] && fareEnvironmentConfigurationData['stops'].hasOwnProperty('stop')) {
                    let eStopsData = fareEnvironmentConfigurationData['stops']['stop'];
                    let eStopsLen = (Array.isArray(eStopsData)) ? eStopsData.length : 1;
                    if (eStopsLen == 1) { eStopsData[0] = eStopsData; }
                    log.info('e_Stops', eStopsLen);
                    for (let i=0; i<eStopsLen; i++) {
                        eStops.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            machineId: eStopsData[i].machineId,
                            stopCode: eStopsData[i].stopCode,
                            stopName: eStopsData[i].stopName,
                            stationCode: eStopsData[i].stationCode,
                            stopGpsLatInMillionthOfDegree: eStopsData[i].stopGpsLatInMillionthOfDegree,
                            stopGpsLonInMillionthOfDegree: eStopsData[i].stopGpsLonInMillionthOfDegree
                        });
                    }
                }
                // fare environment configuration - stations
                let eStations = [];
                if (fareEnvironmentConfigurationData['stations'] && fareEnvironmentConfigurationData['stations'].hasOwnProperty('station')) {
                    let eStationsData = fareEnvironmentConfigurationData['stations']['station'];
                    let eStationsLen = (Array.isArray(eStationsData)) ? eStationsData.length : 1;
                    if (eStationsLen == 1) { eStationsData[0] = eStationsData; }
                    log.info('e_Stations', eStationsLen);
                    for (let i=0; i<eStationsLen; i++) {
                        eStations.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            machineId: eStationsData[i].machineId,
                            stationCode: eStationsData[i].stationCode,
                            stationName: eStationsData[i].stationName
                        });
                    }
                }
                // fare environment configuration - distance segment types
                let eDistanceSegmentTypes = [];
                if (fareEnvironmentConfigurationData['distanceSegmentTypes'] && fareEnvironmentConfigurationData['distanceSegmentTypes'].hasOwnProperty('distanceSegmentType')) {
                    let eDistanceSegmentTypesData = fareEnvironmentConfigurationData['distanceSegmentTypes']['distanceSegmentType'];
                    let eDistanceSegmentTypesLen = (Array.isArray(eDistanceSegmentTypesData)) ? eDistanceSegmentTypesData.length : 1;
                    if (eDistanceSegmentTypesLen == 1) { eDistanceSegmentTypesData[0] = eDistanceSegmentTypesData; }
                    log.info('e_DistanceSegmentTypes', eDistanceSegmentTypesLen);
                    for (let i=0; i<eDistanceSegmentTypesLen; i++) {
                        eDistanceSegmentTypes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: eDistanceSegmentTypesData[i].externalId,
                            name: eDistanceSegmentTypesData[i].name,
                            description: eDistanceSegmentTypesData[i].description,
                            divisionPointEveryInMeters: eDistanceSegmentTypesData[i].divisionPointEveryInMeters,
                            maximumDistanceInMeters: eDistanceSegmentTypesData[i].maximumDistanceInMeters,
                            transitTypes: (eDistanceSegmentTypesData[i].hasOwnProperty('transitTypes')) ? eDistanceSegmentTypesData[i].transitTypes.transitType : [],
                            distanceSegments: (eDistanceSegmentTypesData[i].hasOwnProperty('distanceSegments')) ? eDistanceSegmentTypesData[i].distanceSegments.distanceSegment : []
                        });
                    }
                }
                // fare environment configuration - stage by route border adjustments
                let eStageByRouteBorderAdjustments = [];
                if (fareEnvironmentConfigurationData['stageByRouteBorderAdjustments'] && fareEnvironmentConfigurationData['stageByRouteBorderAdjustments'].hasOwnProperty('routeStageBorderAdjustment')) {
                    let eStageByRouteBorderAdjustmentsData = fareEnvironmentConfigurationData['stageByRouteBorderAdjustments']['routeStageBorderAdjustment'];
                    let eStageByRouteBorderAdjustmentsLen = (Array.isArray(eStageByRouteBorderAdjustmentsData)) ? eStageByRouteBorderAdjustmentsData.length : 1;
                    if (eStageByRouteBorderAdjustmentsLen == 1) { eStageByRouteBorderAdjustmentsData[0] = eStageByRouteBorderAdjustmentsData; }
                    log.info('e_StageByRouteBorderAdjustments', eStageByRouteBorderAdjustmentsLen);
                    for (let i=0; i<eStageByRouteBorderAdjustmentsLen; i++) {
                        eStageByRouteBorderAdjustments.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            route: eStageByRouteBorderAdjustmentsData[i].route.externalId,
                            borderStop: eStageByRouteBorderAdjustmentsData[i].borderStop.stopCode,
                            lowerDistanceSegment: eStageByRouteBorderAdjustmentsData[i].lowerDistanceSegment.sequenceNumber
                        });
                    }
                }
                // fare environment configuration - verification documents
                let eVerificationDocuments = [];
                if (fareEnvironmentConfigurationData['verificationDocuments'] && fareEnvironmentConfigurationData['verificationDocuments'].hasOwnProperty('verificationDocument')) {
                    let eVerificationDocumentsData = fareEnvironmentConfigurationData['verificationDocuments']['verificationDocument'];
                    let eVerificationDocumentsLen = (Array.isArray(eVerificationDocumentsData)) ? eVerificationDocumentsData.length : 1;
                    if (eVerificationDocumentsLen == 1) { eVerificationDocumentsData[0] = eVerificationDocumentsData; }
                    log.info('e_VerificationDocuments', eVerificationDocumentsLen);
                    for (let i=0; i<eVerificationDocumentsLen; i++) {
                        eVerificationDocuments.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            id: eVerificationDocumentsData[i].id,
                            type: eVerificationDocumentsData[i].type,
                            isPersonal: (eVerificationDocumentsData[i].isPersonal != '') ? eVerificationDocumentsData[i].isPersonal : false,
                            description: eVerificationDocumentsData[i].desc,
                            typeTranslations: (eVerificationDocumentsData[i].hasOwnProperty('typeTranslations') && eVerificationDocumentsData[i].typeTranslations != '') ? eVerificationDocumentsData[i].typeTranslations.typeTranslation.text : '',
                            descriptionTranslations: (eVerificationDocumentsData[i].hasOwnProperty('descTranslations') && eVerificationDocumentsData[i].descTranslations != '') ? eVerificationDocumentsData[i].descTranslations.descTranslation.text : '',
                            verificationDocumentElementIds: (eVerificationDocumentsData[i].hasOwnProperty('verificationDocumentElementIds')) ? eVerificationDocumentsData[i].verificationDocumentElementIds.verificationDocumentElementId : []
                        });
                    }
                }
                // fare environment configuration - verification document elements
                let eVerificationDocumentElements = [];
                if (fareEnvironmentConfigurationData['verificationDocumentElements'] && fareEnvironmentConfigurationData['verificationDocumentElements'].hasOwnProperty('verificationDocumentElement')) {
                    let eVerificationDocumentElementsData = fareEnvironmentConfigurationData['verificationDocumentElements']['verificationDocumentElement'];
                    let eVerificationDocumentElementsLen = (Array.isArray(eVerificationDocumentElementsData)) ? eVerificationDocumentElementsData.length : 1;
                    if (eVerificationDocumentElementsLen == 1) { eVerificationDocumentElementsData[0] = eVerificationDocumentElementsData; }
                    log.info('e_VerificationDocumentElements', eVerificationDocumentElementsLen);
                    for (let i=0; i<eVerificationDocumentElementsLen; i++) {
                        eVerificationDocumentElements.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            id: eVerificationDocumentElementsData[i].id,
                            name: eVerificationDocumentElementsData[i].name,
                            description: eVerificationDocumentElementsData[i].description,
                            type: eVerificationDocumentElementsData[i].type,
                            nameTranslations: (eVerificationDocumentElementsData[i].hasOwnProperty('nameTranslations') && eVerificationDocumentElementsData[i].nameTranslations != '') ? eVerificationDocumentElementsData[i].nameTranslations.nameTranslation.text : '',
                            descriptionTranslations: (eVerificationDocumentElementsData[i].hasOwnProperty('descriptionTranslations') && eVerificationDocumentElementsData[i].descriptionTranslations != '') ? eVerificationDocumentElementsData[i].descriptionTranslations.descriptionTranslation.text : ''
                        });
                    }
                }
                // fare environment configuration - rider languages
                let eRiderLanguages = [];
                if (fareEnvironmentConfigurations['riderLanguages'].hasOwnProperty('riderLanguage')) {
                    let eRiderLanguagesData = fareEnvironmentConfigurations['riderLanguages']['riderLanguage'];
                    let eRiderLanguagesLen = (Array.isArray(eRiderLanguagesData)) ? eRiderLanguagesData.length : 1;
                    if (eRiderLanguagesLen == 1) { eRiderLanguagesData[0] = eRiderLanguagesData; }
                    log.info('e_RiderLanguages', eRiderLanguagesLen);
                    for (let i=0; i<eRiderLanguagesLen; i++) {
                        eRiderLanguages.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            languageCode: eRiderLanguagesData[i].code,
                            description: eRiderLanguagesData[i].description,
                            isDefaultLanguage: eRiderLanguagesData[i].isDefaultLanguage
                        });
                    }
                }
                // fare environment insert
                Promise.all([
                    this.fareEnvironmentModel.TransitTypes.insertMany(eTransitTypes),
                    this.fareEnvironmentModel.ZoneTypes.insertMany(eZoneTypes),
                    this.fareEnvironmentModel.Zones.insertMany(eZones),
                    this.fareEnvironmentModel.Routes.insertMany(eRoutes),
                    this.fareEnvironmentModel.Stops.insertMany(eStops),
                    this.fareEnvironmentModel.Stations.insertMany(eStations),
                    this.fareEnvironmentModel.DistanceSegmentTypes.insertMany(eDistanceSegmentTypes),
                    this.fareEnvironmentModel.StageByRouteBorderAdjustments.insertMany(eStageByRouteBorderAdjustments),
                    this.fareEnvironmentModel.VerificationDocuments.insertMany(eVerificationDocuments),
                    this.fareEnvironmentModel.VerificationDocumentElements.insertMany(eVerificationDocumentElements),
                    this.fareEnvironmentModel.RiderLanguages.insertMany(eRiderLanguages)
                ]);
            }

            ////////////////// fare distance configuration //////////////////
            let fareDistanceConfigurations = await this.parseXmltoJson(path.join(distributionFilesPath, 'fare', 'fareDistanceConfiguration.xml'));
            let fareDistanceConfigurationsData = fareDistanceConfigurations['fareDistanceConfigurations']['fareDistanceConfiguration'];
            let fareDistanceConfigurationsDataLen = (Array.isArray(fareDistanceConfigurationsData)) ? fareDistanceConfigurationsData.length : 1;
            if (fareDistanceConfigurationsDataLen == 1) { fareDistanceConfigurationsData[0] = fareDistanceConfigurationsData; }
            for (let d=0; d<fareDistanceConfigurationsDataLen; d++) {
                let fareDistanceConfigurationData = fareDistanceConfigurationsData[d];
                log.info('fareDistance activationDate', fareDistanceConfigurationData.activationDate);

                const fareEnvironmentConfigurationInserted = await this.fareEnvironmentModel.Configuration.find({distributionVersion: heartbeat.distributionVersion, activationDate: moment.utc(fareDistanceConfigurationData.activationDate).toISOString()});

                // fare distance configuration - distance fares
                let dDistanceFares = [];
                if (fareDistanceConfigurationData['distanceFares'] && fareDistanceConfigurationData['distanceFares'].hasOwnProperty('distanceFare')) {
                    let dDistanceFaresData = fareDistanceConfigurationData['distanceFares']['distanceFare'];
                    let dDistanceFaresLen = (Array.isArray(dDistanceFaresData)) ? dDistanceFaresData.length : 1;
                    if (dDistanceFaresLen == 1) { dDistanceFaresData[0] = dDistanceFaresData; }
                    log.info('d_DistanceFares', dDistanceFaresLen);
                    for (let i=0; i<dDistanceFaresLen; i++) {
                        dDistanceFares.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            riderType: dDistanceFaresData[i].riderType.externalId,
                            transitType: dDistanceFaresData[i].transitType.externalId,
                            startDistanceInMeters: dDistanceFaresData[i].startDistanceInMeters,
                            endDistanceInMeters: dDistanceFaresData[i].endDistanceInMeters,
                            stagesOrZonesTraveled: dDistanceFaresData[i].stagesOrZonesTraveled,
                            mediaType: dDistanceFaresData[i].mediaType,
                            baseFare: dDistanceFaresData[i].baseFare.valueInCents
                        });
                    }
                }
                // fare distance configuration - from stop to stop fare adjustments
                let dFromStopToStopFareAdjustments = [];
                if (fareDistanceConfigurationData['fromStopToStopFareAdjustments'] && fareDistanceConfigurationData['fromStopToStopFareAdjustments'].hasOwnProperty('fromStopToStopFareAdjustment')) {
                    let dFromStopToStopFareAdjustmentsData = fareDistanceConfigurationData['fromStopToStopFareAdjustments']['fromStopToStopFareAdjustment'];
                    let dFromStopToStopFareAdjustmentsLen = (Array.isArray(dFromStopToStopFareAdjustmentsData)) ? dFromStopToStopFareAdjustmentsData.length : 1;
                    if (dFromStopToStopFareAdjustmentsLen == 1) { dFromStopToStopFareAdjustmentsData[0] = dFromStopToStopFareAdjustmentsData; }
                    log.info('d_FromStopToStopFareAdjustments', dFromStopToStopFareAdjustmentsLen);
                    for (let i=0; i<dFromStopToStopFareAdjustmentsLen; i++) {
                        dFromStopToStopFareAdjustments.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            riderType: dFromStopToStopFareAdjustmentsData[i].riderType.externalId,
                            route: dFromStopToStopFareAdjustmentsData[i].route.externalId,
                            alightingStop: dFromStopToStopFareAdjustmentsData[i].alightingStop.stopCode,
                            boardingStop: dFromStopToStopFareAdjustmentsData[i].boardingStop.stopCode,
                            mediaType: dFromStopToStopFareAdjustmentsData[i].mediaType,
                            baseFare: dFromStopToStopFareAdjustmentsData[i].baseFare.valueInCents
                        });
                    }
                }
                // fare distance configuration - from to station fares
                let dFromToStationFares = [];
                if (fareDistanceConfigurationData['fromToStationFares'] && fareDistanceConfigurationData['fromToStationFares'].hasOwnProperty('fromToStationFare')) {
                    let dFromToStationFaresData = fareDistanceConfigurationData['fromToStationFares']['fromToStationFare'];
                    let dFromToStationFaresLen = (Array.isArray(dFromToStationFaresData)) ? dFromToStationFaresData.length : 1;
                    if (dFromToStationFaresLen == 1) { dFromToStationFaresData[0] = dFromToStationFaresData; }
                    log.info('d_FromToStationFares', dFromToStationFaresLen);
                    for (let i=0; i<dFromToStationFaresLen; i++) {
                        dFromToStationFares.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            riderType: dFromToStationFaresData[i].riderType.externalId,
                            route: dFromToStationFaresData[i].route.externalId,
                            fromStation: dFromToStationFaresData[i].fromStation.code,
                            toStation: dFromToStationFaresData[i].toStation.code,
                            isOneWayOnly: (dFromToStationFaresData[i].isOneWayOnly != '') ? dFromToStationFaresData[i].isOneWayOnly : false,
                            mediaType: dFromToStationFaresData[i].mediaType,
                            baseFare: dFromToStationFaresData[i].baseFare.valueInCents
                        });
                    }
                }
                // fare distance insert
                Promise.all([
                    this.fareDistanceModel.DistanceFares.insertMany(dDistanceFares),
                    this.fareDistanceModel.FromStopToStopFareAdjustments.insertMany(dFromStopToStopFareAdjustments),
                    this.fareDistanceModel.FromToStationFares.insertMany(dFromToStationFares)
                ]);
            }

            ////////////////// fare table //////////////////
            let fareTables = await this.parseXmltoJson(path.join(distributionFilesPath, 'fare', 'fareTable.xml'));
            let fareTablesData = fareTables['fareTables']['fareTable'];
            let fareTablesDataLen = (Array.isArray(fareTablesData)) ? fareTablesData.length : 1;
            if (fareTablesDataLen == 1) { fareTablesData[0] = fareTablesData; }
            for (let f=0; f<fareTablesDataLen; f++) {
                let fareTableData = fareTablesData[f];
                log.info('fareTable activationDate', fareTableData.activationDate);

                const fareEnvironmentConfigurationInserted = await this.fareEnvironmentModel.Configuration.find({distributionVersion: heartbeat.distributionVersion, activationDate: moment.utc(fareTableData.activationDate).toISOString()});

                await this.fareEnvironmentModel.Configuration.findOneAndUpdate({ _id: fareEnvironmentConfigurationInserted[0]._id }, {
                    faretableTimestamp: moment.utc(fareTableData.dateCreated).toISOString()
                });

                // fare table - flat transfer settings
                let tFlatTransferSettings = [];
                if (fareTableData.hasOwnProperty('flatTransferSettings')) {
                    let tFlatTransferSettingsData = fareTableData['flatTransferSettings'];
                    let tFlatTransferSettingsLen = (Array.isArray(tFlatTransferSettingsData)) ? tFlatTransferSettingsData.length : 1;
                    if (tFlatTransferSettingsLen == 1) { tFlatTransferSettingsData[0] = tFlatTransferSettingsData; }
                    log.info('t_FlatTransferSettings', tFlatTransferSettingsLen);
                    for (let i=0; i<tFlatTransferSettingsLen; i++) {
                        tFlatTransferSettings.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            numberOfTransfers: tFlatTransferSettingsData[i].numberOfTransfers,
                            durationInMinutes: tFlatTransferSettingsData[i].durationInMinutes
                        });
                    }
                }
                // fare table - fare code types
                let tFareCodeTypes = [];
                if (fareTableData['fareCodeTypes'] && fareTableData['fareCodeTypes'].hasOwnProperty('fareCodeType')) {
                    let tFareCodeTypesData = fareTableData['fareCodeTypes']['fareCodeType'];
                    let tFareCodeTypesLen = (Array.isArray(tFareCodeTypesData)) ? tFareCodeTypesData.length : 1;
                    if (tFareCodeTypesLen == 1) { tFareCodeTypesData[0] = tFareCodeTypesData; }
                    log.info('t_FareCodeTypes', tFareCodeTypesLen);
                    for (let i=0; i<tFareCodeTypesLen; i++) {
                        tFareCodeTypes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            name: tFareCodeTypesData[i].name,
                            externalId: tFareCodeTypesData[i].externalId,
                            transitTypes: (tFareCodeTypesData[i].hasOwnProperty('transitTypes')) ? tFareCodeTypesData[i].transitTypes.transitType : [],
                            riderTypes: (tFareCodeTypesData[i].hasOwnProperty('fareCodeTypes')) ? tFareCodeTypesData[i].riderTypes.riderType : []
                        });
                    }
                }
                // fare table - flat fares
                let tFlatFares = [];
                if (fareTableData['flatFares'] && fareTableData['flatFares'].hasOwnProperty('flatFare')) {
                    let tFlatFaresData = fareTableData['flatFares']['flatFare'];
                    let tFlatFaresLen = (Array.isArray(tFlatFaresData)) ? tFlatFaresData.length : 1;
                    if (tFlatFaresLen == 1) { tFlatFaresData[0] = tFlatFaresData; }
                    log.info('t_FlatFares', tFlatFaresLen);
                    for (let i=0; i<tFlatFaresLen; i++) {
                        tFlatFares.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            riderType: tFlatFaresData[i].riderType.externalId,
                            transitType: tFlatFaresData[i].transitType.externalId,
                            mediaType: tFlatFaresData[i].mediaType,
                            baseFare: tFlatFaresData[i].baseFare.valueInCents
                        });
                    }
                }
                // fare table - zone fares
                let tZoneFares = [];
                if (fareTableData['zoneFares'] && fareTableData['zoneFares'].hasOwnProperty('zoneFare')) {
                    let tZoneFaresData = fareTableData['zoneFares']['zoneFare'];
                    let tZoneFaresLen = (Array.isArray(tZoneFaresData)) ? tZoneFaresData.length : 1;
                    if (tZoneFaresLen == 1) { tZoneFaresData[0] = tZoneFaresData; }
                    log.info('t_ZoneFares', tZoneFaresLen);
                    for (let i=0; i<tZoneFaresLen; i++) {
                        tZoneFares.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            riderType: tZoneFaresData[i].riderType.externalId,
                            transitType: tZoneFaresData[i].transitType.externalId,
                            startDistanceInMeters: tZoneFaresData[i].startDistanceInMeters,
                            endDistanceInMeters: tZoneFaresData[i].endDistanceInMeters,
                            zonesTraveled: tZoneFaresData[i].zonesTraveled,
                            mediaType: tZoneFaresData[i].mediaType,
                            baseFare: tZoneFaresData[i].baseFare.valueInCents
                        });
                    }
                }
                // fare table - fare codes
                let tFareCodes = [];
                if (fareTableData['fareCodes'] && fareTableData['fareCodes'].hasOwnProperty('fareCode')) {
                    let tFareCodesData = fareTableData['fareCodes']['fareCode'];
                    let tFareCodesLen = (Array.isArray(tFareCodesData)) ? tFareCodesData.length : 1;
                    if (tFareCodesLen == 1) { tFareCodesData[0] = tFareCodesData; }
                    log.info('t_FareCodes', tFareCodesLen);
                    for (let i=0; i<tFareCodesLen; i++) {
                        tFareCodes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: tFareCodesData[i].externalId,
                            name: tFareCodesData[i].name,
                            transitType: tFareCodesData[i].transitType.externalId,
                            baseFares: (tFareCodesData[i].hasOwnProperty('baseFares')) ? tFareCodesData[i].baseFares : []
                        });
                    }
                }
                // fare table - from to stage fare codes
                let tFromToStageFareCodes = [];
                if (fareTableData['fromToStageFareCodes'] && fareTableData['fromToStageFareCodes'].hasOwnProperty('fromToStageFareCode')) {
                    let tFromToStageFareCodesData = fareTableData['fromToStageFareCodes']['fromToStageFareCode'];
                    let tFromToStageFareCodesLen = (Array.isArray(tFromToStageFareCodesData)) ? tFromToStageFareCodesData.length : 1;
                    if (tFromToStageFareCodesLen == 1) { tFromToStageFareCodesData[0] = tFromToStageFareCodesData; }
                    log.info('t_FromToStageFareCodes', tFromToStageFareCodesLen);
                    for (let i=0; i<tFromToStageFareCodesLen; i++) {
                        tFromToStageFareCodes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            route: tFromToStageFareCodesData[i].route.externalId,
                            boardingStage: tFromToStageFareCodesData[i].boardingStage,
                            alightingStage: tFromToStageFareCodesData[i].alightingStage,
                            mediaType: tFromToStageFareCodesData[i].mediaType,
                            fareCode: tFromToStageFareCodesData[i].fareCode.externalId,
                            transitType: tFromToStageFareCodesData[i].transitType.externalId
                        });
                    }
                }
                // fare table - from to stage fare settings
                let tFromToStageFareSettings = [];
                if (fareTableData['fromToStageFareSettings'] && fareTableData['fromToStageFareSettings'].hasOwnProperty('fromToStageFareSetting')) {
                    let tFromToStageFareSettingsData = fareTableData['fromToStageFareSettings']['fromToStageFareSetting'];
                    let tFromToStageFareSettingsLen = (Array.isArray(tFromToStageFareSettingsData)) ? tFromToStageFareSettingsData.length : 1;
                    if (tFromToStageFareSettingsLen == 1) { tFromToStageFareSettingsData[0] = tFromToStageFareSettingsData; }
                    log.info('t_FromToStageFareSettings', tFromToStageFareSettingsLen);
                    for (let i=0; i<tFromToStageFareSettingsLen; i++) {
                        tFromToStageFareSettings.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            mediaType: tFromToStageFareSettingsData[i].mediaType,
                            isJourneyUpgradeAllowed: (tFromToStageFareSettingsData[i].isJourneyUpgradeAllowed != '') ? tFromToStageFareSettingsData[i].isJourneyUpgradeAllowed : false,
                            journeyDuration: tFromToStageFareSettingsData[i].journeyDuration,
                            extendedTicketTypeCode: tFromToStageFareSettingsData[i].extendedTicketTypeCode
                        });
                    }
                }
                // fare table - from to zone fare codes
                let tFromToZoneFareCodes = [];
                if (fareTableData['fromToZoneFareCodes'] && fareTableData['fromToZoneFareCodes'].hasOwnProperty('fromToZoneFareCode')) {
                    let tFromToZoneFareCodesData = fareTableData['fromToZoneFareCodes']['fromToZoneFareCode'];
                    let tFromToZoneFareCodesLen = (Array.isArray(tFromToZoneFareCodesData)) ? tFromToZoneFareCodesData.length : 1;
                    if (tFromToZoneFareCodesLen == 1) { tFromToZoneFareCodesData[0] = tFromToZoneFareCodesData; }
                    log.info('t_FromToZoneFareCodes', tFromToZoneFareCodesLen);
                    for (let i=0; i<tFromToZoneFareCodesLen; i++) {
                        tFromToZoneFareCodes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            boardingZone: tFromToZoneFareCodesData[i].boardingZone.externalId,
                            alightingZone: tFromToZoneFareCodesData[i].alightingZone.externalId,
                            mediaType: tFromToZoneFareCodesData[i].mediaType,
                            fareCode: tFromToZoneFareCodesData[i].fareCode,
                            transitType: tFromToZoneFareCodesData[i].transitType,
                            riderType: tFromToZoneFareCodesData[i].riderType
                        });
                    }
                }
                // fare table - from to zone fare settings
                let tFromToZoneFareSettings = [];
                if (fareTableData['fromToZoneFareSettings'] && fareTableData['fromToZoneFareSettings'].hasOwnProperty('fromToZoneFareSetting')) {
                    let tFromToZoneFareSettingsData = fareTableData['fromToZoneFareSettings']['fromToZoneFareSetting'];
                    let tFromToZoneFareSettingsLen = (Array.isArray(tFromToZoneFareSettingsData)) ? tFromToZoneFareSettingsData.length : 1;
                    if (tFromToZoneFareSettingsLen == 1) { tFromToZoneFareSettingsData[0] = tFromToZoneFareSettingsData; }
                    log.info('t_FromToZoneFareSettings', tFromToZoneFareSettingsLen);
                    for (let i=0; i<tFromToZoneFareSettingsLen; i++) {
                        tFromToZoneFareSettings.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            mediaType: tFromToZoneFareSettingsData[i].mediaType,
                            isJourneyUpgradeAllowed: (tFromToZoneFareSettingsData[i].isJourneyUpgradeAllowed != '') ? tFromToZoneFareSettingsData[i].isJourneyUpgradeAllowed : false,
                            journeyDuration: tFromToZoneFareSettingsData[i].journeyDuration,
                            extendedTicketTypeCode: tFromToZoneFareSettingsData[i].extendedTicketTypeCode
                        });
                    }
                }
                // fare table - multi price products
                let tMultiPriceProducts = [];
                if (fareTableData['multiPriceProducts'] && fareTableData['multiPriceProducts'].hasOwnProperty('multiPriceProduct')) {
                    let tMultiPriceProductsData = fareTableData['multiPriceProducts']['multiPriceProduct'];
                    let tMultiPriceProductsLen = (Array.isArray(tMultiPriceProductsData)) ? tMultiPriceProductsData.length : 1;
                    if (tMultiPriceProductsLen == 1) { tMultiPriceProductsData[0] = tMultiPriceProductsData; }
                    log.info('t_MultiPriceProducts', tMultiPriceProductsLen);
                    for (let i=0; i<tMultiPriceProductsLen; i++) {
                        tMultiPriceProducts.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: tMultiPriceProductsData[i].externalId,
                            name: tMultiPriceProductsData[i].name,
                            nameTranslations: (tMultiPriceProductsData[i].hasOwnProperty('nameTranslations') && tMultiPriceProductsData[i].nameTranslations != '') ? tMultiPriceProductsData[i].nameTranslations.nameTranslation.text : '',
                            mediaType: tMultiPriceProductsData[i].mediaType,
                            fareType: tMultiPriceProductsData[i].fareType,
                            singleRideDuration: tMultiPriceProductsData[i].singleRideDuration,
                            journeyDuration: tMultiPriceProductsData[i].journeyDuration,
                            timeBetweenRides: tMultiPriceProductsData[i].timeBetweenRides,
                            maxNumberOfRides: tMultiPriceProductsData[i].maxNumberOfRides,
                            print1: tMultiPriceProductsData[i].print1,
                            print1Translations: (tMultiPriceProductsData[i].hasOwnProperty('print1Translations') && tMultiPriceProductsData[i].print1Translations != '') ? tMultiPriceProductsData[i].print1Translations.print1Translation.text : '',
                            print2: tMultiPriceProductsData[i].print2,
                            print2Translations: (tMultiPriceProductsData[i].hasOwnProperty('print2Translations') && tMultiPriceProductsData[i].print2Translations != '') ? tMultiPriceProductsData[i].print2Translations.print2Translation.text : '',
                            print3: tMultiPriceProductsData[i].print3,
                            print3Translations: (tMultiPriceProductsData[i].hasOwnProperty('print3Translations') && tMultiPriceProductsData[i].print3Translations != '') ? tMultiPriceProductsData[i].print3Translations.print3Translation.text : '',
                            sellableAccess: (tMultiPriceProductsData[i].hasOwnProperty('sellableAccess')) ? tMultiPriceProductsData[i].sellableAccess.device : [],
                            adjustments: (tMultiPriceProductsData[i].hasOwnProperty('adjustments')) ? tMultiPriceProductsData[i].adjustments.adjustment : {},
                            upgrades: (tMultiPriceProductsData[i].hasOwnProperty('upgrades')) ? tMultiPriceProductsData[i].upgrades.upgrade : {},
                            deposit: (tMultiPriceProductsData[i].hasOwnProperty('deposit')) ? tMultiPriceProductsData[i].deposit : {},
                            paperTicketValidityInDays: tMultiPriceProductsData[i].paperTicketValidityInDays,
                            digitalTicketValidityInDays: tMultiPriceProductsData[i].digitalTicketValidityInDays,
                            validityCalculationStartCondition: tMultiPriceProductsData[i].validityCalculationStartCondition,
                            returnTicketValidityInDays: tMultiPriceProductsData[i].returnTicketValidityInDays,
                            accountTypes: (tMultiPriceProductsData[i].hasOwnProperty('accountTypes')) ? tMultiPriceProductsData[i].accountTypes.accountType : [],
                            smartCardProfiles: (tMultiPriceProductsData[i].hasOwnProperty('smartCardProfiles')) ? tMultiPriceProductsData[i].smartCardProfiles : {},
                            priceRoundingType: tMultiPriceProductsData[i].priceRoundingType
                        });
                    }
                }
                // fare table - transit types
                let tTransitTypes = [];
                if (fareTableData['transitTypes'] && fareTableData['transitTypes'].hasOwnProperty('transitType')) {
                    let tTransitTypesData = fareTableData['transitTypes']['transitType'];
                    let tTransitTypesLen = (Array.isArray(tTransitTypesData)) ? tTransitTypesData.length : 1;
                    if (tTransitTypesLen == 1) { tTransitTypesData[0] = tTransitTypesData; }
                    log.info('t_TransitTypes', tTransitTypesLen);
                    for (let i=0; i<tTransitTypesLen; i++) {
                        tTransitTypes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: tTransitTypesData[i].externalId,
                            name: tTransitTypesData[i].name
                        });
                    }
                }
                // fare table - rider types
                let tRiderTypes = [];
                if (fareTableData['riderTypes'] && fareTableData['riderTypes'].hasOwnProperty('riderType')) {
                    let tRiderTypesData = fareTableData['riderTypes']['riderType'];
                    let tRiderTypesLen = (Array.isArray(tRiderTypesData)) ? tRiderTypesData.length : 1;
                    if (tRiderTypesLen == 1) { tRiderTypesData[0] = tRiderTypesData; }
                    log.info('t_RiderTypes', tRiderTypesLen);
                    for (let i=0; i<tRiderTypesLen; i++) {
                        tRiderTypes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: tRiderTypesData[i].externalId,
                            name: tRiderTypesData[i].name,
                            nameTranslations: (tRiderTypesData[i].hasOwnProperty('nameTranslations') && tRiderTypesData[i].nameTranslations != '') ? tRiderTypesData[i].nameTranslations.nameTranslation.text : '',
                            type: tRiderTypesData[i].type,
                            isPerson: (tRiderTypesData[i].isPerson != '') ? tRiderTypesData[i].isPerson : false,
                            description: tRiderTypesData[i].description,
                            iconBitmapName: tRiderTypesData[i].iconBitmapName,
                            isGroupFare: (tRiderTypesData[i].isGroupFare != '') ? tRiderTypesData[i].isGroupFare : false,
                            smartCardOnly: (tRiderTypesData[i].smartCardOnly != '') ? tRiderTypesData[i].smartCardOnly : false,
                            hasAccount: (tRiderTypesData[i].hasAccount != '') ? tRiderTypesData[i].hasAccount : false,
                            hasSmartCard: (tRiderTypesData[i].hasSmartCard != '') ? tRiderTypesData[i].hasSmartCard : false,
                            hasCash: (tRiderTypesData[i].hasCash != '') ? tRiderTypesData[i].hasCash : false,
                            isBusinessOnly: (tRiderTypesData[i].isBusinessOnly != '') ? tRiderTypesData[i].isBusinessOnly : false,
                            storedValue: (tRiderTypesData[i].storedValue != '') ? tRiderTypesData[i].storedValue : false,
                            isDefault: (tRiderTypesData[i].isDefault != '') ? tRiderTypesData[i].isDefault : false,
                            ageLimitation: tRiderTypesData[i].ageLimitation,
                            verificationRequired: (tRiderTypesData[i].verificationRequired != '') ? tRiderTypesData[i].verificationRequired : false,
                            statusExpires: (tRiderTypesData[i].statusExpires != '') ? tRiderTypesData[i].statusExpires : false,
                            canStatusBeRenewed: (tRiderTypesData[i].canStatusBeRenewed != '') ? tRiderTypesData[i].canStatusBeRenewed : false,
                            expiryCondition: tRiderTypesData[i].expiryCondition,
                            expirationDate: moment.utc(tRiderTypesData[i].expirationDate).toISOString(),
                            nameTranslations: (tRiderTypesData[i].hasOwnProperty('nameTranslations') && tRiderTypesData[i].nameTranslations != '') ? tRiderTypesData[i].nameTranslations.nameTranslation.text : '',
                            descriptionTranslations: (tRiderTypesData[i].hasOwnProperty('descriptionTranslations') && tRiderTypesData[i].descriptionTranslations != '') ? tRiderTypesData[i].descriptionTranslations.descriptionTranslation.text : '',
                            iconPositions: (tRiderTypesData[i].hasOwnProperty('iconPositions')) ? tRiderTypesData[i].iconPositions.iconPosition : {},
                            assignedRiderTypes: (tRiderTypesData[i].hasOwnProperty('assignedRiderTypes')) ? tRiderTypesData[i].assignedRiderTypes.assignedRiderType : [],
                            verificationEnabledOnDeviceTypes: (tRiderTypesData[i].hasOwnProperty('verificationEnabledOnDeviceTypes')) ? tRiderTypesData[i].verificationEnabledOnDeviceTypes.verificationEnabledOnDeviceType : [],
                            verificationDocuments: (tRiderTypesData[i].hasOwnProperty('verificationDocuments')) ? tRiderTypesData[i].verificationDocuments.verificationDocument : []
                        });
                    }
                }
                // fare table - products
                let tProducts = [];
                if (fareTableData['products'] && fareTableData['products'].hasOwnProperty('product')) {
                    let tProductsData = fareTableData['products']['product'];
                    let tProductsLen = (Array.isArray(tProductsData)) ? tProductsData.length : 1;
                    if (tProductsLen == 1) { tProductsData[0] = tProductsData; }
                    log.info('t_Products', tProductsLen);
                    for (let i=0; i<tProductsLen; i++) {
                        tProducts.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            type: tProductsData[i].type,
                            externalId: tProductsData[i].externalId,
                            name: tProductsData[i].name,
                            nameTranslations: (tProductsData[i].hasOwnProperty('nameTranslations') && tProductsData[i].nameTranslations != '') ? tProductsData[i].nameTranslations.nameTranslation.text : '',
                            priceInCents: tProductsData[i].priceInCents,
                            productEntityClass: tProductsData[i].productEntityClass,
                            activateOn: tProductsData[i].activateOn,
                            paperTicketValidityInDays: tProductsData[i].paperTicketValidityInDays,
                            validityCalculationStartCondition: tProductsData[i].validityCalculationStartCondition,
                            startDate: moment.utc(tProductsData[i].startDate).toISOString(),
                            endDate: moment.utc(tProductsData[i].endDate).toISOString(),
                            mediaType: tProductsData[i].mediaType,
                            iconBitmapName: tProductsData[i].iconBitmapName,
                            fareMultiplier: tProductsData[i].fareMultiplier,
                            maximumFee: tProductsData[i].maximumFee,
                            productType: tProductsData[i].productType,
                            duration: tProductsData[i].duration,
                            print1: tProductsData[i].print1,
                            print1Translations: (tProductsData[i].hasOwnProperty('print1Translations') && tProductsData[i].print1Translations != '') ? tProductsData[i].print1Translations.print1Translation.text : '',
                            print2: tProductsData[i].print2,
                            print2Translations: (tProductsData[i].hasOwnProperty('print2Translations') && tProductsData[i].print2Translations != '') ? tProductsData[i].print2Translations.print2Translation.text : '',
                            print3: tProductsData[i].print3,
                            print3Translations: (tProductsData[i].hasOwnProperty('print3Translations') && tProductsData[i].print3Translations != '') ? tProductsData[i].print3Translations.print3Translation.text : '',
                            print4: tProductsData[i].print4,
                            print4Translations: (tProductsData[i].hasOwnProperty('print4Translations') && tProductsData[i].print4Translations != '') ? tProductsData[i].print4Translations.print4Translation.text : '',
                            valueCredited: tProductsData[i].valueCredited,
                            minimumPrice: tProductsData[i].minimumPrice,
                            maximumPrice: tProductsData[i].maximumPrice,
                            discount: tProductsData[i].discount,
                            sharedCode: tProductsData[i].sharedCode,
                            extendedTicketTypeCode: tProductsData[i].extendedTicketTypeCode,
                            sellingStartDay: tProductsData[i].sellingStartDay,
                            sellingEndDay: tProductsData[i].sellingEndDay,
                            numberOfRides: tProductsData[i].numberOfRides,
                            numberOfItems: tProductsData[i].numberOfItems,
                            continueDuration: tProductsData[i].continueDuration,
                            dayOfIssueUntilTime: tProductsData[i].dayOfIssueUntilTime,
                            fareCode: tProductsData[i].fareCode,
                            allowRouteSelectionOnFirstUse: (tProductsData[i].allowRouteSelectionOnFirstUse != '') ? tProductsData[i].allowRouteSelectionOnFirstUse : false,
                            productRenewal: (tProductsData[i].productRenewal != '') ? tProductsData[i].productRenewal : false,
                            renewalGroupProducts: (tProductsData[i].hasOwnProperty('renewalGroupProductExternalIds')) ? tProductsData[i].renewalGroupProductExternalIds.productExternalId : [],
                            nameTranslations: (tProductsData[i].hasOwnProperty('nameTranslations') && tProductsData[i].nameTranslations != '') ? tProductsData[i].nameTranslations.nameTranslation.text : '',
                            purchasableAccess: (tProductsData[i].hasOwnProperty('purchasableAccess')) ? tProductsData[i].purchasableAccess.riderTypeExternalId : [],
                            iconPositions: (tProductsData[i].hasOwnProperty('iconPositions')) ? tProductsData[i].iconPositions.iconPosition : {},
                            ticketPunchSlots: (tProductsData[i].hasOwnProperty('ticketPunchSlots')) ? tProductsData[i].ticketPunchSlots.ticketPunchSlot : [],
                            sellableAccess: (tProductsData[i].hasOwnProperty('sellableAccess')) ? tProductsData[i].sellableAccess.device : [],
                            transitTypes: (tProductsData[i].hasOwnProperty('transitTypes')) ? tProductsData[i].transitTypes.transitType : [],
                            zones: (tProductsData[i].hasOwnProperty('zones')) ? tProductsData[i].zones.zone : [],
                            adjustments: (tProductsData[i].hasOwnProperty('adjustments')) ? tProductsData[i].adjustments.adjustment : {},
                            accountTypes: (tProductsData[i].hasOwnProperty('accountTypes')) ? tProductsData[i].accountTypes.accountType : []
                        });
                    }
                }
                // fare table - smart card prices
                let tSmartCardPrices = [];
                if (fareTableData['smartCardPrices'] && fareTableData['smartCardPrices'].hasOwnProperty('smartCardPrice')) {
                    let tSmartCardPricesData = fareTableData['smartCardPrices']['smartCardPrice'];
                    let tSmartCardPricesLen = (Array.isArray(tSmartCardPricesData)) ? tSmartCardPricesData.length : 1;
                    if (tSmartCardPricesLen == 1) { tSmartCardPricesData[0] = tSmartCardPricesData; }
                    log.info('t_SmartCardPrices', tSmartCardPricesLen);
                    for (let i=0; i<tSmartCardPricesLen; i++) {
                        tSmartCardPrices.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: tSmartCardPricesData[i].externalId,
                            name: tSmartCardPricesData[i].name,
                            nameTranslations: (tSmartCardPricesData[i].hasOwnProperty('nameTranslations') && tSmartCardPricesData[i].nameTranslations != '') ? tSmartCardPricesData[i].nameTranslations.nameTranslation.text : '',
                            priceInCents: tSmartCardPricesData[i].priceInCents,
                            productEntityClass: tSmartCardPricesData[i].productEntityClass,
                            replacementPrice: tSmartCardPricesData[i].replacementPrice,
                            minimumBalance: tSmartCardPricesData[i].minimumBalance,
                            maximumBalance: tSmartCardPricesData[i].maximumBalance,
                            depositAmount: tSmartCardPricesData[i].depositAmount,
                            discountThreshold: tSmartCardPricesData[i].discountThreshold,
                            activationDate: moment.utc(tSmartCardPricesData[i].activationDate).toISOString(),
                            nameTranslations: (tSmartCardPricesData[i].hasOwnProperty('nameTranslations') && tSmartCardPricesData[i].nameTranslations  != '') ? tSmartCardPricesData[i].nameTranslations.nameTranslation.text : '',
                            purchasableAccess: (tSmartCardPricesData[i].hasOwnProperty('purchasableAccess')) ? tSmartCardPricesData[i].purchasableAccess.riderTypeExternalId : [],
                            smartCardProfiles: (tSmartCardPricesData[i].hasOwnProperty('smartCardProfiles')) ? tSmartCardPricesData[i].smartCardProfiles.smartCardProfile : []
                        });
                    }
                }
                // fare table - functions
                let tFunctions = [];
                if (fareTableData['functions'] && fareTableData['functions'].hasOwnProperty('function')) {
                    let tFunctionsData = fareTableData['functions']['function'];
                    let tFunctionsLen = (Array.isArray(tFunctionsData)) ? tFunctionsData.length : 1;
                    if (tFunctionsLen == 1) { tFunctionsData[0] = tFunctionsData; }
                    log.info('t_Functions', tFunctionsLen);
                    for (let i=0; i<tFunctionsLen; i++) {
                        tFunctions.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            functionTypeNumber: tFunctionsData[i].functionTypeNumber,
                            name: tFunctionsData[i].name,
                            type: tFunctionsData[i].type,
                            iconBitmapName: tFunctionsData[i].iconBitmapName,
                            iconPositions: (tFunctionsData[i].hasOwnProperty('iconPositions')) ? tFunctionsData[i].iconPositions.iconPosition : {},
                        });
                    }
                }
                // fare table - media price accounts
                let tMediaPriceAccounts = [];
                if (fareTableData['mediaPriceAccounts'] && fareTableData['mediaPriceAccounts'].hasOwnProperty('mediaPriceAccount')) {
                    let tMediaPriceAccountsData = fareTableData['mediaPriceAccounts']['mediaPriceAccount'];
                    let tMediaPriceAccountsLen = (Array.isArray(tMediaPriceAccountsData)) ? tMediaPriceAccountsData.length : 1;
                    if (tMediaPriceAccountsLen == 1) { tMediaPriceAccountsData[0] = tMediaPriceAccountsData; }
                    log.info('t_MediaPriceAccounts', tMediaPriceAccountsLen);
                    for (let i=0; i<tMediaPriceAccountsLen; i++) {
                        tMediaPriceAccounts.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: tMediaPriceAccountsData[i].externalId,
                            minBalanceInCents: tMediaPriceAccountsData[i].minBalanceInCents,
                            maxBalanceInCents: tMediaPriceAccountsData[i].maxBalanceInCents,
                            discountThreshold: tMediaPriceAccountsData[i].discountThreshold,
                            riderTypeExternalIds: (tMediaPriceAccountsData[i].hasOwnProperty('riderTypeExternalIds')) ? tMediaPriceAccountsData[i].riderTypeExternalIds.riderTypeExternalId : [],
                            accountTypes: (tMediaPriceAccountsData[i].hasOwnProperty('accountTypes')) ? tMediaPriceAccountsData[i].accountTypes.accountType : []
                        });
                    }
                }
                // fare table - flat transfers
                let tFlatTransfers = [];
                if (fareTableData['flatTransfers'] && fareTableData['flatTransfers'].hasOwnProperty('flatTransfer')) {
                    let tFlatTransfersData = fareTableData['flatTransfers']['flatTransfer'];
                    let tFlatTransfersLen = (Array.isArray(tFlatTransfersData)) ? tFlatTransfersData.length : 1;
                    if (tFlatTransfersLen == 1) { tFlatTransfersData[0] = tFlatTransfersData; }
                    log.info('t_FlatTransfers', tFlatTransfersLen);
                    for (let i=0; i<tFlatTransfersLen; i++) {
                        tFlatTransfers.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            riderTypeExternalId: tFlatTransfersData[i].riderTypeExternalId,
                            transferNumber: tFlatTransfersData[i].transferNumber,
                            transferPrices: tFlatTransfersData[i].transferPrices
                        });
                    }
                }
                // fare table - smart card profiles
                let tSmartCardProfiles = [];
                if (fareTableData['smartCardProfiles'] && fareTableData['smartCardProfiles'].hasOwnProperty('smartCardProfile')) {
                    let tSmartCardProfilesData = fareTableData['smartCardProfiles']['smartCardProfile'];
                    let tSmartCardProfilesLen = (Array.isArray(tSmartCardProfilesData)) ? tSmartCardProfilesData.length : 1;
                    if (tSmartCardProfilesLen == 1) { tSmartCardProfilesData[0] = tSmartCardProfilesData; }
                    log.info('t_SmartCardProfiles', tSmartCardProfilesLen);
                    for (let i=0; i<tSmartCardProfilesLen; i++) {
                        tSmartCardProfiles.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: tSmartCardProfilesData[i].externalId,
                            cardTypeShortNumber: tSmartCardProfilesData[i].cardTypeShortNumber,
                            cardType: tSmartCardProfilesData[i].cardType,
                            cardVersionShortNumber: tSmartCardProfilesData[i].cardVersionShortNumber,
                            cardVersion: tSmartCardProfilesData[i].cardVersion,
                            description: tSmartCardProfilesData[i].description,
                            expiryPeriodInDays: tSmartCardProfilesData[i].expiryPeriodInDays,
                            artworkTypeName: tSmartCardProfilesData[i].artworkTypeName,
                            artworkTypeId: tSmartCardProfilesData[i].artworkTypeId,
                            cardTechnology: tSmartCardProfilesData[i].cardTechnology,
                            settingName: tSmartCardProfilesData[i].settingName,
                            settingId: tSmartCardProfilesData[i].settingId,
                            physicalCardType: tSmartCardProfilesData[i].physicalCardType,
                            mediaPurposeType: tSmartCardProfilesData[i].mediaPurposeType,
                            createdTimestamp: moment.utc(tSmartCardProfilesData[i].createdTimestamp).toISOString(),
                            lastChangedTimestamp: moment.utc(tSmartCardProfilesData[i].lastChangedTimestamp).toISOString(),
                            isEnabled: (tSmartCardProfilesData[i].isEnabled != '') ? tSmartCardProfilesData[i].isEnabled : false,
                            noOfAssignedCards: tSmartCardProfilesData[i].noOfAssignedCards,
                            nameTranslations: (tSmartCardProfilesData[i].hasOwnProperty('nameTranslations') && tSmartCardProfilesData[i].nameTranslations  != '') ? tSmartCardProfilesData[i].nameTranslations.nameTranslation.text : '',
                        });
                    }
                }
                // fare table - smart card artwork types
                let tSmartCardArtworkTypes = [];
                if (fareTableData['smartCardArtworkTypes'] && fareTableData['smartCardArtworkTypes'].hasOwnProperty('smartCardArtworkType')) {
                    let tSmartCardArtworkTypesData = fareTableData['smartCardArtworkTypes']['smartCardArtworkType'];
                    let tSmartCardArtworkTypesLen = (Array.isArray(tSmartCardArtworkTypesData)) ? tSmartCardArtworkTypesData.length : 1;
                    if (tSmartCardArtworkTypesLen == 1) { tSmartCardArtworkTypesData[0] = tSmartCardArtworkTypesData; }
                    log.info('t_SmartCardArtworkTypes', tSmartCardArtworkTypesLen);
                    for (let i=0; i<tSmartCardArtworkTypesLen; i++) {
                        tSmartCardArtworkTypes.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            name: tSmartCardArtworkTypesData[i].name,
                            description: tSmartCardArtworkTypesData[i].description,
                            originalFrontsideFilename: tSmartCardArtworkTypesData[i].originalFrontsideFilename,
                            originalBacksideFilename: tSmartCardArtworkTypesData[i].originalBacksideFilename,
                            createdTimestamp: moment.utc(tSmartCardArtworkTypesData[i].createdTimestamp).toISOString(),
                            lastChangedTimestamp: moment.utc(tSmartCardArtworkTypesData[i].lastChangedTimestamp).toISOString(),
                            isEnabled: (tSmartCardArtworkTypesData[i].isEnabled != '') ? tSmartCardArtworkTypesData[i].isEnabled : false,
                            noOfAssignedCardProfiles: tSmartCardArtworkTypesData[i].noOfAssignedCardProfiles
                        });
                    }
                }
                // fare table - smart card type settings
                let tSmartCardTypeSettings = [];
                if (fareTableData['smartCardTypeSettings'] && fareTableData['smartCardTypeSettings'].hasOwnProperty('smartCardTypeSetting')) {
                    let tSmartCardTypeSettingsData = fareTableData['smartCardTypeSettings']['smartCardTypeSetting'];
                    let tSmartCardTypeSettingsLen = (Array.isArray(tSmartCardTypeSettingsData)) ? tSmartCardTypeSettingsData.length : 1;
                    if (tSmartCardTypeSettingsLen == 1) { tSmartCardTypeSettingsData[0] = tSmartCardTypeSettingsData; }
                    log.info('t_SmartCardTypeSettings', tSmartCardTypeSettingsLen);
                    for (let i=0; i<tSmartCardTypeSettingsLen; i++) {
                        tSmartCardTypeSettings.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            name: tSmartCardTypeSettingsData[i].name,
                            description: tSmartCardTypeSettingsData[i].description,
                            originalFilename: tSmartCardTypeSettingsData[i].originalFilename,
                            createdTimestamp: moment.utc(tSmartCardTypeSettingsData[i].createdTimestamp).toISOString(),
                            lastChangedTimestamp: moment.utc(tSmartCardTypeSettingsData[i].lastChangedTimestamp).toISOString(),
                            isEnabled: (tSmartCardTypeSettingsData[i].isEnabled != '') ? tSmartCardTypeSettingsData[i].isEnabled : false,
                            noOfAssignedCardProfiles: tSmartCardTypeSettingsData[i].noOfAssignedCardProfiles
                        });
                    }
                }
                // fare table - vat rates
                let tVatRates = [];
                if (fareTableData['vatRates'] && fareTableData['vatRates'].hasOwnProperty('vatRate')) {
                    let tVatRatesData = fareTableData['vatRates']['vatRate'];
                    let tVatRatesLen = (Array.isArray(tVatRatesData)) ? tVatRatesData.length : 1;
                    if (tVatRatesLen == 1) { tVatRatesData[0] = tVatRatesData; }
                    log.info('t_VatRates', tVatRatesLen);
                    for (let i=0; i<tVatRatesLen; i++) {
                        tVatRates.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            externalId: tVatRatesData[i].externalId,
                            name: tVatRatesData[i].name,
                            description: tVatRatesData[i].description,
                            code: tVatRatesData[i].code,
                            percents: tVatRatesData[i].percentageAmountInOneHundredths,
                            isDefault: (tVatRatesData[i].isDefault != '') ? tVatRatesData[i].isDefault : false,
                            associatedProductTypes: tVatRatesData[i].productTypes
                        });
                    }
                }
                // fare table - vat rules
                let tVatRules = [];
                if (fareTableData['vatRules'] && fareTableData['vatRules'].hasOwnProperty('vatRule')) {
                    let tVatRulesData = fareTableData['vatRules']['vatRule'];
                    let tVatRulesLen = (Array.isArray(tVatRulesData)) ? tVatRulesData.length : 1;
                    if (tVatRulesLen == 1) { tVatRulesData[0] = tVatRulesData; }
                    log.info('t_VatRules', tVatRulesLen);
                    for (let i=0; i<tVatRulesLen; i++) {
                        tVatRules.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            paymentInstrumentType: tVatRulesData[i].paymentInstrumentType,
                            isVatDeductedDuringTopup: tVatRulesData[i].isVatDeductedDuringTopup
                        });
                    }
                }
                // fare table insert
                Promise.all([
                    this.fareTableModel.FlatTransferSettings.insertMany(tFlatTransferSettings),
                    this.fareTableModel.FareCodeTypes.insertMany(tFareCodeTypes),
                    this.fareTableModel.FlatFares.insertMany(tFlatFares),
                    this.fareTableModel.ZoneFares.insertMany(tZoneFares),
                    this.fareTableModel.FareCodes.insertMany(tFareCodes),
                    this.fareTableModel.FromToStageFareCodes.insertMany(tFromToStageFareCodes),
                    this.fareTableModel.FromToStageFareSettings.insertMany(tFromToStageFareSettings),
                    this.fareTableModel.FromToZoneFareCodes.insertMany(tFromToZoneFareCodes),
                    this.fareTableModel.FromToZoneFareSettings.insertMany(tFromToZoneFareSettings),
                    this.fareTableModel.MultiPriceProducts.insertMany(tMultiPriceProducts),
                    this.fareTableModel.TransitTypes.insertMany(tTransitTypes),
                    this.fareTableModel.RiderTypes.insertMany(tRiderTypes),
                    this.fareTableModel.Products.insertMany(tProducts),
                    this.fareTableModel.SmartCardPrices.insertMany(tSmartCardPrices),
                    this.fareTableModel.Functions.insertMany(tFunctions),
                    this.fareTableModel.MediaPriceAccounts.insertMany(tMediaPriceAccounts),
                    this.fareTableModel.FlatTransfers.insertMany(tFlatTransfers),
                    this.fareTableModel.SmartCardProfiles.insertMany(tSmartCardProfiles),
                    this.fareTableModel.SmartCardArtworkTypes.insertMany(tSmartCardArtworkTypes),
                    this.fareTableModel.SmartCardTypeSettings.insertMany(tSmartCardTypeSettings),
                    this.fareTableModel.VatRates.insertMany(tVatRates),
                    this.fareTableModel.VatRules.insertMany(tVatRules)
                ]);
            }

            ////////////////// translations //////////////////
            let translationConfigs = await this.parseXmltoJson(path.join(distributionFilesPath, 'fare', 'translations.xml'));
            let translationConfigsData = translationConfigs['translationConfig'];
            let translationConfigsLen = (Array.isArray(translationConfigsData)) ? translationConfigsData.length : 1;
            if (translationConfigsLen == 1) { translationConfigsData[0] = translationConfigsData; }
            for (let t=0; t<translationConfigsLen; t++) {
                let translationsData = translationConfigsData[t];
                log.info('translations activationDate', translationsData.activationDate);

                const fareEnvironmentConfigurationInserted = await this.fareEnvironmentModel.Configuration.find({distributionVersion: heartbeat.distributionVersion, activationDate: moment.utc(translationsData.activationDate).toISOString()});

                // translations
                let tTranslations = [];
                if (translationsData['translations'] && translationsData['translations'].hasOwnProperty('translation')) {
                    let tTranslationsData = translationsData['translations']['translation'];
                    let tTranslationsLen = (Array.isArray(tTranslationsData)) ? tTranslationsData.length : 1;
                    if (tTranslationsLen == 1) { tTranslationsData[0] = tTranslationsData; }
                    log.info('t_Translations', tTranslationsLen);
                    for (let i=0; i<tTranslationsLen; i++) {
                        tTranslations.push({
                            fareEnvironmentConfigurationId: fareEnvironmentConfigurationInserted[0]._id,
                            locale: tTranslationsData[i].locale.replace('_', '-'),
                            type: tTranslationsData[i].translationType,
                            key: tTranslationsData[i].translationKey,
                            value: tTranslationsData[i].translationValue
                        });
                    }
                }
                // translations insert
                Promise.all([
                    this.translationsModel.insertMany(tTranslations)
                ]);
            }
        } catch (err) {
            log.error(err);
        }
    }

    distributionClean = async () => {
        try {
            let deviceMnemonic = process.env.CLIENT_DEVICE_MNEMONIC;
            let distributionFilesPath = path.join(process.cwd(), process.env.FOLDER_DISTRIBUTION);

            if (fs.existsSync(path.join(distributionFilesPath, 'fare'))) {
                fs.rmSync(path.join(distributionFilesPath, 'fare'), { recursive: true });
            }
            if (fs.existsSync(path.join(distributionFilesPath, deviceMnemonic))) {
                fs.rmSync(path.join(distributionFilesPath, deviceMnemonic), { recursive: true });
            }
        } catch (err) {
            log.error(err);
        }
    }

    distributionRemove = async () => {
        try {
            const removeDistribution = await this.fareEnvironmentModel.Configuration.findOne({isActive: false, isPending: false});
            if (removeDistribution) {
                Promise.all([
                    this.fareEnvironmentModel.Configuration.deleteMany({_id: removeDistribution._id}),
                    this.fareEnvironmentModel.TransitTypes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.ZoneTypes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.Zones.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.Routes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.Stops.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.Stations.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.DistanceSegmentTypes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.StageByRouteBorderAdjustments.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.VerificationDocuments.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.VerificationDocumentElements.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareEnvironmentModel.RiderLanguages.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareDistanceModel.DistanceFares.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareDistanceModel.FromStopToStopFareAdjustments.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareDistanceModel.FromToStationFares.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.FlatTransferSettings.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.FareCodeTypes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.FlatFares.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.ZoneFares.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.FareCodes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.FromToStageFareCodes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.FromToStageFareSettings.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.FromToZoneFareCodes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.MultiPriceProducts.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.TransitTypes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.RiderTypes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.Products.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.SmartCardPrices.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.Functions.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.MediaPriceAccounts.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.FlatTransfers.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.SmartCardProfiles.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.SmartCardArtworkTypes.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.SmartCardTypeSettings.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.VatRates.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.fareTableModel.VatRules.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.translationsModel.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id}),
                    this.employeeRoleModel.deleteMany({fareEnvironmentConfigurationId: removeDistribution._id})
                ]);
                return true;
            }
            return false;
        } catch (err) {
            log.error(err);
        }
    }

    softwarePkgSwitch = async (active) => {
        try {
            let distributionFilesPath = path.join(process.cwd(), process.env.FOLDER_DISTRIBUTION);
            let documentsFilesPath = path.join(process.cwd(), process.env.FOLDER_DOCUMENTS);
            let applicationFilesPath = 'C:/PointOfSales';

            let softwarePkgFolder = fs.readdirSync(distributionFilesPath).find((folder) => folder == 'pos_software_pkg_' + active);
            if (softwarePkgFolder) {
                if (fs.existsSync(path.join(distributionFilesPath, softwarePkgFolder))) {
                    if (global.currentPosData.appVersion != active && parseInt(global.currentPosData.appVersion.replace(/\./g, '')) < parseInt(active.replace(/\./g, ''))) {
                        log.info('softwarePkgSwitch-switch', active);
                        // stop sam smartcard lib if in use
                        if (process.env.CLIENT_SECURE_ACCESS_MODULE == 'true') {
                            this.helpersCtrl.samSmartCardLibStop();
                        }
                        // set appVersion to extracted software package version
                        await this.model.findOneAndUpdate({deviceId: global.currentPosData.deviceId}, {appVersion: active});
                        // remove unneeded extracted software package folder
                        fs.rmSync(path.join(distributionFilesPath, softwarePkgFolder, 'POSFiles'), { recursive: true });
                        // move or delete extracted software package folder
                        if (fs.existsSync(path.join(documentsFilesPath, softwarePkgFolder))) {
                            fs.rmSync(path.join(distributionFilesPath, softwarePkgFolder), { recursive: true });
                        } else {
                            fs.renameSync(path.join(distributionFilesPath, softwarePkgFolder), path.join(documentsFilesPath, softwarePkgFolder));
                        }
                        // update application with extracted software package files
                        fse.copySync(path.join(documentsFilesPath, softwarePkgFolder), applicationFilesPath);
                        fs.rmSync(path.join(documentsFilesPath, softwarePkgFolder), { recursive: true });
                        log.info('softwarePkgSwitch-done');
                        // install software package dependencies if changed
                        spawnSync('npm.cmd', ['install'], {cwd: path.join(process.cwd())});
                        // deamonize application
                        spawnSync('npm.cmd', ['link', 'node-windows'], {cwd: path.join(process.cwd())});
                        // restart application
                        spawnSync('npm.cmd', ['run', 'service'], {cwd: path.join(process.cwd())});
                    } else {
                        log.info('softwarePkgSwitch-skiped', active);
                    }
                }
            }
        } catch (err) {
            log.error(err);
        }
    }

    actionListDownload = async (current, heartbeat) => {
        try {
            const actionListZipFile = await this.actionList(current, heartbeat);
            // const actionListZipFile = path.join(process.cwd(), process.env.FOLDER_ACTION_LIST, '240425120000.zip');

            ////////////////// action list delete //////////////////
            await this.smartCardActionsModel.deleteMany();

            ////////////////// action list unzip //////////////////
            let actionListFilesPath = path.join(process.cwd(), process.env.FOLDER_ACTION_LIST);
            let actionListFile = new AdmZip(actionListZipFile);
            actionListFile.extractAllTo(actionListFilesPath, true);

            ////////////////// action list actions //////////////////
            let actionListEntries = [];
            actionListFile.getEntries().forEach(async (entry) => {
                let actionListSqlliteDb = path.join(actionListFilesPath, entry.entryName);
                let actionListSqlliteTable = (entry.entryName.startsWith('sactm_')) ? 'action_queue' : 'action_queue_dif';
                let smartCardActions = await this.actionListData(actionListSqlliteDb, actionListSqlliteTable);

                for (let i=0; i<smartCardActions.length; i++) {
                    let xml = await xml2js.parseStringPromise(smartCardActions[i].xml, { attrkey: 'att', charkey: 'text', explicitArray: false, mergeAttrs: true });
                    let smartCardAction = {type: Object.keys(xml)[0].replace('Dto', '')};

                    switch (Object.keys(xml)[0]) {
                    // immediate actions
                        case 'CardBlockDto':
                        case 'CardUnblockDto':
                            smartCardAction.value = xml[Object.keys(xml)[0]].reasonCode;
                            smartCardAction.params = {};
                        break;
                        case 'contractBlock':
                        case 'contractUnblock':
                            smartCardAction.value = xml[Object.keys(xml)[0]].contractID;
                            smartCardAction.params = {reasonCode: xml[Object.keys(xml)[0]].reasonCode, instanceID: xml[Object.keys(xml)[0]].instanceID};
                        break;
                        case 'recreateCard':
                            smartCardAction.value = xml[Object.keys(xml)[0]].card.readOnly.uid;
                            smartCardAction.params = {card: xml[Object.keys(xml)[0]].card};
                        break;
                    // waiting actions
                        case 'RemoveContractDto':
                            smartCardAction.value = xml[Object.keys(xml)[0]].contractID;
                            smartCardAction.params = {reasonCode: xml[Object.keys(xml)[0]].reasonCode, instanceID: xml[Object.keys(xml)[0]].instanceID};
                        break;
                        case 'RechargeValueDto':
                            smartCardAction.value = xml[Object.keys(xml)[0]].valueCents;
                            smartCardAction.params = {contractID: xml[Object.keys(xml)[0]].contractID};
                        break;
                        case 'refundableDeposit':
                            smartCardAction.value = xml[Object.keys(xml)[0]].price;
                            smartCardAction.params = {};
                        break;
                        case 'changeRiderType':
                            smartCardAction.value = xml[Object.keys(xml)[0]].riderType;
                            smartCardAction.params = {};
                        break;
                        case 'AddEPurseContractDto':
                            smartCardAction.value = xml[Object.keys(xml)[0]].contractID;
                            smartCardAction.params = {};
                        break;
                        case 'addPeriodPass':
                            smartCardAction.value = xml[Object.keys(xml)[0]].contractID;
                            smartCardAction.params = {reasonID: xml[Object.keys(xml)[0]].reasonID, productID: xml[Object.keys(xml)[0]].productID, creationDate: moment.utc((xml[Object.keys(xml)[0]].preactivated.creationDate) ? xml[Object.keys(xml)[0]].preactivated.creationDate : 0).toISOString(), expiryDate: moment.utc((xml[Object.keys(xml)[0]].preactivated.expiryDate) ? xml[Object.keys(xml)[0]].preactivated.expiryDate : 0).toISOString()};
                        break;
                        case 'addStoredRides':
                            smartCardAction.value = xml[Object.keys(xml)[0]].contractID;
                            smartCardAction.params = {rides: xml[Object.keys(xml)[0]].rides};
                        break;
                        case 'modifyContract':
                            smartCardAction.value = xml[Object.keys(xml)[0]].contractID;
                            smartCardAction.params = {reasonCode: xml[Object.keys(xml)[0]].reasonCode, instanceID: xml[Object.keys(xml)[0]].instanceID, expiryDate: moment.utc((xml[Object.keys(xml)[0]].expiryDate) ? xml[Object.keys(xml)[0]].expiryDate : 0).toISOString(), rides: (xml[Object.keys(xml)[0]].storedRides) ? xml[Object.keys(xml)[0]].storedRides : 0};
                        break;
                        default:
                            smartCardAction.value = 'unknown';
                            smartCardAction.params = {data: xml[Object.keys(xml)[0]]};
                    }

                    await this.smartCardActionsModel.create({
                        cardType: smartCardActions[i].card_type,
                        cardUid: smartCardActions[i].card_uid,
                        queue: (smartCardActions[i].queue == 1) ? true : false,
                        queueCounter: smartCardActions[i].queue_counter,
                        sequenceNumber: smartCardActions[i].sequence_number,

                        action: smartCardAction.type,
                        actionValue: smartCardAction.value,
                        actionParams: smartCardAction.params
                    }).catch((err) => { });
                }
            });

        } catch (err) {
            log.error(err);
        }
    }

    actionListData = async (database, table) => {
        return new Promise((resolve, reject) => {
            let data;
            let db = new sqlite3.Database(database);

            db.all('SELECT card_type, card_uid, queue, queue_counter, sequence_number, xml FROM ' + table, (err, rows) => {
                if (err) { reject(err.message); }
                data = rows;
            });

            db.close((err) => {
                if (err) { reject(err.message); }
                fs.unlinkSync(database);
                resolve(data);
            });
        });
    }

    getCommands = async (current) => {
        try {
            const getCommandsResponse = await this.service.getCommands(current.deviceId);

            return (getCommandsResponse) ? getCommandsResponse : [];
        } catch (err) {
            log.error(err);
        }
    }

    executeCommands = async (commands) => {
        try {
            log.info('Pending commands', commands.length);
            let executedCommands = [];

            const knownCommands = ['startAnyDesk', 'getAnyDeskId', 'setAnyDeskPassword'];

            for (let i=0; i<commands.length; i++) {
                const commandId = commands[i].id;
                const command = commands[i].command.split(':');

                if (knownCommands.includes(command[0])) {
                    await this.service.markCommandReceived(global.currentPosData.deviceId, commandId, {eventTimestamp: moment.utc().toISOString()});
                    let message = '';

                    if (command[0] == 'startAnyDesk') {
                        const anyDeskFolder = path.join('C:/Program Files (x86)', 'AnyDesk');
                        execSync('AnyDesk.exe --start', {cwd: anyDeskFolder});
                        message = 'anyDeskStarted';
                    }
                    if (command[0] == 'getAnyDeskId') {
                        const anyDeskFile = envfile.parse(fs.readFileSync(path.join('C:/Users/'+os.userInfo().username, 'AppData', 'Roaming', 'AnyDesk', 'system.conf')));
                        message = 'anyDeskId:'+anyDeskFile['ad.anynet.id'];
                    }
                    if (command[0] == 'setAnyDeskPassword') {
                        const anyDeskPass = (command[1]) ? command[1] : 'Temp1234!';
                        const anyDeskFolder = path.join('C:/Program Files (x86)', 'AnyDesk');
                        execSync('echo '+anyDeskPass+' | AnyDesk.exe --set-password', {cwd: anyDeskFolder});
                        message = 'anyDeskPasswordSet:'+anyDeskPass;
                    }

                    const markCommandExecuted = await this.service.markCommandExecuted(global.currentPosData.deviceId, commandId, {eventTimestamp: moment.utc().toISOString(), message: message});
                    if (markCommandExecuted) {
                        executedCommands.push({command: command[0], message: message});
                    }
                }
            }

            log.info('Executed commands', executedCommands.length, executedCommands);
        } catch (err) {
            log.error(err);
        }
    }

}

module.exports = PosCtrl;
