const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const aesCmac = require('node-aes-cmac').aesCmac;
const moment = require('moment');
const ByteBuffer = require('bytebuffer');
const xml2js = require('xml2js');
const log = require('loglevel');

const constants = require('../models/constants');
const enumsXml = require('../models/enums-xml');

const SmartCardServ = require('../services/smart-card');
const HelpersCtrl = require('../controllers/helpers');

class FinancialTransactionServ {
    constructor () {
        this.axios = global.axiosInstances.axiosAfcInstance;

        this.smartCardServ = new SmartCardServ();
        this.helpersCtrl = new HelpersCtrl();
        this.dateTTimeFormat = 'YYYY-MM-DD[T]HH:mm:ss[Z]';
        this.dateFormat = 'YYYY-MM-DD';

        this.qrCompression = (['kuwait', 'hafil', 'sharjah', 'mok'].includes(process.env.CLIENT_NAME)) ? constants.qrCodeContentsCompression.gzip : constants.qrCodeContentsCompression.none;

        this.certDescriptor = (process.env.CLIENT_FISCAL_VERIFICATION == 'true') ? this.getCertData() : null; // this.getCertData('10596283-1', '3OR7DJDF8485')
        this.softwareSupplierTaxNumber = 25639072; // Ridango SI
    }

    getQrCodeDefinition = async () => {
        try {
            let qrCodeId;
            let qrCodeVersion;
            let qrcodeSize;
            let qrcodeStructure;
            if (['oman'].includes(process.env.CLIENT_NAME)) {
                qrCodeId = 128;
                qrCodeVersion = 3;
                qrcodeSize = 152;
                qrcodeStructure = {
                    qrCodeIdReference:                              {type: 'unsignedByte', offset: 0},
                    qrCodeVersionReference:                         {type: 'unsignedByte', offset: 1},
                    qrCodeIssuingTimestamp:                         {type: 'posixTimestamp', offset: 2},
                    qrCodeExpirationTimestamp:                      {type: 'posixTimestamp', offset: 6},
                    qrCodeIssuingTerminalId:                        {type: 'unsignedInt', offset: 10},
                    qrCodeMerchantOrganizationId:                   {type: 'unsignedInt', offset: 14},
                    fareItemExternalId:                             {type: 'unsignedInt', offset: 18},
                    fareItemTypeShortNumber:                        {type: 'unsignedByte', offset: 22},
                    fareItemCategoryShortNumber:                    {type: 'unsignedByte', offset: 23},
                    fareItemIssuedNumber:                           {type: 'unsignedLong', offset: 24},
                    fareItemIssuedStatus:                           {type: 'unsignedByte', offset: 32},
                    issuedBoardingStation:                          {type: 'unsignedShort', offset: 33},
                    issuedAlighthingStation:                        {type: 'unsignedShort', offset: 35},
                    issuedRouteType:                                {type: 'unsignedByte', offset: 37},
                    issuedRouteTypeTransferEligability:             {type: 'unsignedInt', offset: 38},
                    fareItemStatus:                                 {type: 'unsignedByte', offset: 42},
                    fareItemStatusZonesLeft:                        {type: 'unsignedInt', offset: 43},
                    fareItemStatusLastChangedTimestamp:             {type: 'posixTimestamp', offset: 47},
                    fareItemStatusTransfersLeft:                    {type: 'unsignedByte', offset: 51},
                    fareItemStatusRemainingRides:                   {type: 'unsignedByte', offset: 52},
                    fareItemExpirationTimeStamp:                    {type: 'posixTimestamp', offset: 53},
                    passengerRiderTypeExternalIdFirst:              {type: 'unsignedByte', offset: 57},
                    noOfPassengersFirst:                            {type: 'unsignedByte', offset: 58},
                    passengerRiderTypeExternalIdSecond:             {type: 'unsignedByte', offset: 59},
                    noOfPassengersSecond:                           {type: 'unsignedByte', offset: 60},
                    passengerRiderTypeExternalIdThird:              {type: 'unsignedByte', offset: 61},
                    noOfPassengersThird:                            {type: 'unsignedByte', offset: 62},
                    passengerRiderTypeExternalIdFourth:             {type: 'unsignedByte', offset: 63},
                    noOfPassengersFourth:                           {type: 'unsignedByte', offset: 64},
                    accountRiderType:                               {type: 'unsignedByte', offset: 65},
                    accountId:                                      {type: 'unsignedInt', offset: 66},
                    epurseBalanceInCents:                           {type: 'unsignedInt', offset: 70},
                    dailyCumulativeSpendingInCents:                 {type: 'unsignedInt', offset: 74},
                    weeklyCumulativeSpendingInCents:                {type: 'unsignedInt', offset: 78},
                    monthlyCumulativeSpendingInCents:               {type: 'unsignedInt', offset: 82},
                    logOneTransactionTimestamp:                     {type: 'posixTimestamp', offset: 86},
                    logOneJourneyId:                                {type: 'unsignedInt', offset: 90},
                    logOneJourneySequenceCounter:                   {type: 'unsignedByte', offset: 94},
                    logOneRouteExternalId:                          {type: 'unsignedShort', offset: 95},
                    logOneBoardingActivityShortNumber:              {type: 'unsignedByte', offset: 97},
                    logOneActualAlightingStopCodeMachineId:         {type: 'unsignedInt', offset: 98},
                    logOneActualBoardingStopCodeMachineId:          {type: 'unsignedInt', offset: 102},
                    logOneJourneyTraveledAreasNumber:               {type: 'unsignedByte', offset: 106},
                    logOneJourneyVisitedZones:                      {type: 'unsignedByte', offset: 107},
                    logOneJourneyVisitedSharedZones:                {type: 'unsignedByte', offset: 108},
                    logOneJourneyReducedVisitedIntersectionOne:     {type: 'unsignedByte', offset: 109},
                    logOneJourneyReducedVisitedIntersectionTwo:     {type: 'unsignedByte', offset: 110},
                    logOneJourneyReducedVisitedIntersectionThree:   {type: 'unsignedByte', offset: 111},
                    logOneJourneyReducedVisitedIntersectionFour:    {type: 'unsignedByte', offset: 112},
                    logOneJourneyReducedVisitedIntersectionFive:    {type: 'unsignedByte', offset: 113},
                    logOneJourneyReducedVisitedIntersectionSix:     {type: 'unsignedByte', offset: 114},
                    logOneJourneyReducedVisitedIntersectionSeven:   {type: 'unsignedByte', offset: 115},
                    logOneJourneyReducedVisitedIntersectionEight:   {type: 'unsignedByte', offset: 116},
                    logTwoTransactionTimestamp:                     {type: 'posixTimestamp', offset: 117},
                    logTwoJourneyId:                                {type: 'unsignedInt', offset: 121},
                    logTwoJourneySequenceCounter:                   {type: 'unsignedByte', offset: 125},
                    logTwoRouteExternalId:                          {type: 'unsignedShort', offset: 126},
                    logTwoBoardingActivityShortNumber:              {type: 'unsignedByte', offset: 128},
                    logTwoActualAlightingStopCodeMachineId:         {type: 'unsignedInt', offset: 129},
                    logTwoActualBoardingStopCodeMachineId:          {type: 'unsignedInt', offset: 133},
                    logTwoJourneyTraveledAreasNumber:               {type: 'unsignedByte', offset: 137},
                    logTwoJourneyVisitedZones:                      {type: 'unsignedByte', offset: 138},
                    logTwoJourneyVisitedSharedZones:                {type: 'unsignedByte', offset: 139},
                    logTwoJourneyReducedVisitedIntersectionOne:     {type: 'unsignedByte', offset: 140},
                    logTwoJourneyReducedVisitedIntersectionTwo:     {type: 'unsignedByte', offset: 141},
                    logTwoJourneyReducedVisitedIntersectionThree:   {type: 'unsignedByte', offset: 142},
                    logTwoJourneyReducedVisitedIntersectionFour:    {type: 'unsignedByte', offset: 143},
                    logTwoJourneyReducedVisitedIntersectionFive:    {type: 'unsignedByte', offset: 144},
                    logTwoJourneyReducedVisitedIntersectionSix:     {type: 'unsignedByte', offset: 145},
                    logTwoJourneyReducedVisitedIntersectionSeven:   {type: 'unsignedByte', offset: 146},
                    logTwoJourneyReducedVisitedIntersectionEight:   {type: 'unsignedByte', offset: 147},
                    issuedRoute:                                    {type: 'unsignedInt', offset: 148}
                };
            }
            if (['kuwait'].includes(process.env.CLIENT_NAME)) {
                qrCodeId = 3;
                qrCodeVersion = 1;
                qrcodeSize = 111;
                qrcodeStructure = {
                    qrCodeIdReference:                              {type: 'unsignedByte', offset: 0},
                    qrCodeVersionReference:                         {type: 'unsignedByte', offset: 1},
                    qrCodeIssuingTimestamp:                         {type: 'posixTimestamp', offset: 2},
                    qrCodeExpirationTimestamp:                      {type: 'posixTimestamp', offset: 6},
                    qrCodeIssuingTerminalId:                        {type: 'unsignedInt', offset: 10},
                    qrCodeMerchantOrganizationId:                   {type: 'unsignedInt', offset: 14},
                    fareItemExternalId:                             {type: 'unsignedInt', offset: 18},
                    fareItemTypeShortNumber:                        {type: 'unsignedByte', offset: 22},
                    fareItemCategoryShortNumber:                    {type: 'unsignedByte', offset: 23},
                    fareItemIssuedNumber:                           {type: 'unsignedLong', offset: 24},
                    fareItemIssuedStatus:                           {type: 'unsignedByte', offset: 32},
                    issuedBoardingStation:                          {type: 'unsignedShort', offset: 33},
                    issuedAlightingStation:                         {type: 'unsignedShort', offset: 35},
                    issuedBoardingStop:                             {type: 'unsignedShort', offset: 37},
                    issuedAlightingStop:                            {type: 'unsignedShort', offset: 39},
                    issuedRouteType:                                {type: 'unsignedByte', offset: 41},
                    issuedRouteTypeTransferEligibility:             {type: 'unsignedInt', offset: 42},
                    fareProductTypeShortNumber:                     {type: 'unsignedByte', offset: 46},
                    fareItemStatus:                                 {type: 'unsignedByte', offset: 47},
                    fareItemStatusZonesLeft:                        {type: 'unsignedInt', offset: 48},
                    fareItemStatusLastChangedTimestamp:             {type: 'posixTimestamp', offset: 52},
                    fareItemStatusTransfersLeft:                    {type: 'unsignedByte', offset: 56},
                    fareItemStatusRemainingRides:                   {type: 'unsignedByte', offset: 57},
                    fareItemExpirationTimestamp:                    {type: 'posixTimestamp', offset: 58},
                    passengerRiderTypeExternalIdFirst:              {type: 'unsignedByte', offset: 62},
                    noOfPassengersFirst:                            {type: 'unsignedByte', offset: 63},
                    passengerRiderTypeExternalIdSecond:             {type: 'unsignedByte', offset: 64},
                    noOfPassengersSecond:                           {type: 'unsignedByte', offset: 65},
                    accountRiderType:                               {type: 'unsignedByte', offset: 66},
                    accountId:                                      {type: 'unsignedInt', offset: 67},
                    ePurseBalanceInCents:                           {type: 'unsignedInt', offset: 71},
                    dailyCumulativeSpendingInCents:                 {type: 'unsignedInt', offset: 75},
                    weeklyCumulativeSpendingInCents:                {type: 'unsignedInt', offset: 79},
                    monthlyCumulativeSpendingInCents:               {type: 'unsignedInt', offset: 83},
                    logOneTransactionTimestamp:                     {type: 'posixTimestamp', offset: 87},
                    logOneJourneyId:                                {type: 'unsignedInt', offset: 91},
                    logOneJourneySequenceCounter:                   {type: 'unsignedByte', offset: 95},
                    logOneRouteExternalId:                          {type: 'unsignedShort', offset: 96},
                    logOneBoardingActivityShortNumber:              {type: 'unsignedByte', offset: 98},
                    logOneActualAlightingStopCodeMachineId:         {type: 'unsignedInt', offset: 99},
                    logOneActualBoardingStopCodeMachineId:          {type: 'unsignedInt', offset: 103},
                    issuedRoute:                                    {type: 'unsignedInt', offset: 107}
                };
            }
            if (['hafil'].includes(process.env.CLIENT_NAME)) {
                qrCodeId = 4;
                qrCodeVersion = 1;
                qrcodeSize = 69;
                qrcodeStructure = {
                    qrCodeIdReference:                              {type: 'unsignedByte', offset: 0},
                    qrCodeVersionReference:                         {type: 'unsignedByte', offset: 1},
                    qrCodeIssuingTimestamp:                         {type: 'posixTimestamp', offset: 2},
                    qrCodeExpirationTimestamp:                      {type: 'posixTimestamp', offset: 6},
                    qrCodeIssuingTerminalId:                        {type: 'unsignedInt', offset: 10},
                    qrCodeMerchantOrganizationId:                   {type: 'unsignedInt', offset: 14},
                    fareItemExternalId:                             {type: 'unsignedInt', offset: 18},
                    fareItemTypeShortNumber:                        {type: 'unsignedByte', offset: 22},
                    fareItemCategoryShortNumber:                    {type: 'unsignedByte', offset: 23},
                    fareItemIssuedNumber:                           {type: 'unsignedLong', offset: 24},
                    fareItemIssuedStatus:                           {type: 'unsignedByte', offset: 32},
                    issuedRouteType:                                {type: 'unsignedByte', offset: 33},
                    contractCategoryShortNumber:                    {type: 'unsignedByte', offset: 34},
                    issuedBoardingStation:                          {type: 'unsignedShort', offset: 35},//unsignedByte
                    issuedAlightingStation:                         {type: 'unsignedShort', offset: 37},//unsignedByte
                    fareItemStatus:                                 {type: 'unsignedByte', offset: 39},
                    fareItemStatusLastChangedTimestamp:             {type: 'posixTimestamp', offset: 40},
                    fareItemStatusTransfersLeft:                    {type: 'unsignedByte', offset: 44},
                    fareItemStatusRemainingRides:                   {type: 'unsignedByte', offset: 45},
                    fareItemExpirationTimestamp:                    {type: 'posixTimestamp', offset: 46},
                    accountRiderType:                               {type: 'unsignedByte', offset: 50},
                    accountId:                                      {type: 'unsignedInt', offset: 51},
                    issuedTripId:                                   {type: 'unsignedShort', offset: 55},
                    issuedRoute:                                    {type: 'unsignedInt', offset: 57},
                    issuedTripStartTimestamp:                       {type: 'posixTimestamp', offset: 61},
                    issuedTripEndEstimatedTimestamp:                {type: 'posixTimestamp', offset: 65}
                };
            }
            if (['sharjah'].includes(process.env.CLIENT_NAME)) {
                qrCodeId = 5;
                qrCodeVersion = 1;
                qrcodeSize = 53;
                qrcodeStructure = {
                    qrCodeIdReference:                              {type: 'unsignedByte', offset: 0},
                    qrCodeVersionReference:                         {type: 'unsignedByte', offset: 1},
                    qrCodeIssuingTimestamp:                         {type: 'posixTimestamp', offset: 2},
                    qrCodeExpirationTimestamp:                      {type: 'posixTimestamp', offset: 6},
                    qrCodeIssuingTerminalId:                        {type: 'unsignedInt', offset: 10},
                    qrCodeMerchantOrganizationId:                   {type: 'unsignedInt', offset: 14},
                    fareItemExternalId:                             {type: 'unsignedInt', offset: 18},
                    fareItemTypeShortNumber:                        {type: 'unsignedByte', offset: 22},
                    fareItemCategoryShortNumber:                    {type: 'unsignedByte', offset: 23},
                    fareItemIssuedNumber:                           {type: 'unsignedLong', offset: 24},
                    fareItemIssuedStatus:                           {type: 'unsignedByte', offset: 32},
                    issuedBoardingStop:                             {type: 'unsignedShort', offset: 33},
                    issuedAlightingStop:                            {type: 'unsignedShort', offset: 35},
                    issuedRoute:                                    {type: 'unsignedInt', offset: 37},
                    issuedTripId:                                   {type: 'unsignedShort', offset: 41},
                    issuedTripStartTimestamp:                       {type: 'posixTimestamp', offset: 43},
                    fareItemExpirationTimestamp:                    {type: 'posixTimestamp', offset: 47},
                    passengerRiderTypeExternalIdFirst:              {type: 'unsignedByte', offset: 51},
                    noOfPassengersFirst:                            {type: 'unsignedByte', offset: 52}
                };
            }
            if (['mok'].includes(process.env.CLIENT_NAME)) {
                qrCodeId = 6;
                qrCodeVersion = 2;
                qrcodeSize = 39;
                qrcodeStructure = {
                    qrCodeIdReference:                              {type: 'unsignedByte', offset: 0},
                    qrCodeVersionReference:                         {type: 'unsignedByte', offset: 1},
                    qrCodeIssuingTimestamp:                         {type: 'posixTimestamp', offset: 2},
                    qrCodeExpirationTimestamp:                      {type: 'posixTimestamp', offset: 6},
                    qrCodeIssuingTerminalId:                        {type: 'unsignedInt', offset: 10},
                    qrCodeMerchantOrganizationId:                   {type: 'unsignedInt', offset: 14},
                    fareItemExternalId:                             {type: 'unsignedInt', offset: 18},
                    fareItemTypeShortNumber:                        {type: 'unsignedByte', offset: 22},
                    fareItemCategoryShortNumber:                    {type: 'unsignedByte', offset: 23},
                    fareItemIssuedNumber:                           {type: 'unsignedLong', offset: 24},
                    fareItemIssuedStatus:                           {type: 'unsignedByte', offset: 32},
                    fareItemExpirationTimestamp:                    {type: 'posixTimestamp', offset: 33},
                    passengerRiderTypeExternalIdFirst:              {type: 'unsignedByte', offset: 37},
                    noOfPassengersFirst:                            {type: 'unsignedByte', offset: 38}
                };
            }

            return {
                id: qrCodeId,
                version: qrCodeVersion,
                size: qrcodeSize,
                structure: qrcodeStructure
            };
        } catch (err) {
            log.error(err);
        }
    }

