diff --git a/bruno/Common Cents/Expenses/LOC DEL Expense.bru b/bruno/Common Cents/Expenses/LOC DEL Expense.bru new file mode 100644 index 0000000..dfb6024 --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC DEL Expense.bru @@ -0,0 +1,11 @@ +meta { + name: LOC DEL Expense + type: http + seq: 3 +} + +delete { + url: {{localBaseUrl}}/expenses/2aa94170-2c57-4c4f-a1e7-13544ba72917 + body: none + auth: inherit +} diff --git a/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru b/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru new file mode 100644 index 0000000..935e4ee --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru @@ -0,0 +1,11 @@ +meta { + name: LOC GET Expense By ID + type: http + seq: 2 +} + +get { + url: {{localBaseUrl}}/expenses/2aa94170-2c57-4c4f-a1e7-13544ba72917 + body: none + auth: inherit +} diff --git a/bruno/Common Cents/Expenses/Expenses.bru b/bruno/Common Cents/Expenses/LOC GET Expenses.bru similarity index 80% rename from bruno/Common Cents/Expenses/Expenses.bru rename to bruno/Common Cents/Expenses/LOC GET Expenses.bru index fc8335c..b2e8e2c 100644 --- a/bruno/Common Cents/Expenses/Expenses.bru +++ b/bruno/Common Cents/Expenses/LOC GET Expenses.bru @@ -1,5 +1,5 @@ meta { - name: Expenses + name: LOC GET Expenses type: http seq: 1 } diff --git a/bruno/Common Cents/Expenses/LOC POST Expense Full.bru b/bruno/Common Cents/Expenses/LOC POST Expense Full.bru new file mode 100644 index 0000000..d0e6451 --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC POST Expense Full.bru @@ -0,0 +1,23 @@ +meta { + name: LOC POST Expense Full + type: http + seq: 4 +} + +post { + url: {{localBaseUrl}}/expenses + body: json + auth: inherit +} + +body:json { + { + "date": "2025-12-01", + "cents": 987, + "categoryId": "1", + "merchantId": "1", + "subcategoryIds": ["1","2"], + "tagIds": ["1","2"], + "description": "NEW FULL expense" + } +} diff --git a/bruno/Common Cents/Expenses/LOC POST Expense Partial.bru b/bruno/Common Cents/Expenses/LOC POST Expense Partial.bru new file mode 100644 index 0000000..bfc7302 --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC POST Expense Partial.bru @@ -0,0 +1,20 @@ +meta { + name: LOC POST Expense Partial + type: http + seq: 5 +} + +post { + url: {{localBaseUrl}}/expenses + body: json + auth: inherit +} + +body:json { + { + "date": "2025-12-01", + "cents": 987, + "categoryId": "1", + "description": "NEW PARTIAL expense" + } +} diff --git a/src/controllers/expenses/expense.dto.ts b/src/controllers/expenses/expense.dto.ts index 8f595ff..134683b 100644 --- a/src/controllers/expenses/expense.dto.ts +++ b/src/controllers/expenses/expense.dto.ts @@ -1,19 +1,24 @@ import { PartialType } from '@nestjs/mapped-types'; export class CreateExpenseDto { - year: string; - month: string; - day: string; + date: Date; cents: number; - category: string; - merchant: string; - subcategory: string[]; - tags: string[]; + categoryId: string; + merchantId?: string; + subcategoryIds?: string[]; + tagIds?: string[]; description?: string; } -export class UpdateExpenseDto extends PartialType(CreateExpenseDto) { } +export class UpdateExpenseDto extends PartialType(CreateExpenseDto) {} -export class GetExpenseDto extends CreateExpenseDto{ - id: number; +export class GetExpenseDto { + id: string; + date: Date; + cents: number; + category: string; + merchant?: string; + subcategories?: string[]; + tags?: string[]; + description?: string; } diff --git a/src/controllers/expenses/expense.entities.ts b/src/controllers/expenses/expense.entities.ts index 1fd939a..5288205 100644 --- a/src/controllers/expenses/expense.entities.ts +++ b/src/controllers/expenses/expense.entities.ts @@ -1,32 +1,30 @@ export class Expense { - id: number; - year: string; - month: string; - day: string; - cents: number; - category: Category; - subcategory?: SubCategory[]; - merchant?: Merchant; - tags?: Tag[]; - description?: string; + id: string; + date: Date; + cents: number; + categoryId: string; + merchantId?: string; + subcategoryIds: string[]; + tagIds: string[]; + description?: string; } export class Category { - id: number; - category: string; + id: string; + category: string; } export class SubCategory { - id: number; - subcategory: string; + id: string; + subcategory: string; } export class Merchant { - id: number; - merchant: string; + id: string; + merchant: string; } export class Tag { - id: number; - tag: string; + id: string; + tag: string; } diff --git a/src/controllers/expenses/expenses.controller.ts b/src/controllers/expenses/expenses.controller.ts index 2b3e75d..64523c1 100644 --- a/src/controllers/expenses/expenses.controller.ts +++ b/src/controllers/expenses/expenses.controller.ts @@ -1,6 +1,6 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, NotFoundException, HttpCode } from '@nestjs/common'; import { ExpensesService } from '../../services/expenses.service'; -import { CreateExpenseDto, GetExpenseDto, UpdateExpenseDto } from './expense.dto'; +import { CreateExpenseDto, UpdateExpenseDto } from './expense.dto'; @Controller('expenses') export class ExpensesController { @@ -12,22 +12,33 @@ export class ExpensesController { } @Get() - findAll(): GetExpenseDto[] { + findAll() { return this.expensesService.findAll(); } @Get(':id') findOne(@Param('id') id: string) { - return this.expensesService.findOne(+id); + const expense = this.expensesService.findOne(id); + if (!expense) { + throw new NotFoundException(); + } + + return expense; } - // @Patch(':id') - // update(@Param('id') id: string, @Body() updateExpenseDto: UpdateExpenseDto) { - // return this.expensesService.update(+id, updateExpenseDto); - // } - // - // @Delete(':id') - // remove(@Param('id') id: string) { - // return this.expensesService.remove(+id); - // } + @Patch(':id') + update(@Param('id') id: string, @Body() updateExpenseDto: UpdateExpenseDto) { + const expense = this.expensesService.update(id, updateExpenseDto); + if (!expense) { + throw new NotFoundException(); + } + + return expense; + } + + @Delete(':id') + @HttpCode(204) + remove(@Param('id') id: string) { + return this.expensesService.remove(id); + } } diff --git a/src/services/expenses.service.ts b/src/services/expenses.service.ts index 91b1a83..f58da2e 100644 --- a/src/services/expenses.service.ts +++ b/src/services/expenses.service.ts @@ -1,40 +1,142 @@ import { Injectable } from '@nestjs/common'; -import { Expense } from '../controllers/expenses/expense.entities'; +import { Category, Expense, Merchant, SubCategory, Tag } from '../controllers/expenses/expense.entities'; import { CreateExpenseDto, GetExpenseDto, UpdateExpenseDto } from '../controllers/expenses/expense.dto'; +import { randomUUID } from 'node:crypto'; @Injectable() export class ExpensesService { - private fakeTempData: GetExpenseDto[] = [ + private categories: Category[] = [ { - id: 123, - year: '2025', - month: '12', - day: '10', - cents: 987, - category: 'Automotive', - merchant: '', - subcategory: [], - tags: [] + id: '1', + category: 'Category 1' + } + ]; + private merchants: Merchant[] = [ + { + id: '1', + merchant: 'Merchant 1' + } + ]; + private subcategories: SubCategory[] = [ + { + id: '1', + subcategory: 'Subcategory 1' + }, + { + id: '2', + subcategory: 'Subcategory 2' + } + ]; + private tags: Tag[] = [ + { + id: '1', + tag: 'Tag 1' + }, + { + id: '2', + tag: 'Tag 2' + } + ]; + private expenses: Expense[] = [ + { + id: '2aa94170-2c57-4c4f-a1e7-13544ba72917', + date: new Date('2025-12-01'), + cents: 15443, + categoryId: '1', + merchantId: '1', + subcategoryIds: ['1', '2'], + tagIds: ['1', '2'], + description: 'Full existing expense' + }, + { + id: '0708e7f7-3a2a-4b93-81da-38954925ca78', + date: new Date('2025-12-02'), + cents: 8723, + categoryId: '1', + subcategoryIds: [], + tagIds: [], + description: 'Partial existing expense' } ]; create(createExpenseDto: CreateExpenseDto) { - return 'This action adds a new expense'; + const expense = { + id: randomUUID(), + date: new Date(createExpenseDto.date), + cents: createExpenseDto.cents, + categoryId: createExpenseDto.categoryId, + merchantId: createExpenseDto.merchantId, + subcategoryIds: createExpenseDto.subcategoryIds ?? [], + tagIds: createExpenseDto.tagIds ?? [], + description: createExpenseDto.description + } as Expense; + this.expenses.push(expense); + + return this.mapExpense(expense); } - findAll(): GetExpenseDto[] { - return this.fakeTempData; + findAll() { + return this.expenses.map((expense) => { + return this.mapExpense(expense); + }); } - findOne(id: number) { - return `This action returns a #${id} expense`; + findOne(id: string) { + const expense = this.getExpense(id); + return expense ? this.mapExpense(expense) : undefined; } - // update(id: number, updateExpenseDto: UpdateExpenseDto) { - // return `This action updates a #${id} expense`; - // } - // - // remove(id: number) { - // return `This action removes a #${id} expense`; - // } + update(id: string, updateExpenseDto: UpdateExpenseDto) { + const index = this.expenses.findIndex((expense) => expense.id === id); + if (index) { + const expense = this.expenses[index]; + this.expenses[index] = { + ...expense, + ...updateExpenseDto + // date: updateExpenseDto.date ?? expense.date, + // cents: updateExpenseDto.cents ?? expense.cents, + // categoryId: updateExpenseDto.categoryId ?? expense.categoryId, + // merchant: updateExpenseDto.merchantId ?? expense.merchantId, + // subcategoryIds: updateExpenseDto.subcategoryIds ?? expense.subcategoryIds, + // tagIds: updateExpenseDto.tagIds ?? expense.tagIds, + // description: updateExpenseDto.description ?? expense.description + }; + + return this.mapExpense(this.expenses[index]); + } + + return undefined; + } + + remove(id: string) { + this.expenses = this.expenses.filter((expense) => expense.id !== id); + } + + private getExpense(id: string) { + return this.expenses.find((expense) => expense.id === id); + } + + private getSubcategories(ids: string[]) { + return this.subcategories.filter((sub) => ids.includes(sub.id)).map((s) => s.subcategory); + } + + private getTags(ids: string[]) { + return this.tags.filter((tag) => ids.includes(tag.id)).map((t) => t.tag); + } + + private mapExpense(expense: Expense) { + const category = this.categories.find((category) => category.id === expense.categoryId); + const merchant = this.merchants.find((merchant) => merchant.id === expense.merchantId); + + return { + id: expense.id, + date: expense.date, + cents: expense.cents, + category: category?.category, + merchant: merchant?.merchant, + subcategories: this.getSubcategories(expense.subcategoryIds), + tags: this.getTags(expense.tagIds), + description: expense.description + } as GetExpenseDto; + } }