Skip to content

Commit

Permalink
Merge pull request #89 from letehaha/feat/extend-balance-trend-chart-…
Browse files Browse the repository at this point in the history
…functionality

Improve balance chart functionality
  • Loading branch information
letehaha authored Aug 20, 2023
2 parents 269b667 + 2ba5d27 commit 939feaa
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 85 deletions.
4 changes: 4 additions & 0 deletions shared-types/routes/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ export interface GetBalanceHistoryPayload extends QueryPayload {
// yyyy-mm-dd
to?: string;
}

export interface GetTotalBalancePayload extends QueryPayload {
date: string;
}
48 changes: 39 additions & 9 deletions src/controllers/stats.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { API_RESPONSE_STATUS, endpointsTypes } from 'shared-types';
import { API_RESPONSE_STATUS, BalanceModel, endpointsTypes } from 'shared-types';
import { isValid, isBefore } from 'date-fns';
import { CustomResponse } from '@common/types';
import * as statsService from '@services/stats';
Expand All @@ -7,7 +7,7 @@ import { ValidationError } from '@js/errors';

export const getBalanceHistory = async (req, res: CustomResponse) => {
const { id: userId } = req.user;
const { accountId, from, to }: endpointsTypes.GetBalanceHistoryPayload = req.query;
const { from, to, accountId }: endpointsTypes.GetBalanceHistoryPayload = req.query;

try {
if (from && !isValid(new Date(from))) throw new ValidationError({ message: '"from" is invalid date.' })
Expand All @@ -16,13 +16,21 @@ export const getBalanceHistory = async (req, res: CustomResponse) => {
throw new ValidationError({ message: '"from" cannot be greater than "to" date.' })
}

// TODO:L validation for "from" and "to"
const balanceHistory = await statsService.getBalanceHistory({
userId,
accountId: Number(accountId),
from,
to,
});
let balanceHistory: BalanceModel[];
if (accountId) {
balanceHistory = await statsService.getBalanceHistoryForAccount({
userId,
from,
to,
accountId,
});
} else {
balanceHistory = await statsService.getBalanceHistory({
userId,
from,
to,
});
}

return res.status(200).json({
status: API_RESPONSE_STATUS.success,
Expand All @@ -32,3 +40,25 @@ export const getBalanceHistory = async (req, res: CustomResponse) => {
errorHandler(res, err);
}
};

export const getTotalBalance = async (req, res: CustomResponse) => {
const { id: userId } = req.user;
const { date }: endpointsTypes.GetTotalBalancePayload = req.query;

try {
if (!isValid(new Date(date))) throw new ValidationError({ message: '"date" is invalid date.' })

const totalBalance = await statsService.getTotalBalance({
userId,
date,
});

return res.status(200).json({
status: API_RESPONSE_STATUS.success,
response: totalBalance,
});

} catch (err) {
errorHandler(res, err);
}
}
63 changes: 0 additions & 63 deletions src/models/Balances.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,66 +298,3 @@ export default class Balances extends Model<BalanceModel> {
}
}
}

export interface DateQuery {
// yyyy-mm-dd
from?: string;
// yyyy-mm-dd
to?: string
}

const getWhereConditionForTime = ({ from, to }: DateQuery) => {
const where: { date?: Record<symbol, Date[] | Date> } = {}

if (from && to) {
where.date = {
[Op.between]: [new Date(from), new Date(to)],
};
} else if (from) {
where.date = {
[Op.gte]: new Date(from),
};
} else if (to) {
where.date = {
[Op.lte]: new Date(to),
};
}

return where;
};

// Method to get all balances
export const getBalances = async (
{ userId, from, to }: { userId: number } & DateQuery,
attributes: GenericSequelizeModelAttributes = {},
): Promise<BalanceModel[]> => {
return Balances.findAll({
where: getWhereConditionForTime({ from, to }),
order: [['date', 'ASC']],
include: [{
model: Accounts,
where: { userId },
attributes: [],
}],
attributes: ['date', 'amount', 'accountId'],
...attributes,
});
}

// Method to get the balance for a specific account
export const getAccountBalanceHistory = async (
{ accountId, userId, from, to }: { accountId: number; userId: number; } & DateQuery,
attributes: GenericSequelizeModelAttributes = {},
): Promise<Balances[]> => {
return Balances.findAll({
where: getWhereConditionForTime({ from, to }),
order: [['date', 'ASC']],
include: [{
model: Accounts,
where: { userId, id: accountId },
attributes: [],
}],
attributes: ['date', 'amount'],
...attributes,
});
}
5 changes: 3 additions & 2 deletions src/routes/stats.route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Router } from 'express';
import { getBalanceHistory } from '@controllers/stats.controller';
import * as statsController from '@controllers/stats.controller';
import { authenticateJwt } from '@middlewares/passport';