    getQrCodeDefinitionApi = async () => {
        try {
            const res = await this.axios.get('qr-code-config/definition');
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return res;
        } catch (err) {
            log.error(err);
        }
    }

    getQrCodeDefinitionFile = async () => {
        try {
            const axios = require('axios');

            const res = await axios.get(process.env.CLIENT_APIURL+'/rest/api-spec-files/qr-schema', {headers: {'Content-Type': 'application/xml'}});
            if (typeof res === 'undefined' || res.status >= 400) { return null; }

            const fileName = ['schema', process.env.CLIENT_NAME].join('_')+'.xml';
            const filePath = path.join(process.cwd(), 'files', fileName);
            fs.writeFileSync(filePath, res.data);

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

    parseSchemaXmltoJson = async () => {
        try {
            let schemaFile = fs.readdirSync(path.join(process.cwd(), 'files')).filter((file) => file.toLowerCase().startsWith('schema') && file.endsWith('.xml'));
            if (schemaFile.length == 0) {
                await this.getQrCodeDefinitionFile();
                schemaFile = fs.readdirSync(path.join(process.cwd(), 'files')).filter((file) => file.toLowerCase().startsWith('schema') && file.endsWith('.xml'))[0].toString();
            } else {
                schemaFile = schemaFile[0].toString();
            }

            let schema = await xml2js.parseStringPromise(fs.readFileSync(path.join(process.cwd(), 'files', schemaFile), 'utf8'), { attrkey: 'att', charkey: 'text', explicitRoot: false, explicitArray: false, mergeAttrs: true });

            let schemaQrCodeName = schema.qrCodeName;
            let schemaQrCodeId = parseInt(schema.qrCodeId);
            let schemaQrCodeVersion = parseInt(schema.qrCodeVersion);

            let schemaQrCodeStructure = schema.qrCodeStructure;
            let schemaFareItemTypeShortNumberMapDefinition = schema.fareItemTypeShortNumberMapDefinition;
            let schemaFareItemCategoryTypeShortNumberMapDefinition = schema.fareItemCategoryTypeShortNumberMapDefinition;
            let schemaFareItemStatusShortNumberMapDefinition = schema.fareItemStatusShortNumberMapDefinition;
            let schemaBoardingActivityShortNumberMapDefinition = schema.boardingActivityShortNumberMapDefinition;
            let schemaFareProductTypeShortNumberMapDefinition = schema.fareProductTypeShortNumberMapDefinition;

            let schemaStructure = {};

            for (let i=0; i<schemaQrCodeStructure.qrCodeMember.length; i++) {
                schemaStructure[schemaQrCodeStructure.qrCodeMember[i]['name']] = {type: (schemaQrCodeStructure.qrCodeMember[i]['name'].includes('Timestamp')) ? 'posixTimestamp' : schemaQrCodeStructure.qrCodeMember[i]['xmlType'], offset: parseInt(schemaQrCodeStructure.qrCodeMember[i]['offsetDecimal'])};
            }

            let schemaQrCodeStructureSize = parseInt(schemaQrCodeStructure.qrCodeMember[schemaQrCodeStructure.qrCodeMember.length-1]['sizeDecimal']) + parseInt(schemaQrCodeStructure.qrCodeMember[schemaQrCodeStructure.qrCodeMember.length-1]['offsetDecimal']);

            let schemaEnums = {};

            schemaEnums[schemaFareItemStatusShortNumberMapDefinition.shortNumberMapName] = {};
            for (let i=0; i<schemaFareItemStatusShortNumberMapDefinition.fareItemStatusShortNumberMapDetails.shortNumberEnumItem.length; i++) {
                schemaEnums[schemaFareItemStatusShortNumberMapDefinition.shortNumberMapName][schemaFareItemStatusShortNumberMapDefinition.fareItemStatusShortNumberMapDetails.shortNumberEnumItem[i]['fareItemStatusShort']] = parseInt(schemaFareItemStatusShortNumberMapDefinition.fareItemStatusShortNumberMapDetails.shortNumberEnumItem[i]['shortNumberEnumValue']);
            }

            schemaEnums[schemaFareItemCategoryTypeShortNumberMapDefinition.shortNumberMapName] = {};
            for (let i=0; i<schemaFareItemCategoryTypeShortNumberMapDefinition.fareItemCategoryTypeShortNumberMapDetails.shortNumberEnumItem.length; i++) {
                schemaEnums[schemaFareItemCategoryTypeShortNumberMapDefinition.shortNumberMapName][schemaFareItemCategoryTypeShortNumberMapDefinition.fareItemCategoryTypeShortNumberMapDetails.shortNumberEnumItem[i]['fareItemCategoryType']] = parseInt(schemaFareItemCategoryTypeShortNumberMapDefinition.fareItemCategoryTypeShortNumberMapDetails.shortNumberEnumItem[i]['shortNumberEnumValue']);
            }

            schemaEnums[schemaFareItemTypeShortNumberMapDefinition.shortNumberMapName] = {};
            for (let i=0; i<schemaFareItemTypeShortNumberMapDefinition.fareItemTypeShortNumberMapDetails.shortNumberEnumItem.length; i++) {
                schemaEnums[schemaFareItemTypeShortNumberMapDefinition.shortNumberMapName][schemaFareItemTypeShortNumberMapDefinition.fareItemTypeShortNumberMapDetails.shortNumberEnumItem[i]['fareItemType']] = parseInt(schemaFareItemTypeShortNumberMapDefinition.fareItemTypeShortNumberMapDetails.shortNumberEnumItem[i]['shortNumberEnumValue']);
            }

            let qrCodeDefinition = {
                name: schemaQrCodeName,
                id: schemaQrCodeId,
                version: schemaQrCodeVersion,
                size: schemaQrCodeStructureSize,
                structure: schemaStructure
            };

            log.info('pos-schema', qrCodeDefinition);

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

    generateQrCode = async (data) => {
        try {
            const qrcodeData = await this.qrCodeData('write', data);

            const qrcode = qrcodeData.buffer;
            const secretKey = await this.qrCodeKey(parseInt(qrcodeData.readUint8(1)), parseInt(qrcodeData.readUint32(2)), parseInt(qrcodeData.readUint32(6)), parseInt(qrcodeData.readUint64(24)));

            const aesCmacSecret = aesCmac(new Buffer.from(secretKey.secret, 'hex'), new Buffer.from(qrcode), {returnAsBuffer: true});

            qrcodeData.resize(qrcodeData.capacity() + 4);
            qrcodeData.writeUint8(parseInt(aesCmacSecret[0]));
            qrcodeData.writeUint8(parseInt(aesCmacSecret[4]));
            qrcodeData.writeUint8(parseInt(aesCmacSecret[8]));
            qrcodeData.writeUint8(parseInt(aesCmacSecret[12]));

            const qr = qrcodeData.toBase64(0, qrcodeData.capacity());

            return (this.qrCompression == constants.qrCodeContentsCompression.gzip) ? zlib.gzipSync(Buffer.from(qr, 'base64')).toString('base64') : qr;
        } catch (err) {
            log.error(err);
        }
    }

    readQrCode = async (data, cmacCheck) => {
        try {
            const qrcodeData = ByteBuffer.fromBase64(data);

            if (cmacCheck) {
                const qrcode = new Uint8Array(qrcodeData.copy(0, qrcodeData.capacity() - 4).buffer);
                const secretKey = await this.qrCodeKey(parseInt(qrcodeData.readUint8(1)), parseInt(qrcodeData.readUint32(2)), parseInt(qrcodeData.readUint32(6)), parseInt(qrcodeData.readUint64(24)));

                const aesCmacDefault = aesCmac(new Buffer.from(secretKey.default, 'hex'), new Buffer.from(qrcode), {returnAsBuffer: true});
                const aesCmacSecret = aesCmac(new Buffer.from(secretKey.secret, 'hex'), new Buffer.from(qrcode), {returnAsBuffer: true});

                const aesCmacKeys = [
                    new Buffer.from(new Uint8Array([aesCmacDefault[0], aesCmacDefault[4], aesCmacDefault[8], aesCmacDefault[12]])).toString('hex'),
                    new Buffer.from(new Uint8Array([aesCmacSecret[0], aesCmacSecret[4], aesCmacSecret[8], aesCmacSecret[12]])).toString('hex')
                ];

                const qrcodeKey = qrcodeData.copy(qrcodeData.capacity() - 4, qrcodeData.capacity()).toString('hex');

                if (!aesCmacKeys.includes(qrcodeKey)) {
                    log.info('Invalid QR code message: the CMAC doesn\'t match');
                    log.info('QR code CMAC', qrcodeKey);
                    log.info('CMAC keys', aesCmacKeys);
                    return false;
                }
            }

            const qr = (this.qrCompression == constants.qrCodeContentsCompression.gzip) ? ByteBuffer.fromBase64(zlib.gunzipSync(Buffer.from(qrcodeData.buffer, 'utf8')).toString('base64')) : qrcodeData;

            return await this.qrCodeData('read', qr);
        } catch (err) {
            log.error(err);
        }
    }

    testQrCode = async () => {
        try {
            let qrcodeData = null;
            if (['oman'].includes(process.env.CLIENT_NAME)) {

            }
            if (['kuwait'].includes(process.env.CLIENT_NAME)) {
                qrcodeData = {
                    qrCodeIssuingTimestamp: '2023-05-18T10:40:22Z',             //
                    qrCodeExpirationTimestamp: '2023-05-19T08:00:00Z',          //
                    qrCodeIssuingTerminalId: 1000,                              //
                    qrCodeMerchantOrganizationId: 0,                            //
                    fareItemExternalId: 7000,                                   //
                    fareItemTypeShortNumber: 5,                                 //    - enumsXml.fareItemType.singleRideTicket
                    fareItemCategoryShortNumber: 2,                             //    - enumsXml.fareItemCategoryType.fareSingleRide
                    fareItemIssuedNumber: 4294967297521,                        //
                    fareItemIssuedStatus: 1,                                    //    - enumsXml.fareItemStatus.active
                    issuedBoardingStation: 282,                                 //
                    issuedAlightingStation: 364,                                //
                    issuedBoardingStop: 282,                                    //
                    issuedAlightingStop: 364,                                   //
                    issuedRouteType: 0,                                         //
                    issuedRouteTypeTransferEligibility: 0,                      //
                    fareProductTypeShortNumber: 0,                              //     - enumsXml.smartCardContractCategory.undefined
                    fareItemStatus: 1,                                          //     - enumsXml.fareItemStatus.active
                    fareItemStatusZonesLeft: 0,                                 //
                    fareItemStatusLastChangedTimestamp: '2023-05-18T10:40:22Z', //
                    fareItemStatusTransfersLeft: 0,                             //
                    fareItemStatusRemainingRides: 1,                            //
                    fareItemExpirationTimestamp: '2023-05-19T08:00:00Z',        //
                    passengerRiderTypeExternalIdFirst: 1,                       //
                    noOfPassengersFirst: 1,                                     //
                    passengerRiderTypeExternalIdSecond: 0,                      //
                    noOfPassengersSecond: 0,                                    //
                    passengerRiderTypeExternalIdThird: 0,                       //
                    noOfPassengersThird: 0,                                     //
                    passengerRiderTypeExternalIdFourth: 0,                      //
                    noOfPassengersFourth: 0,                                    //
                    accountRiderType: 0,                                        //
                    accountId: 0,                                               //
                    ePurseBalanceInCents: 0,                                    //
                    dailyCumulativeSpendingInCents: 0,                          //
                    weeklyCumulativeSpendingInCents: 0,                         //
                    monthlyCumulativeSpendingInCents: 0,                        //
                    issuedRoute: 81                                             //
                };
            }
            if (['hafil'].includes(process.env.CLIENT_NAME)) {
                qrcodeData = {
                    qrCodeIdReference: 4,
                    qrCodeVersionReference: 1,
                    qrCodeIssuingTimestamp: '2023-10-05T09:00:00',
                    qrCodeExpirationTimestamp: '2023-10-08T09:00:00',
                    qrCodeIssuingTerminalId: 0,
                    qrCodeMerchantOrganizationId: 0,
                    fareItemExternalId: 30001,
                    fareItemTypeShortNumber: 5,
                    fareItemCategoryShortNumber: 2,
                    fareItemIssuedNumber: 255,
                    fareItemIssuedStatus: 1,
                    issuedRouteType: 0,
                    contractCategoryShortNumber: 0,
                    issuedBoardingStation: 13,
                    issuedAlightingStation: 10,
                    fareItemStatus: 1,
                    fareItemStatusLastChangedTimestamp: '2023-10-05T09:00:00',
                    fareItemStatusTransfersLeft: 0,
                    fareItemStatusRemainingRides: 1,
                    fareItemExpirationTimestamp: '2023-10-08T09:00:00',
                    accountRiderType: 0,
                    accountId: 0,
                    issuedTripId: 0,
                    issuedRoute: 273,
                    issuedTripStartTimestamp: '1970-01-01T00:00:00',
                    issuedTripEndEstimatedTimestamp: '1970-01-01T00:00:13'
                };
            }
            if (['sharjah'].includes(process.env.CLIENT_NAME)) {
                qrcodeData = {
                    qrCodeIssuingTimestamp: '2024-11-11T10:00:00Z',             //
                    qrCodeExpirationTimestamp: '2024-11-12T08:00:00Z',          //
                    qrCodeIssuingTerminalId: 1000,                              //
                    qrCodeMerchantOrganizationId: 0,                            //
                    fareItemExternalId: 7000,                                   //
                    fareItemTypeShortNumber: 5,                                 //    - enumsXml.fareItemType.singleRideTicket
                    fareItemCategoryShortNumber: 2,                             //    - enumsXml.fareItemCategoryType.fareSingleRide
                    fareItemIssuedNumber: 4294967297521,                        //
                    fareItemIssuedStatus: 1,                                    //    - enumsXml.fareItemStatus.active
                    issuedBoardingStop: 282,                                    //
                    issuedAlightingStop: 364,                                   //
                    issuedRoute: 81,                                            //
                    issuedTripId: 1,                                            //
                    issuedTripStartTimestamp: '2024-11-11T10:00:00Z',           //
                    fareItemExpirationTimestamp: '2024-11-12T08:00:00Z',        //
                    passengerRiderTypeExternalIdFirst: 1,                       //
                    noOfPassengersFirst: 1                                      //
                };
            }
            if (['mok'].includes(process.env.CLIENT_NAME)) {
                qrcodeData = {
                    qrCodeIssuingTimestamp: '2025-06-04T10:00:00Z',             //
                    qrCodeExpirationTimestamp: '2025-06-05T08:00:00Z',          //
                    qrCodeIssuingTerminalId: 1000,                              //
                    qrCodeMerchantOrganizationId: 0,                            //
                    fareItemExternalId: 7000,                                   //
                    fareItemTypeShortNumber: 5,                                 //    - enumsXml.fareItemType.singleRideTicket
                    fareItemCategoryShortNumber: 2,                             //    - enumsXml.fareItemCategoryType.fareSingleRide
                    fareItemIssuedNumber: 4294967297521,                        //
                    fareItemIssuedStatus: 1,                                    //    - enumsXml.fareItemStatus.active
                    fareItemExpirationTimestamp: '2025-06-05T08:00:00Z',        //
                    passengerRiderTypeExternalIdFirst: 1,                       //
                    noOfPassengersFirst: 1                                      //
                };
            }
            log.info(qrcodeData);

            const qrgen = await this.generateQrCode(qrcodeData);
            log.info(qrgen);

            // kuwait
            //const qrgen = "H4sIAAAAAAAAAGNmTEldl5OS9v8VAwPzWwYwkI5gZYLyShlBAoxSjDkQKcb6////pygWvf7PCNLDCJRmZCABhL+RUwIAPkgvknMAAAA=";   // MPOS gzip
            //const qrgen = "AwFkZgCWZGcsgAAAA+gAAAAAAAAbWAUCAAAD6AAABfEBARoBbAEaAWwAAAAAAAABAAAAAGRmAJYAAWRnLIABAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABRS9F+gg=="; // SPOS

            // hafil
            //const qrgen = "BAFlHnsQZSJvkAAAAAAAAAAAAAB1MQUCAAAAAAAAAP8BAAAADQAKAWUeexAAAWUib5AAAAAAAAAAAAABEQAAAAAAAAANNp21aA=="; // SPOS
            //const qrgen = "H4sIAAAAAAAACmNhzJxhz5u+242BgYH5FQMYlBqyMkF5KYxMDAxcDKZgVQyMIHWMICWsngwMjI3pu3kPpO8x2tEsdowLAN5Q00lJAAAA"; // SPOS gzip

            // sharjah
            //const qrgen = "AQNnMdWgZzMLAAAAA+gAAAAAAAAbWAUCAAAD6AAABfEBARoBbAAAAFEAAAAAAAAAAWcx1aBnMwsAAQHnRXP4AAAAAA=="; // SPOS
            //const qrgen = "H4sIAAAAAAAACmNlzNzCKJi59UUBAwMDIwMYSEewMkF5wYxM5mBGM0QKpJCR8TaTFC8AbAHk9jkAAAA="; // SPOS gzip

            // mok
            //const qrgen = "H4sIAAAAAAAACmNkznCQVMhw9GtgYGB+wQAG0hGsTBAe60dGkBQjY+th9VaQFACyGmd2LwAAAA=="; // SPOS
            //const qrgen = "H4sIAAAAAAAACmNjyjj45QEIMzAwyDBAAA8PO5R3kBEkxcgYvddkBwA+l0wZKwAAAA==";

            const qrred = await this.readQrCode(qrgen, false);
            log.info(qrred);

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

    qrCodeKey = async (version, issuing, expiration, issuedNumber) => {
        try {
            const isoQrCodeIssuingTimestamp = moment.unix(issuing).utc();
            const issuingTimestamp = new Buffer.from(isoQrCodeIssuingTimestamp.format(this.dateFormat));

            const isoQrCodeExpirationTimestamp = moment.unix(expiration).utc();
            const expirationTimestamp = new Buffer.from(isoQrCodeExpirationTimestamp.format(this.dateFormat));

            const fareItemIssuedNumberBuffer = ByteBuffer.allocate(8).writeUint64(issuedNumber);
            const fareItemIssuedNumber = new Buffer.from(fareItemIssuedNumberBuffer.buffer);

            const key1 = ByteBuffer.allocate(16);
            key1.writeUint8(parseInt(expirationTimestamp[5]));
            key1.writeUint8(parseInt(expirationTimestamp[4]));
            key1.writeUint8(parseInt(expirationTimestamp[3]));
            key1.writeUint8(parseInt(expirationTimestamp[2]));
            key1.writeUint8(parseInt(expirationTimestamp[1]));
            key1.writeUint8(parseInt(expirationTimestamp[0]));
            key1.writeUint8(parseInt(issuingTimestamp[9]));
            key1.writeUint8(parseInt(issuingTimestamp[8]));
            key1.writeUint8(parseInt(issuingTimestamp[7]));
            key1.writeUint8(parseInt(issuingTimestamp[6]));
            key1.writeUint8(parseInt(issuingTimestamp[5]));
            key1.writeUint8(parseInt(issuingTimestamp[4]));
            key1.writeUint8(parseInt(issuingTimestamp[3]));
            key1.writeUint8(parseInt(issuingTimestamp[2]));
            key1.writeUint8(parseInt(issuingTimestamp[1]));
            key1.writeUint8(parseInt(issuingTimestamp[0]));
            const keyInput1 = new Buffer.from(key1.buffer);

            const key2 = ByteBuffer.allocate(16);
            key2.writeUint8(parseInt(fareItemIssuedNumber[6]));
            key2.writeUint8(~parseInt(fareItemIssuedNumber[4]));
            key2.writeUint8(parseInt(fareItemIssuedNumber[2]));
            key2.writeUint8(~parseInt(fareItemIssuedNumber[0]));
            key2.writeUint8(parseInt(fareItemIssuedNumber[7]));
            key2.writeUint8(~parseInt(fareItemIssuedNumber[5]));
            key2.writeUint8(parseInt(fareItemIssuedNumber[3]));
            key2.writeUint8(~parseInt(fareItemIssuedNumber[1]));
            key2.writeUint8(~parseInt(fareItemIssuedNumber[7]));
            key2.writeUint8(parseInt(fareItemIssuedNumber[5]));
            key2.writeUint8(~parseInt(fareItemIssuedNumber[3]));
            key2.writeUint8(parseInt(fareItemIssuedNumber[1]));
            key2.writeUint8(~parseInt(fareItemIssuedNumber[6]));
            key2.writeUint8(parseInt(fareItemIssuedNumber[4]));
            key2.writeUint8(~parseInt(fareItemIssuedNumber[2]));
            key2.writeUint8(parseInt(fareItemIssuedNumber[0]));
            const keyInput2 = new Buffer.from(key2.buffer);

            const keyInput = this.helpersCtrl.xor(keyInput1, keyInput2);

            const secret = ByteBuffer.allocate(16);
            secret.writeUint8(parseInt(keyInput[0]));
            secret.writeUint8(parseInt(keyInput[1]));
            secret.writeUint8(parseInt(keyInput[2]));
            secret.writeUint8(parseInt(keyInput[3]));
            secret.writeUint8(parseInt(keyInput[4]));
            secret.writeUint8(parseInt(keyInput[5]));
            secret.writeUint8(parseInt(keyInput[6]));
            secret.writeUint8(parseInt(keyInput[7]));
            secret.writeUint8(parseInt(keyInput[8]));
            secret.writeUint8(parseInt(keyInput[9]));
            secret.writeUint8(parseInt(keyInput[10]));
            secret.writeUint8(parseInt(keyInput[11]));
            secret.writeUint8(parseInt(keyInput[12]));
            secret.writeUint8(parseInt(keyInput[13]));
            secret.writeUint8(parseInt(keyInput[14]));
            secret.writeUint8(parseInt(keyInput[15]));

            const defaultKey = '11112222333344440000000000000000';
            const rotateLeftSize = parseInt(isoQrCodeIssuingTimestamp.get('month')+1);
            const secretKey = secret.copy(rotateLeftSize, 16).toString('hex') + secret.copy(0, rotateLeftSize).toString('hex');

            // log.info('Parsed QR version', version);
            // log.info('issuing   ', issuingTimestamp.toString('hex'));
            // log.info('expiration', expirationTimestamp.toString('hex'));
            // log.info('keyInput1 ', keyInput1.toString('hex'));
            // log.info('keyInput2 ', keyInput2.toString('hex'));
            // log.info('secretKey ', secretKey);

            return {default: defaultKey, secret: secretKey};
        } catch (err) {
            log.error(err);
        }
    }

    qrCodeData = async (mode, data) => {
        try {
            let qrcode = null;
            let qrCodeDefinition = await this.getQrCodeDefinition();

            data.qrCodeIdReference = qrCodeDefinition.id;
            data.qrCodeVersionReference = qrCodeDefinition.version;

            if (mode == 'write') {
                qrcode = ByteBuffer.allocate(qrCodeDefinition.size);

                for (const [key, value] of Object.entries(qrCodeDefinition.structure)) {
                    switch (value.type) {
                        case 'unsignedByte':
                            qrcode.writeUint8(parseInt((data[key]) ? data[key] : 0));
                        break;
                        case 'unsignedShort':
                            qrcode.writeUint16(parseInt((data[key]) ? data[key] : 0));
                        break;
                        case 'unsignedInt':
                            qrcode.writeUint32(parseInt((data[key]) ? data[key] : 0));
                        break;
                        case 'signedInt':
                            qrcode.writeInt32(parseInt((data[key]) ? data[key] : 0));
                        break;
                        case 'posixTimestamp':
                            qrcode.writeUint32(parseInt((data[key]) ? moment.utc(data[key]).format('X') : 0));
                        break;
                        case 'unsignedLong':
                            qrcode.writeUint64(parseInt((data[key]) ? data[key] : 0));
                        break;
                    }
                }
            }

            if (mode == 'read') {
                qrcode = {};

                for (const [key, value] of Object.entries(qrCodeDefinition.structure)) {
                    switch (value.type) {
                        case 'unsignedByte':
                            qrcode[key] = parseInt(data.readUint8(value.offset));
                        break;
                        case 'unsignedShort':
                            qrcode[key] = parseInt(data.readUint16(value.offset));
                        break;
                        case 'unsignedInt':
                            qrcode[key] = parseInt(data.readUint32(value.offset));
                        break;
                        case 'posixTimestamp':
                            qrcode[key] = moment.unix(parseInt(data.readUint32(value.offset))).utc().format(this.dateTTimeFormat);
                        break;
                        case 'unsignedLong':
                            qrcode[key] = parseInt(data.readUint64(value.offset));
                        break;
                    }
                }
            }

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

    qrCodeTripIdToCrc16ccitt = async (tripId) => {
        const string = tripId.toString();
        const crc16_ccitt_table = [
            0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,0x8108,
            0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210,
            0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B,
            0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401,
            0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE,
            0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6,
            0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D,
            0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
            0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5,
            0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC,
            0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4,
            0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD,
            0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13,
            0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A,
            0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E,
            0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
            0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1,
            0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB,
            0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0,
            0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8,
            0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657,
            0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9,
            0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882,
            0x28A3, 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
            0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E,
            0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07,
            0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D,
            0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74,
            0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
        ];
        let crc = 0xFFFF;
        let j;

        for (let i=0; i<string.length; i++) {
            j = ((crc >> 8) ^ string.charCodeAt(i)) & 0x00FF;
            crc = (crc << 8) ^ crc16_ccitt_table[j];
        }

        return ((crc ^ 0) & 0xFFFF);
    }

    getSmartCard = async (smartCardUid) => {
        try {
            if (process.env.CLIENT_SECURE_ACCESS_MODULE == 'demo') {
                return await this.smartCardServ.demoCardDataApi();
            }
            const res = await this.axios.get('smartcard/uid/'+smartCardUid);
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return res;
        } catch (err) {
            log.error(err);
        }
    }

    getSmartCardByPrintedId = async (smartCardPid) => {
        try {
            const res = await this.axios.get('smartcard/printed-id/'+smartCardPid);
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return res;
        } catch (err) {
            log.error(err);
        }
    }

    getSmartCardProfiles = async () => {
        try {
            if (process.env.CLIENT_SECURE_ACCESS_MODULE == 'demo') {
                return await this.smartCardServ.demoCardProfilesApi();
            }
            const res = await this.axios.get('smart-card-profiles');
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return res;
        } catch (err) {
            log.error(err);
        }
    }

    createCardHolder = async (cardholder) => {
        try {
            const xmlBuilder = new xml2js.Builder({xmldec: {version: '1.0', encoding: 'UTF-8'} });
            const xml = await xmlBuilder.buildObject(cardholder);
            const res = await this.axios.post('smartcard/cardholder', xml, {headers: {'Content-Type': 'application/xml'}});
            if (typeof res === 'undefined' || res.status >= 500) { return {head: null}; }
            return await xml2js.parseStringPromise(res, { explicitRoot: false, explicitArray: false });
        } catch (err) {
            log.error(err);
        }
    }

    getCardHolderById = async (cardHolderId) => {
        try {
            const res = await this.axios.get('smartcard/cardholder/'+cardHolderId);
            if (typeof res === 'undefined' || res.status >= 500) { return {head: null}; }
            return await xml2js.parseStringPromise(res, { explicitRoot: false, explicitArray: false });
        } catch (err) {
            log.error(err);
        }
    }

    getCardHolderByIdentificationId = async (identificationId) => {
        try {
            const res = await this.axios.get('smartcard/cardholder/identification-id/'+identificationId);
            if (typeof res === 'undefined' || res.status >= 500) { return {head: null}; }
            return await xml2js.parseStringPromise(res, { explicitRoot: false, explicitArray: false });
        } catch (err) {
            log.error(err);
        }
    }

    getCardHolderBySmartCardUId = async (smartCardUId) => {
        try {
            const res = await this.axios.get('smartcard/cardholder/smartcard/'+smartCardUId);
            if (typeof res === 'undefined' || res.status >= 500) { return {head: null}; }
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return await xml2js.parseStringPromise(res, { explicitRoot: false, explicitArray: false });
        } catch (err) {
            log.error(err);
        }
    }

    getCardHolderPhoto = async (cardHolderId) => {
        try {
            const res = await this.axios.get('smartcard/cardholder/'+cardHolderId+'/photo', {responseType: 'arraybuffer'});
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return Buffer.from(res, 'binary').toString('base64');
        } catch (err) {
            log.error(err);
        }
    }

    uploadCardHolderPhoto = async (cardHolderId, imageType, imageData) => {
        try {
            const res = await this.axios.post('smartcard/cardholder/'+cardHolderId+'/photo/'+imageType, imageData, {headers: {'Content-Type': 'multipart/form-data'}});
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return true;
        } catch (err) {
            log.error(err);
        }
    }

    getCardHolderIdentityProof = async (cardHolderId) => {
        try {
            const res = await this.axios.get('smartcard/cardholder/'+cardHolderId+'/proof', {responseType: 'arraybuffer'});
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return Buffer.from(res, 'binary').toString('base64');
        } catch (err) {
            log.error(err);
        }
    }

    uploadCardHolderIdentityProof = async (cardHolderId, imageType, imageData) => {
        try {
            const res = await this.axios.post('smartcard/cardholder/'+cardHolderId+'/proof/'+imageType, imageData, {headers: {'Content-Type': 'multipart/form-data'}});
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return true;
        } catch (err) {
            log.error(err);
        }
    }

    registerCardHolderToSmartCard = async (cardHolderId, smartCardUId) => {
        try {
            const res = await this.axios.put('smartcard/cardholder/'+cardHolderId+'/smartcard/'+smartCardUId, {});
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return true;
        } catch (err) {
            log.error(err);
        }
    }

    updateCardHolder = async (cardHolderId, cardholder) => {
        try {
            const xmlBuilder = new xml2js.Builder({xmldec: {version: '1.0', encoding: 'UTF-8'} });
            const xml = await xmlBuilder.buildObject(cardholder);
            const res = await this.axios.put('smartcard/cardholder/'+cardHolderId, xml, {headers: {'Content-Type': 'application/xml'}});
            if (typeof res === 'undefined' || res.status >= 500) { return {head: null}; }
            return await xml2js.parseStringPromise(res, { explicitRoot: false, explicitArray: false });
        } catch (err) {
            log.error(err);
        }
    }

    recreateSmartCard = async (smartCardUIdOld, smartCardUIdNew) => {
        try {
            const res = await this.axios.put('smartcard/recreate/'+smartCardUIdOld+'/'+smartCardUIdNew, {});
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return true;
        } catch (err) {
            log.error(err);
        }
    }

    replaceSmartCard = async (smartCardUIdOld, smartCardUIdNew) => {
        try {
            const res = await this.axios.put('smartcard/'+smartCardUIdOld+'/replace/'+smartCardUIdNew, {});
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return true;
        } catch (err) {
            log.error(err);
        }
    }

    getActionListBySmartCardUid = async (smartCardUId) => {
        try {
            const res = await this.axios.get('actionlist/items/CardUID/'+smartCardUId+'/all', {headers: {'Content-Type': 'application/xml'}});
            if (typeof res === 'undefined' || res.status >= 400) { return {head: null}; }
            return await xml2js.parseStringPromise(res, { explicitRoot: false, explicitArray: false });
        } catch (err) {
            log.error(err);
        }
    }

    getActionListBySmartCardUidv2 = async (smartCardUId) => {
        try {
            const res = await this.axios.get('actionlist/v2/items/CardUID/'+smartCardUId+'/all', {headers: {'Accept': 'application/json'}});
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return res;
        } catch (err) {
            log.error(err);
        }
    }

    allActionListSmartCard = async () => {
        try {
            const res = await this.axios.get('actionlist/all', {headers: {'Content-Type': 'application/xml'}});
            if (typeof res === 'undefined' || res.status >= 400) { return {head: null}; }
            return await xml2js.parseStringPromise(res, { explicitRoot: false, explicitArray: false });
        } catch (err) {
            log.error(err);
        }
    }

    getCardPurchases = async (smartCardUId) => {
        try {
            if (process.env.CLIENT_SECURE_ACCESS_MODULE == 'demo') {
                return await this.smartCardServ.demoCardPurchasesApi();
            }
            const res = await this.axios.get('webportal/uid/'+smartCardUId+'/card-purchase');
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return res;
        } catch (err) {
            log.error(err);
        }
    }

    getCardRides = async (smartCardUId) => {
        try {
            if (process.env.CLIENT_SECURE_ACCESS_MODULE == 'demo') {
                return await this.smartCardServ.demoCardRidesApi();
            }
            const res = await this.axios.get('webportal/uid/'+smartCardUId+'/card-ride');
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return res;
        } catch (err) {
            log.error(err);
        }
    }

    generateTaxQrCode = async (data) => {
        try {
            const organizationName = await this.setTagLengthValue('01', data.organizationName.toString());
            const organizationVatNumber = await this.setTagLengthValue('02', data.organizationVatNumber.toString());
            const timestamp = await this.setTagLengthValue('03', data.timestamp.toString());
            const invoiceTotal = await this.setTagLengthValue('04', data.invoiceTotal.toString());
            const vatTotal = await this.setTagLengthValue('05', data.vatTotal.toString());

            const qrcodeData = new Buffer.concat([organizationName, organizationVatNumber, timestamp, invoiceTotal, vatTotal]);

            return qrcodeData.toString('base64');
        } catch (err) {
            log.error(err);
        }
    }

    readTaxQrCode = async (data) => {
        try {
            const qrcodeData = new Buffer.from(data, 'base64').toString('hex');
            const qrcodeDataObject = await this.parseTagLengthValue(qrcodeData);

            const organizationName = await this.getTagLengthValue('01', qrcodeDataObject);
            const organizationVatNumber = await this.getTagLengthValue('02', qrcodeDataObject);
            const timestamp = await this.getTagLengthValue('03', qrcodeDataObject);
            const invoiceTotal = await this.getTagLengthValue('04', qrcodeDataObject);
            const vatTotal = await this.getTagLengthValue('05', qrcodeDataObject);

            return {
                organizationName: new Buffer.from(organizationName, 'hex').toString(),
                organizationVatNumber: new Buffer.from(organizationVatNumber, 'hex').toString(),
                timestamp: new Buffer.from(timestamp, 'hex').toString(),
                invoiceTotal: new Buffer.from(invoiceTotal, 'hex').toString(),
                vatTotal: new Buffer.from(vatTotal, 'hex').toString()
            };
        } catch (err) {
            log.error(err);
        }
    }

    testTaxQrCode = async () => {
        try {
            const qr = {
                organizationName: 'Bobs Records',
                organizationVatNumber: '310122393500003',
                timestamp: '2022-04-25T15:30:00Z',
                invoiceTotal: '1000.00',
                vatTotal: '150.00'
            };
            log.info(qr);

            const qrgen = await this.generateTaxQrCode(qr);
            log.info(qrgen);

            //hafil
            //const qrgen = "AQxEYXJiIEFsd2F0YW4CDzMxMTUxMDk0MDMwMDAwMwMTMzAvMTAvMjAyMyAxMzozNzozNAQDMTQ3BQUyMi4wNQ=="; // SPOS

            const qrred = await this.readTaxQrCode(qrgen);
            log.info(qrred);

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

    setTagLengthValue = async (tagNum, tagValue) => {
        try {
            const tagBuf = new Buffer.from([tagNum], 'utf8');
            const tagValueLenBuf = new Buffer.from([tagValue.length], 'utf8');
            const tagValueBuf = new Buffer.from(tagValue, 'utf8');

            return new Buffer.concat([tagBuf, tagValueLenBuf, tagValueBuf]);
        } catch (err) {
            log.error(err);
        }
    }

    getTagLengthValue = async (tagNum, data) => {
        try {
            return data[tagNum];
        } catch (err) {
            log.error(err);
        }
    }

    parseTagLengthValue = async (data) => {
        var TLV = {};

        function readData(amt, tag) {
            data = data.slice(amt);

            var length = data.slice(0, 2);
            data = data.slice(2);
            length = 1 * ('0x' + length);

            if (length & 0x80) {
                highBitsByteExisted = 1;
                var lengthOfLength = length & 0x1F;
                lengthOfLength *= 2;
                length = data.slice(0, lengthOfLength);
                data = data.slice(lengthOfLength);
                length = 1 * ('0x' + length);
            }

            length *= 2;
            var V = data.slice(0, length);
            TLV[tag] = V;

            if (tag.slice(0,2) == 'FF') {
                tmptlv = this.parseTagLengthValue(V);
                for (var t in tmptlv)
                TLV[t] = tmptlv[t];
            }
            data = data.slice(length);
        }

        var TLV_TAG = 1;
        var amtRead, tag;
        var _KnownTags = {'01': true, '02': true, '03': true, '04': true, '05': true};

        while(TLV_TAG) {
            if (data == '') break;

            for (amtRead = 2; amtRead <= 6; amtRead += 2)
            if ((tag = data.slice(0,amtRead).toUpperCase()) in _KnownTags)
            break;
            else if (amtRead == 6) {
                data = data.slice(2);
                log.error('Expected a tag, found none. Data: n' + data);
            }

            readData(amtRead, tag);
        }

        return TLV;
    }

    testFiscalVerification = async () => {
        try {
            const echoData = {
                echo: 'fiscalVerificationEcho'
            };
            log.info(echoData);

            const fiscalVerificationEcho = await this.fiscalVerificationEcho(echoData);
            log.info('EchoResponse', fiscalVerificationEcho);

            /*const premiseData = {
                timestamp: moment.utc().utcOffset(global.currentPosData.utcOffset),
                businessPremiseId: 'BPID1',
                validityDate: moment.utc().subtract(1, 'days').utcOffset(global.currentPosData.utcOffset),
                // realEstate: {
                //     cadastralNumber: 365,
                //     buildingNumber: 12,
                //     buildingSectionNumber: 3,
                //     street: 'Ljubljanska cesta',
                //     houseNumber: '1',
                //     //houseNumberAdditional: 'A',
                //     community: 'Ljubljana',
                //     city: 'Ljubljana',
                //     postalCode: '1000'
                // }
            };
            log.info(premiseData);

            const fiscalVerificationPremise = await this.fiscalVerificationPremise(premiseData);
            log.info('BusinessPremiseResponse', fiscalVerificationPremise);*/

            /*const invoiceData = {
                issueDateTime: moment.utc().utcOffset(global.currentPosData.utcOffset),
                invoiceNumber: 'BPID1-EDID1-20252',                                     // businessPremiseId - electronicDeviceId - invoiceNumber
                invoiceAmount: 100,
                vatDetails: [{
                    taxableAmountInCents: 82,
                    vatRate: 22,
                    vatInCents: 18
                }]
            };
            log.info(invoiceData);

            const fiscalVerificationInvoice = await this.fiscalVerificationInvoice(invoiceData);
            log.info('InvoiceResponse', fiscalVerificationInvoice);*/
        } catch (err) {
            log.error(err);
        }
    }

    fiscalVerificationEcho = async (data) => {
        try {
            const echoBody = {
                EchoRequest: data.echo
            };

            const echoData = await this.fursRequest('echo', echoBody);

            return (echoData && !echoData['EchoResponse'].hasOwnProperty('Error')) ? echoData['EchoResponse'] : echoData['EchoResponse']['Error']['ErrorMessage'];
        } catch (err) {
            log.error(err);
        }
    }

    fiscalVerificationPremise = async (data) => {
        try {
            const uuidv4 = require('uuid').v4;

            const premisesHeader = {
                alg: 'RS256',
                subject_name: this.certDescriptor.subject,
                issuer_name: this.certDescriptor.issuer,
                serial: this.certDescriptor.serial
            };
            const header = this.getJsonBase64(premisesHeader);

            let premisesBody = {
                BusinessPremiseRequest: {
                    Header: {
                        MessageID: uuidv4(),
                        DateTime: data.timestamp.format(this.dateTTimeFormat)
                    },
                    BusinessPremise: {
                        TaxNumber: this.certDescriptor.taxNumber,
                        BusinessPremiseID: data.businessPremiseId,
                        BPIdentifier: {
                            PremiseType: 'B'                                            // defaults to B - object at a permanent location
                        },
                        ValidityDate: data.validityDate.format(this.dateFormat),
                        SoftwareSupplier: [{
                            TaxNumber: this.softwareSupplierTaxNumber
                        }]
                    }
                }
            };
            if (data.realEstate) {
                delete premisesBody.BusinessPremiseRequest.BusinessPremise.BPIdentifier.PremiseType;
                premisesBody.BusinessPremiseRequest.BusinessPremise.BPIdentifier.RealEstateBP = {
                    PropertyID: {
                        CadastralNumber: data.realEstate.cadastralNumber,
                        BuildingNumber: data.realEstate.buildingNumber,
                        BuildingSectionNumber: data.realEstate.buildingSectionNumber
                    },
                    Address: {
                        Street: data.realEstate.street,
                        HouseNumber: data.realEstate.houseNumber,
                        HouseNumberAdditional: data.realEstate.houseNumberAdditional,
                        Community: data.realEstate.community,
                        City: data.realEstate.city,
                        PostalCode: data.realEstate.postalCode
                    }
                };

                if (!data.realEstate.houseNumberAdditional) {
                    delete premisesBody.BusinessPremiseRequest.BusinessPremise.BPIdentifier.RealEstateBP.Address.HouseNumberAdditional;
                }
            }
            const body = this.getJsonBase64(premisesBody);

            const token = this.getSignedToken(header, body);

            const premisesToken = {token: token};

            const premisesData = await this.fursRequest('invoices/register', premisesToken);

            const premises = this.getResponseBody(premisesData);

            return (premises && !premises['BusinessPremiseResponse'].hasOwnProperty('Error')) ? premises['BusinessPremiseResponse']['Header']['DateTime'] : premises['BusinessPremiseResponse']['Error']['ErrorMessage'];
        } catch (err) {
            log.error(err);
        }
    }

    fiscalVerificationInvoice = async (data) => {
        try {
            const uuidv4 = require('uuid').v4;

            const invoiceHeader = {
                alg: 'RS256',
                subject_name: this.certDescriptor.subject,
                issuer_name: this.certDescriptor.issuer,
                serial: this.certDescriptor.serial
            };
            const header = this.getJsonBase64(invoiceHeader);

            const zoiData = this.calculateZoi(data);

            let invoiceBody = {
                InvoiceRequest: {
                    Header: {
                        MessageID: uuidv4(),
                        DateTime: data.issueDateTime.format(this.dateTTimeFormat)
                    },
                    Invoice: {
                        TaxNumber: this.certDescriptor.taxNumber,
                        IssueDateTime: data.issueDateTime.format(this.dateTTimeFormat),
                        NumberingStructure: 'B',                                        // defaults to B - numbering per individual electronic device
                        InvoiceIdentifier: {
                            BusinessPremiseID: data.invoiceNumber.split('-')[0],
                            ElectronicDeviceID: data.invoiceNumber.split('-')[1],
                            InvoiceNumber: data.invoiceNumber.split('-')[2]
                        },
                        InvoiceAmount: data.invoiceAmount,
                        PaymentAmount: data.invoiceAmount,
                        TaxesPerSeller: [{
                            VAT: await this.taxSpecification(data.vatDetails)
                        }],
                        ProtectedID: zoiData,
                        SpecialNotes: (data.specialNotes) ? data.specialNotes : ''
                    }
                }
            };
            if (data.operatorTaxNumber) {
                invoiceBody.InvoiceRequest.Invoice.OperatorTaxNumber = data.operatorTaxNumber; // Tax number of the register operator
            }
            if (data.referenceNumber) {
                invoiceBody.InvoiceRequest.Invoice.ReferenceInvoice = [{
                    ReferenceInvoiceIdentifier: {
                        BusinessPremiseID: data.referenceNumber.split('-')[0],
                        ElectronicDeviceID: data.referenceNumber.split('-')[1],
                        InvoiceNumber: data.referenceNumber.split('-')[2]
                    },
                    ReferenceInvoiceIssueDateTime: data.referenceIssueDateTime.format(this.dateTTimeFormat)
                }];
            }
            const body = this.getJsonBase64(invoiceBody);

            const token = this.getSignedToken(header, body);

            const invoiceToken = {token: token};

            const invoiceData = await this.fursRequest('invoices', invoiceToken);

            const invoice = this.getResponseBody(invoiceData);

            const eorData = (invoice && !invoice['InvoiceResponse'].hasOwnProperty('Error')) ? invoice['InvoiceResponse']['UniqueInvoiceID'] : invoice['InvoiceResponse']['Error']['ErrorMessage'];

            const qrCodeData = this.prepareQrCode(data.taxNumber, data.issueDateTime, zoiData);

            return {
                eor: eorData,
                zoi: zoiData,
                qrCode: qrCodeData
            };
        } catch (err) {
            log.error(err);
        }
    }

    fursRequest = async (service, data) => {
        try {
            const axios = require('axios');
            const https = require('https');

            const fursUrl = process.env.CLIENT_FISCAL_VERIFICATION_URL;
            const options = {
                headers: {
                    'Content-Type': 'application/json; charset=UTF-8'
                },
                httpsAgent: new https.Agent({
                    ca: this.certDescriptor.ca,
                    cert: this.certDescriptor.cert,
                    key: this.certDescriptor.key,
                    passphrase: this.certDescriptor.passphrase
                })
            };
            const res = await axios.post(fursUrl+'/v1/cash_registers/'+service, JSON.stringify(data), options);
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return res.data;
        } catch (err) {
            log.error(err);
        }
    }

    calculateZoi = (data) => {
        const crypto = require('crypto');

        const zoi = '' + data.taxNumber + data.issueDateTime.format('DD.MM.YYYY HH:mm:ss') + data.invoiceNumber.split('-')[2] + data.invoiceNumber.split('-')[0] + data.invoiceNumber.split('-')[1] + data.invoiceAmount;
        const signedZoi = this.parseHexString(this.signString(zoi));
        const zoiData = crypto.createHash('md5').update(Buffer.from(signedZoi, 'hex')).digest('hex');

        return zoiData;
    }

    prepareQrCode = (taxNumber, issueDateTime, zoi) => {
        let zoiBase = this.parseDecString(zoi);
        zoiBase = zoiBase.padStart(39, '0');

        const data = [zoiBase, taxNumber, issueDateTime.format('YYMMDDHHmmss')].join('');
        let control = 0;

        for (let i = 0; i < data.length; i++) {
            control = control + parseInt(data[i]);
        }

        control = control % 10;

        return data + control;
    }

    taxSpecification = (vatDetails) => {
        let vatSpec = [];

        for (let i = 0; i < vatDetails.length; i++) {
            vatSpec.push({
                TaxRate: vatDetails[i].vatRate / Math.pow(10, global.currentPosData.currencyDecimal),
                TaxableAmount: vatDetails[i].taxableAmountInCents / Math.pow(10, global.currentPosData.currencyDecimal),
                TaxAmount: vatDetails[i].vatInCents / Math.pow(10, global.currentPosData.currencyDecimal)
            });
        }

        return vatSpec;
    }

    getResponseBody = (res) => {
        const responseFields = res.token.split('.');
        const token = Buffer.from(responseFields[0] + '.' + responseFields[1]);
        const body = Buffer.from(this.fromBase64url(responseFields[1]), 'base64').toString('utf8');
        const signature = Buffer.from(this.fromBase64url(responseFields[2]), 'base64');

        if (this.verifyString(token, signature)) {
            return JSON.parse(body);
        } else {
            log.info('verifyString failed');
            return null;
        }
    }

    getSignedToken = (header, body) => {
        const token = header + '.' + body;
        const signature = this.toBase64url(Buffer.from(this.parseHexString(this.signString(token)), 'hex').toString('base64'));

        return token + '.' + signature;
    }

    signString = (value)  => {
        const crypto = require('crypto');

        const privateKey = crypto.createPrivateKey({
            key: this.certDescriptor.key,
            format: 'pem',
            type: 'pkcs8',
            passphrase: this.certDescriptor.passphrase
        });

        let sign = crypto.createSign('sha256WithRSAEncryption');
        sign.write(value);
        sign.end();
        return sign.sign(privateKey);
    }

    verifyString = (value, signature)  => {
        const crypto = require('crypto');

        const publicKey = crypto.createPublicKey({
            key: this.certDescriptor.sign,
            format: 'pem',
            type: 'pkcs1'
        });

        let verify = crypto.createVerify('sha256WithRSAEncryption');
        verify.write(value);
        verify.end();
        return verify.verify(publicKey, signature);
    }

    getJsonBase64 = (json) => {
        const jsonBytes = Buffer.from(JSON.stringify(json));
        const jsonBase64 = this.toBase64url(Buffer.from(jsonBytes).toString('base64'));

        return jsonBase64;
    }

    toBase64url = (str) => {
        return str.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
    }

    fromBase64url = (str) => {
        return str.replace(/\-/g, '+').replace(/_/g, '/');
    }

    parseDecString = (str) => {
        function add(x, y) {
            var c = 0, r = [];
            var x = x.split('').map(Number);
            var y = y.split('').map(Number);
            while (x.length || y.length) {
                var s = (x.pop() || 0) + (y.pop() || 0) + c;
                r.unshift(s < 10 ? s : s - 10);
                c = s < 10 ? 0 : 1;
            }
            if (c) r.unshift(c);
            return r.join('');
        }

        var dec = '0';
        str.split('').forEach(function(chr) {
            var n = parseInt(chr, 16);
            for (var t = 8; t; t >>= 1) {
                dec = add(dec, dec);
                if (n & t) dec = add(dec, '1');
            }
        });
        return dec;
    }

    parseHexString = (str) => {
        return Buffer.from(str).toString('hex');
    }

    getCertData = (certificate, passphrase) => {
        const certFolder = path.join(process.cwd(), 'configs');

        const fursPem = this.getFursCerts(certFolder);
        const clientPem = this.getClientCert(certFolder, certificate, passphrase);

        return {
            ca: fursPem.ca,
            cert: clientPem.cert,
            key: clientPem.key,
            sign: fursPem.sign,
            passphrase: clientPem.passphrase,
            subject: clientPem.subject,
            issuer: clientPem.issuer,
            serial: clientPem.serial,
            taxNumber: clientPem.taxNumber
        };
    }

    getFursCerts = (certFolder) => {
        if (!fs.existsSync(path.join(certFolder, 'furs_sign.pem')) && !fs.existsSync(path.join(certFolder, 'furs_bundle.pem'))) {
            log.info('Cant find "furs_sign.pem" or "furs_bundle.pem" in "configs" folder');

            if (!fs.existsSync(path.join(certFolder, 'furs'))) {
                log.info('Cant find "furs_bundle" folder');
                return;
            }

            // https://edavki.durs.si/EdavkiPortal/OpenPortal/CommonPages/Opdynp/PageD.aspx?category=dpr_teh_spec

            execSync('openssl x509 -in DavPotRacTEST.cer -out DavPotRacTEST.pem -outform PEM', {cwd: path.join(certFolder, 'furs')});                       // https://datoteke.durs.gov.si/dpr/files/DavPotRacTEST.cer
            const signCertPem = fs.readFileSync(path.join(certFolder, 'furs', 'DavPotRacTEST.pem'));
            execSync('openssl x509 -in blagajne-test.fu.gov.si.cer -out blagajne-test.fu.gov.si.pem -outform PEM', {cwd: path.join(certFolder, 'furs')});   // https://datoteke.durs.gov.si/dpr/files/blagajne-test.fu.gov.si.cer
            const tlsCertPem = fs.readFileSync(path.join(certFolder, 'furs', 'blagajne-test.fu.gov.si.pem'));
            execSync('openssl x509 -in sigov-ca2.xcert.crt -out sigov-ca2.xcert.pem -outform PEM', {cwd: path.join(certFolder, 'furs')});                   // https://www.si-trust.gov.si/assets/si-trust-root/povezovalni-podrejeni/sigovca-2/sigov-ca2.xcert.crt
            const ce2CertPem = fs.readFileSync(path.join(certFolder, 'furs', 'sigov-ca2.xcert.pem'));
            execSync('openssl x509 -in si-trust-root.crt -out si-trust-root.pem -outform PEM', {cwd: path.join(certFolder, 'furs')});                       // https://www.si-trust.gov.si/assets/si-trust-root/korensko-potrdilo/si-trust-root.crt
            const rotCertPem = fs.readFileSync(path.join(certFolder, 'furs', 'si-trust-root.pem'));

            fs.writeFileSync(path.join(certFolder, 'furs_sign.pem'), signCertPem);
            fs.writeFileSync(path.join(certFolder, 'furs_bundle.pem'), tlsCertPem + ce2CertPem + rotCertPem);
            fs.rmSync(path.join(certFolder, 'furs'), { recursive: true });

            log.info('Bundle created in "configs" folder');
        }

        return {
            ca: fs.readFileSync(path.join(certFolder, 'furs_bundle.pem')).toString(),
            sign: fs.readFileSync(path.join(certFolder, 'furs_sign.pem')).toString()
        };
    }

    getClientCert = (certFolder, certificate, passphrase) => {
        const crypto = require('crypto');

        if (!fs.existsSync(path.join(certFolder, 'client_cert.pem')) && !fs.existsSync(path.join(certFolder, 'client_key.pem'))) {
            log.info('Cant find "client_cert.pem" or "client_key.pem" in "configs" folder');

            if (!fs.existsSync(path.join(certFolder, certificate+'.p12'))) {
                log.info('Cant find "'+certificate+'.p12" in "configs" folder');
            }

            execSync('openssl pkcs12 -in '+certificate+'.p12 -clcerts -nokeys -out client_cert.crt -passin pass:'+passphrase, {cwd: path.join(certFolder)});
            execSync('openssl x509 -in client_cert.crt -out client_cert.pem -outform PEM', {cwd: path.join(certFolder)});
            execSync('openssl pkcs12 -in '+certificate+'.p12 -nocerts -out client_key.key -passin pass:'+passphrase+' -passout pass:'+passphrase, {cwd: path.join(certFolder)});
            execSync('openssl ec -in client_key.key -out client_key.pem -outform PEM -passin pass:'+passphrase, {cwd: path.join(certFolder)});

            fs.rmSync(path.join(certFolder, 'client_cert.crt'));
            fs.rmSync(path.join(certFolder, 'client_key.key'));
            fs.rmSync(path.join(certFolder, certificate+'.p12'));

            log.info('Client files created in "configs" folder');
        }

        const certFile = fs.readFileSync(path.join(certFolder, 'client_cert.pem')).toString();
        const keyFile = fs.readFileSync(path.join(certFolder, 'client_key.pem')).toString();
        const client_cert = new crypto.X509Certificate(certFile);

        return {
            cert: certFile,
            key: keyFile,
            passphrase: (typeof passphrase !== 'undefined') ? passphrase : process.env.CLIENT_FISCAL_VERIFICATION_KEY,
            subject: client_cert.subject.replace(/\n/g, ','),
            issuer: client_cert.issuer.replace(/\n/g, ','),
            serial: this.parseDecString(client_cert.serialNumber),
            taxNumber: parseInt(client_cert.subject.replace(/\n/g, ',').split(',')[3].replace('OU=', ''))
        };
    }

    getDevices = async (deviceMnemonic) => {
        try {
            const res = await this.axios.get(process.env.CLIENT_APIURL + '/rest/afc/device/device-type/' + deviceMnemonic + '/status');
            if (typeof res === 'undefined' || res.status >= 400) { return null; }
            return res;
        } catch (err) {
            log.error(err);
        }
    }

    getDeviceTransactionHistory = async (deviceId, filter) => {
        try {
            const res = await this.axios.post(process.env.CLIENT_APIURL + '/rest/afc/device/' + deviceId + '/transaction-history', filter);
            if (typeof res == 'undefined' || res.status >= 400) { return null; }
            return res;
        } catch (err) {
            log.error(err);
        }
    }

    getTicketTransactionFile = async (deviceMnemonic, batchId) => {
        try {
            let res = await this.axios.get(process.env.CLIENT_APIURL + '/rest/afc/transaction/download/' + deviceMnemonic + '/' + batchId, {responseType: 'arraybuffer'});
            if (typeof res === 'undefined' || res.status >= 400) { return {head: null}; }
            return Buffer.from(res, 'binary');
        } catch (err) {
            log.error(err);
        }
    }

    test = async () => {
        try {
            //await this.testQrCode();
            //await this.testTaxQrCode();
            //await this.testFiscalVerification();

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

}

module.exports = FinancialTransactionServ;
