Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\src\\main.js"
}
]
}
1 change: 0 additions & 1 deletion src/datasources/BankDataSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,3 @@ class BankDataSource {
}

module.exports = BankDataSource;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why delete newline?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why delete newline?

It was a mistake

183 changes: 182 additions & 1 deletion src/datasources/privat24/privat24.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,184 @@
'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 BankDataSource = require('../BankDataSource');

class PrivatDataSource extends BankDataSource {
constructor() {
super();
}

createSignature(xml, password) {
const regexp = /(?<=<data>).*(?=<\/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;
}

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;
}

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));
}

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);
}

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);
}

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 this.getTransactionsData(conf);
}
/* additional method for getting balances of cards array
conf is array of objects similar to dataForBalanceRequest */
async getBalance(conf) {
return this.getBalanceData(conf);
}
}

module.exports = PrivatDataSource;
36 changes: 36 additions & 0 deletions src/datasources/privat24/privat24_usage.js
Original file line number Diff line number Diff line change
@@ -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);
})();
32 changes: 32 additions & 0 deletions src/datasources/privat24/utils/balance.js
Original file line number Diff line number Diff line change
@@ -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 };
29 changes: 29 additions & 0 deletions src/datasources/privat24/utils/dates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const stringToDate = date => {
const [day, month, year] = date.split('.');
return new Date(`${year}-${month}-${day}`);
};

const dateToString = date =>
`${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 };
30 changes: 30 additions & 0 deletions src/datasources/privat24/utils/transactions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'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 };
16 changes: 16 additions & 0 deletions src/datasources/privat24/xml_data/balance.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<request version="1.0">
<merchant>
<id></id>
<signature></signature>
</merchant>
<data>
<oper>cmt</oper>
<wait>0</wait>
<test>0</test>
<payment id="">
<prop name="cardnum" value=""></prop>
<prop name="country" value=""></prop>
</payment>
</data>
</request>
17 changes: 17 additions & 0 deletions src/datasources/privat24/xml_data/transactions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<request version="1.0">
<merchant>
<id></id>
<signature></signature>
</merchant>
<data>
<oper>cmt</oper>
<wait></wait>
<test></test>
<payment id="">
<prop name="sd" value=""></prop>
<prop name="ed" value=""></prop>
<prop name="card" value=""></prop>
</payment>
</data>
</request>
3 changes: 2 additions & 1 deletion src/model/Transaction.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'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
* transaction. Amount is negative if money were transferred from card
* and positive in other case.
* @property {string} type - Description of transaction.
* This value is not determined by Lemon.
Expand Down