const router = Router({});

router.get('/balance-history', authenticateJwt, getBalanceHistory);
router.get('/balance-history', authenticateJwt, statsController.getBalanceHistory);
router.get('/total-balance', authenticateJwt, statsController.getTotalBalance);

export default router;
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const callGelFullBalanceHistory = async (raw = false) => {
return raw ? helpers.extractResponse(result) : result
}

describe('Balances model', () => {
describe('Balances service', () => {
it('the balances table correctly managing account creation', async () => {
const account = await helpers.createAccount({ raw: true })

Expand Down
146 changes: 146 additions & 0 deletions src/services/stats/get-balance-history-for-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Op } from 'sequelize';
import { BalanceModel } from 'shared-types';
import { GenericSequelizeModelAttributes } from '@common/types';

import { connection } from '@models/index';
import * as Balances from '@models/Balances.model';
import * as Accounts from '@models/Accounts.model';

interface DateQuery {
// yyyy-mm-dd
from?: string;
// yyyy-mm-dd
to?: string;
}

const getWhereConditionForTime = ({ from, to }: DateQuery) => {
const where: { date?: Record<symbol, Date[] | Date> } = {}

if (from && to) {
where.date = {
[Op.between]: [new Date(from), new Date(to)],
};
} else if (from) {
where.date = {
[Op.gte]: new Date(from),
};
} else if (to) {
where.date = {
[Op.lte]: new Date(to),
};
}

return where;
};

/**
* Retrieves the balances for the requested account for a user within a specified date range.
* If no balance record is found for an account between the "from" and "to" dates,
* and also no record before the "from" date, it checks for records after the "to" date
* that have a positive balance.
*
* @param {Object} params - The parameters for fetching balances.
* @param {number} params.userId - The ID of the user for whom balances are to be fetched.
* @param {number} params.accountId - The ID of the account for which balances are to be fetched.
* @param {string} [params.from] - The start date (inclusive) of the date range in 'yyyy-mm-dd' format.
* @param {string} [params.to] - The end date (inclusive) of the date range in 'yyyy-mm-dd' format.
* @param {GenericSequelizeModelAttributes} [attributes={}] - Additional Sequelize model attributes for the query.
* @returns {Promise<BalanceModel[]>} - A promise that resolves to an array of balance records.
* @throws {Error} - Throws an error if the database query fails.
*
* @example
* const balances = await getBalanceHistoryForAccount({ userId: 1, accountId: 1 from: '2023-01-01', to: '2023-12-31' });
*/
export const getBalanceHistoryForAccount = async (
{ userId, from, to, accountId }: {
userId: number;
accountId: number;
from?: string;
to?: string;
},
attributes: GenericSequelizeModelAttributes = {},
): Promise<BalanceModel[]> => {
const isTxPassedFromAbove = attributes.transaction !== undefined;
const transaction = attributes.transaction ?? await connection.sequelize.transaction();

try {
let data: BalanceModel[] = []

const dataAttributes = ['date', 'amount'];
const balancesInRange = await Balances.default.findAll({
where: getWhereConditionForTime({ from, to }),
order: [['date', 'ASC']],
include: [{
model: Accounts.default,
where: { userId, id: accountId },
attributes: [],
}],
raw: attributes.raw || true,
attributes: dataAttributes,
transaction,
});

data = balancesInRange;

if (!balancesInRange.length) {
let balanceRecord: BalanceModel;

if (from) {
// Check for records before "from" date
balanceRecord = await Balances.default.findOne({
where: {
date: {
[Op.lt]: new Date(from),
},
accountId,
},
order: [['date', 'DESC']],
attributes: dataAttributes,
raw: attributes.raw ?? true,
transaction,
});
}

if (!balanceRecord && to) {
// If no record found before "from" date, check for records after "to"
// date with amount > 0
balanceRecord = await Balances.default.findOne({
where: {
accountId,
date: {
[Op.gt]: new Date(to)
},
amount: {
[Op.gt]: 0
}
},
order: [['date', 'ASC']],
attributes: dataAttributes,
raw: attributes.raw ?? true,
transaction,
});
}

// Combine the results
data = [
...data,
// filter(Boolean) to remove any null values
{
...balanceRecord,
date: new Date(to ?? from ?? new Date()),
},
];
}

if (!isTxPassedFromAbove) {
await transaction.commit();
}

return data;
} catch (err) {
if (!isTxPassedFromAbove) {
await transaction.rollback();
}
throw err;
}
};
Loading

0 comments on commit 939feaa

Please sign in to comment.