Skip to content

Commit

Permalink
feat: Add transactions fetching with filters
Browse files Browse the repository at this point in the history
  • Loading branch information
letehaha committed Sep 22, 2024
1 parent 6cb23b0 commit 8d2e0cf
Show file tree
Hide file tree
Showing 8 changed files with 420 additions and 96 deletions.
49 changes: 1 addition & 48 deletions src/controllers/transactions.controller.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,9 @@
import { API_RESPONSE_STATUS, SORT_DIRECTIONS, endpointsTypes } from 'shared-types';
import { API_RESPONSE_STATUS } from 'shared-types';
import { CustomResponse } from '@common/types';
import { ValidationError } from '@js/errors';
import * as transactionsService from '@services/transactions';
import { errorHandler } from './helpers';

export const getTransactions = async (req, res: CustomResponse) => {
try {
const { id: userId } = req.user;
const {
sort = SORT_DIRECTIONS.desc,
limit,
from = 0,
type: transactionType,
accountType,
accountId,
includeUser,
includeAccount,
includeCategory,
includeAll,
nestedInclude,
// isRaw,
excludeTransfer,
excludeRefunds,
}: endpointsTypes.GetTransactionsQuery = req.query;

const data = await transactionsService.getTransactions({
userId,
transactionType,
sortDirection: sort,
limit,
from,
accountType,
accountId,
includeUser,
includeAccount,
includeCategory,
includeAll,
nestedInclude,
excludeTransfer,
excludeRefunds,
isRaw: false,
});

return res.status(200).json({
status: API_RESPONSE_STATUS.success,
response: data,
});
} catch (err) {
errorHandler(res, err);
}
};

