diff --git a/bfx-report-ui b/bfx-report-ui index 4ca99819e..63c4ebd4a 160000 --- a/bfx-report-ui +++ b/bfx-report-ui @@ -1 +1 @@ -Subproject commit 4ca99819ef8a2b22b3bdd38961a84b7aea01b09a +Subproject commit 63c4ebd4a272186d78eab3893e2f039679b1ef2f diff --git a/package.json b/package.json index dd58cd1fb..ee3fb9021 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bfx-reports-framework", - "version": "4.0.0", + "version": "4.1.0", "description": "Bitfinex reports framework", "main": "worker.js", "license": "Apache-2.0", diff --git a/test/1-api-sync-mode-sqlite.spec.js b/test/1-api-sync-mode-sqlite.spec.js index 4cbbaea43..5b43858d2 100644 --- a/test/1-api-sync-mode-sqlite.spec.js +++ b/test/1-api-sync-mode-sqlite.spec.js @@ -27,7 +27,8 @@ const agent = request.agent(app) const { apiSyncModeSqliteTestCases, - signUpTestCase + signUpTestCase, + removeUserTestCases } = require('./test-cases') let wrkReportServiceApi = null @@ -94,4 +95,6 @@ describe('Sync mode API with SQLite', () => { signUpTestCase(agent, params) apiSyncModeSqliteTestCases(agent, params) + signUpTestCase(agent, params) + removeUserTestCases(agent, params) }) diff --git a/test/3-api-filter-sync-mode-sqlite.spec.js b/test/3-api-filter-sync-mode-sqlite.spec.js index da9841198..e55ae8e86 100644 --- a/test/3-api-filter-sync-mode-sqlite.spec.js +++ b/test/3-api-filter-sync-mode-sqlite.spec.js @@ -20,8 +20,7 @@ const { startEnvironment } = require('./helpers/helpers.boot') const { - emptyDB, - delay + emptyDB } = require('./helpers/helpers.core') const { createMockRESTv2SrvWithDate @@ -31,7 +30,10 @@ process.env.NODE_CONFIG_DIR = path.join(__dirname, 'config') const { app } = require('bfx-report-express') const agent = request.agent(app) -const { signUpTestCase } = require('./test-cases') +const { + signUpTestCase, + getSyncProgressTestCase +} = require('./test-cases') let wrkReportServiceApi = null let processorQueue = null @@ -120,35 +122,7 @@ describe('API filter', () => { ) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the editPublicTradesConf method', async function () { this.timeout(5000) @@ -175,35 +149,7 @@ describe('API filter', () => { assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the editTickersHistoryConf method', async function () { this.timeout(5000) @@ -230,35 +176,7 @@ describe('API filter', () => { assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the api methods', async function () { this.timeout(10000) diff --git a/test/4-sub-account.spec.js b/test/4-sub-account.spec.js index 379be2189..cc9508b7c 100644 --- a/test/4-sub-account.spec.js +++ b/test/4-sub-account.spec.js @@ -32,7 +32,8 @@ const agent = request.agent(app) const { apiSyncModeSqliteTestCases, additionalApiSyncModeSqliteTestCases, - signUpTestCase + signUpTestCase, + removeUserTestCases } = require('./test-cases') let wrkReportServiceApi = null @@ -429,5 +430,10 @@ describe('Sub-account', () => { additionalApiSyncModeSqliteTestCases(agent, params) }) + describe('Removing sub-account API', () => { + before(beforeFn) + + removeUserTestCases(agent, params) + }) }) }) diff --git a/test/6-update-sub-account.spec.js b/test/6-update-sub-account.spec.js index 30b9a40c4..e3ee1440c 100644 --- a/test/6-update-sub-account.spec.js +++ b/test/6-update-sub-account.spec.js @@ -17,8 +17,7 @@ const { } = require('./helpers/helpers.boot') const { emptyDB, - getRServiceProxy, - delay + getRServiceProxy } = require('./helpers/helpers.core') const { createMockRESTv2SrvWithDate, @@ -31,7 +30,8 @@ const { app } = require('bfx-report-express') const agent = request.agent(app) const { - signUpTestCase + signUpTestCase, + getSyncProgressTestCase } = require('./test-cases') let wrkReportServiceApi = null @@ -289,35 +289,7 @@ describe('Update sub-account', () => { assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth: subAccountAuth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth: subAccountAuth }) it('it should be successfully performed by the getLedgers method, without params', async function () { this.timeout(5000) @@ -517,35 +489,7 @@ describe('Update sub-account', () => { assert.isString(res.body.result.token) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth: subAccountAuth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth: subAccountAuth }) it('it should be successfully performed by the getUsers method', async function () { this.timeout(5000) @@ -587,35 +531,7 @@ describe('Update sub-account', () => { }) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth: subAccountAuth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth: subAccountAuth }) it('it should be successfully performed by the getLedgers method, without params', async function () { this.timeout(5000) diff --git a/test/test-cases/additional-api-sync-mode-sqlite-test-cases.js b/test/test-cases/additional-api-sync-mode-sqlite-test-cases.js index 6cbb6975b..8f8153d9d 100644 --- a/test/test-cases/additional-api-sync-mode-sqlite-test-cases.js +++ b/test/test-cases/additional-api-sync-mode-sqlite-test-cases.js @@ -10,13 +10,14 @@ const { } = require('bfx-report/test/helpers/helpers.tests') const { - delay, getParamsArrToTestTimeframeGrouping } = require('../helpers/helpers.core') const { testCsvPathHasCommonFolder } = require('../helpers/helpers.tests') +const getSyncProgressTestCase = require('./get-sync-progress-test-case') + module.exports = ( agent, params = {} @@ -123,35 +124,7 @@ module.exports = ( ) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the getBalanceHistory method', async function () { this.timeout(5000) @@ -581,6 +554,50 @@ module.exports = ( } }) + it('it should be successfully performed by the getWeightedAveragesReport method', async function () { + this.timeout(120000) + + const paramsArr = [ + { end, start }, + { + end, + start: end - (10 * 60 * 60 * 1000), + symbol: ['tBTCUSD'] + } + ] + + for (const params of paramsArr) { + const res = await agent + .post(`${basePath}/json-rpc`) + .type('json') + .send({ + auth, + method: 'getWeightedAveragesReport', + params, + id: 5 + }) + .expect('Content-Type', /json/) + .expect(200) + + assert.isObject(res.body) + assert.propertyVal(res.body, 'id', 5) + assert.isArray(res.body.result) + + const resItem = res.body.result[0] + + assert.isObject(resItem) + assert.containsAllKeys(resItem, [ + 'symbol', + 'buyingWeightedPrice', + 'buyingAmount', + 'sellingWeightedPrice', + 'sellingAmount', + 'cumulativeWeightedPrice', + 'cumulativeAmount' + ]) + } + }) + it('it should be successfully performed by the getMultipleCsv method', async function () { this.timeout(60000) @@ -966,4 +983,29 @@ module.exports = ( await testMethodOfGettingCsv(procPromise, aggrPromise, res) }) + + it('it should be successfully performed by the getWeightedAveragesReportCsv method', async function () { + this.timeout(60000) + + const procPromise = queueToPromise(params.processorQueue) + const aggrPromise = queueToPromise(params.aggregatorQueue) + + const res = await agent + .post(`${basePath}/json-rpc`) + .type('json') + .send({ + auth, + method: 'getWeightedAveragesReportCsv', + params: { + end, + start, + email + }, + id: 5 + }) + .expect('Content-Type', /json/) + .expect(200) + + await testMethodOfGettingCsv(procPromise, aggrPromise, res) + }) } diff --git a/test/test-cases/api-sync-mode-sqlite-test-cases.js b/test/test-cases/api-sync-mode-sqlite-test-cases.js index 5c561f4b2..e90ce6f35 100644 --- a/test/test-cases/api-sync-mode-sqlite-test-cases.js +++ b/test/test-cases/api-sync-mode-sqlite-test-cases.js @@ -11,7 +11,7 @@ const { testProcQueue } = require('bfx-report/test/helpers/helpers.tests') -const { delay } = require('../helpers/helpers.core') +const getSyncProgressTestCase = require('./get-sync-progress-test-case') const { getMockData } = require('../helpers/helpers.mock-rest-v2') module.exports = ( @@ -414,35 +414,7 @@ module.exports = ( assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the haveCollsBeenSyncedAtLeastOnce method, returns true', async function () { this.timeout(60000) @@ -488,35 +460,7 @@ module.exports = ( assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the editPublicTradesConf method', async function () { this.timeout(5000) @@ -548,35 +492,7 @@ module.exports = ( assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the getPublicTradesConf method', async function () { this.timeout(5000) @@ -630,35 +546,7 @@ module.exports = ( assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the editTickersHistoryConf method', async function () { this.timeout(5000) @@ -690,35 +578,7 @@ module.exports = ( assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the getTickersHistoryConf method', async function () { this.timeout(5000) @@ -774,35 +634,7 @@ module.exports = ( assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the getStatusMessagesConf method', async function () { this.timeout(5000) @@ -855,35 +687,7 @@ module.exports = ( assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the getCandlesConf method', async function () { this.timeout(5000) @@ -965,35 +769,7 @@ module.exports = ( assert.isOk(res.body.result) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the getAllPublicСollsСonfs method', async function () { this.timeout(5000) @@ -1116,35 +892,7 @@ module.exports = ( ) }) - it('it should be successfully performed by the getSyncProgress method', async function () { - this.timeout(60000) - - while (true) { - const res = await agent - .post(`${basePath}/json-rpc`) - .type('json') - .send({ - auth, - method: 'getSyncProgress', - id: 5 - }) - .expect('Content-Type', /json/) - .expect(200) - - assert.isObject(res.body) - assert.propertyVal(res.body, 'id', 5) - assert.isNumber(res.body.result) - - if ( - typeof res.body.result !== 'number' || - res.body.result === 100 - ) { - break - } - - await delay() - } - }) + getSyncProgressTestCase(agent, { basePath, auth }) it('it should be successfully performed by the isSyncModeWithDbData method', async function () { this.timeout(5000) @@ -3561,7 +3309,11 @@ module.exports = ( assert.isObject(res.body) assert.propertyVal(res.body, 'id', 5) - assert.isNotOk(res.body.result) + assert.isObject(res.body.result) + assert.isNotOk(res.body.result.progress) + assert.isNull(res.body.result.syncStartedAt) + assert.isNull(res.body.result.spentTime) + assert.isNull(res.body.result.leftTime) }) it('it should be successfully performed by the disableSyncMode method', async function () { @@ -3602,7 +3354,7 @@ module.exports = ( assert.isNotOk(res.body.result) }) - it('it should be successfully performed by the removeUser method', async function () { + it('it should be successfully performed by the removeUser method with token', async function () { this.timeout(5000) const res = await agent diff --git a/test/test-cases/get-sync-progress-test-case.js b/test/test-cases/get-sync-progress-test-case.js new file mode 100644 index 000000000..ff1b7566f --- /dev/null +++ b/test/test-cases/get-sync-progress-test-case.js @@ -0,0 +1,55 @@ +'use strict' + +const { assert } = require('chai') + +const { delay } = require('../helpers/helpers.core') + +module.exports = ( + agent, + params = {} +) => { + const { + basePath, + auth + } = params + + it('it should be successfully performed by the getSyncProgress method', async function () { + this.timeout(60000) + + while (true) { + const res = await agent + .post(`${basePath}/json-rpc`) + .type('json') + .send({ + auth, + method: 'getSyncProgress', + id: 5 + }) + .expect('Content-Type', /json/) + .expect(200) + + assert.isObject(res.body) + assert.propertyVal(res.body, 'id', 5) + assert.isObject(res.body.result) + assert.isNumber(res.body.result.progress) + assert.isNumber(res.body.result.syncStartedAt) + assert.isNumber(res.body.result.spentTime) + + if ( + !Number.isFinite(res.body.result.progress) || + res.body.result.progress <= 0 || + res.body.result.progress > 100 + ) { + assert.isNull(res.body.result.leftTime) + } else { + assert.isNumber(res.body.result.leftTime) + } + + if (res.body.result.progress === 100) { + break + } + + await delay() + } + }) +} diff --git a/test/test-cases/index.js b/test/test-cases/index.js index d76609ef1..dbbdce10f 100644 --- a/test/test-cases/index.js +++ b/test/test-cases/index.js @@ -7,9 +7,13 @@ const additionalApiSyncModeSqliteTestCases = require( './additional-api-sync-mode-sqlite-test-cases' ) const signUpTestCase = require('./sign-up-test-case') +const getSyncProgressTestCase = require('./get-sync-progress-test-case') +const removeUserTestCases = require('./remove-user-test-cases') module.exports = { apiSyncModeSqliteTestCases, additionalApiSyncModeSqliteTestCases, - signUpTestCase + signUpTestCase, + getSyncProgressTestCase, + removeUserTestCases } diff --git a/test/test-cases/remove-user-test-cases.js b/test/test-cases/remove-user-test-cases.js new file mode 100644 index 000000000..dea3107e0 --- /dev/null +++ b/test/test-cases/remove-user-test-cases.js @@ -0,0 +1,91 @@ +'use strict' + +const { assert } = require('chai') + +module.exports = ( + agent, + params = {} +) => { + const { + basePath, + auth: { + email, + password, + isSubAccount + } + } = params + const auth = { token: '' } + + it('it should be successfully performed by the signIn method', async function () { + this.timeout(5000) + + const res = await agent + .post(`${basePath}/json-rpc`) + .type('json') + .send({ + auth: { + email, + password, + isSubAccount + }, + method: 'signIn', + id: 5 + }) + .expect('Content-Type', /json/) + .expect(200) + + assert.isObject(res.body) + assert.propertyVal(res.body, 'id', 5) + assert.isObject(res.body.result) + assert.strictEqual(res.body.result.email, email) + assert.strictEqual(res.body.result.isSubAccount, isSubAccount) + assert.isString(res.body.result.token) + + auth.token = res.body.result.token + }) + + it('it should be successfully performed by the removeUser method with email', async function () { + this.timeout(5000) + + const res = await agent + .post(`${basePath}/json-rpc`) + .type('json') + .send({ + auth: { + email, + password, + isSubAccount + }, + method: 'removeUser', + id: 5 + }) + .expect('Content-Type', /json/) + .expect(200) + + assert.isObject(res.body) + assert.propertyVal(res.body, 'id', 5) + assert.isBoolean(res.body.result) + assert.isOk(res.body.result) + }) + + it('it should not be successfully performed by the verifyUser method', async function () { + this.timeout(5000) + + const res = await agent + .post(`${basePath}/json-rpc`) + .type('json') + .send({ + auth, + method: 'verifyUser', + id: 5 + }) + .expect('Content-Type', /json/) + .expect(401) + + assert.isObject(res.body) + assert.isObject(res.body.error) + assert.propertyVal(res.body.error, 'code', 401) + assert.propertyVal(res.body.error, 'message', 'Unauthorized') + assert.propertyVal(res.body, 'id', 5) + }) +} diff --git a/workers/loc.api/di/app.deps.js b/workers/loc.api/di/app.deps.js index 0517e2d87..eebec41dd 100644 --- a/workers/loc.api/di/app.deps.js +++ b/workers/loc.api/di/app.deps.js @@ -90,9 +90,11 @@ const CurrencyConverter = require('../sync/currency.converter') const CsvJobData = require('../generate-csv/csv.job.data') const { fullSnapshotReportCsvWriter, - fullTaxReportCsvWriter + fullTaxReportCsvWriter, + weightedAveragesReportCsvWriter } = require('../generate-csv/csv-writer') const FullTaxReport = require('../sync/full.tax.report') +const WeightedAveragesReport = require('../sync/weighted.averages.report') const SqliteDbMigrator = require( '../sync/dao/db-migrations/sqlite.db.migrator' ) @@ -152,6 +154,7 @@ module.exports = ({ ['_syncCollsManager', TYPES.SyncCollsManager], ['_dataConsistencyChecker', TYPES.DataConsistencyChecker], ['_winLossVSAccountBalance', TYPES.WinLossVSAccountBalance], + ['_weightedAveragesReport', TYPES.WeightedAveragesReport], ['_getDataFromApi', TYPES.GetDataFromApi] ] }) @@ -358,8 +361,20 @@ module.exports = ({ ] ) ) + bind(TYPES.WeightedAveragesReportCsvWriter) + .toConstantValue( + bindDepsToFn( + weightedAveragesReportCsvWriter, + [ + TYPES.RService, + TYPES.GetDataFromApi + ] + ) + ) bind(TYPES.FullTaxReport) .to(FullTaxReport) + bind(TYPES.WeightedAveragesReport) + .to(WeightedAveragesReport) rebind(TYPES.CsvJobData) .to(CsvJobData) .inSingletonScope() diff --git a/workers/loc.api/di/types.js b/workers/loc.api/di/types.js index 06eefc171..b970ec5ba 100644 --- a/workers/loc.api/di/types.js +++ b/workers/loc.api/di/types.js @@ -66,5 +66,7 @@ module.exports = { SyncTempTablesManager: Symbol.for('SyncTempTablesManager'), SyncUserStepManager: Symbol.for('SyncUserStepManager'), SyncUserStepData: Symbol.for('SyncUserStepData'), - SyncUserStepDataFactory: Symbol.for('SyncUserStepDataFactory') + SyncUserStepDataFactory: Symbol.for('SyncUserStepDataFactory'), + WeightedAveragesReport: Symbol.for('WeightedAveragesReport'), + WeightedAveragesReportCsvWriter: Symbol.for('WeightedAveragesReportCsvWriter') } diff --git a/workers/loc.api/generate-csv/csv-writer/index.js b/workers/loc.api/generate-csv/csv-writer/index.js index 1ef83bf4f..f9a8660f5 100644 --- a/workers/loc.api/generate-csv/csv-writer/index.js +++ b/workers/loc.api/generate-csv/csv-writer/index.js @@ -6,8 +6,12 @@ const fullSnapshotReportCsvWriter = require( const fullTaxReportCsvWriter = require( './full-tax-report-csv-writer' ) +const weightedAveragesReportCsvWriter = require( + './weighted-averages-report-csv-writer' +) module.exports = { fullSnapshotReportCsvWriter, - fullTaxReportCsvWriter + fullTaxReportCsvWriter, + weightedAveragesReportCsvWriter } diff --git a/workers/loc.api/generate-csv/csv-writer/weighted-averages-report-csv-writer.js b/workers/loc.api/generate-csv/csv-writer/weighted-averages-report-csv-writer.js new file mode 100644 index 000000000..e620ee95c --- /dev/null +++ b/workers/loc.api/generate-csv/csv-writer/weighted-averages-report-csv-writer.js @@ -0,0 +1,77 @@ +'use strict' + +const { pipeline } = require('stream') +const { stringify } = require('csv') + +const { + write +} = require('bfx-report/workers/loc.api/queue/write-data-to-stream/helpers') + +const nope = () => {} + +module.exports = ( + rService, + getDataFromApi +) => async ( + wStream, + jobData +) => { + const queue = rService.ctx.lokue_aggregator.q + const { + args, + columnsCsv, + formatSettings, + name + } = jobData ?? {} + const { params } = args ?? {} + + queue.emit('progress', 0) + + if (typeof jobData === 'string') { + const stringifier = stringify( + { columns: ['mess'] } + ) + + pipeline(stringifier, wStream, nope) + write([{ mess: jobData }], stringifier) + queue.emit('progress', 100) + stringifier.end() + + return + } + + wStream.setMaxListeners(50) + + const headerStringifier = stringify( + { columns: ['empty', 'buy', 'empty', 'sell', 'empty', 'cumulative', 'empty'] } + ) + const resStringifier = stringify({ + header: true, + columns: columnsCsv + }) + + pipeline(headerStringifier, wStream, nope) + pipeline(resStringifier, wStream, nope) + + const res = await getDataFromApi({ + getData: rService[name].bind(rService), + args, + callerName: 'CSV_WRITER' + }) + + write( + [{ empty: '', buy: 'Buy', sell: 'Sell', cumulative: 'Cumulative' }], + headerStringifier + ) + write( + res, + resStringifier, + formatSettings, + params + ) + + queue.emit('progress', 100) + + headerStringifier.end() + resStringifier.end() +} diff --git a/workers/loc.api/generate-csv/csv.job.data.js b/workers/loc.api/generate-csv/csv.job.data.js index c769bb234..a4128fd1f 100644 --- a/workers/loc.api/generate-csv/csv.job.data.js +++ b/workers/loc.api/generate-csv/csv.job.data.js @@ -20,18 +20,21 @@ const { decorateInjectable } = require('../di/utils') const depsTypes = (TYPES) => [ TYPES.RService, TYPES.FullSnapshotReportCsvWriter, - TYPES.FullTaxReportCsvWriter + TYPES.FullTaxReportCsvWriter, + TYPES.WeightedAveragesReportCsvWriter ] class CsvJobData extends BaseCsvJobData { constructor ( rService, fullSnapshotReportCsvWriter, - fullTaxReportCsvWriter + fullTaxReportCsvWriter, + weightedAveragesReportCsvWriter ) { super(rService) this.fullSnapshotReportCsvWriter = fullSnapshotReportCsvWriter this.fullTaxReportCsvWriter = fullTaxReportCsvWriter + this.weightedAveragesReportCsvWriter = weightedAveragesReportCsvWriter } _addColumnsBySchema (columnsCsv = {}, schema = {}) { @@ -667,6 +670,48 @@ class CsvJobData extends BaseCsvJobData { return jobData } + + async getWeightedAveragesReportCsvJobData ( + args, + uId, + uInfo + ) { + checkParams(args, 'paramsSchemaForWeightedAveragesReportApiCsv') + + const { + userId, + userInfo + } = await checkJobAndGetUserData( + this.rService, + uId, + uInfo + ) + + const csvArgs = getCsvArgs(args) + + const jobData = { + userInfo, + userId, + name: 'getWeightedAveragesReport', + fileNamesMap: [['getWeightedAveragesReport', 'weighted-averages-report']], + args: csvArgs, + columnsCsv: { + symbol: 'PAIR', + buyingWeightedPrice: 'WEIGHTED PRICE', + buyingAmount: 'AMOUNT', + sellingWeightedPrice: 'WEIGHTED PRICE', + sellingAmount: 'AMOUNT', + cumulativeWeightedPrice: 'WEIGHTED PRICE', + cumulativeAmount: 'AMOUNT' + }, + formatSettings: { + symbol: 'symbol' + }, + csvCustomWriter: this.weightedAveragesReportCsvWriter + } + + return jobData + } } decorateInjectable(CsvJobData, depsTypes) diff --git a/workers/loc.api/helpers/schema.js b/workers/loc.api/helpers/schema.js index 6fbb2d89e..03142f22e 100644 --- a/workers/loc.api/helpers/schema.js +++ b/workers/loc.api/helpers/schema.js @@ -259,6 +259,21 @@ const paramsSchemaForWinLossVSAccountBalanceApi = { } } +const paramsSchemaForWeightedAveragesReportApi = { + type: 'object', + properties: { + start: { + type: 'integer' + }, + end: { + type: 'integer' + }, + symbol: { + type: ['string', 'array'] + } + } +} + const paramsSchemaForTradedVolumeApi = { type: 'object', properties: { @@ -369,6 +384,15 @@ const paramsSchemaForWinLossVSAccountBalanceCsv = { } } +const paramsSchemaForWeightedAveragesReportApiCsv = { + type: 'object', + properties: { + ...cloneDeep(paramsSchemaForWeightedAveragesReportApi.properties), + timezone, + dateFormat + } +} + const paramsSchemaForPositionsSnapshotCsv = { type: 'object', properties: { @@ -445,6 +469,7 @@ module.exports = { paramsSchemaForBalanceHistoryApi, paramsSchemaForWinLossApi, paramsSchemaForWinLossVSAccountBalanceApi, + paramsSchemaForWeightedAveragesReportApi, paramsSchemaForPositionsSnapshotApi, paramsSchemaForFullSnapshotReportApi, paramsSchemaForFullTaxReportApi, @@ -455,6 +480,7 @@ module.exports = { paramsSchemaForBalanceHistoryCsv, paramsSchemaForWinLossCsv, paramsSchemaForWinLossVSAccountBalanceCsv, + paramsSchemaForWeightedAveragesReportApiCsv, paramsSchemaForPositionsSnapshotCsv, paramsSchemaForFullSnapshotReportCsv, paramsSchemaForFullTaxReportCsv, diff --git a/workers/loc.api/service.report.framework.js b/workers/loc.api/service.report.framework.js index 72499745a..fa863ee28 100644 --- a/workers/loc.api/service.report.framework.js +++ b/workers/loc.api/service.report.framework.js @@ -331,7 +331,12 @@ class FrameworkReportService extends ReportService { isSchedulerEnabled ) ? this._progress.getProgress() - : false + : { + progress: false, + syncStartedAt: null, + spentTime: null, + leftTime: null + } }, 'getSyncProgress', args, cb) } @@ -1317,6 +1322,15 @@ class FrameworkReportService extends ReportService { }, 'getWinLossVSAccountBalance', args, cb) } + getWeightedAveragesReport (space, args, cb) { + return this._privResponder(async () => { + checkParams(args, 'paramsSchemaForWeightedAveragesReportApi') + + return this._weightedAveragesReport + .getWeightedAveragesReport(args) + }, 'getWeightedAveragesReport', args, cb) + } + /** * @override */ @@ -1421,6 +1435,15 @@ class FrameworkReportService extends ReportService { ) }, 'getWinLossVSAccountBalanceCsv', args, cb) } + + getWeightedAveragesReportCsv (space, args, cb) { + return this._responder(() => { + return this._generateCsv( + 'getWeightedAveragesReportCsvJobData', + args + ) + }, 'getWeightedAveragesReportCsv', args, cb) + } } module.exports = FrameworkReportService diff --git a/workers/loc.api/sync/authenticator/index.js b/workers/loc.api/sync/authenticator/index.js index f5f943d07..67e6f0dd0 100644 --- a/workers/loc.api/sync/authenticator/index.js +++ b/workers/loc.api/sync/authenticator/index.js @@ -52,6 +52,7 @@ class Authenticator { * It may only work for one grenache worker instance */ this.userSessions = new Map() + this.userTokenMapByEmail = new Map() } async signUp (args, opts) { @@ -302,7 +303,7 @@ class Authenticator { typeof token === 'string' ) ? token - : this.getUserSessionByEmail({ email, isSubAccount }).token + : this.getUserSessionByEmail({ email, isSubAccount })?.token const createdToken = ( existedToken && typeof existedToken === 'string' @@ -361,7 +362,11 @@ class Authenticator { throw new AuthError() } - this.removeUserSessionByToken(token) + this.removeUserSession({ + email, + isSubAccount, + token + }) if (isSchedulerEnabled) { await this.dao.updateRecordOf( @@ -510,7 +515,7 @@ class Authenticator { const existedToken = this.getUserSessionByEmail( { email, isSubAccount } - ).token + )?.token const createdToken = ( existedToken && typeof existedToken === 'string' @@ -602,7 +607,7 @@ class Authenticator { token, isReturnedPassword ) - const { apiKey, apiSecret } = { ...session } + const { apiKey, apiSecret } = session ?? {} if ( !apiKey || @@ -830,15 +835,21 @@ class Authenticator { throw new UserRemovingError() } - this.removeUserSessionByToken(token) + this.removeUserSession({ + email, + isSubAccount, + token + }) return true } setUserSession (user) { const { token } = user ?? {} + const tokenKey = this._getTokenKeyByEmailField(user) this.userSessions.set(token, { ...user }) + this.userTokenMapByEmail.set(tokenKey, token) } getUserSessionByToken (token, isReturnedPassword) { @@ -847,18 +858,10 @@ class Authenticator { return pickSessionProps(session, isReturnedPassword) } - getUserSessionByEmail (args, isReturnedPassword) { - const { - email, - isSubAccount = false - } = args ?? {} - const keyVal = [...this.userSessions].find(([, session]) => { - return ( - email === session.email && - isSubAccount === session.isSubAccount - ) - }) - const session = Array.isArray(keyVal) ? keyVal[1] : {} + getUserSessionByEmail (user, isReturnedPassword) { + const tokenKey = this._getTokenKeyByEmailField(user) + const token = this.userTokenMapByEmail.get(tokenKey) + const session = this.userSessions.get(token) return pickSessionProps(session, isReturnedPassword) } @@ -873,8 +876,19 @@ class Authenticator { return new Map(sessionsMap) } - removeUserSessionByToken (token) { - return this.userSessions.delete(token) + removeUserSession (user) { + const { token } = user ?? {} + const tokenKey = this._getTokenKeyByEmailField(user) + const _token = ( + token && + typeof token === 'string' + ) + ? token + : this.userTokenMapByEmail.get(tokenKey) + + this.userTokenMapByEmail.delete(tokenKey) + + return this.userSessions.delete(_token) } async decryptApiKeys (password, users) { @@ -905,6 +919,18 @@ class Authenticator { return isArray ? res : res[0] } + + _getTokenKeyByEmailField (user) { + const { + email, + isSubAccount + } = user ?? {} + const suffix = isSubAccount + ? ':sub-account' + : '' + + return `${email}${suffix}` + } } decorateInjectable(Authenticator, depsTypes) diff --git a/workers/loc.api/sync/data.consistency.checker/index.js b/workers/loc.api/sync/data.consistency.checker/index.js index fedbb2529..d74f15b1f 100644 --- a/workers/loc.api/sync/data.consistency.checker/index.js +++ b/workers/loc.api/sync/data.consistency.checker/index.js @@ -40,7 +40,9 @@ class DataConsistencyChecker { const isValid = await check(auth) if (!isValid) { - const currProgress = await this.progress.getProgress() + const { + progress: currProgress + } = await this.progress.getProgress() const isDBSyncing = currProgress < 100 if (isDBSyncing) { diff --git a/workers/loc.api/sync/index.js b/workers/loc.api/sync/index.js index b4b4e3ed8..4cfc9642d 100644 --- a/workers/loc.api/sync/index.js +++ b/workers/loc.api/sync/index.js @@ -37,8 +37,11 @@ class Sync { let progressForInterrupter = this.syncInterrupter .INITIAL_PROGRESS + this.progress.deactivateSyncTimeEstimate() + if (!error) { try { + this.progress.activateSyncTimeEstimate() progressForInterrupter = await this.syncQueue.process(params) } catch (err) { errorForInterrupter = err @@ -64,7 +67,9 @@ class Sync { ) } - const currProgress = await this.progress.getProgress() + const { + progress: currProgress + } = await this.progress.getProgress() if (this.syncInterrupter.hasInterrupted()) { this.syncInterrupter.emitInterrupted( @@ -99,7 +104,9 @@ class Sync { } const isEnable = await this.rService.isSchedulerEnabled() - const currProgress = await this.progress.getProgress() + const { + progress: currProgress + } = await this.progress.getProgress() if (isEnable) { await this.syncQueue.add({ @@ -112,7 +119,7 @@ class Sync { (currProgress < 100) || !isEnable ) { - return this.progress.getProgress() + return (await this.progress.getProgress())?.progress } await this.rService.pingApi() @@ -142,7 +149,11 @@ class Sync { } async stop () { - const currProgress = await this.progress.getProgress() + const { + progress: currProgress + } = await this.progress + .deactivateSyncTimeEstimate() + .getProgress() if (currProgress < 100) { return this.syncInterrupter.interrupt() diff --git a/workers/loc.api/sync/progress/index.js b/workers/loc.api/sync/progress/index.js index d4323bf94..6503c5deb 100644 --- a/workers/loc.api/sync/progress/index.js +++ b/workers/loc.api/sync/progress/index.js @@ -1,7 +1,6 @@ 'use strict' const EventEmitter = require('events') -const { isEmpty } = require('lodash') const { tryParseJSON @@ -28,6 +27,8 @@ class Progress extends EventEmitter { this.TABLES_NAMES = TABLES_NAMES this.wsEventEmitter = wsEventEmitter this.logger = logger + + this._syncStartedAt = null } async setProgress (progress) { @@ -44,8 +45,12 @@ class Progress extends EventEmitter { this.TABLES_NAMES.PROGRESS, { value: JSON.stringify(_progress) } ) - this.emit(_progress) - await this.wsEventEmitter.emitProgress(() => _progress) + const estimatedSyncTime = this._estimateSyncTime({ + progress: _progress + }) + + this.emit(estimatedSyncTime) + await this.wsEventEmitter.emitProgress(() => estimatedSyncTime) } catch (e) { this.logger.error( `PROGRESS:SYNC:SET: ${e.stack || e}` @@ -60,15 +65,76 @@ class Progress extends EventEmitter { } async getProgress () { - const progress = await this.dao + const progressObj = await this.dao .getElemInCollBy(this.TABLES_NAMES.PROGRESS) - return ( - !isEmpty(progress) && - typeof progress.value === 'string' - ) - ? tryParseJSON(progress.value, true) + const progress = typeof progressObj?.value === 'string' + ? tryParseJSON(progressObj.value, true) : 'SYNCHRONIZATION_HAS_NOT_STARTED_YET' + const estimatedSyncTime = this._estimateSyncTime({ progress }) + + return estimatedSyncTime + } + + activateSyncTimeEstimate () { + this._syncStartedAt = Date.now() + + return this + } + + deactivateSyncTimeEstimate () { + this._syncStartedAt = null + + return this + } + + async _estimateSyncTime (params) { + const { + progress + } = params ?? {} + + const syncStartedAt = this._getSyncStartedAt() + const nowMts = Date.now() + + if ( + !Number.isFinite(syncStartedAt) || + syncStartedAt > nowMts + ) { + return { + progress, + syncStartedAt: null, + spentTime: null, + leftTime: null + } + } + + const spentTime = nowMts - syncStartedAt + + if ( + !Number.isFinite(progress) || + progress <= 0 || + progress > 100 + ) { + return { + progress, + syncStartedAt, + spentTime, + leftTime: null + } + } + + const leftTime = (spentTime / progress) * (100 - progress) + + return { + progress, + syncStartedAt, + spentTime, + leftTime + } + } + + _getSyncStartedAt () { + return this._syncStartedAt ?? null } } diff --git a/workers/loc.api/sync/weighted.averages.report/index.js b/workers/loc.api/sync/weighted.averages.report/index.js new file mode 100644 index 000000000..fa99fb806 --- /dev/null +++ b/workers/loc.api/sync/weighted.averages.report/index.js @@ -0,0 +1,196 @@ +'use strict' + +const { decorateInjectable } = require('../../di/utils') + +const depsTypes = (TYPES) => [ + TYPES.DAO, + TYPES.Authenticator, + TYPES.SyncSchema, + TYPES.ALLOWED_COLLS +] +class WeightedAveragesReport { + constructor ( + dao, + authenticator, + syncSchema, + ALLOWED_COLLS + ) { + this.dao = dao + this.authenticator = authenticator + this.syncSchema = syncSchema + this.ALLOWED_COLLS = ALLOWED_COLLS + + this.tradesModel = this.syncSchema.getModelsMap() + .get(this.ALLOWED_COLLS.TRADES) + } + + async getWeightedAveragesReport (args = {}) { + const { + auth = {}, + params = {} + } = args ?? {} + + const user = await this.authenticator + .verifyRequestUser({ auth }) + + const { + start = 0, + end = Date.now(), + symbol: _symbol = [] + } = params ?? {} + const symbolArr = Array.isArray(_symbol) + ? _symbol + : [_symbol] + const symbol = symbolArr.filter((s) => ( + s && typeof s === 'string' + )) + + const trades = await this._getTrades({ + user, + start, + end, + symbol + }) + const calcedTrades = this._calcTrades(trades) + + return calcedTrades + } + + async _getTrades (args) { + const { + user = {}, + start = 0, + end = Date.now(), + symbol = [] + } = args ?? {} + + const symbFilter = ( + Array.isArray(symbol) && + symbol.length !== 0 + ) + ? { $in: { symbol } } + : {} + + return this.dao.getElemsInCollBy( + this.ALLOWED_COLLS.TRADES, + { + filter: { + user_id: user._id, + $lte: { mtsCreate: end }, + $gte: { mtsCreate: start }, + ...symbFilter + }, + sort: [['mtsCreate', -1]], + projection: this.tradesModel, + exclude: ['user_id'], + isExcludePrivate: true + } + ) + } + + _calcTrades (trades = []) { + const symbResMap = new Map() + + for (const trade of trades) { + const { + symbol, + execAmount, + execPrice + } = trade ?? {} + + if ( + !symbol || + typeof symbol !== 'string' || + !Number.isFinite(execAmount) || + execAmount === 0 || + !Number.isFinite(execPrice) || + execPrice === 0 + ) { + continue + } + + const isBuying = execAmount > 0 + const spent = execAmount * execPrice + + const existedSymbRes = symbResMap.get(symbol) + const { + sumSpent: _sumSpent = 0, + sumAmount: _sumAmount = 0, + sumBuyingSpent: _sumBuyingSpent = 0, + sumBuyingAmount: _sumBuyingAmount = 0, + sumSellingSpent: _sumSellingSpent = 0, + sumSellingAmount: _sumSellingAmount = 0 + } = existedSymbRes ?? {} + + const sumSpent = Number.isFinite(spent) + ? _sumSpent + spent + : _sumSpent + const sumAmount = _sumAmount + execAmount + const sumBuyingSpent = ( + isBuying && + Number.isFinite(spent) + ) + ? _sumBuyingSpent + spent + : _sumBuyingSpent + const sumBuyingAmount = isBuying + ? _sumBuyingAmount + execAmount + : _sumBuyingAmount + const sumSellingSpent = ( + !isBuying && + Number.isFinite(spent) + ) + ? _sumSellingSpent + spent + : _sumSellingSpent + const sumSellingAmount = !isBuying + ? _sumSellingAmount + execAmount + : _sumSellingAmount + + symbResMap.set(symbol, { + sumSpent, + sumAmount, + sumBuyingSpent, + sumBuyingAmount, + sumSellingSpent, + sumSellingAmount, + + buyingWeightedPrice: sumBuyingAmount === 0 + ? 0 + : sumBuyingSpent / sumBuyingAmount, + buyingAmount: sumBuyingAmount, + sellingWeightedPrice: sumSellingAmount === 0 + ? 0 + : sumSellingSpent / sumSellingAmount, + sellingAmount: sumSellingAmount, + cumulativeWeightedPrice: sumAmount === 0 + ? 0 + : sumSpent / sumAmount, + cumulativeAmount: sumAmount + }) + } + + return [...symbResMap].map(([symbol, val]) => { + const { + buyingWeightedPrice = 0, + buyingAmount = 0, + sellingWeightedPrice = 0, + sellingAmount = 0, + cumulativeWeightedPrice = 0, + cumulativeAmount = 0 + } = val ?? {} + + return { + symbol, + buyingWeightedPrice, + buyingAmount, + sellingWeightedPrice, + sellingAmount, + cumulativeWeightedPrice, + cumulativeAmount + } + }) + } +} + +decorateInjectable(WeightedAveragesReport, depsTypes) + +module.exports = WeightedAveragesReport