Skip to content

Commit

Permalink
feat: progress
Browse files Browse the repository at this point in the history
– Add account balance history endpoint
– Update Accounts model
– Update migrations
– Add part of e2e tests for Balances model
  • Loading branch information
letehaha committed Jul 14, 2023
1 parent 4899983 commit 0923862
Show file tree
Hide file tree
Showing 14 changed files with 677 additions and 225 deletions.
4 changes: 3 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import categoriesRoutes from './routes/categories.route';
import modelsCurrenciesRoutes from './routes/currencies.route';
import monobankRoutes from './routes/banks/monobank.route';
import binanceRoutes from './routes/crypto/binance.route';
import statsRoutes from './routes/stats.route';

import { supportedLocales } from './translations';

Expand Down Expand Up @@ -73,8 +74,9 @@ app.use(`${apiPrefix}/models/account-types`, modelsAccountTypesRoutes);
app.use(`${apiPrefix}/models/currencies`, modelsCurrenciesRoutes);
app.use(`${apiPrefix}/banks/monobank`, monobankRoutes);
app.use(`${apiPrefix}/crypto/binance`, binanceRoutes);
app.use(`${apiPrefix}/stats`, statsRoutes);

export const serverInstance = app.listen(app.get('port'), () => {
export const serverInstance = app.listen(process.env.NODE_ENV === 'test' ? 0 : app.get('port'), () => {
// eslint-disable-next-line no-console
// eslint-disable-next-line no-undef
logger.info(`[OK] Server is running on localhost:${app.get('port')}`);
Expand Down
20 changes: 20 additions & 0 deletions src/controllers/stats.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { API_RESPONSE_STATUS } from 'shared-types';
import { CustomResponse } from '@common/types';
import * as statsService from '@services/stats';
import { errorHandler } from './helpers';

export const getAccountBalanceHistory = async (req, res: CustomResponse) => {
const { id } = req.user;
const { id: accountId } = req.params;

try {
const balanceHistory = await statsService.getAccountBalanceHistory({ userId: id, accountId });

return res.status(200).json({
status: API_RESPONSE_STATUS.success,
response: balanceHistory,
});
} catch (err) {
errorHandler(res, err);
}
};
20 changes: 0 additions & 20 deletions src/migrations/1609339516073-create-users.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,6 @@ module.exports = {
allowNull: true,
},
});

if (process.env.NODE_ENV === 'test') {
await queryInterface.bulkInsert('Users', [
{
username: 'test1',
password: bcrypt.hashSync('test1', salt),
},
{
username: 'test2',
password: bcrypt.hashSync('test2', salt),
},
]);
console.log(`Inserted users:
username: test1;
password: test1;
&
username: test2;
password: test2
`)
}
},
down: async (queryInterface) => {
await queryInterface.dropTable('Users');
Expand Down
143 changes: 143 additions & 0 deletions src/migrations/1689160385756-balances-history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
'use strict';

/**
* Creates Balances table, and populates it with a history of balances based on existing transactions and accounts.
*
* - Retrieves all transactions and accounts from the database.
* - Iterates through each transaction and calculates the balance incrementally for each account.
* - Inserts a new entry into the Balances table for each transaction, including the date, account ID, and calculated balance.
* - Handles accounts without transactions and inserts entries with a balance of 0 in the Balances table.
*/

module.exports = {
up: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction();

try {
await queryInterface.addColumn(
'Accounts',
'initialBalance',
{
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0,
},
{ transaction },
);

// Create Balances table
await queryInterface.createTable('Balances', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
date: {
allowNull: false,
type: Sequelize.DATEONLY,
},
amount: {
allowNull: false,
type: Sequelize.INTEGER,
},
accountId: {
allowNull: false,
type: Sequelize.INTEGER,
references: {
model: 'Accounts',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
createdAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now'),
},
updatedAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.fn('now'),
},
}, { transaction });

// TODO: Improve migration
// // Retrieve all transactions ordered by date in ascending order
// const transactions = await queryInterface.sequelize.query(
// 'SELECT * FROM "Transactions" ORDER BY "time" ASC',
// { type: Sequelize.QueryTypes.SELECT },
// );

// // Map to track the current balance for each account on a specific date
// const accountBalances = new Map();
// // Map to track what accounts are linked to transactions, to later find
// // unlinked accounts
// const accountIdsUsedForTransactions = new Set();

// // Loop through each transaction
// for (const transaction of transactions) {
// const { accountId, amount } = transaction;

// // If accountId is not linked, ignore and go to next item
// if (!accountId) continue

// accountIdsUsedForTransactions.add(accountId);
// const transactionDate = transaction.time.toISOString().split('T')[0];

// // Get the current balance for the account on the specific date or default to 0 if not found
// const currentBalance = accountBalances.get(`${accountId}-${transactionDate}`) || 0;

// // Calculate the new balance by accumulating the transaction amount for the specific date
// const newBalance = currentBalance + amount;

// // Update the current balance for the account on the specific date in the map
// accountBalances.set(`${accountId}-${transactionDate}`, newBalance);
// }

// // Retrieve all distinct account IDs from Transactions table
// const accountsData = await queryInterface.sequelize.query(
// 'SELECT "id", "currentBalance" FROM "Accounts" GROUP BY "id"',
// { type: Sequelize.QueryTypes.SELECT }
// );

// // Loop through all existing accounts
// for (const { id: accountId, currentBalance } of accountsData) {
// // Check if the account does not have any associated transactions
// if (accountId && !accountIdsUsedForTransactions.has(accountId)) {
// // Insert a new entry into Balances with account's current balance
// await queryInterface.bulkInsert('Balances', [
// {
// date: new Date(),
// accountId: accountId,
// amount: currentBalance,
// },
// ], { transaction });
// }
// }

// // Insert a new entry into Balances table for each account and date with the accumulated balance
// const balanceEntries = Array.from(accountBalances).map(([key, value]) => {
// const [accountId, ...date] = key.split('-');
// return {
// date: new Date(date.join('-')),
// accountId: parseInt(accountId),
// amount: value,
// };
// });

// if (balanceEntries.length) {
// await queryInterface.bulkInsert('Balances', balanceEntries, { transaction });
// }

await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},

down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('Accounts', 'initialBalance')
await queryInterface.dropTable('Balances');
},
};
139 changes: 0 additions & 139 deletions src/migrations/1689160385756-balances-table.js

This file was deleted.

Loading

0 comments on commit 0923862

Please sign in to comment.