export const getTransactionById = async (req, res: CustomResponse) => {
try {
const { id } = req.params;
Expand Down
85 changes: 85 additions & 0 deletions src/controllers/transactions.controller/get-transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { z } from 'zod';
import {
API_RESPONSE_STATUS,
SORT_DIRECTIONS,
TRANSACTION_TYPES,
ACCOUNT_TYPES,
} from 'shared-types';
import { CustomRequest, CustomResponse } from '@common/types';
import * as transactionsService from '@services/transactions';
import { errorHandler } from '../helpers';

export const getTransactions = async (
req: CustomRequest<typeof getTransactionsSchema>,
res: CustomResponse,
) => {
try {
const { id: userId } = req.user;
const { ...restParams } = req.validated.query;

const data = await transactionsService.getTransactions({
...restParams,
userId,
});

return res.status(200).json({
status: API_RESPONSE_STATUS.success,
response: data,
});
} catch (err) {
console.log('err', err);
errorHandler(res, err);
}
};

const parseCommaSeparatedNumbers = (value: string) =>
value
.split(',')
.map(Number)
.filter((n) => !isNaN(n));

export const getTransactionsSchema = z.object({
query: z
.object({
order: z.nativeEnum(SORT_DIRECTIONS).optional().default(SORT_DIRECTIONS.desc),
limit: z.preprocess((val) => Number(val), z.number().int().positive()).optional(),
from: z
.preprocess((val) => Number(val), z.number().int().nonnegative())
.optional()
.default(0),
transactionType: z.nativeEnum(TRANSACTION_TYPES).optional(),
accountType: z.nativeEnum(ACCOUNT_TYPES).optional(),
accountIds: z
.preprocess(
(val) => (typeof val === 'string' ? parseCommaSeparatedNumbers(val) : val),
z.array(z.number().int().positive()),
)
.optional(),
includeUser: z.preprocess((val) => val === 'true', z.boolean()).optional(),
includeAccount: z.preprocess((val) => val === 'true', z.boolean()).optional(),
includeCategory: z.preprocess((val) => val === 'true', z.boolean()).optional(),
includeAll: z.preprocess((val) => val === 'true', z.boolean()).optional(),
nestedInclude: z.preprocess((val) => val === 'true', z.boolean()).optional(),
excludeTransfer: z.preprocess((val) => val === 'true', z.boolean()).optional(),
excludeRefunds: z.preprocess((val) => val === 'true', z.boolean()).optional(),
startDate: z
.string()
.datetime({ message: 'Invalid ISO date string for startDate' })
.optional(),
endDate: z.string().datetime({ message: 'Invalid ISO date string for endDate' }).optional(),
amountLte: z.preprocess((val) => Number(val), z.number().positive()).optional(),
amountGte: z.preprocess((val) => Number(val), z.number().positive()).optional(),
})
.refine(
(data) => {
if (data.amountGte && data.amountLte) {
return data.amountGte <= data.amountLte;
}
return true;
},
{
message: 'amountGte must be less than or equal to amountLte',
path: ['amountGte'],
},
),
});
75 changes: 58 additions & 17 deletions src/models/Transactions.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
SORT_DIRECTIONS,
TransactionModel,
} from 'shared-types';
import { Op, Includeable } from 'sequelize';
import { Op, Includeable, WhereOptions } from 'sequelize';
import {
Table,
BeforeCreate,
Expand Down Expand Up @@ -329,13 +329,13 @@ export default class Transactions extends Model {
}
}

export const getTransactions = async ({
export const findWithFilters = async ({
from = 0,
limit = 20,
accountType,
accountId,
accountIds,
userId,
sortDirection = SORT_DIRECTIONS.desc,
order = SORT_DIRECTIONS.desc,
includeUser,
includeAccount,
transactionType,
Expand All @@ -345,14 +345,18 @@ export const getTransactions = async ({
isRaw = false,
excludeTransfer,
excludeRefunds,
startDate,
endDate,
amountGte,
amountLte,
}: {
from: number;
limit?: number;
accountType?: ACCOUNT_TYPES;
transactionType?: TRANSACTION_TYPES;
accountId?: number;
accountIds?: number[];
userId: number;
sortDirection: SORT_DIRECTIONS;
order?: SORT_DIRECTIONS;
includeUser?: boolean;
includeAccount?: boolean;
includeCategory?: boolean;
Expand All @@ -361,6 +365,10 @@ export const getTransactions = async ({
isRaw: boolean;
excludeTransfer?: boolean;
excludeRefunds?: boolean;
startDate?: string;
endDate?: string;
amountGte?: number;
amountLte?: number;
}) => {
const include = prepareTXInclude({
includeUser,
Expand All @@ -370,21 +378,54 @@ export const getTransactions = async ({
nestedInclude,
});

const whereClause: WhereOptions<Transactions> = {
userId,
...removeUndefinedKeys({
accountType,
transactionType,
transferNature: excludeTransfer ? TRANSACTION_TRANSFER_NATURE.not_transfer : undefined,
refundLinked: excludeRefunds ? false : undefined,
}),
};

if (accountIds && accountIds.length > 0) {
whereClause.accountId = {
[Op.in]: accountIds,
};
}

if (startDate || endDate) {
whereClause.time = {};
if (startDate && endDate) {
whereClause.time = {
[Op.between]: [new Date(startDate), new Date(endDate)],
};
} else if (startDate) {
whereClause.time[Op.gte] = new Date(startDate);
} else if (endDate) {
whereClause.time[Op.lte] = new Date(endDate);
}
}

if (amountGte || amountLte) {
whereClause.amount = {};
if (amountGte && amountLte) {
whereClause.amount = {
[Op.between]: [amountGte, amountLte],
};
} else if (amountGte) {
whereClause.amount[Op.gte] = amountGte;
} else if (amountLte) {
whereClause.amount[Op.lte] = amountLte;
}
}

const transactions = await Transactions.findAll({
include,
where: {
userId,
...removeUndefinedKeys({
accountType,
accountId,
transactionType,
transferNature: excludeTransfer ? TRANSACTION_TRANSFER_NATURE.not_transfer : undefined,
refundLinked: excludeRefunds ? false : undefined,
}),
},
where: whereClause,
offset: from,
limit: limit,
order: [['time', sortDirection]],
order: [['time', order]],
raw: isRaw,
});

Expand Down
19 changes: 0 additions & 19 deletions src/models/transactions.ts

This file was deleted.

7 changes: 5 additions & 2 deletions src/routes/transactions.route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Router } from 'express';
import {
getTransactions,
getTransactionById,
getTransactionsByTransferId,
createTransaction,
Expand All @@ -13,6 +12,10 @@ import {
} from '@controllers/transactions.controller';
import { createRefund } from '@controllers/transactions.controller/refunds/create-refund';
import { deleteRefund } from '@controllers/transactions.controller/refunds/delete-refund';
import {
getTransactions,
getTransactionsSchema,
} from '@controllers/transactions.controller/get-transaction';
import { getRefund } from '@controllers/transactions.controller/refunds/get-refund';
import { getRefunds } from '@controllers/transactions.controller/refunds/get-refunds';
import { getRefundsForTransactionById } from '@controllers/transactions.controller/refunds/get-refunds-for-transaction-by-id';
Expand All @@ -27,7 +30,7 @@ router.get('/refunds', authenticateJwt, getRefunds);
router.post('/refund', authenticateJwt, createRefund);
router.delete('/refund', authenticateJwt, deleteRefund);

router.get('/', authenticateJwt, getTransactions);
router.get('/', authenticateJwt, validateEndpoint(getTransactionsSchema), getTransactions);
router.get('/:id', authenticateJwt, getTransactionById);
router.get('/:id/refunds', authenticateJwt, getRefundsForTransactionById);
router.get('/transfer/:transferId', authenticateJwt, getTransactionsByTransferId);
Expand Down
Loading

0 comments on commit 8d2e0cf

Please sign in to comment.