From 6600745072e17eb7579f9a2ac2080cbd2545c4b9 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 9 Feb 2026 20:26:34 +0000 Subject: [PATCH] Rework Expenses resource (#3) Co-authored-by: Joe Arndt Reviewed-on: https://git.8yte.me/joe/common-cents-api/pulls/3 --- .gitignore | 3 + bruno/Common Cents/Categories/folder.bru | 1 + .../Common Cents/Expenses/LOC DEL Expense.bru | 11 --- .../Expenses/LOC DELETE Expense.bru | 15 ++++ .../Expenses/LOC GET Expense By ID.bru | 6 +- .../Expenses/LOC GET Expenses.bru | 2 +- .../Expenses/LOC PATCH Expense Full.bru | 23 ------ .../Expenses/LOC POST Expense Full.bru | 23 ------ .../Expenses/LOC POST Expense Partial.bru | 20 ----- .../Expenses/LOC POST Expense.bru | 38 +++++++++ .../Common Cents/Expenses/LOC PUT Expense.bru | 21 +++++ bruno/Common Cents/Expenses/folder.bru | 9 ++- bruno/Common Cents/Merchants/folder.bru | 1 + bruno/Common Cents/Sub-categories/folder.bru | 1 + bruno/Common Cents/Tags/folder.bru | 1 + common-cents.db | Bin 45056 -> 0 bytes src/app.module.ts | 14 ++-- src/categories/entities/category.entity.ts | 2 +- src/expenses/dto/create-expense.dto.ts | 10 +++ src/expenses/dto/update-expense.dto.ts | 6 ++ src/expenses/entities/expense.entity.ts | 39 +++++++++ src/expenses/expense-data.service.ts | 34 ++++++++ src/expenses/expenses.controller.ts | 75 ++++++++++++++++++ src/expenses/expenses.module.ts | 10 +++ src/expenses/expenses.service.ts | 35 ++++++++ src/merchants/entities/merchant.entity.ts | 2 +- .../entities/sub-category.entity.ts | 2 +- src/tags/entities/tag.entity.ts | 2 +- 28 files changed, 317 insertions(+), 89 deletions(-) delete mode 100644 bruno/Common Cents/Expenses/LOC DEL Expense.bru create mode 100644 bruno/Common Cents/Expenses/LOC DELETE Expense.bru delete mode 100644 bruno/Common Cents/Expenses/LOC PATCH Expense Full.bru delete mode 100644 bruno/Common Cents/Expenses/LOC POST Expense Full.bru delete mode 100644 bruno/Common Cents/Expenses/LOC POST Expense Partial.bru create mode 100644 bruno/Common Cents/Expenses/LOC POST Expense.bru create mode 100644 bruno/Common Cents/Expenses/LOC PUT Expense.bru delete mode 100644 common-cents.db create mode 100644 src/expenses/dto/create-expense.dto.ts create mode 100644 src/expenses/dto/update-expense.dto.ts create mode 100644 src/expenses/entities/expense.entity.ts create mode 100644 src/expenses/expense-data.service.ts create mode 100644 src/expenses/expenses.controller.ts create mode 100644 src/expenses/expenses.module.ts create mode 100644 src/expenses/expenses.service.ts diff --git a/.gitignore b/.gitignore index 4b56acf..2709a60 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Database +common-cents.db \ No newline at end of file diff --git a/bruno/Common Cents/Categories/folder.bru b/bruno/Common Cents/Categories/folder.bru index c32401e..ae65a21 100644 --- a/bruno/Common Cents/Categories/folder.bru +++ b/bruno/Common Cents/Categories/folder.bru @@ -1,5 +1,6 @@ meta { name: Categories + seq: 2 } auth { diff --git a/bruno/Common Cents/Expenses/LOC DEL Expense.bru b/bruno/Common Cents/Expenses/LOC DEL Expense.bru deleted file mode 100644 index dfb6024..0000000 --- a/bruno/Common Cents/Expenses/LOC DEL Expense.bru +++ /dev/null @@ -1,11 +0,0 @@ -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 DELETE Expense.bru b/bruno/Common Cents/Expenses/LOC DELETE Expense.bru new file mode 100644 index 0000000..6725687 --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC DELETE Expense.bru @@ -0,0 +1,15 @@ +meta { + name: LOC DELETE Expense + type: http + seq: 5 +} + +delete { + url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}} + body: none + auth: inherit +} + +vars:pre-request { + resourceId: 41294f87-ab77-4fdd-9509-6968e6533962 +} diff --git a/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru b/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru index 935e4ee..c591a98 100644 --- a/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru +++ b/bruno/Common Cents/Expenses/LOC GET Expense By ID.bru @@ -5,7 +5,11 @@ meta { } get { - url: {{localBaseUrl}}/expenses/2aa94170-2c57-4c4f-a1e7-13544ba72917 + url: {{localBaseUrl}}/{{resourcePath}}/{{resourceId}} body: none auth: inherit } + +vars:pre-request { + resourceId: 1d6d2842-b271-489b-bd93-e3ceaee5a139 +} diff --git a/bruno/Common Cents/Expenses/LOC GET Expenses.bru b/bruno/Common Cents/Expenses/LOC GET Expenses.bru index b2e8e2c..17a85b6 100644 --- a/bruno/Common Cents/Expenses/LOC GET Expenses.bru +++ b/bruno/Common Cents/Expenses/LOC GET Expenses.bru @@ -5,7 +5,7 @@ meta { } get { - url: {{localBaseUrl}}/expenses + url: {{localBaseUrl}}/{{resourcePath}} body: none auth: inherit } diff --git a/bruno/Common Cents/Expenses/LOC PATCH Expense Full.bru b/bruno/Common Cents/Expenses/LOC PATCH Expense Full.bru deleted file mode 100644 index e9a5f02..0000000 --- a/bruno/Common Cents/Expenses/LOC PATCH Expense Full.bru +++ /dev/null @@ -1,23 +0,0 @@ -meta { - name: LOC PATCH Expense Full - type: http - seq: 6 -} - -patch { - url: {{localBaseUrl}}/expenses/0708e7f7-3a2a-4b93-81da-38954925ca78 - body: json - auth: inherit -} - -body:json { - { - "date": "2025-12-15", - "cents": 888, - "categoryId": "1", - "merchantId": "1", - "subcategoryIds": ["2"], - "tagIds": ["2"], - "description": "PATCHED Desc." - } -} diff --git a/bruno/Common Cents/Expenses/LOC POST Expense Full.bru b/bruno/Common Cents/Expenses/LOC POST Expense Full.bru deleted file mode 100644 index d0e6451..0000000 --- a/bruno/Common Cents/Expenses/LOC POST Expense Full.bru +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index bfc7302..0000000 --- a/bruno/Common Cents/Expenses/LOC POST Expense Partial.bru +++ /dev/null @@ -1,20 +0,0 @@ -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/bruno/Common Cents/Expenses/LOC POST Expense.bru b/bruno/Common Cents/Expenses/LOC POST Expense.bru new file mode 100644 index 0000000..a458095 --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC POST Expense.bru @@ -0,0 +1,38 @@ +meta { + name: LOC POST Expense + type: http + seq: 3 +} + +post { + url: {{localBaseUrl}}/{{resourcePath}} + body: json + auth: inherit +} + +body:json { + { + "year": "2026", + "month": "01", + "day": "02", + "cents": 1000, + "description": "With cat, subcat and merchant and two tags", + "category": { + "id": "a73fbfdd-1949-4be9-8cdc-ea9197b9b8b6" + }, + "subCategory": { + "id": "270ceaea-9cb8-4c6a-846f-ea35ed4d12f7" + }, + "merchant": { + "id": "61246db4-3110-4fe2-bdab-d819b8fd0705" + }, + "tags": [ + { + "id": "2616f724-f3ce-46df-8b30-897f147f6b74" + }, + { + "id": "16ed84e0-2d17-45c7-ada8-6fe7f8cc8720" + } + ] + } +} diff --git a/bruno/Common Cents/Expenses/LOC PUT Expense.bru b/bruno/Common Cents/Expenses/LOC PUT Expense.bru new file mode 100644 index 0000000..7042846 --- /dev/null +++ b/bruno/Common Cents/Expenses/LOC PUT Expense.bru @@ -0,0 +1,21 @@ +meta { + name: LOC PUT Expense + type: http + seq: 4 +} + +put { + url: {{localBaseUrl}}/{{resourcePath}} + body: json + auth: inherit +} + +body:json { + { + "id": "0254a07d-9dfa-4c06-b7b3-48d30efff991", + "description": "Cat, no sub-cat or merchant", + "category": { + "id": "db434e98-5f32-4202-b69c-eb7d1d248b3e" + } + } +} diff --git a/bruno/Common Cents/Expenses/folder.bru b/bruno/Common Cents/Expenses/folder.bru index 9a3fbf0..5e0ac02 100644 --- a/bruno/Common Cents/Expenses/folder.bru +++ b/bruno/Common Cents/Expenses/folder.bru @@ -1,4 +1,11 @@ meta { name: Expenses - seq: 2 +} + +auth { + mode: inherit +} + +vars:pre-request { + resourcePath: expenses } diff --git a/bruno/Common Cents/Merchants/folder.bru b/bruno/Common Cents/Merchants/folder.bru index 2ffb460..dc13882 100644 --- a/bruno/Common Cents/Merchants/folder.bru +++ b/bruno/Common Cents/Merchants/folder.bru @@ -1,5 +1,6 @@ meta { name: Merchants + seq: 3 } auth { diff --git a/bruno/Common Cents/Sub-categories/folder.bru b/bruno/Common Cents/Sub-categories/folder.bru index a7f720c..47ece91 100644 --- a/bruno/Common Cents/Sub-categories/folder.bru +++ b/bruno/Common Cents/Sub-categories/folder.bru @@ -1,5 +1,6 @@ meta { name: Sub-categories + seq: 4 } auth { diff --git a/bruno/Common Cents/Tags/folder.bru b/bruno/Common Cents/Tags/folder.bru index aa07bea..ba06b7e 100644 --- a/bruno/Common Cents/Tags/folder.bru +++ b/bruno/Common Cents/Tags/folder.bru @@ -1,5 +1,6 @@ meta { name: Tags + seq: 5 } auth { diff --git a/common-cents.db b/common-cents.db deleted file mode 100644 index 656df02793aebdfd293419782f8ff440411bd172..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45056 zcmeI*O;6)S7zc2hP~L-Eu~JkaR4WoHD|T3qXZ)tBQYqrl!oKlx;1tc+V-{&iN(iNU z=z+@~d+fRD7wEO$U{&?fW4}UAz3#D390CdIc2gmc*!(RSJGRF&^RtDNnRzDL&o&QQ z#?2eeL)CJH^`T|k))%g8S=K#Tr)iy_)uwfd)=^r!OZ(WnmV4Gp^XKyHXVzqS#&UL? z*R#Jlto&Dbz5MI!*X5a+H}ARu&O-nK5P$##AOHafKmY=FLEvnDYP^yp_Sv(ukIwf# zZz=u4w9Mf*jize8tR0`|TB=%RztMd8??@eFE`iSYHxz7yHq{Ms_wU{ znLbxd_vzMGk5;$#+>P};_wkb*_wnxL<^#7{SBK{EqV9?N8`EQzT4GxV^~`*CeDs38 z1l1xswz1fMOr5kE#Wj6}t?}NHOdm~*R~8oRv!JM7w?^IVSbrV5hp$)uhm&KKwS~8p zwVOwcclRcrj*V58m+ifxi02hPHb*C>o|-Ek$NLI=<>XQu^Z}*3@4T^`%HpDZ*16~R z^pM-1IuE(mr+5Fx9;hyjSLWvIv*pe^ruJKEf3&Y6RXTD#^u^J!N;3C0bh>?w)1MEH z?B3Vm(6ryrdaJt~=`U3G@byaeaOA&Uhi)D<-rXzFe~|X~daF!p$*Eb+@6M03!2$sY zKmY;|fB*y_009U<00Izz!2cHrN~PWVA@^mNX~_iV9+SE8na-4ESC`iFg)fek>^mKjKWr ziKe^~!Hh_aGA2-5Bo~RE)J?lnzvR5KoIjmkXoCd;5P$##AOHafKmY;|fB*y_0D&75 zSSsz>gR~Nq+7}MeE->2|HpYe#vAdQ&HRLsKMM0uR1apZeDYQHlQV1-5k%qN1z4DeX7 zIMN}Lp(mNvGNA5#lW7qs8OE|maoSj+K>+i9$iqDHCCjPTpRUhx7Hi?LIEivDqde46 zaeZ(9|Eu;0fFXJms44^?009U<00Izz00bZa0SG|g)&<(#`hzqJ6#f5$v<4LY|ARCG zApZZ>A2u?C00bZa0SG_<0uX=z1Rwx`_fMdR|5uU7b)IF6Co*9a%%8A0%~EDm!jnkT z8~{38JD&!#C;s1A&Ut{r=&jMi77i1Rwwb2tWV=5P$##AOL}( z68P9IB@@LDY&dplaiaJ47>3FLm4yHVAOHafKmY;|fB*y_009ULsldhj|A_w|(l-Fr zh5!U0009U<00Izz00bZaf!h;k$Nvu!B~Zlw4-ywp#QzTx3($%G-?p6B&JWJ^?bR4L zK>z{}fB*y_009U<00Izz00i#1KthxJb3gJ@ql{uns$(Y8kS6b!VNNmsA~2cExSvPc zCz_>wasLs`_RsP>$bC=wOzB9_bOBMubi@sdxe2sTbW)oBq^FIDru}Dzdt7 Category, { eager: true }) + category: Category; + + @ManyToOne(() => SubCategory, { nullable: true, eager: true }) + subCategory: SubCategory; + + @ManyToOne(() => Merchant, { nullable: true, eager: true }) + merchant: Merchant; + + @ManyToMany(() => Tag, { nullable: true, eager: true }) + @JoinTable() + tags: Tag[]; +} diff --git a/src/expenses/expense-data.service.ts b/src/expenses/expense-data.service.ts new file mode 100644 index 0000000..ceff180 --- /dev/null +++ b/src/expenses/expense-data.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource, Repository } from 'typeorm'; +import { Expense } from './entities/expense.entity'; +import { UpdateExpenseDto } from './dto/update-expense.dto'; +import { CreateExpenseDto } from './dto/create-expense.dto'; + +@Injectable() +export class ExpenseDataService { + private expenses: Repository; + + public constructor(private dataSource: DataSource) { + this.expenses = this.dataSource.getRepository(Expense); + } + + public async getAll(): Promise { + return await this.expenses.find(); + } + + public async getById(id: string): Promise { + return await this.expenses.findOneBy({ id }); + } + + public async create(expense: CreateExpenseDto): Promise { + return await this.expenses.save(expense); + } + + public async update(expense: UpdateExpenseDto): Promise { + return await this.expenses.save(expense); + } + + public async delete(id: string): Promise { + await this.expenses.delete({ id }); + } +} diff --git a/src/expenses/expenses.controller.ts b/src/expenses/expenses.controller.ts new file mode 100644 index 0000000..98350b9 --- /dev/null +++ b/src/expenses/expenses.controller.ts @@ -0,0 +1,75 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Body, + Param, + HttpCode, + HttpStatus, + BadRequestException, + NotFoundException, + InternalServerErrorException +} from '@nestjs/common'; +import { ExpensesService } from './expenses.service'; +import { CreateExpenseDto } from './dto/create-expense.dto'; +import { UpdateExpenseDto } from './dto/update-expense.dto'; +import { Expense } from './entities/expense.entity'; + +@Controller('expenses') +export class ExpensesController { + constructor(private readonly expensesService: ExpensesService) { } + + @Get() + @HttpCode(HttpStatus.OK) + public async findAll(): Promise { + return await this.expensesService.findAll(); + } + + @Get(':id') + @HttpCode(HttpStatus.OK) + public async findOne(@Param('id') id: string): Promise { + if (!id) { + throw new BadRequestException('No ID provided.'); + } + + try { + return await this.expensesService.findById(id); + } + catch (error) { + throw new NotFoundException(error); + } + } + + @Post() + @HttpCode(HttpStatus.CREATED) + public async create(@Body() expense: CreateExpenseDto): Promise { + if (!expense) { + throw new BadRequestException('Expense name cannot be empty.'); + } + + try { + return await this.expensesService.create(expense); + } + catch (error) { + throw new InternalServerErrorException(error); + } + } + + @Put() + @HttpCode(HttpStatus.OK) + public async update(@Body() expense: UpdateExpenseDto): Promise { + if (!expense.id) { + throw new BadRequestException('Expense ID cannot be empty.'); + } + + return await this.expensesService.update(expense); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + public async remove(@Param('id') id: string): Promise { + return await this.expensesService.remove(id); + } +} diff --git a/src/expenses/expenses.module.ts b/src/expenses/expenses.module.ts new file mode 100644 index 0000000..2179916 --- /dev/null +++ b/src/expenses/expenses.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ExpensesService } from './expenses.service'; +import { ExpensesController } from './expenses.controller'; +import { ExpenseDataService } from './expense-data.service'; + +@Module({ + controllers: [ExpensesController], + providers: [ExpensesService, ExpenseDataService], +}) +export class ExpensesModule { } diff --git a/src/expenses/expenses.service.ts b/src/expenses/expenses.service.ts new file mode 100644 index 0000000..4e822e6 --- /dev/null +++ b/src/expenses/expenses.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { CreateExpenseDto } from './dto/create-expense.dto'; +import { UpdateExpenseDto } from './dto/update-expense.dto'; +import { ExpenseDataService } from './expense-data.service'; +import { Expense } from './entities/expense.entity'; + +@Injectable() +export class ExpensesService { + public constructor(private expenseDataService: ExpenseDataService) { } + + public async findAll(): Promise { + return await this.expenseDataService.getAll(); + } + + public async findById(id: string): Promise { + const expense = await this.expenseDataService.getById(id); + if (!expense) { + throw new Error('No expense found'); + } + + return expense; + } + + public async create(expense: CreateExpenseDto): Promise { + return await this.expenseDataService.create(expense); + } + + public async update(expense: UpdateExpenseDto): Promise { + return await this.expenseDataService.update(expense); + } + + public async remove(id: string): Promise { + await this.expenseDataService.delete(id); + } +} diff --git a/src/merchants/entities/merchant.entity.ts b/src/merchants/entities/merchant.entity.ts index 0a5297f..87a61f9 100644 --- a/src/merchants/entities/merchant.entity.ts +++ b/src/merchants/entities/merchant.entity.ts @@ -5,6 +5,6 @@ export class Merchant { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ unique: true }) name: string; } diff --git a/src/sub-categories/entities/sub-category.entity.ts b/src/sub-categories/entities/sub-category.entity.ts index 87f85ae..f6bf282 100644 --- a/src/sub-categories/entities/sub-category.entity.ts +++ b/src/sub-categories/entities/sub-category.entity.ts @@ -5,6 +5,6 @@ export class SubCategory { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ unique: true }) name: string; } diff --git a/src/tags/entities/tag.entity.ts b/src/tags/entities/tag.entity.ts index fdf7a2d..ed84b59 100644 --- a/src/tags/entities/tag.entity.ts +++ b/src/tags/entities/tag.entity.ts @@ -5,6 +5,6 @@ export class Tag { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ unique: true }) name: string; }