Skip to content

Commit

Permalink
Merge pull request #134 from letehaha/fix/issues
Browse files Browse the repository at this point in the history
App stability improvements
  • Loading branch information
letehaha committed Sep 22, 2024
2 parents 69c05d7 + 8d2e0cf commit 77df742
Show file tree
Hide file tree
Showing 67 changed files with 1,489 additions and 626 deletions.
1 change: 1 addition & 0 deletions shared-types/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum API_ERROR_CODES {
notFound = 'NOT_FOUND',
unexpected = 'UNEXPECTED',
validationError = 'VALIDATION_ERROR',
conflict = 'CONFLICT',
forbidden = 'FORBIDDEN',
BadRequest = 'BAD_REQUEST',

Expand Down
8 changes: 4 additions & 4 deletions shared-types/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { UserModel } from 'shared-types';
import { BodyPayload } from './index';

export interface AuthLoginBody extends BodyPayload {
username: UserModel['username'];
password: UserModel['password'];
username: string;
password: string;
}
// Bearer token
export interface AuthLoginResponse {
token: string;
}

export interface AuthRegisterBody extends BodyPayload {
username: UserModel['username'];
password: UserModel['password'];
username: string;
password: string;
}
export interface AuthRegisterResponse {
user: UserModel;
Expand Down
15 changes: 8 additions & 7 deletions src/controllers/accounts.controller.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ describe('Accounts controller', () => {
expect(account.refCreditLimit).toStrictEqual(creditLimit);
});
it('should correctly create account with correct balance for external currency', async () => {
const currency = (await helpers.addUserCurrencies({ currencyCodes: ['UAH'], raw: true }))[0];
const currency = (await helpers.addUserCurrencies({ currencyCodes: ['UAH'], raw: true }))
.currencies[0]!;

const account = await helpers.createAccount({
payload: {
Expand All @@ -42,19 +43,19 @@ describe('Accounts controller', () => {

expect(account.initialBalance).toStrictEqual(initialBalance);
expect(account.refInitialBalance).toStrictEqual(
Math.floor(initialBalance * currencyRate.rate),
Math.floor(initialBalance * currencyRate!.rate),
);
expect(account.currentBalance).toStrictEqual(initialBalance);
expect(account.refCurrentBalance).toStrictEqual(
Math.floor(initialBalance * currencyRate.rate),
Math.floor(initialBalance * currencyRate!.rate),
);
expect(account.creditLimit).toStrictEqual(creditLimit);
expect(account.refCreditLimit).toStrictEqual(Math.floor(creditLimit * currencyRate.rate));
expect(account.refCreditLimit).toStrictEqual(Math.floor(creditLimit * currencyRate!.rate));
});
});
describe('update account', () => {
it('should return 404 if try to update unexisting account', async () => {
const res = await helpers.updateAccount({
const res = await helpers.updateAccount<helpers.ErrorResponse>({
id: 1,
});

Expand Down Expand Up @@ -132,11 +133,11 @@ describe('Accounts controller', () => {
currencyCodes: [newCurrency],
raw: true,
})
)[0];
).currencies[0];
const account = await helpers.createAccount({
payload: {
...helpers.buildAccountPayload(),
currencyId: currency.currencyId,
currencyId: currency!.currencyId,
},
raw: true,
});
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/auth.controller.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { API_ERROR_CODES, API_RESPONSE_STATUS } from 'shared-types';
import { makeRequest } from '@tests/helpers';
import { makeRequest, ErrorResponse } from '@tests/helpers';

describe('Auth', () => {
describe('Login', () => {
it('should return correct error for unexisting user', async () => {
const res = await makeRequest({
const res = await makeRequest<ErrorResponse>({
method: 'post',
url: '/auth/login',
payload: {
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/banks/monobank.controller.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('Monobank integration', () => {
it('throws error if invalid "token" is passed', async () => {
const result = await helpers.monobank.callPair();

expect(result.status).toEqual(ERROR_CODES.NotFoundError);
expect(result.status).toEqual(ERROR_CODES.Forbidden);
});
it('creates Monobank user and correct accounts with valid token', async () => {
const mockedClientData = helpers.monobank.mockedClient();
Expand Down Expand Up @@ -49,7 +49,7 @@ describe('Monobank integration', () => {
const rates = await helpers.getCurrenciesRates();
const rate = rates.find(
(r) => r.baseCode === CURRENCY_NUMBER_TO_CODE[item.currencyCode],
).rate;
)!.rate;

expect(resultItem.initialBalance).toBe(mockedAccount.balance);
expect(resultItem.refInitialBalance).toBe(Math.floor(mockedAccount.balance * rate));
Expand Down
24 changes: 12 additions & 12 deletions src/controllers/banks/monobank.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const hostname = config.get('bankIntegrations.monobank.apiEndpoint');

function dateRange({ from, to }: { from: number; to: number }): { start: number; end: number }[] {
const difference = differenceInCalendarMonths(new Date(to), new Date(from));
const dates = [];
const dates: { start: number; end: number }[] = [];

for (let i = 0; i <= difference; i++) {
const start = startOfMonth(addMonths(new Date(from), i));
Expand Down Expand Up @@ -85,7 +85,7 @@ async function createMonoTransaction({

if (isTransactionExists) return;

const userData = await usersService.getUser(account.userId);
const userData = (await usersService.getUser(account.userId))!;

let mccId = await MerchantCategoryCodes.getByCode({ code: data.mcc });

Expand All @@ -99,12 +99,14 @@ async function createMonoTransaction({
userId: userData.id,
});

let categoryId;
let categoryId: number;

if (userMcc.length) {
categoryId = userMcc[0].get('categoryId');
categoryId = userMcc[0]!.get('categoryId');
} else {
categoryId = (await Users.getUserDefaultCategory({ id: userData.id })).get('defaultCategoryId');
categoryId = (await Users.getUserDefaultCategory({ id: userData.id }))!.get(
'defaultCategoryId',
);

await UserMerchantCategoryCodes.createEntry({
mccId: mccId.get('id'),
Expand Down Expand Up @@ -156,7 +158,7 @@ export const pairAccount = async (req, res: CustomResponse) => {
userId: systemUserId,
});

if ('connected' in result && result.connected) {
if (result && 'connected' in result && result.connected) {
return res.status(404).json({
status: API_RESPONSE_STATUS.error,
response: {
Expand Down Expand Up @@ -527,13 +529,12 @@ export const refreshAccounts = async (req, res) => {
externalIds: clientInfo.accounts.map((item) => item.id),
});

const accountsToUpdate = [];
const accountsToCreate = [];
const promises: Promise<unknown>[] = [];
clientInfo.accounts.forEach((account) => {
const existingAccount = existingAccounts.find((acc) => acc.externalId === account.id);

if (existingAccount) {
accountsToUpdate.push(
promises.push(
accountsService.updateAccount({
id: existingAccount.id,
currentBalance: account.balance,
Expand All @@ -547,7 +548,7 @@ export const refreshAccounts = async (req, res) => {
}),
);
} else {
accountsToCreate.push(
promises.push(
accountsService.createSystemAccountsFromMonobankAccounts({
userId: systemUserId,
monoAccounts: [account],
Expand All @@ -556,8 +557,7 @@ export const refreshAccounts = async (req, res) => {
}
});

await Promise.all(accountsToUpdate);
await Promise.all(accountsToCreate);
await Promise.all(promises);

const accounts = await accountsService.getAccounts({
userId: monoUser.systemUserId,
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/categories.controller/create-category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const createCategory = async (req, res: CustomResponse) => {
export const CreateCategoryPayloadSchema = z
.object({
name: z.string().min(1).max(200, 'The name must not exceed 200 characters'),
imageUrl: z.string().url().max(500, 'The URL must not exceed 500 characters').nullish(),
imageUrl: z.string().url().max(500, 'The URL must not exceed 500 characters').optional(),
type: z
.enum(Object.values(CATEGORY_TYPES) as [string, ...string[]])
.default(CATEGORY_TYPES.custom),
Expand All @@ -41,7 +41,7 @@ export const CreateCategoryPayloadSchema = z
color: z
.string()
.regex(/^#[0-9A-F]{6}$/i)
.nullish(),
.optional(),
}),
z.object({
parentId: z.undefined(),
Expand Down
44 changes: 44 additions & 0 deletions src/controllers/currencies/add-user-currencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { z } from 'zod';
import { API_RESPONSE_STATUS } from 'shared-types';
import { CustomResponse } from '@common/types';
import * as userCurrenciesService from '@services/currencies/add-user-currency';
import { errorHandler } from '../helpers';

export const addUserCurrencies = async (req, res: CustomResponse) => {
try {
const { id: userId } = req.user;
const { currencies }: AddUserCurrenciesParams = req.validated.body;

const result = await userCurrenciesService.addUserCurrencies(
currencies.map((item) => ({ userId, ...item })),
);

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

const recordId = () => z.number().int().positive().finite();
const UserCurrencySchema = z
.object({
currencyId: recordId(),
exchangeRate: z.number().positive().optional(),
liveRateUpdate: z.boolean().optional(),
})
.strict();

const bodyZodSchema = z
.object({
currencies: z.array(UserCurrencySchema).nonempty(),
})
.strict();

type AddUserCurrenciesParams = z.infer<typeof bodyZodSchema>;

export const addUserCurrenciesSchema = z.object({
body: bodyZodSchema,
});
62 changes: 62 additions & 0 deletions src/controllers/currencies/edit-currency-exchange-rate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { z } from 'zod';
import cc from 'currency-codes';
import { API_RESPONSE_STATUS } from 'shared-types';
import { CustomResponse } from '@common/types';
import * as userExchangeRates from '@services/user-exchange-rate';
import { errorHandler } from '@controllers/helpers';

export const editCurrencyExchangeRate = async (req, res: CustomResponse) => {
try {
const { id: userId } = req.user;
const { pairs }: EditCurrencyExchangeRateParams = req.validated.body;

const data = await userExchangeRates.editUserExchangeRates({
userId,
pairs,
});

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

const isValidCurrencyCode = (code: string) => cc.code(code) !== undefined;

const CurrencyCodeSchema = z.string().refine(
(code) => isValidCurrencyCode(code),
(code) => ({ message: `Invalid currency code: ${code}. Use ISO 4217 Code. For example: USD` }),
);

const UpdateExchangeRatePairSchema = z
.object({
baseCode: CurrencyCodeSchema,
quoteCode: CurrencyCodeSchema,
rate: z.number().positive(),
})
.strict();

const bodyZodSchema = z
.object({
pairs: z
.array(UpdateExchangeRatePairSchema)
.nonempty()
.refine(
(pairs) => pairs.every((pair) => pair.baseCode !== pair.quoteCode),
'You cannot edit pair with the same base and quote currency code.',
)
.refine(
(pairs) => pairs.every((pair) => pairs.some((item) => item.baseCode === pair.quoteCode)),
"When changing base-quote pair rate, you need to also change opposite pair's rate.",
),
})
.strict();

type EditCurrencyExchangeRateParams = z.infer<typeof bodyZodSchema>;

export const editCurrencyExchangeRateSchema = z.object({
body: bodyZodSchema,
});
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
Loading

0 comments on commit 77df742

Please sign in to comment.