Skip to content

Commit

Permalink
feat: Add a separate endpoint for tx linking
Browse files Browse the repository at this point in the history
  • Loading branch information
letehaha committed Jan 20, 2024
1 parent d50d83a commit a4c7cb1
Show file tree
Hide file tree
Showing 17 changed files with 420 additions and 113 deletions.
4 changes: 4 additions & 0 deletions shared-types/routes/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ export interface UpdateTransactionBody {
export interface UnlinkTransferTransactionsBody {
transferIds: string[];
}
// Array of income/expense pairs to link between each other
export interface LinkTransactionsBody {
ids: [number, number][];
}
4 changes: 2 additions & 2 deletions src/controllers/transactions.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const getTransactions = async (req, res: CustomResponse) => {
includeAll,
nestedInclude,
isRaw,
} = req.query
} = req.query;

const data = await transactionsService.getTransactions({
userId,
Expand All @@ -41,7 +41,7 @@ export const getTransactions = async (req, res: CustomResponse) => {
return res.status(200).json({
status: API_RESPONSE_STATUS.success,
response: data,
})
});
} catch (err) {
errorHandler(res, err);
}
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/transactions.controller/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
export * from './create-transaction';
export * from './delete-transaction';
export * from './update-transaction';
export * from './transfer-linking/unlink-transfer-transactions';
export * from './transfer-linking';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './link-transactions';
export * from './unlink-transfer-transactions';
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { TRANSACTION_TRANSFER_NATURE, TRANSACTION_TYPES } from 'shared-types';
import * as helpers from '@tests/helpers';
import { ERROR_CODES } from '@js/errors';

