From 642bf853ecfe122b7986f9a545a01ce3d4e1689e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BB=D0=B0=D0=B4=D0=B8=D1=81=D0=BB=D0=B0=D0=B2=20?= =?UTF-8?q?=D0=9A=D1=83=D1=87=D0=B8=D0=BD?= Date: Sat, 24 Oct 2020 12:15:32 +0300 Subject: [PATCH 1/7] Implement PrivatBank api (transactions and cards info) --- privat-api/privat_api.js | 190 +++++++++++++++++++++++++++ privat-api/utils/asyncify.js | 8 ++ privat-api/utils/balance.js | 32 +++++ privat-api/utils/dates.js | 30 +++++ privat-api/utils/transactions.js | 29 ++++ privat-api/xml_data/balance.xml | 16 +++ privat-api/xml_data/transactions.xml | 17 +++ 7 files changed, 322 insertions(+) create mode 100644 privat-api/privat_api.js create mode 100644 privat-api/utils/asyncify.js create mode 100644 privat-api/utils/balance.js create mode 100644 privat-api/utils/dates.js create mode 100644 privat-api/utils/transactions.js create mode 100644 privat-api/xml_data/balance.xml create mode 100644 privat-api/xml_data/transactions.xml diff --git a/privat-api/privat_api.js b/privat-api/privat_api.js new file mode 100644 index 0000000..3f02ceb --- /dev/null +++ b/privat-api/privat_api.js @@ -0,0 +1,190 @@ +'use strict'; + +const http = require('https'); +const xmlJs = require('xml-js'); +const crypto = require('crypto'); +const {fillTransactionsXml} = require('./utils/transactions'); +const {fillBalanceXml} = require('./utils/balance'); +const { + stringToDate, + dateToString, + divideIntoPeriods +} = require('./utils/dates'); + +const createSignature = (xml, password) => { + const regexp = /(?<=).*(?=<\/data)/; + const dataString = xml.match(regexp)[0]; + const md5Signature = crypto.createHash('md5') + .update(`${dataString}${password}`, 'utf8') + .digest('hex'); + const signature = crypto.createHash('sha1') + .update(md5Signature, 'utf8') + .digest('hex'); + return signature; +}; + +const fillXmlWithSignature = (xml, signature) => { + const xmlObj = xmlJs.xml2js(xml, {compact: true}); + + const xmlMerchant = xmlObj.request.merchant; + xmlMerchant.signature._text = signature; + + const xmlWithSignature = xmlJs.js2xml(xmlObj, { + spaces: 0, + compact: true, + fullTagEmptyElement: true + }); + return xmlWithSignature; +}; + +const handleBankResponse = (cb, resolve, reject, res) => { + let data = ''; + if (res.statusCode != 200) { + const errMsg = `Server did not send data. STATUS CODE: ${res.statusCode}`; + reject(new Error(errMsg)); + } + res.setEncoding('utf8'); + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + cb(resolve, reject, data); + }); + res.on('error', err => reject(err)); +}; + +const handleTransactionsData = (resolve, reject, dataXml) => { + let dataObj; + try { + dataObj = xmlJs.xml2js(dataXml, {compact: true}); + } catch (err) { + reject(new Error(err)); + return; + } + + if (dataObj.response.data.error != null) { + const errorMessage = dataObj.response.data.error._attributes.message; + reject(new Error(errorMessage)); + return; + } + + const result = []; + let transactions = dataObj.response.data.info.statements.statement; + if (!Array.isArray(transactions)) transactions = [transactions]; + transactions.map(val => result.push(val._attributes)); + resolve(result); +}; + +const handleBalanceData = (resolve, reject, dataXml) => { + let dataObj; + try { + dataObj = xmlJs.xml2js(dataXml, {compact: true}); + } catch (err) { + reject(new Error('Invalid XML')); + return; + } + + if (dataObj.response.data.error != null) { + const errorMessage = dataObj.response.data.error._attributes.message; + reject(new Error(errorMessage)); + return; + } + const cardBalance = dataObj.response.data.info.cardbalance; + const card = cardBalance.card; + + const result = { + cardNumber: card.card_number._text, + currency: card.currency._text, + balance: cardBalance.balance._text, + availableBalance: cardBalance.av_balance._text, + dateBalance: cardBalance.bal_date._text, + creditLimit: cardBalance.fin_limit._text + }; + resolve(result); +}; + +const getData = (dataForRequest, cb) => { + const path = cb.name === 'handleTransactionsData' ? '/p24api/rest_fiz' + : cb.name === 'handleBalanceData' ? '/p24api/balance' + : null; + const options = { + hostname: 'api.privatbank.ua', + path: path, + method: 'POST', + headers: + {'Content-Type': 'application/xml; charset=UTF-8'} + }; + const req = http.request(options); + const xml = cb.name === 'handleTransactionsData' ? + fillTransactionsXml(dataForRequest) + : cb.name === 'handleBalanceData' ? fillBalanceXml(dataForRequest) + : null; + + const signature = createSignature(xml, dataForRequest.merchantPassword); + const xmlWithSignature = fillXmlWithSignature(xml, signature); + req.write(xmlWithSignature); + req.end(); + + return new Promise((resolve, reject) => { + req.on('response', handleBankResponse.bind(null, cb, resolve, reject)); + req.on('error', err => reject(err)); + }); +}; + +const getTransactionsData = async dataForTransactions => { + const startDate = stringToDate(dataForTransactions.startDate); + const endDate = stringToDate(dataForTransactions.endDate); + + const periods = divideIntoPeriods(startDate, endDate); + const promises = []; + + periods.forEach((period, index) => { + const copyDataForTransactions = Object.assign({}, dataForTransactions); + copyDataForTransactions.startDate = dateToString(period[0]); + copyDataForTransactions.endDate = dateToString(period[1]); + promises[index] = getData(copyDataForTransactions, handleTransactionsData); + }); + const transactions = await Promise.all(promises); + + let result = []; + for (const array of transactions) result = [...result, ...array.reverse()]; + + return {transactions: result}; +}; + +const getBalanceData = async dataForBalance => { + const result = []; + for (const data of dataForBalance) { + result.push(getData(data, handleBalanceData)); + } + return Promise.allSettled(Object.values(result)); +}; + +const dataForTransactionsRequest = { // sample data for transactions request + merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', + merchantId: 123456, + wait: 10, + test: 0, + paymentId: '', + startDate: '11.05.2020', + endDate: '23.10.2020', + cardNumber: '1234567890123456' +}; + +const dataForBalanceRequest = { // sample data for balance request + merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', + merchantId: 123456, + wait: 10, + test: 0, + paymentId: '', + cardNumber: '1234567890123456', + country: 'UA' +}; + +(async () => { + const tranData = await getTransactionsData(dataForTransactionsRequest); + const balanceData = await getBalanceData(dataForBalanceRequest); + // dataForBalanceRequest can be array of data objects + console.log(tranData); + console.log(balanceData); +})(); diff --git a/privat-api/utils/asyncify.js b/privat-api/utils/asyncify.js new file mode 100644 index 0000000..37e5d13 --- /dev/null +++ b/privat-api/utils/asyncify.js @@ -0,0 +1,8 @@ +'use strict'; + +const asincify = fn => (...args) => new Promise( + (resolve, reject) => fn(...args, + (err, data) => err === null ? resolve(data) : reject(err)) +); + +module.exports = asincify; \ No newline at end of file diff --git a/privat-api/utils/balance.js b/privat-api/utils/balance.js new file mode 100644 index 0000000..0e21aa3 --- /dev/null +++ b/privat-api/utils/balance.js @@ -0,0 +1,32 @@ +'use strict'; + +const fs = require('fs'); +const xmlJs = require('xml-js'); +const balanceXML = fs.readFileSync('./xml_data/balance.xml', 'utf-8'); + +const fillBalanceXml = dataForBalance => { + const xmlObj = xmlJs.xml2js(balanceXML, {compact: true}); + + const xmlMerchant = xmlObj.request.merchant; + xmlMerchant.id._text = dataForBalance.merchantId; + + const xmlData = xmlObj.request.data; + xmlData.wait._text = dataForBalance.wait; + xmlData.test._text = dataForBalance.test; + + const xmlPayment = xmlData.payment; + xmlPayment._attributes.id = dataForBalance.paymentId; + + const [xmlCardNumber, xmlCountry] = xmlPayment.prop; + xmlCardNumber._attributes.value = dataForBalance.cardNumber; + xmlCountry._attributes.value = dataForBalance.country; + + const xmlWithData = xmlJs.js2xml(xmlObj, { + spaces: 0, + compact: true, + fullTagEmptyElement: true + }); + return xmlWithData; +}; + +module.exports = {fillBalanceXml}; \ No newline at end of file diff --git a/privat-api/utils/dates.js b/privat-api/utils/dates.js new file mode 100644 index 0000000..487310b --- /dev/null +++ b/privat-api/utils/dates.js @@ -0,0 +1,30 @@ +'use strict'; + +const stringToDate = date => { + const [day, month, year] = date.split('.'); + return new Date(`${year}-${month}-${day}`); +}; + +const dateToString = date => { + return `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`; +}; + +const divideIntoPeriods = (startDate, endDate) => { + const periods = []; + const day = 1000 * 3600 * 24; + const threeMonths = day * 30 * 3; + const periodEnd = new Date(startDate.getTime() + threeMonths); + periods.push([startDate, periodEnd]); + + while (periods[periods.length - 1][1] < endDate) { + const lastPeriodEnd = periods[periods.length - 1][1]; + const newPeriodStart = new Date(lastPeriodEnd.getTime() + day); + const newPeriodEnd = new Date(newPeriodStart.getTime() + threeMonths); + periods.push([newPeriodStart, newPeriodEnd]); + } + periods[periods.length - 1][1] = endDate; + + return periods; +}; + +module.exports = {stringToDate, dateToString, divideIntoPeriods}; \ No newline at end of file diff --git a/privat-api/utils/transactions.js b/privat-api/utils/transactions.js new file mode 100644 index 0000000..4e56c09 --- /dev/null +++ b/privat-api/utils/transactions.js @@ -0,0 +1,29 @@ +'use strict' + +const fs = require('fs') +const xmlJs = require('xml-js') +const transactionsXML = fs.readFileSync('./xml_data/transactions.xml', 'utf-8') + +const fillTransactionsXml = dataForTransactions => { + const xmlObj = xmlJs.xml2js(transactionsXML, {compact: true}) + + const xmlMerchant = xmlObj.request.merchant + xmlMerchant.id._text = dataForTransactions.merchantId + + const xmlData = xmlObj.request.data + xmlData.wait._text = dataForTransactions.wait + xmlData.test._text = dataForTransactions.test + + const xmlPayment = xmlData.payment + xmlPayment._attributes.id = dataForTransactions.paymentId + + const [xmlStartDate, xmlEndDate, xmlCardNumber] = xmlPayment.prop + xmlStartDate._attributes.value = dataForTransactions.startDate + xmlEndDate._attributes.value = dataForTransactions.endDate + xmlCardNumber._attributes.value = dataForTransactions.cardNumber + + const xmlWithData = xmlJs.js2xml(xmlObj, {spaces: 0, compact: true, fullTagEmptyElement: true}) + return xmlWithData +} + +module.exports = {fillTransactionsXml} \ No newline at end of file diff --git a/privat-api/xml_data/balance.xml b/privat-api/xml_data/balance.xml new file mode 100644 index 0000000..c536ab9 --- /dev/null +++ b/privat-api/xml_data/balance.xml @@ -0,0 +1,16 @@ + + + + + + + + cmt + 0 + 0 + + + + + + \ No newline at end of file diff --git a/privat-api/xml_data/transactions.xml b/privat-api/xml_data/transactions.xml new file mode 100644 index 0000000..176dcea --- /dev/null +++ b/privat-api/xml_data/transactions.xml @@ -0,0 +1,17 @@ + + + + + + + + cmt + + + + + + + + + \ No newline at end of file From e1f13eaa18b7ad13270eae19d214042ff5026b8c Mon Sep 17 00:00:00 2001 From: PaIIadium Date: Thu, 7 Jan 2021 02:33:51 +0200 Subject: [PATCH 2/7] restructuring --- privat-api/privat_api.js | 190 ------------------ privat-api/utils/asyncify.js | 8 - privat-api/utils/transactions.js | 29 --- src/datasources/privat24/privat24.js | 189 ++++++++++++++++- .../datasources/privat24}/utils/balance.js | 0 .../datasources/privat24}/utils/dates.js | 0 .../privat24/utils/transactions.js | 29 +++ .../privat24}/xml_data/balance.xml | 0 .../privat24}/xml_data/transactions.xml | 0 9 files changed, 217 insertions(+), 228 deletions(-) delete mode 100644 privat-api/privat_api.js delete mode 100644 privat-api/utils/asyncify.js delete mode 100644 privat-api/utils/transactions.js rename {privat-api => src/datasources/privat24}/utils/balance.js (100%) rename {privat-api => src/datasources/privat24}/utils/dates.js (100%) create mode 100644 src/datasources/privat24/utils/transactions.js rename {privat-api => src/datasources/privat24}/xml_data/balance.xml (100%) rename {privat-api => src/datasources/privat24}/xml_data/transactions.xml (100%) diff --git a/privat-api/privat_api.js b/privat-api/privat_api.js deleted file mode 100644 index 3f02ceb..0000000 --- a/privat-api/privat_api.js +++ /dev/null @@ -1,190 +0,0 @@ -'use strict'; - -const http = require('https'); -const xmlJs = require('xml-js'); -const crypto = require('crypto'); -const {fillTransactionsXml} = require('./utils/transactions'); -const {fillBalanceXml} = require('./utils/balance'); -const { - stringToDate, - dateToString, - divideIntoPeriods -} = require('./utils/dates'); - -const createSignature = (xml, password) => { - const regexp = /(?<=).*(?=<\/data)/; - const dataString = xml.match(regexp)[0]; - const md5Signature = crypto.createHash('md5') - .update(`${dataString}${password}`, 'utf8') - .digest('hex'); - const signature = crypto.createHash('sha1') - .update(md5Signature, 'utf8') - .digest('hex'); - return signature; -}; - -const fillXmlWithSignature = (xml, signature) => { - const xmlObj = xmlJs.xml2js(xml, {compact: true}); - - const xmlMerchant = xmlObj.request.merchant; - xmlMerchant.signature._text = signature; - - const xmlWithSignature = xmlJs.js2xml(xmlObj, { - spaces: 0, - compact: true, - fullTagEmptyElement: true - }); - return xmlWithSignature; -}; - -const handleBankResponse = (cb, resolve, reject, res) => { - let data = ''; - if (res.statusCode != 200) { - const errMsg = `Server did not send data. STATUS CODE: ${res.statusCode}`; - reject(new Error(errMsg)); - } - res.setEncoding('utf8'); - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - cb(resolve, reject, data); - }); - res.on('error', err => reject(err)); -}; - -const handleTransactionsData = (resolve, reject, dataXml) => { - let dataObj; - try { - dataObj = xmlJs.xml2js(dataXml, {compact: true}); - } catch (err) { - reject(new Error(err)); - return; - } - - if (dataObj.response.data.error != null) { - const errorMessage = dataObj.response.data.error._attributes.message; - reject(new Error(errorMessage)); - return; - } - - const result = []; - let transactions = dataObj.response.data.info.statements.statement; - if (!Array.isArray(transactions)) transactions = [transactions]; - transactions.map(val => result.push(val._attributes)); - resolve(result); -}; - -const handleBalanceData = (resolve, reject, dataXml) => { - let dataObj; - try { - dataObj = xmlJs.xml2js(dataXml, {compact: true}); - } catch (err) { - reject(new Error('Invalid XML')); - return; - } - - if (dataObj.response.data.error != null) { - const errorMessage = dataObj.response.data.error._attributes.message; - reject(new Error(errorMessage)); - return; - } - const cardBalance = dataObj.response.data.info.cardbalance; - const card = cardBalance.card; - - const result = { - cardNumber: card.card_number._text, - currency: card.currency._text, - balance: cardBalance.balance._text, - availableBalance: cardBalance.av_balance._text, - dateBalance: cardBalance.bal_date._text, - creditLimit: cardBalance.fin_limit._text - }; - resolve(result); -}; - -const getData = (dataForRequest, cb) => { - const path = cb.name === 'handleTransactionsData' ? '/p24api/rest_fiz' - : cb.name === 'handleBalanceData' ? '/p24api/balance' - : null; - const options = { - hostname: 'api.privatbank.ua', - path: path, - method: 'POST', - headers: - {'Content-Type': 'application/xml; charset=UTF-8'} - }; - const req = http.request(options); - const xml = cb.name === 'handleTransactionsData' ? - fillTransactionsXml(dataForRequest) - : cb.name === 'handleBalanceData' ? fillBalanceXml(dataForRequest) - : null; - - const signature = createSignature(xml, dataForRequest.merchantPassword); - const xmlWithSignature = fillXmlWithSignature(xml, signature); - req.write(xmlWithSignature); - req.end(); - - return new Promise((resolve, reject) => { - req.on('response', handleBankResponse.bind(null, cb, resolve, reject)); - req.on('error', err => reject(err)); - }); -}; - -const getTransactionsData = async dataForTransactions => { - const startDate = stringToDate(dataForTransactions.startDate); - const endDate = stringToDate(dataForTransactions.endDate); - - const periods = divideIntoPeriods(startDate, endDate); - const promises = []; - - periods.forEach((period, index) => { - const copyDataForTransactions = Object.assign({}, dataForTransactions); - copyDataForTransactions.startDate = dateToString(period[0]); - copyDataForTransactions.endDate = dateToString(period[1]); - promises[index] = getData(copyDataForTransactions, handleTransactionsData); - }); - const transactions = await Promise.all(promises); - - let result = []; - for (const array of transactions) result = [...result, ...array.reverse()]; - - return {transactions: result}; -}; - -const getBalanceData = async dataForBalance => { - const result = []; - for (const data of dataForBalance) { - result.push(getData(data, handleBalanceData)); - } - return Promise.allSettled(Object.values(result)); -}; - -const dataForTransactionsRequest = { // sample data for transactions request - merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', - merchantId: 123456, - wait: 10, - test: 0, - paymentId: '', - startDate: '11.05.2020', - endDate: '23.10.2020', - cardNumber: '1234567890123456' -}; - -const dataForBalanceRequest = { // sample data for balance request - merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', - merchantId: 123456, - wait: 10, - test: 0, - paymentId: '', - cardNumber: '1234567890123456', - country: 'UA' -}; - -(async () => { - const tranData = await getTransactionsData(dataForTransactionsRequest); - const balanceData = await getBalanceData(dataForBalanceRequest); - // dataForBalanceRequest can be array of data objects - console.log(tranData); - console.log(balanceData); -})(); diff --git a/privat-api/utils/asyncify.js b/privat-api/utils/asyncify.js deleted file mode 100644 index 37e5d13..0000000 --- a/privat-api/utils/asyncify.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -const asincify = fn => (...args) => new Promise( - (resolve, reject) => fn(...args, - (err, data) => err === null ? resolve(data) : reject(err)) -); - -module.exports = asincify; \ No newline at end of file diff --git a/privat-api/utils/transactions.js b/privat-api/utils/transactions.js deleted file mode 100644 index 4e56c09..0000000 --- a/privat-api/utils/transactions.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict' - -const fs = require('fs') -const xmlJs = require('xml-js') -const transactionsXML = fs.readFileSync('./xml_data/transactions.xml', 'utf-8') - -const fillTransactionsXml = dataForTransactions => { - const xmlObj = xmlJs.xml2js(transactionsXML, {compact: true}) - - const xmlMerchant = xmlObj.request.merchant - xmlMerchant.id._text = dataForTransactions.merchantId - - const xmlData = xmlObj.request.data - xmlData.wait._text = dataForTransactions.wait - xmlData.test._text = dataForTransactions.test - - const xmlPayment = xmlData.payment - xmlPayment._attributes.id = dataForTransactions.paymentId - - const [xmlStartDate, xmlEndDate, xmlCardNumber] = xmlPayment.prop - xmlStartDate._attributes.value = dataForTransactions.startDate - xmlEndDate._attributes.value = dataForTransactions.endDate - xmlCardNumber._attributes.value = dataForTransactions.cardNumber - - const xmlWithData = xmlJs.js2xml(xmlObj, {spaces: 0, compact: true, fullTagEmptyElement: true}) - return xmlWithData -} - -module.exports = {fillTransactionsXml} \ No newline at end of file diff --git a/src/datasources/privat24/privat24.js b/src/datasources/privat24/privat24.js index f2df15a..3f02ceb 100644 --- a/src/datasources/privat24/privat24.js +++ b/src/datasources/privat24/privat24.js @@ -1,3 +1,190 @@ 'use strict'; -// TODO: Add implementation of Privat24 API Handler +const http = require('https'); +const xmlJs = require('xml-js'); +const crypto = require('crypto'); +const {fillTransactionsXml} = require('./utils/transactions'); +const {fillBalanceXml} = require('./utils/balance'); +const { + stringToDate, + dateToString, + divideIntoPeriods +} = require('./utils/dates'); + +const createSignature = (xml, password) => { + const regexp = /(?<=).*(?=<\/data)/; + const dataString = xml.match(regexp)[0]; + const md5Signature = crypto.createHash('md5') + .update(`${dataString}${password}`, 'utf8') + .digest('hex'); + const signature = crypto.createHash('sha1') + .update(md5Signature, 'utf8') + .digest('hex'); + return signature; +}; + +const fillXmlWithSignature = (xml, signature) => { + const xmlObj = xmlJs.xml2js(xml, {compact: true}); + + const xmlMerchant = xmlObj.request.merchant; + xmlMerchant.signature._text = signature; + + const xmlWithSignature = xmlJs.js2xml(xmlObj, { + spaces: 0, + compact: true, + fullTagEmptyElement: true + }); + return xmlWithSignature; +}; + +const handleBankResponse = (cb, resolve, reject, res) => { + let data = ''; + if (res.statusCode != 200) { + const errMsg = `Server did not send data. STATUS CODE: ${res.statusCode}`; + reject(new Error(errMsg)); + } + res.setEncoding('utf8'); + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + cb(resolve, reject, data); + }); + res.on('error', err => reject(err)); +}; + +const handleTransactionsData = (resolve, reject, dataXml) => { + let dataObj; + try { + dataObj = xmlJs.xml2js(dataXml, {compact: true}); + } catch (err) { + reject(new Error(err)); + return; + } + + if (dataObj.response.data.error != null) { + const errorMessage = dataObj.response.data.error._attributes.message; + reject(new Error(errorMessage)); + return; + } + + const result = []; + let transactions = dataObj.response.data.info.statements.statement; + if (!Array.isArray(transactions)) transactions = [transactions]; + transactions.map(val => result.push(val._attributes)); + resolve(result); +}; + +const handleBalanceData = (resolve, reject, dataXml) => { + let dataObj; + try { + dataObj = xmlJs.xml2js(dataXml, {compact: true}); + } catch (err) { + reject(new Error('Invalid XML')); + return; + } + + if (dataObj.response.data.error != null) { + const errorMessage = dataObj.response.data.error._attributes.message; + reject(new Error(errorMessage)); + return; + } + const cardBalance = dataObj.response.data.info.cardbalance; + const card = cardBalance.card; + + const result = { + cardNumber: card.card_number._text, + currency: card.currency._text, + balance: cardBalance.balance._text, + availableBalance: cardBalance.av_balance._text, + dateBalance: cardBalance.bal_date._text, + creditLimit: cardBalance.fin_limit._text + }; + resolve(result); +}; + +const getData = (dataForRequest, cb) => { + const path = cb.name === 'handleTransactionsData' ? '/p24api/rest_fiz' + : cb.name === 'handleBalanceData' ? '/p24api/balance' + : null; + const options = { + hostname: 'api.privatbank.ua', + path: path, + method: 'POST', + headers: + {'Content-Type': 'application/xml; charset=UTF-8'} + }; + const req = http.request(options); + const xml = cb.name === 'handleTransactionsData' ? + fillTransactionsXml(dataForRequest) + : cb.name === 'handleBalanceData' ? fillBalanceXml(dataForRequest) + : null; + + const signature = createSignature(xml, dataForRequest.merchantPassword); + const xmlWithSignature = fillXmlWithSignature(xml, signature); + req.write(xmlWithSignature); + req.end(); + + return new Promise((resolve, reject) => { + req.on('response', handleBankResponse.bind(null, cb, resolve, reject)); + req.on('error', err => reject(err)); + }); +}; + +const getTransactionsData = async dataForTransactions => { + const startDate = stringToDate(dataForTransactions.startDate); + const endDate = stringToDate(dataForTransactions.endDate); + + const periods = divideIntoPeriods(startDate, endDate); + const promises = []; + + periods.forEach((period, index) => { + const copyDataForTransactions = Object.assign({}, dataForTransactions); + copyDataForTransactions.startDate = dateToString(period[0]); + copyDataForTransactions.endDate = dateToString(period[1]); + promises[index] = getData(copyDataForTransactions, handleTransactionsData); + }); + const transactions = await Promise.all(promises); + + let result = []; + for (const array of transactions) result = [...result, ...array.reverse()]; + + return {transactions: result}; +}; + +const getBalanceData = async dataForBalance => { + const result = []; + for (const data of dataForBalance) { + result.push(getData(data, handleBalanceData)); + } + return Promise.allSettled(Object.values(result)); +}; + +const dataForTransactionsRequest = { // sample data for transactions request + merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', + merchantId: 123456, + wait: 10, + test: 0, + paymentId: '', + startDate: '11.05.2020', + endDate: '23.10.2020', + cardNumber: '1234567890123456' +}; + +const dataForBalanceRequest = { // sample data for balance request + merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', + merchantId: 123456, + wait: 10, + test: 0, + paymentId: '', + cardNumber: '1234567890123456', + country: 'UA' +}; + +(async () => { + const tranData = await getTransactionsData(dataForTransactionsRequest); + const balanceData = await getBalanceData(dataForBalanceRequest); + // dataForBalanceRequest can be array of data objects + console.log(tranData); + console.log(balanceData); +})(); diff --git a/privat-api/utils/balance.js b/src/datasources/privat24/utils/balance.js similarity index 100% rename from privat-api/utils/balance.js rename to src/datasources/privat24/utils/balance.js diff --git a/privat-api/utils/dates.js b/src/datasources/privat24/utils/dates.js similarity index 100% rename from privat-api/utils/dates.js rename to src/datasources/privat24/utils/dates.js diff --git a/src/datasources/privat24/utils/transactions.js b/src/datasources/privat24/utils/transactions.js new file mode 100644 index 0000000..a53cd13 --- /dev/null +++ b/src/datasources/privat24/utils/transactions.js @@ -0,0 +1,29 @@ +'use strict'; + +const fs = require('fs'); +const xmlJs = require('xml-js'); +const transactionsXML = fs.readFileSync('./xml_data/transactions.xml', 'utf-8'); + +const fillTransactionsXml = dataForTransactions => { + const xmlObj = xmlJs.xml2js(transactionsXML, {compact: true}); + + const xmlMerchant = xmlObj.request.merchant; + xmlMerchant.id._text = dataForTransactions.merchantId; + + const xmlData = xmlObj.request.data; + xmlData.wait._text = dataForTransactions.wait; + xmlData.test._text = dataForTransactions.test; + + const xmlPayment = xmlData.payment; + xmlPayment._attributes.id = dataForTransactions.paymentId; + + const [xmlStartDate, xmlEndDate, xmlCardNumber] = xmlPayment.prop; + xmlStartDate._attributes.value = dataForTransactions.startDate; + xmlEndDate._attributes.value = dataForTransactions.endDate; + xmlCardNumber._attributes.value = dataForTransactions.cardNumber; + + const xmlWithData = xmlJs.js2xml(xmlObj, {spaces: 0, compact: true, fullTagEmptyElement: true}); + return xmlWithData; +}; + +module.exports = {fillTransactionsXml}; \ No newline at end of file diff --git a/privat-api/xml_data/balance.xml b/src/datasources/privat24/xml_data/balance.xml similarity index 100% rename from privat-api/xml_data/balance.xml rename to src/datasources/privat24/xml_data/balance.xml diff --git a/privat-api/xml_data/transactions.xml b/src/datasources/privat24/xml_data/transactions.xml similarity index 100% rename from privat-api/xml_data/transactions.xml rename to src/datasources/privat24/xml_data/transactions.xml From 997f05522553c83d8fcc2bea476235fa06ae44ea Mon Sep 17 00:00:00 2001 From: PaIIadium Date: Thu, 7 Jan 2021 02:44:32 +0200 Subject: [PATCH 3/7] style fixes --- src/datasources/privat24/privat24.js | 48 +++++++++++------------ src/datasources/privat24/utils/balance.js | 10 ++--- src/datasources/privat24/utils/dates.js | 9 ++--- 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/datasources/privat24/privat24.js b/src/datasources/privat24/privat24.js index 3f02ceb..68840fc 100644 --- a/src/datasources/privat24/privat24.js +++ b/src/datasources/privat24/privat24.js @@ -1,13 +1,11 @@ -'use strict'; - const http = require('https'); const xmlJs = require('xml-js'); const crypto = require('crypto'); -const {fillTransactionsXml} = require('./utils/transactions'); -const {fillBalanceXml} = require('./utils/balance'); +const { fillTransactionsXml } = require('./utils/transactions'); +const { fillBalanceXml } = require('./utils/balance'); const { - stringToDate, - dateToString, + stringToDate, + dateToString, divideIntoPeriods } = require('./utils/dates'); @@ -24,14 +22,14 @@ const createSignature = (xml, password) => { }; const fillXmlWithSignature = (xml, signature) => { - const xmlObj = xmlJs.xml2js(xml, {compact: true}); + const xmlObj = xmlJs.xml2js(xml, { compact: true }); const xmlMerchant = xmlObj.request.merchant; xmlMerchant.signature._text = signature; const xmlWithSignature = xmlJs.js2xml(xmlObj, { - spaces: 0, - compact: true, + spaces: 0, + compact: true, fullTagEmptyElement: true }); return xmlWithSignature; @@ -39,12 +37,12 @@ const fillXmlWithSignature = (xml, signature) => { const handleBankResponse = (cb, resolve, reject, res) => { let data = ''; - if (res.statusCode != 200) { + if (res.statusCode !== 200) { const errMsg = `Server did not send data. STATUS CODE: ${res.statusCode}`; reject(new Error(errMsg)); } res.setEncoding('utf8'); - res.on('data', (chunk) => { + res.on('data', chunk => { data += chunk; }); res.on('end', () => { @@ -56,13 +54,13 @@ const handleBankResponse = (cb, resolve, reject, res) => { const handleTransactionsData = (resolve, reject, dataXml) => { let dataObj; try { - dataObj = xmlJs.xml2js(dataXml, {compact: true}); + dataObj = xmlJs.xml2js(dataXml, { compact: true }); } catch (err) { reject(new Error(err)); return; } - if (dataObj.response.data.error != null) { + if (dataObj.response.data.error !== null) { const errorMessage = dataObj.response.data.error._attributes.message; reject(new Error(errorMessage)); return; @@ -78,13 +76,13 @@ const handleTransactionsData = (resolve, reject, dataXml) => { const handleBalanceData = (resolve, reject, dataXml) => { let dataObj; try { - dataObj = xmlJs.xml2js(dataXml, {compact: true}); + dataObj = xmlJs.xml2js(dataXml, { compact: true }); } catch (err) { reject(new Error('Invalid XML')); return; } - if (dataObj.response.data.error != null) { + if (dataObj.response.data.error !== null) { const errorMessage = dataObj.response.data.error._attributes.message; reject(new Error(errorMessage)); return; @@ -104,21 +102,21 @@ const handleBalanceData = (resolve, reject, dataXml) => { }; const getData = (dataForRequest, cb) => { - const path = cb.name === 'handleTransactionsData' ? '/p24api/rest_fiz' - : cb.name === 'handleBalanceData' ? '/p24api/balance' - : null; + const path = cb.name === 'handleTransactionsData' ? '/p24api/rest_fiz' : + cb.name === 'handleBalanceData' ? '/p24api/balance' : + null; const options = { hostname: 'api.privatbank.ua', - path: path, + path, method: 'POST', headers: - {'Content-Type': 'application/xml; charset=UTF-8'} + { 'Content-Type': 'application/xml; charset=UTF-8' } }; const req = http.request(options); - const xml = cb.name === 'handleTransactionsData' ? - fillTransactionsXml(dataForRequest) - : cb.name === 'handleBalanceData' ? fillBalanceXml(dataForRequest) - : null; + const xml = cb.name === 'handleTransactionsData' ? + fillTransactionsXml(dataForRequest) : + cb.name === 'handleBalanceData' ? fillBalanceXml(dataForRequest) : + null; const signature = createSignature(xml, dataForRequest.merchantPassword); const xmlWithSignature = fillXmlWithSignature(xml, signature); @@ -149,7 +147,7 @@ const getTransactionsData = async dataForTransactions => { let result = []; for (const array of transactions) result = [...result, ...array.reverse()]; - return {transactions: result}; + return { transactions: result }; }; const getBalanceData = async dataForBalance => { diff --git a/src/datasources/privat24/utils/balance.js b/src/datasources/privat24/utils/balance.js index 0e21aa3..6e58a5a 100644 --- a/src/datasources/privat24/utils/balance.js +++ b/src/datasources/privat24/utils/balance.js @@ -1,11 +1,9 @@ -'use strict'; - const fs = require('fs'); const xmlJs = require('xml-js'); const balanceXML = fs.readFileSync('./xml_data/balance.xml', 'utf-8'); const fillBalanceXml = dataForBalance => { - const xmlObj = xmlJs.xml2js(balanceXML, {compact: true}); + const xmlObj = xmlJs.xml2js(balanceXML, { compact: true }); const xmlMerchant = xmlObj.request.merchant; xmlMerchant.id._text = dataForBalance.merchantId; @@ -22,11 +20,11 @@ const fillBalanceXml = dataForBalance => { xmlCountry._attributes.value = dataForBalance.country; const xmlWithData = xmlJs.js2xml(xmlObj, { - spaces: 0, - compact: true, + spaces: 0, + compact: true, fullTagEmptyElement: true }); return xmlWithData; }; -module.exports = {fillBalanceXml}; \ No newline at end of file +module.exports = { fillBalanceXml }; diff --git a/src/datasources/privat24/utils/dates.js b/src/datasources/privat24/utils/dates.js index 487310b..59a93d7 100644 --- a/src/datasources/privat24/utils/dates.js +++ b/src/datasources/privat24/utils/dates.js @@ -1,13 +1,10 @@ -'use strict'; - const stringToDate = date => { const [day, month, year] = date.split('.'); return new Date(`${year}-${month}-${day}`); }; -const dateToString = date => { - return `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`; -}; +const dateToString = date => + `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`; const divideIntoPeriods = (startDate, endDate) => { const periods = []; @@ -27,4 +24,4 @@ const divideIntoPeriods = (startDate, endDate) => { return periods; }; -module.exports = {stringToDate, dateToString, divideIntoPeriods}; \ No newline at end of file +module.exports = { stringToDate, dateToString, divideIntoPeriods }; From 364bc2c7939eca4c1e403b04e9d171ef0d96cbf3 Mon Sep 17 00:00:00 2001 From: PaIIadium Date: Thu, 7 Jan 2021 03:55:57 +0200 Subject: [PATCH 4/7] style fixes --- src/datasources/privat24/utils/transactions.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/datasources/privat24/utils/transactions.js b/src/datasources/privat24/utils/transactions.js index a53cd13..1c50505 100644 --- a/src/datasources/privat24/utils/transactions.js +++ b/src/datasources/privat24/utils/transactions.js @@ -1,11 +1,9 @@ -'use strict'; - const fs = require('fs'); const xmlJs = require('xml-js'); const transactionsXML = fs.readFileSync('./xml_data/transactions.xml', 'utf-8'); const fillTransactionsXml = dataForTransactions => { - const xmlObj = xmlJs.xml2js(transactionsXML, {compact: true}); + const xmlObj = xmlJs.xml2js(transactionsXML, { compact: true }); const xmlMerchant = xmlObj.request.merchant; xmlMerchant.id._text = dataForTransactions.merchantId; @@ -22,8 +20,9 @@ const fillTransactionsXml = dataForTransactions => { xmlEndDate._attributes.value = dataForTransactions.endDate; xmlCardNumber._attributes.value = dataForTransactions.cardNumber; - const xmlWithData = xmlJs.js2xml(xmlObj, {spaces: 0, compact: true, fullTagEmptyElement: true}); + const xmlWithData = xmlJs.js2xml(xmlObj, + { spaces: 0, compact: true, fullTagEmptyElement: true }); return xmlWithData; }; -module.exports = {fillTransactionsXml}; \ No newline at end of file +module.exports = { fillTransactionsXml }; From 28560e1da3587d32618fa5e902eb21dd08976f70 Mon Sep 17 00:00:00 2001 From: PaIIadium Date: Thu, 7 Jan 2021 05:11:59 +0200 Subject: [PATCH 5/7] BankDataSource interface is implemented --- src/datasources/BankDataSource.js | 4 ++-- src/datasources/privat24/privat24.js | 28 +++++++++++++++++++++++++--- src/model/Card.js | 4 +--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/datasources/BankDataSource.js b/src/datasources/BankDataSource.js index 2326230..7076414 100644 --- a/src/datasources/BankDataSource.js +++ b/src/datasources/BankDataSource.js @@ -1,5 +1,3 @@ -'use strict'; - /** Makes transactions to bank API */ class BankDataSource { /** Obtains transactions from card @@ -10,3 +8,5 @@ class BankDataSource { throw new Error('Not implemented'); } } + +module.exports = { BankDataSource }; diff --git a/src/datasources/privat24/privat24.js b/src/datasources/privat24/privat24.js index 68840fc..d2531b6 100644 --- a/src/datasources/privat24/privat24.js +++ b/src/datasources/privat24/privat24.js @@ -8,6 +8,7 @@ const { dateToString, divideIntoPeriods } = require('./utils/dates'); +const { BankDataSource } = require('../BankDataSource'); const createSignature = (xml, password) => { const regexp = /(?<=).*(?=<\/data)/; @@ -147,12 +148,13 @@ const getTransactionsData = async dataForTransactions => { let result = []; for (const array of transactions) result = [...result, ...array.reverse()]; - return { transactions: result }; + return result; }; const getBalanceData = async dataForBalance => { const result = []; for (const data of dataForBalance) { + console.log(11); result.push(getData(data, handleBalanceData)); } return Promise.allSettled(Object.values(result)); @@ -179,10 +181,30 @@ const dataForBalanceRequest = { // sample data for balance request country: 'UA' }; +// example of using (async () => { const tranData = await getTransactionsData(dataForTransactionsRequest); - const balanceData = await getBalanceData(dataForBalanceRequest); - // dataForBalanceRequest can be array of data objects + const balanceData = await getBalanceData([dataForBalanceRequest]); + // dataForBalanceRequest is array of data objects console.log(tranData); console.log(balanceData); })(); + +// just to implement the interface +class PrivatDataSource extends BankDataSource { + constructor() { + super(); + } + // conf is similar to dataForTransactionsRequest + async getTransactions(card, conf) { + conf.cardNumber = card.cardNum + ''; + return await getTransactionsData(conf); + } + // additional method for getting balances of cards array + // conf is array of objects similar to dataForBalanceRequest + async getBalance(conf) { + return await getBalanceData(conf); + } +} + +module.exports = PrivatDataSource; diff --git a/src/model/Card.js b/src/model/Card.js index 75a18b1..f937328 100644 --- a/src/model/Card.js +++ b/src/model/Card.js @@ -1,10 +1,8 @@ -'use strict'; - /** Represents bank card * * @property {number} cardId - Card's id in Lemon DB. * @property {string} bank - Name of the bank card belongs to - * @property {number} cardNum - Card number (can be either all number or last 4 digits only) + * @property {number} cardNum - Card number (must be full number) * @property {string} type - Card type in bank's system * @property {number} balance - Amount of money on card in lowest units * @property {string} currency - Currency of transactions from this cards */ From 0e76cc56ed9af51549e5484572c9de9209546a54 Mon Sep 17 00:00:00 2001 From: PaIIadium Date: Thu, 7 Jan 2021 05:15:13 +0200 Subject: [PATCH 6/7] style fixes --- src/datasources/privat24/privat24.js | 1 - src/model/Transaction.js | 9 +++++---- src/model/User.js | 2 -- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/datasources/privat24/privat24.js b/src/datasources/privat24/privat24.js index d2531b6..d37f44c 100644 --- a/src/datasources/privat24/privat24.js +++ b/src/datasources/privat24/privat24.js @@ -154,7 +154,6 @@ const getTransactionsData = async dataForTransactions => { const getBalanceData = async dataForBalance => { const result = []; for (const data of dataForBalance) { - console.log(11); result.push(getData(data, handleBalanceData)); } return Promise.allSettled(Object.values(result)); diff --git a/src/model/Transaction.js b/src/model/Transaction.js index 0d4be76..d3d6b84 100644 --- a/src/model/Transaction.js +++ b/src/model/Transaction.js @@ -1,10 +1,11 @@ -'use strict'; /** Represents transaction with card * * @property {number} cardId - Card's id in Lemon DB. - * @property {number} amount - Amount of money transfered during this transaction. - Amount is negative is money were transfered from card and positive in other case. - * @property {string} type - Description of transaction. This value is not determined by Lemon. + * @property {number} amount - Amount of money transfered during this + * transaction. Amount is negative if money were transfered from card + * and positive in other case. + * @property {string} type - Description of transaction. + * This value is not determined by Lemon. * @property {Date} date - Transaction date in ISO 8601. */ class Transaction { constructor(cardId, amount, type, date) { diff --git a/src/model/User.js b/src/model/User.js index 35155fe..f1b8042 100644 --- a/src/model/User.js +++ b/src/model/User.js @@ -1,5 +1,3 @@ -'use strict'; - /** @class User * * @property {string} name - User's name From ef3d01a912f73c6b35c7a8b022d7e0255c7042fa Mon Sep 17 00:00:00 2001 From: PaIIadium Date: Fri, 4 Jun 2021 01:59:33 +0300 Subject: [PATCH 7/7] Refactoring --- .vscode/launch.json | 17 + src/datasources/BankDataSource.js | 4 +- src/datasources/privat24/privat24.js | 335 ++++++++---------- src/datasources/privat24/privat24_usage.js | 36 ++ src/datasources/privat24/utils/balance.js | 2 + src/datasources/privat24/utils/dates.js | 2 + .../privat24/utils/transactions.js | 2 + src/model/Card.js | 4 +- src/model/Transaction.js | 2 + src/model/User.js | 2 + 10 files changed, 224 insertions(+), 182 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/datasources/privat24/privat24_usage.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..77165f9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\src\\main.js" + } + ] +} \ No newline at end of file diff --git a/src/datasources/BankDataSource.js b/src/datasources/BankDataSource.js index daa90a2..0851f99 100644 --- a/src/datasources/BankDataSource.js +++ b/src/datasources/BankDataSource.js @@ -1,3 +1,5 @@ +'use strict'; + /** Makes transactions to bank API */ class BankDataSource { /** Obtains transactions from card @@ -9,4 +11,4 @@ class BankDataSource { } } -module.exports = BankDataSource +module.exports = BankDataSource; diff --git a/src/datasources/privat24/privat24.js b/src/datasources/privat24/privat24.js index d37f44c..59314b9 100644 --- a/src/datasources/privat24/privat24.js +++ b/src/datasources/privat24/privat24.js @@ -1,3 +1,5 @@ +'use strict'; + const http = require('https'); const xmlJs = require('xml-js'); const crypto = require('crypto'); @@ -8,201 +10,174 @@ const { dateToString, divideIntoPeriods } = require('./utils/dates'); -const { BankDataSource } = require('../BankDataSource'); - -const createSignature = (xml, password) => { - const regexp = /(?<=).*(?=<\/data)/; - const dataString = xml.match(regexp)[0]; - const md5Signature = crypto.createHash('md5') - .update(`${dataString}${password}`, 'utf8') - .digest('hex'); - const signature = crypto.createHash('sha1') - .update(md5Signature, 'utf8') - .digest('hex'); - return signature; -}; - -const fillXmlWithSignature = (xml, signature) => { - const xmlObj = xmlJs.xml2js(xml, { compact: true }); - - const xmlMerchant = xmlObj.request.merchant; - xmlMerchant.signature._text = signature; - - const xmlWithSignature = xmlJs.js2xml(xmlObj, { - spaces: 0, - compact: true, - fullTagEmptyElement: true - }); - return xmlWithSignature; -}; - -const handleBankResponse = (cb, resolve, reject, res) => { - let data = ''; - if (res.statusCode !== 200) { - const errMsg = `Server did not send data. STATUS CODE: ${res.statusCode}`; - reject(new Error(errMsg)); + +const BankDataSource = require('../BankDataSource'); + +class PrivatDataSource extends BankDataSource { + constructor() { + super(); } - res.setEncoding('utf8'); - res.on('data', chunk => { - data += chunk; - }); - res.on('end', () => { - cb(resolve, reject, data); - }); - res.on('error', err => reject(err)); -}; - -const handleTransactionsData = (resolve, reject, dataXml) => { - let dataObj; - try { - dataObj = xmlJs.xml2js(dataXml, { compact: true }); - } catch (err) { - reject(new Error(err)); - return; + + createSignature(xml, password) { + const regexp = /(?<=).*(?=<\/data)/; + const dataString = xml.match(regexp)[0]; + const md5Signature = crypto.createHash('md5') + .update(`${dataString}${password}`, 'utf8') + .digest('hex'); + const signature = crypto.createHash('sha1') + .update(md5Signature, 'utf8') + .digest('hex'); + return signature; } - if (dataObj.response.data.error !== null) { - const errorMessage = dataObj.response.data.error._attributes.message; - reject(new Error(errorMessage)); - return; + fillXmlWithSignature(xml, signature) { + const xmlObj = xmlJs.xml2js(xml, { compact: true }); + + const xmlMerchant = xmlObj.request.merchant; + xmlMerchant.signature._text = signature; + + const xmlWithSignature = xmlJs.js2xml(xmlObj, { + spaces: 0, + compact: true, + fullTagEmptyElement: true + }); + return xmlWithSignature; } - const result = []; - let transactions = dataObj.response.data.info.statements.statement; - if (!Array.isArray(transactions)) transactions = [transactions]; - transactions.map(val => result.push(val._attributes)); - resolve(result); -}; - -const handleBalanceData = (resolve, reject, dataXml) => { - let dataObj; - try { - dataObj = xmlJs.xml2js(dataXml, { compact: true }); - } catch (err) { - reject(new Error('Invalid XML')); - return; + handleBankResponse(cb, resolve, reject, res) { + let data = ''; + if (res.statusCode !== 200) { + const errMsg = `Server did not send data. STATUS CODE: ${res.statusCode}`; + reject(new Error(errMsg)); + } + res.setEncoding('utf8'); + res.on('data', chunk => { + data += chunk; + }); + res.on('end', () => { + cb(resolve, reject, data); + }); + res.on('error', err => reject(err)); } - if (dataObj.response.data.error !== null) { - const errorMessage = dataObj.response.data.error._attributes.message; - reject(new Error(errorMessage)); - return; + handleTransactionsData(resolve, reject, dataXml) { + let dataObj; + try { + dataObj = xmlJs.xml2js(dataXml, { compact: true }); + } catch (err) { + reject(new Error(err)); + return; + } + + if (dataObj.response.data.error !== null) { + const errorMessage = dataObj.response.data.error._attributes.message; + reject(new Error(errorMessage)); + return; + } + + const result = []; + let transactions = dataObj.response.data.info.statements.statement; + if (!Array.isArray(transactions)) transactions = [transactions]; + transactions.map(val => result.push(val._attributes)); + resolve(result); } - const cardBalance = dataObj.response.data.info.cardbalance; - const card = cardBalance.card; - - const result = { - cardNumber: card.card_number._text, - currency: card.currency._text, - balance: cardBalance.balance._text, - availableBalance: cardBalance.av_balance._text, - dateBalance: cardBalance.bal_date._text, - creditLimit: cardBalance.fin_limit._text - }; - resolve(result); -}; - -const getData = (dataForRequest, cb) => { - const path = cb.name === 'handleTransactionsData' ? '/p24api/rest_fiz' : - cb.name === 'handleBalanceData' ? '/p24api/balance' : - null; - const options = { - hostname: 'api.privatbank.ua', - path, - method: 'POST', - headers: - { 'Content-Type': 'application/xml; charset=UTF-8' } - }; - const req = http.request(options); - const xml = cb.name === 'handleTransactionsData' ? - fillTransactionsXml(dataForRequest) : - cb.name === 'handleBalanceData' ? fillBalanceXml(dataForRequest) : - null; - - const signature = createSignature(xml, dataForRequest.merchantPassword); - const xmlWithSignature = fillXmlWithSignature(xml, signature); - req.write(xmlWithSignature); - req.end(); - - return new Promise((resolve, reject) => { - req.on('response', handleBankResponse.bind(null, cb, resolve, reject)); - req.on('error', err => reject(err)); - }); -}; - -const getTransactionsData = async dataForTransactions => { - const startDate = stringToDate(dataForTransactions.startDate); - const endDate = stringToDate(dataForTransactions.endDate); - - const periods = divideIntoPeriods(startDate, endDate); - const promises = []; - - periods.forEach((period, index) => { - const copyDataForTransactions = Object.assign({}, dataForTransactions); - copyDataForTransactions.startDate = dateToString(period[0]); - copyDataForTransactions.endDate = dateToString(period[1]); - promises[index] = getData(copyDataForTransactions, handleTransactionsData); - }); - const transactions = await Promise.all(promises); - - let result = []; - for (const array of transactions) result = [...result, ...array.reverse()]; - - return result; -}; - -const getBalanceData = async dataForBalance => { - const result = []; - for (const data of dataForBalance) { - result.push(getData(data, handleBalanceData)); + + handleBalanceData(resolve, reject, dataXml) { + let dataObj; + try { + dataObj = xmlJs.xml2js(dataXml, { compact: true }); + } catch (err) { + reject(new Error('Invalid XML')); + return; + } + + if (dataObj.response.data.error !== null) { + const errorMessage = dataObj.response.data.error._attributes.message; + reject(new Error(errorMessage)); + return; + } + const cardBalance = dataObj.response.data.info.cardbalance; + const card = cardBalance.card; + + const result = { + cardNumber: card.card_number._text, + currency: card.currency._text, + balance: cardBalance.balance._text, + availableBalance: cardBalance.av_balance._text, + dateBalance: cardBalance.bal_date._text, + creditLimit: cardBalance.fin_limit._text + }; + resolve(result); } - return Promise.allSettled(Object.values(result)); -}; - -const dataForTransactionsRequest = { // sample data for transactions request - merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', - merchantId: 123456, - wait: 10, - test: 0, - paymentId: '', - startDate: '11.05.2020', - endDate: '23.10.2020', - cardNumber: '1234567890123456' -}; - -const dataForBalanceRequest = { // sample data for balance request - merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', - merchantId: 123456, - wait: 10, - test: 0, - paymentId: '', - cardNumber: '1234567890123456', - country: 'UA' -}; - -// example of using -(async () => { - const tranData = await getTransactionsData(dataForTransactionsRequest); - const balanceData = await getBalanceData([dataForBalanceRequest]); - // dataForBalanceRequest is array of data objects - console.log(tranData); - console.log(balanceData); -})(); - -// just to implement the interface -class PrivatDataSource extends BankDataSource { - constructor() { - super(); + + getData(dataForRequest, cb, path, xml) { + const options = { + hostname: 'api.privatbank.ua', + path, + method: 'POST', + headers: + { 'Content-Type': 'application/xml; charset=UTF-8' } + }; + const req = http.request(options); + + const password = dataForRequest.merchantPassword; + const signature = this.createSignature(xml, password); + const xmlWithSignature = this.fillXmlWithSignature(xml, signature); + req.write(xmlWithSignature); + req.end(); + + return new Promise((resolve, reject) => { + const handler = this.handleBankResponse.bind(this, cb, resolve, reject); + req.on('response', handler); + req.on('error', err => reject(err)); + }); + } + + async getTransactionsData(dataForTransactions) { + const startDate = stringToDate(dataForTransactions.startDate); + const endDate = stringToDate(dataForTransactions.endDate); + + const periods = divideIntoPeriods(startDate, endDate); + const promises = []; + + const path = '/p24api/rest_fiz'; + const xml = fillTransactionsXml(dataForTransactions); + const cb = this.handleTransactionsData.bind(this); + + periods.forEach(period => { + const copyDataForTransactions = Object.assign({}, dataForTransactions); + copyDataForTransactions.startDate = dateToString(period[0]); + copyDataForTransactions.endDate = dateToString(period[1]); + promises.push(this.getData(copyDataForTransactions, cb, path, xml)); + }); + const transactions = await Promise.all(promises); + + const result = []; + transactions.forEach(arr => result.push(...arr.reverse())); + + return result; } + + async getBalanceData(dataForBalance) { + const result = []; + const path = '/p24api/balance'; + const xml = fillBalanceXml(dataForBalance); + const cb = this.handleBalanceData.bind(this); + for (const data of dataForBalance) { + result.push(this.getData(data, cb, path, xml)); + } + return Promise.all(result); + } + // conf is similar to dataForTransactionsRequest async getTransactions(card, conf) { conf.cardNumber = card.cardNum + ''; - return await getTransactionsData(conf); + return this.getTransactionsData(conf); } - // additional method for getting balances of cards array - // conf is array of objects similar to dataForBalanceRequest + /* additional method for getting balances of cards array + conf is array of objects similar to dataForBalanceRequest */ async getBalance(conf) { - return await getBalanceData(conf); + return this.getBalanceData(conf); } } diff --git a/src/datasources/privat24/privat24_usage.js b/src/datasources/privat24/privat24_usage.js new file mode 100644 index 0000000..48443fa --- /dev/null +++ b/src/datasources/privat24/privat24_usage.js @@ -0,0 +1,36 @@ +'use strict'; + +const PrivatDataSource = require('./privat24.js'); + +// sample data for transactions request +const dataForTransactionsRequest = { + merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', + merchantId: 123456, + wait: 10, + test: 0, + paymentId: '', + startDate: '11.05.2020', + endDate: '23.10.2020', + cardNumber: '1234567890123456' +}; + +// sample data for balance request +const dataForBalanceRequest = { + merchantPassword: '55x3Ft9C96yx7s1cAMO2KVn1apuDA0X6', + merchantId: 123456, + wait: 10, + test: 0, + paymentId: '', + cardNumber: '1234567890123456', + country: 'UA' +}; + +// example of using +(async () => { + const pds = new PrivatDataSource(); + const tranData = await pds.getTransactionsData(dataForTransactionsRequest); + const balanceData = await pds.getBalanceData([dataForBalanceRequest]); + // dataForBalanceRequest is array of data objects + console.log(tranData); + console.log(balanceData); +})(); diff --git a/src/datasources/privat24/utils/balance.js b/src/datasources/privat24/utils/balance.js index 6e58a5a..66b1d8c 100644 --- a/src/datasources/privat24/utils/balance.js +++ b/src/datasources/privat24/utils/balance.js @@ -1,3 +1,5 @@ +'use strict'; + const fs = require('fs'); const xmlJs = require('xml-js'); const balanceXML = fs.readFileSync('./xml_data/balance.xml', 'utf-8'); diff --git a/src/datasources/privat24/utils/dates.js b/src/datasources/privat24/utils/dates.js index 59a93d7..9f93095 100644 --- a/src/datasources/privat24/utils/dates.js +++ b/src/datasources/privat24/utils/dates.js @@ -1,3 +1,5 @@ +'use strict'; + const stringToDate = date => { const [day, month, year] = date.split('.'); return new Date(`${year}-${month}-${day}`); diff --git a/src/datasources/privat24/utils/transactions.js b/src/datasources/privat24/utils/transactions.js index 1c50505..1474dce 100644 --- a/src/datasources/privat24/utils/transactions.js +++ b/src/datasources/privat24/utils/transactions.js @@ -1,3 +1,5 @@ +'use strict'; + const fs = require('fs'); const xmlJs = require('xml-js'); const transactionsXML = fs.readFileSync('./xml_data/transactions.xml', 'utf-8'); diff --git a/src/model/Card.js b/src/model/Card.js index f937328..75a18b1 100644 --- a/src/model/Card.js +++ b/src/model/Card.js @@ -1,8 +1,10 @@ +'use strict'; + /** Represents bank card * * @property {number} cardId - Card's id in Lemon DB. * @property {string} bank - Name of the bank card belongs to - * @property {number} cardNum - Card number (must be full number) + * @property {number} cardNum - Card number (can be either all number or last 4 digits only) * @property {string} type - Card type in bank's system * @property {number} balance - Amount of money on card in lowest units * @property {string} currency - Currency of transactions from this cards */ diff --git a/src/model/Transaction.js b/src/model/Transaction.js index d3d6b84..7cfa0db 100644 --- a/src/model/Transaction.js +++ b/src/model/Transaction.js @@ -1,3 +1,5 @@ +'use strict'; + /** Represents transaction with card * * @property {number} cardId - Card's id in Lemon DB. diff --git a/src/model/User.js b/src/model/User.js index f1b8042..35155fe 100644 --- a/src/model/User.js +++ b/src/model/User.js @@ -1,3 +1,5 @@ +'use strict'; + /** @class User * * @property {string} name - User's name