From ee823d015b365c294de84dc0d1a7c91504975827 Mon Sep 17 00:00:00 2001 From: Joe Arndt Date: Thu, 19 Feb 2026 14:03:28 -0600 Subject: [PATCH] refactored expense date to be a Temporal.PlainDate --- .../Categories/LOC POST Category.bru | 2 +- .../Expenses/LOC POST Expense.bru | 30 ++----- .../Merchants/LOC POST Merchant.bru | 2 +- bruno/Common Cents/seed-data.js | 80 +++++++++++++++++++ package-lock.json | 19 +++++ package.json | 1 + src/expenses/dto/create-expense.dto.ts | 32 +++----- src/expenses/dto/get-expense.dto.ts | 43 ++++++++++ src/expenses/entities/expense.entity.ts | 8 +- src/expenses/expense-data.service.ts | 25 +++++- src/expenses/expenses.controller.ts | 10 +-- src/expenses/expenses.service.ts | 55 ++++++++++--- 12 files changed, 233 insertions(+), 74 deletions(-) create mode 100644 bruno/Common Cents/seed-data.js create mode 100644 src/expenses/dto/get-expense.dto.ts diff --git a/bruno/Common Cents/Categories/LOC POST Category.bru b/bruno/Common Cents/Categories/LOC POST Category.bru index 016814e..97251b0 100644 --- a/bruno/Common Cents/Categories/LOC POST Category.bru +++ b/bruno/Common Cents/Categories/LOC POST Category.bru @@ -12,6 +12,6 @@ post { body:json { { - "name": "Household:Supplies" + "name": "Gas" } } diff --git a/bruno/Common Cents/Expenses/LOC POST Expense.bru b/bruno/Common Cents/Expenses/LOC POST Expense.bru index 7819293..1427b9c 100644 --- a/bruno/Common Cents/Expenses/LOC POST Expense.bru +++ b/bruno/Common Cents/Expenses/LOC POST Expense.bru @@ -12,30 +12,10 @@ post { body:json { { - "year": "2026", - "month": "01", - "day": "03", - "cents": 1234, - "note": "Additional Expense Test", - "category": { - "id": "96db93d8-2f7e-462c-b520-8754a5900253" - }, - "merchant": { - "id": "9afbe4fd-9077-473a-9d08-f5fb4c8f692f" - }, - "tags": [ - { - "id": "db6d60d9-66e1-4005-a5a3-1e165812aa3c", - "name": "Tag One" - }, - { - "id": "999a8e68-5232-43e0-adf5-98af75aa01ba", - "name": "Tag Two" - }, - { - "id": "619d5c95-65fc-4795-8828-ee1adfe289f8", - "name": "Tag Three" - } - ] + "date": "2026-01-03", + "cents": 1010, + "note": "Gas", + "categoryId": "db21acbb-0e3e-4ea1-89e1-3d52e5d72cb4", + "marchantId": "b9028a32-1305-4699-8611-d8fd812ebc04" } } diff --git a/bruno/Common Cents/Merchants/LOC POST Merchant.bru b/bruno/Common Cents/Merchants/LOC POST Merchant.bru index 60e5466..b5665ab 100644 --- a/bruno/Common Cents/Merchants/LOC POST Merchant.bru +++ b/bruno/Common Cents/Merchants/LOC POST Merchant.bru @@ -12,6 +12,6 @@ post { body:json { { - "name": "Merchant Three" + "name": "Casey's" } } diff --git a/bruno/Common Cents/seed-data.js b/bruno/Common Cents/seed-data.js new file mode 100644 index 0000000..56ebf46 --- /dev/null +++ b/bruno/Common Cents/seed-data.js @@ -0,0 +1,80 @@ +const postFakeCategories = [ + { + name: "Utilities" + }, + { + name: "Automotive:Gasoline" + }, + { + name: "Groceries:Food" + } +]; +const postFakeMerchants = [ + { + name: "Walmart" + }, + { + name: "Casey's" + }, + { + name: "Electric Company" + } +]; +const postFakeTags = [ + { + name: "Truck" + }, + { + name: "Van" + } +]; +const postFakeExpenses = [ + { + year: "2025", + month: "12", + day: "28", + cents: 12153, + category: { + name: "Utilities" + }, + merchant: { + name: "Electric Company" + } + }, + { + year: "2025", + month: "12", + day: "31", + cents: 5500, + category: { + name: "Automotive:Gasoline" + }, + merchant: { + name: "Casey's" + }, + tags: [ + { + name: "Truck" + } + ] + } +]; + +console.log({ postFakeCategories, postFakeMerchants, postFakeTags, postFakeExpenses }); + + +// async function fetchData(url) { +// try { +// const response = await fetch(url); +// if (!response.ok) { +// throw new Error('Network response was not ok'); +// } +// const data = await response.json(); // Wait for the JSON data to be parsed +// console.log(data); +// } catch (error) { +// console.error('Error:', error); +// } +// } + +// // Call the async function +// fetchData('https://api.example.com/data'); diff --git a/package-lock.json b/package-lock.json index 0dbd7e9..ae2ff50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@js-temporal/polyfill": "^0.5.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", @@ -2063,6 +2064,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-temporal/polyfill": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz", + "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==", + "license": "ISC", + "dependencies": { + "jsbi": "^4.3.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -7831,6 +7844,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz", + "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==", + "license": "Apache-2.0" + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", diff --git a/package.json b/package.json index 2a178a2..9237d8a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@js-temporal/polyfill": "^0.5.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.3", "@nestjs/core": "^11.0.1", diff --git a/src/expenses/dto/create-expense.dto.ts b/src/expenses/dto/create-expense.dto.ts index a5fd18e..829a5be 100644 --- a/src/expenses/dto/create-expense.dto.ts +++ b/src/expenses/dto/create-expense.dto.ts @@ -1,23 +1,11 @@ -import { Category } from '../../categories/entities/category.entity'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Merchant } from '../../merchants/entities/merchant.entity'; -import { Tag } from '../../tags/entities/tag.entity'; +import { Temporal } from '@js-temporal/polyfill'; export class CreateExpenseDto { @ApiProperty({ - description: '4-digit year of expense' + description: 'Date in YYYY-MM-DD format' }) - year: string; - - @ApiProperty({ - description: '2-digit month of expense' - }) - month: string; - - @ApiProperty({ - description: '2-digit day of expense' - }) - day: string; + date: Temporal.PlainDate; @ApiProperty({ description: 'Amount of expense in cents' @@ -25,9 +13,9 @@ export class CreateExpenseDto { cents: number; @ApiProperty({ - description: 'Category of expense' + description: 'Category ID of expense' }) - category: Category + categoryId: string; @ApiPropertyOptional({ description: 'Optional note about expense' @@ -35,13 +23,13 @@ export class CreateExpenseDto { note?: string; @ApiPropertyOptional({ - description: 'Optional merchant for the expense' + description: 'Optional merchant ID for the expense' }) - merchant?: Merchant; + merchantId?: string; @ApiPropertyOptional({ - type: [Tag], - description: 'Optional list of tags for the expense' + type: [String], + description: 'Optional list of tag IDs for the expense' }) - tags?: Tag[]; + tagIds: string[]; } diff --git a/src/expenses/dto/get-expense.dto.ts b/src/expenses/dto/get-expense.dto.ts new file mode 100644 index 0000000..0e9454e --- /dev/null +++ b/src/expenses/dto/get-expense.dto.ts @@ -0,0 +1,43 @@ +import { Category } from '../../categories/entities/category.entity'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Merchant } from '../../merchants/entities/merchant.entity'; +import { Tag } from '../../tags/entities/tag.entity'; +import { Temporal } from '@js-temporal/polyfill'; + +export class GetExpenseDto { + @ApiProperty({ + description: 'Unique ID of expense' + }) + id: string; + + @ApiProperty({ + description: 'Date in YYYY-MM-DD format' + }) + date: Temporal.PlainDate; + + @ApiProperty({ + description: 'Amount of expense in cents' + }) + cents: number; + + @ApiProperty({ + description: 'Category of expense' + }) + category: Category + + @ApiPropertyOptional({ + description: 'Note about expense' + }) + note?: string; + + @ApiPropertyOptional({ + description: 'Merchant for the expense' + }) + merchant?: Merchant; + + @ApiPropertyOptional({ + type: [Tag], + description: 'List of tags for the expense' + }) + tags?: Tag[]; +} diff --git a/src/expenses/entities/expense.entity.ts b/src/expenses/entities/expense.entity.ts index 33340ef..75b7cab 100644 --- a/src/expenses/entities/expense.entity.ts +++ b/src/expenses/entities/expense.entity.ts @@ -9,13 +9,7 @@ export class Expense { id: string; @Column() - year: string; - - @Column() - month: string; - - @Column() - day: string; + date: string; @Column() cents: number; diff --git a/src/expenses/expense-data.service.ts b/src/expenses/expense-data.service.ts index ceff180..b860822 100644 --- a/src/expenses/expense-data.service.ts +++ b/src/expenses/expense-data.service.ts @@ -1,8 +1,6 @@ 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 { @@ -20,11 +18,11 @@ export class ExpenseDataService { return await this.expenses.findOneBy({ id }); } - public async create(expense: CreateExpenseDto): Promise { + public async create(expense: CreateExpense): Promise { return await this.expenses.save(expense); } - public async update(expense: UpdateExpenseDto): Promise { + public async update(expense: UpdateExpense): Promise { return await this.expenses.save(expense); } @@ -32,3 +30,22 @@ export class ExpenseDataService { await this.expenses.delete({ id }); } } + +export interface CreateExpense { + date: string; + cents: number; + category: { + id: string; + }; + note?: string; + merchant?: { + id: string; + }; + tags?: { + id: string; + }[]; +} + +export interface UpdateExpense extends CreateExpense{ + id: string; +} diff --git a/src/expenses/expenses.controller.ts b/src/expenses/expenses.controller.ts index 98350b9..7550273 100644 --- a/src/expenses/expenses.controller.ts +++ b/src/expenses/expenses.controller.ts @@ -15,7 +15,7 @@ import { 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'; +import { GetExpenseDto } from './dto/get-expense.dto'; @Controller('expenses') export class ExpensesController { @@ -23,13 +23,13 @@ export class ExpensesController { @Get() @HttpCode(HttpStatus.OK) - public async findAll(): Promise { + public async findAll(): Promise { return await this.expensesService.findAll(); } @Get(':id') @HttpCode(HttpStatus.OK) - public async findOne(@Param('id') id: string): Promise { + public async findOne(@Param('id') id: string): Promise { if (!id) { throw new BadRequestException('No ID provided.'); } @@ -44,7 +44,7 @@ export class ExpensesController { @Post() @HttpCode(HttpStatus.CREATED) - public async create(@Body() expense: CreateExpenseDto): Promise { + public async create(@Body() expense: CreateExpenseDto): Promise { if (!expense) { throw new BadRequestException('Expense name cannot be empty.'); } @@ -59,7 +59,7 @@ export class ExpensesController { @Put() @HttpCode(HttpStatus.OK) - public async update(@Body() expense: UpdateExpenseDto): Promise { + public async update(@Body() expense: UpdateExpenseDto): Promise { if (!expense.id) { throw new BadRequestException('Expense ID cannot be empty.'); } diff --git a/src/expenses/expenses.service.ts b/src/expenses/expenses.service.ts index 4e822e6..c419e00 100644 --- a/src/expenses/expenses.service.ts +++ b/src/expenses/expenses.service.ts @@ -2,31 +2,68 @@ 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'; +import { GetExpenseDto } from './dto/get-expense.dto'; +import { Temporal } from '@js-temporal/polyfill'; @Injectable() export class ExpensesService { public constructor(private expenseDataService: ExpenseDataService) { } - public async findAll(): Promise { - return await this.expenseDataService.getAll(); + public async findAll(): Promise { + const expenses = await this.expenseDataService.getAll(); + + return expenses.map(exp => { + return { ...exp, date: Temporal.PlainDate.from(exp.date) }; + }) } - public async findById(id: string): Promise { + public async findById(id: string): Promise { const expense = await this.expenseDataService.getById(id); if (!expense) { throw new Error('No expense found'); } - return expense; + return { ...expense, date: Temporal.PlainDate.from(expense.date) }; } - public async create(expense: CreateExpenseDto): Promise { - return await this.expenseDataService.create(expense); + public async create(createExpense: CreateExpenseDto): Promise { + const date = createExpense.date.toString(); + const category = { id: createExpense.categoryId }; + const merchant = createExpense.merchantId ? { id: createExpense.merchantId } : undefined; + const tags = createExpense.tagIds?.map(id => { + return { id }; + }) + + const expense = await this.expenseDataService.create({ + date, + cents: createExpense.cents, + note: createExpense.note, + category, + merchant, + tags + }); + + return { ...expense, date: Temporal.PlainDate.from(expense.date) }; } - public async update(expense: UpdateExpenseDto): Promise { - return await this.expenseDataService.update(expense); + public async update(updateExpense: UpdateExpenseDto): Promise { + const date = updateExpense.date.toString(); + const category = { id: updateExpense.categoryId }; + const merchant = updateExpense.merchantId ? { id: updateExpense.merchantId } : undefined; + const tags = updateExpense.tagIds?.map(id => { + return { id }; + }) + const expense = await this.expenseDataService.update({ + id: updateExpense.id, + date, + cents: updateExpense.cents, + note: updateExpense.note, + category, + merchant, + tags + }); + + return { ...expense, date: Temporal.PlainDate.from(expense.date) }; } public async remove(id: string): Promise {