Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions backend/src/cost/__test__/cashflow-cost.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { CostService } from '../cashflow-cost.service';
import { CostType } from '../../../../middle-layer/types/CostType';
import { Frequency } from '../../../../middle-layer/types/Frequency';
import { CashflowCost } from '../../../../middle-layer/types/CashflowCost';
import { TDateISO } from '../../utils/date';

Expand Down Expand Up @@ -170,6 +171,7 @@ describe('CostService', () => {
name: ' Food ',
amount: 200,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
};

Expand All @@ -179,6 +181,7 @@ describe('CostService', () => {
name: 'Food',
amount: 200,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22',
});
expect(mockPut).toHaveBeenCalledWith({
Expand All @@ -187,6 +190,7 @@ describe('CostService', () => {
name: 'Food',
amount: 200,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22',
},
ConditionExpression: 'attribute_not_exists(#name)',
Expand All @@ -202,6 +206,7 @@ describe('CostService', () => {
name: 'Food',
amount: 0,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(BadRequestException);
Expand All @@ -210,6 +215,7 @@ describe('CostService', () => {
name: 'Food',
amount: 0,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow('amount must be a finite positive number');
Expand All @@ -221,6 +227,19 @@ describe('CostService', () => {
name: 'Food',
amount: 100,
type: 'INVALID' as unknown as CostType,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(BadRequestException);
});

it('throws BadRequestException for invalid frequency', async () => {
await expect(
service.createCost({
name: 'Food',
amount: 100,
type: CostType.MealsFood,
frequency: 'INVALID' as unknown as Frequency,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(BadRequestException);
Expand All @@ -232,6 +251,7 @@ describe('CostService', () => {
name: ' ',
amount: 100,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(BadRequestException);
Expand All @@ -240,6 +260,7 @@ describe('CostService', () => {
name: ' ',
amount: 100,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow('name must be a non-empty string');
Expand All @@ -253,6 +274,7 @@ describe('CostService', () => {
name: 'Food',
amount: 100,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(ConflictException);
Expand All @@ -261,6 +283,7 @@ describe('CostService', () => {
name: 'Food',
amount: 100,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow('Cost with name Food already exists');
Expand All @@ -274,6 +297,7 @@ describe('CostService', () => {
name: 'Food',
amount: 100,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(InternalServerErrorException);
Expand All @@ -287,6 +311,7 @@ describe('CostService', () => {
name: 'Food',
amount: 100,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(InternalServerErrorException);
Expand All @@ -295,6 +320,7 @@ describe('CostService', () => {
name: 'Food',
amount: 100,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow('Failed to create cost');
Expand All @@ -308,6 +334,7 @@ describe('CostService', () => {
name: 'Food',
amount: 200,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22',
},
});
Expand All @@ -318,6 +345,7 @@ describe('CostService', () => {
name: 'Food',
amount: 300,
type: CostType.Services,
frequency: Frequency.OneTime,
date: '2026-03-22' as TDateISO,
};
mockPutPromise.mockResolvedValue({});
Expand All @@ -326,6 +354,7 @@ describe('CostService', () => {
name: 'Food',
amount: 300,
type: CostType.Services,
frequency: Frequency.OneTime,
date: '2026-03-22' as TDateISO,
});

Expand All @@ -340,6 +369,7 @@ describe('CostService', () => {
name: 'Food',
amount: 300,
type: CostType.Services,
frequency: Frequency.OneTime,
date: '2026-03-22',
},
ConditionExpression: 'attribute_exists(#name)',
Expand All @@ -354,13 +384,15 @@ describe('CostService', () => {
name: 'Food',
amount: 200,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
});

expect(result).toEqual({
name: 'Food',
amount: 200,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22',
});
expect(mockPut).not.toHaveBeenCalled();
Expand All @@ -373,6 +405,7 @@ describe('CostService', () => {
name: 'Food',
amount: Number.NaN,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(BadRequestException);
Expand All @@ -384,6 +417,19 @@ describe('CostService', () => {
name: 'Food',
amount: 250,
type: 'INVALID' as unknown as CostType,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(BadRequestException);
});

it('throws BadRequestException for invalid frequency', async () => {
await expect(
service.updateCost('Food', {
name: 'Food',
amount: 250,
type: CostType.MealsFood,
frequency: 'INVALID' as unknown as Frequency,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(BadRequestException);
Expand All @@ -395,6 +441,7 @@ describe('CostService', () => {
name: 'Food',
amount: 250,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: 'not-a-date' as unknown as TDateISO,
}),
).rejects.toThrow(BadRequestException);
Expand All @@ -403,6 +450,7 @@ describe('CostService', () => {
name: 'Food',
amount: 250,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: 'not-a-date' as unknown as TDateISO,
}),
).rejects.toThrow('date must be a valid ISO 8601 format string');
Expand All @@ -416,6 +464,7 @@ describe('CostService', () => {
name: 'Food',
amount: 250,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(NotFoundException);
Expand All @@ -424,6 +473,7 @@ describe('CostService', () => {
name: 'Food',
amount: 250,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow('Cost with name Food not found');
Expand All @@ -438,6 +488,7 @@ describe('CostService', () => {
name: 'Food',
amount: 250,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(InternalServerErrorException);
Expand All @@ -446,6 +497,7 @@ describe('CostService', () => {
name: 'Food',
amount: 250,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow('Failed to update cost Food');
Expand All @@ -461,13 +513,15 @@ describe('CostService', () => {
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
});

expect(result).toEqual({
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22',
});
expect(mockGet).toHaveBeenCalledWith({
Expand All @@ -483,6 +537,7 @@ describe('CostService', () => {
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22',
},
ConditionExpression: 'attribute_not_exists(#name)',
Expand Down Expand Up @@ -515,13 +570,15 @@ describe('CostService', () => {
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
});

expect(result).toEqual({
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22',
});
});
Expand All @@ -534,6 +591,7 @@ describe('CostService', () => {
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(NotFoundException);
Expand All @@ -542,6 +600,7 @@ describe('CostService', () => {
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow('Cost with name Food not found');
Expand All @@ -560,6 +619,7 @@ describe('CostService', () => {
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(ConflictException);
Expand All @@ -568,6 +628,7 @@ describe('CostService', () => {
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow('Cost with name Meals already exists');
Expand All @@ -584,6 +645,7 @@ describe('CostService', () => {
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(InternalServerErrorException);
Expand All @@ -592,6 +654,7 @@ describe('CostService', () => {
name: 'Meals',
amount: 300,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow('Failed to update cost Food');
Expand All @@ -605,6 +668,7 @@ describe('CostService', () => {
name: 'Food',
amount: 200,
type: CostType.MealsFood,
frequency: Frequency.Yearly,
date: '2026-03-22' as TDateISO,
}),
).rejects.toThrow(InternalServerErrorException);
Expand Down
13 changes: 13 additions & 0 deletions backend/src/cost/cashflow-cost.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import * as AWS from 'aws-sdk';
import { CashflowCost } from '../../../middle-layer/types/CashflowCost';
import { CostType } from '../../../middle-layer/types/CostType';
import { Frequency } from '../../../middle-layer/types/Frequency';

interface UpdateCostBody {
amount?: number;
Expand All @@ -30,6 +31,15 @@ export class CostService {
}
}

private validateFrequency(frequency: string) {
if (!Object.values(Frequency).includes(frequency as Frequency) || frequency === null) {
throw new BadRequestException(
`frequency must be one of: ${Object.values(Frequency).join(', ')}`,
);
}
}


private validateAmount(amount: number) {
if (!Number.isFinite(amount) || amount <= 0 || amount === null) {
throw new BadRequestException('amount must be a finite positive number');
Expand Down Expand Up @@ -121,6 +131,7 @@ export class CostService {
const tableName = process.env.CASHFLOW_COST_TABLE_NAME || '';
this.validateAmount(cost.amount);
this.validateCostType(cost.type);
this.validateFrequency(cost.frequency);
this.validateName(cost.name);
const normalizedName = cost.name.trim();

Expand Down Expand Up @@ -178,6 +189,7 @@ export class CostService {

this.validateAmount(updates.amount);
this.validateCostType(updates.type);
this.validateFrequency(updates.frequency);

if (updates.name !== undefined) {
this.validateName(updates.name);
Expand Down Expand Up @@ -209,6 +221,7 @@ export class CostService {
existingCost.name === updates.name &&
existingCost.amount === updates.amount &&
existingCost.type === updates.type &&
existingCost.frequency === updates.frequency &&
datesAreEqual;

if (isUnchanged) {
Expand Down
Loading
Loading