describe('link transactions between each other', () => {
it('link two valid transactions', async () => {
// Create 2 income and 2 expense to check that multiple updation is possible
const accountA = await helpers.createAccount({ raw: true });
const accountB = await helpers.createAccount({ raw: true });

const [incomeA] = await helpers.createTransaction({
payload: helpers.buildTransactionPayload({
accountId: accountA.id,
transactionType: TRANSACTION_TYPES.income,
}),
raw: true,
});
const [incomeB] = await helpers.createTransaction({
payload: helpers.buildTransactionPayload({
accountId: accountB.id,
transactionType: TRANSACTION_TYPES.income,
}),
raw: true,
});
const [expenseA] = await helpers.createTransaction({
payload: helpers.buildTransactionPayload({
accountId: accountA.id,
transactionType: TRANSACTION_TYPES.expense,
}),
raw: true,
});
const [expenseB] = await helpers.createTransaction({
payload: helpers.buildTransactionPayload({
accountId: accountB.id,
transactionType: TRANSACTION_TYPES.expense,
}),
raw: true,
});

const linkingResult = await helpers.linkTransactions({
payload: {
ids: [
[incomeA.id, expenseB.id],
[incomeB.id, expenseA.id],
],
},
raw: true,
});

// Check that linkind response is coorect
[incomeA, incomeB, expenseA, expenseB].forEach((tx) => {
const txAfter = linkingResult.flat().find((t) => t.id === tx.id);
// Expect that only transferNature and transferId were changed
expect({ ...tx }).toEqual({
...txAfter,
transferNature: expect.toBeAnythingOrNull(),
transferId: expect.toBeAnythingOrNull(),
});

expect(txAfter.transferNature).toBe(
TRANSACTION_TRANSFER_NATURE.common_transfer,
);
expect(txAfter.transferId).toEqual(expect.any(String));
});

// Check that transactions fetching also returns correct result
const txsAfterUpdation = await helpers.getTransactions({ raw: true });
[incomeA, incomeB, expenseA, expenseB].forEach((tx) => {
const txAfter = txsAfterUpdation.find((t) => t.id === tx.id);
// Expect that only transferNature and transferId were changed
expect({ ...tx }).toEqual({
...txAfter,
transferNature: expect.toBeAnythingOrNull(),
transferId: expect.toBeAnythingOrNull(),
});

expect(txAfter.transferNature).toBe(
TRANSACTION_TRANSFER_NATURE.common_transfer,
);
expect(txAfter.transferId).toEqual(expect.any(String));
});

expect(incomeA.transferId).toBe(expenseB.transferId);
expect(incomeB.transferId).toBe(expenseA.transferId);
});

it('throws an error when trying to link tx from the same account', async () => {
await helpers.monobank.pair();
const { transactions } = await helpers.monobank.mockTransactions();

const tx1 = transactions.find(
(item) => item.transactionType === TRANSACTION_TYPES.expense,
);
const tx2 = transactions.find(
(item) => item.transactionType === TRANSACTION_TYPES.income,
);

const result = await helpers.linkTransactions({
payload: {
ids: [[tx1.id, tx2.id]],
},
});
expect(result.statusCode).toBe(ERROR_CODES.ValidationError);
});

it.each([[TRANSACTION_TYPES.expense], [TRANSACTION_TYPES.income]])(
'throws an error when trying to link tx with same transactionType. test %s type',
async (txType) => {
const accountA = await helpers.createAccount({ raw: true });
const accountB = await helpers.createAccount({ raw: true });

const [tx1] = await helpers.createTransaction({
payload: helpers.buildTransactionPayload({
accountId: accountA.id,
transactionType: txType,
}),
raw: true,
});
const [tx2] = await helpers.createTransaction({
payload: helpers.buildTransactionPayload({
accountId: accountB.id,
transactionType: txType,
}),
raw: true,
});

const result = await helpers.linkTransactions({
payload: {
ids: [[tx1.id, tx2.id]],
},
});

expect(result.statusCode).toBe(ERROR_CODES.ValidationError);
},
);

it.each([[TRANSACTION_TYPES.expense], [TRANSACTION_TYPES.income]])(
'throws an error when trying to link to the transaction that is already a transfer. test %s type',
async (txType) => {
const accountA = await helpers.createAccount({ raw: true });
const accountB = await helpers.createAccount({ raw: true });
const accountC = await helpers.createAccount({ raw: true });

const [tx1] = await helpers.createTransaction({
payload: helpers.buildTransactionPayload({
accountId: accountA.id,
transactionType: txType,
}),
raw: true,
});
const transactions = await helpers.createTransaction({
payload: {
...helpers.buildTransactionPayload({
accountId: accountB.id,
amount: 10,
transferNature: TRANSACTION_TRANSFER_NATURE.common_transfer,
destinationAmount: 20,
destinationAccountId: accountC.id,
}),
},
raw: true,
});

const expenseTx = transactions.find(
(t) => t.transactionType === TRANSACTION_TYPES.expense,
);
const incomeTx = transactions.find(
(t) => t.transactionType === TRANSACTION_TYPES.income,
);

const result = await helpers.linkTransactions({
payload: {
ids: [
[
tx1.id,
txType === TRANSACTION_TYPES.income ? expenseTx.id : incomeTx.id,
],
],
},
});

expect(result.statusCode).toBe(ERROR_CODES.ValidationError);
},
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { API_RESPONSE_STATUS, endpointsTypes } from 'shared-types';
import { CustomResponse } from '@common/types';
import { errorHandler } from '@controllers/helpers';
import * as transactionsService from '@services/transactions';
import { ValidationError } from '@js/errors';

export const linkTransactions = async (req, res: CustomResponse) => {
try {
const { ids }: endpointsTypes.LinkTransactionsBody = req.body;
const { id: userId } = req.user;

if (!ids || !Array.isArray(ids)) {
throw new ValidationError({
message: '"ids" field is missing or invalid.',
});
}

const data = await transactionsService.linkTransactions({
userId,
ids,
});

return res
.status(200)
.json({ status: API_RESPONSE_STATUS.success, response: data });
} catch (err) {
errorHandler(res, err);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,12 @@ describe('Unlink transfer transactions', () => {
});

// Now link 1 external with 1 system for each type
const updatedA = await helpers.updateTransaction({
id: expenseExternalTx.id,
const [updatedA, updatedB] = await helpers.linkTransactions({
payload: {
transferNature: TRANSACTION_TRANSFER_NATURE.common_transfer,
destinationTransactionId: incomeSystemTx.id,
},
raw: true,
});
const updatedB = await helpers.updateTransaction({
id: incomeExternalTx.id,
payload: {
transferNature: TRANSACTION_TRANSFER_NATURE.common_transfer,
destinationTransactionId: expenseSystemTx.id,
ids: [
[expenseExternalTx.id, incomeSystemTx.id],
[incomeExternalTx.id, expenseSystemTx.id],
],
},
raw: true,
});
Expand Down
9 changes: 6 additions & 3 deletions src/models/Transactions.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
TRANSACTION_TYPES,
TRANSACTION_TRANSFER_NATURE,
SORT_DIRECTIONS,
TransactionModel,
} from 'shared-types';
import { Op } from 'sequelize';
import { Transaction } from 'sequelize/types';
Expand Down Expand Up @@ -508,7 +509,9 @@ export const getTransactionsByTransferId = (
});
};

export const getTransactionsByArrayOfField = async (
export const getTransactionsByArrayOfField = async <
T extends keyof TransactionModel,
>(
{
fieldValues,
fieldName,
Expand All @@ -519,8 +522,8 @@ export const getTransactionsByArrayOfField = async (
includeAll,
nestedInclude,
}: {
fieldValues: unknown[];
fieldName: string;
fieldValues: TransactionModel[T][];
fieldName: T;
userId: number;
includeUser?: boolean;
includeAccount?: boolean;
Expand Down
2 changes: 2 additions & 0 deletions src/routes/transactions.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createTransaction,
updateTransaction,
unlinkTransferTransactions,
linkTransactions,
deleteTransaction,
} from '@controllers/transactions.controller';
import { authenticateJwt } from '@middlewares/passport';
Expand All @@ -21,6 +22,7 @@ router.get(
);
router.post('/', authenticateJwt, createTransaction);
router.put('/unlink', authenticateJwt, unlinkTransferTransactions);
router.put('/link', authenticateJwt, linkTransactions);
router.put('/:id', authenticateJwt, updateTransaction);
router.delete('/:id', authenticateJwt, deleteTransaction);

Expand Down
8 changes: 4 additions & 4 deletions src/services/transactions/create-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import * as Accounts from '@models/Accounts.model';
import * as UsersCurrencies from '@models/UsersCurrencies.model';
import { calculateRefAmount } from '@services/calculate-ref-amount.service';

import { linkTransactions } from './link-transaction';
import { linkTransactions } from './transactions-linking';
import type { CreateTransactionParams, UpdateTransactionParams } from './types';

type CreateOppositeTransactionParams = [
Expand Down Expand Up @@ -273,11 +273,11 @@ export const createTransaction = async (
* doesn't exist
*/
if (destinationTransactionId) {
const { baseTx, oppositeTx } = await linkTransactions(
const [[baseTx, oppositeTx]] = await linkTransactions(
{
userId,
baseTx: baseTransaction,
destinationTransactionId,
ids: [[baseTransaction.id, destinationTransactionId]],
ignoreBaseTxTypeValidation: true,
},
{ transaction },
);
Expand Down
19 changes: 11 additions & 8 deletions src/services/transactions/get-transactions.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import * as Transactions from '@models/Transactions.model';
import { GenericSequelizeModelAttributes } from '@common/types';
import { type GetTransactionsParams } from '@models/transactions';
import type { GetTransactionsParams } from '@models/transactions';

export const getTransactions = async (
params: GetTransactionsParams,
attributes: GenericSequelizeModelAttributes = {},
params: GetTransactionsParams,
attributes: GenericSequelizeModelAttributes = {},
) => {
// eslint-disable-next-line no-useless-catch
try {
const data = await Transactions.getTransactions(params, { transaction: attributes.transaction });

const data = await Transactions.getTransactions(params, {
transaction: attributes.transaction,
});

return data;
} catch(err) {
throw new err;
} catch (err) {
throw err;
}
};
};
2 changes: 1 addition & 1 deletion src/services/transactions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export * from './get-transactions';
export * from './update-transaction';
export * from './get-by-transfer-id';
export * from './get-transaction-by-some-id';
export * from './transactions-linking/unlink-transfer-transactions';
export * from './transactions-linking';
Loading

0 comments on commit a4c7cb1

Please sign in to comment.