Compare commits

...

3 commits

Author SHA1 Message Date
Joe Arndt
b5ced781c9 added merchants resource 2026-02-08 15:21:47 -06:00
Joe Arndt
d7ad5828af remove prettier 2026-02-07 21:58:23 -06:00
Joe Arndt
ccc6540ae8 installed typeorm and added initial merchant resource 2026-02-07 21:50:04 -06:00
23 changed files with 2126 additions and 338 deletions

View file

@ -1,6 +0,0 @@
{
"singleQuote": true,
"trailingComma": "none",
"printWidth": 160,
"bracketSameLine": true
}

View file

@ -0,0 +1,11 @@
meta {
name: LOC DELETE Merchant
type: http
seq: 5
}
delete {
url: {{localBaseUrl}}/merchants/cbf30070-9ff7-419f-a567-f7d145be445b
body: none
auth: inherit
}

View file

@ -0,0 +1,15 @@
meta {
name: LOC GET Merchant By ID
type: http
seq: 2
}
get {
url: {{localBaseUrl}}/merchants/{{merchantId}}
body: none
auth: inherit
}
vars:pre-request {
merchantId: 1d6d2842-b271-489b-bd93-e3ceaee5a139
}

View file

@ -0,0 +1,11 @@
meta {
name: LOC GET Merchants
type: http
seq: 1
}
get {
url: {{localBaseUrl}}/merchants
body: none
auth: inherit
}

View file

@ -0,0 +1,17 @@
meta {
name: LOC POST Merchant
type: http
seq: 3
}
post {
url: {{localBaseUrl}}/merchants
body: json
auth: inherit
}
body:json {
{
"name": "Walmart"
}
}

View file

@ -0,0 +1,18 @@
meta {
name: LOC PUT Merchant
type: http
seq: 4
}
put {
url: {{localBaseUrl}}/merchants
body: json
auth: inherit
}
body:json {
{
"id": "1d6d2842-b271-489b-bd93-e3ceaee5a139",
"name": "Walmart"
}
}

View file

@ -0,0 +1,8 @@
meta {
name: Merchants
seq: 3
}
auth {
mode: inherit
}

BIN
common-cents.db Normal file

Binary file not shown.

View file

@ -1,6 +1,5 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
@ -10,7 +9,6 @@ export default tseslint.config(
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
@ -21,15 +19,14 @@ export default tseslint.config(
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
}
}
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn',
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
},
'@typescript-eslint/no-unsafe-argument': 'warn'
}
}
);

1929
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -24,8 +24,11 @@
"@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"sqlite3": "^5.1.7",
"typeorm": "^0.3.28"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@ -42,7 +45,6 @@
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",

View file

@ -1,11 +1,20 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ExpensesService } from './services/expenses.service';
import { ExpensesController } from './controllers/expenses/expenses.controller';
import { MerchantsModule } from './merchants/merchants.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Merchant } from './merchants/entities/merchant.entity';
@Module({
imports: [],
controllers: [AppController, ExpensesController],
providers: [ExpensesService]
imports: [
MerchantsModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'common-cents.db',
entities: [Merchant],
// synchronize: true // schema:sync
})
],
controllers: [AppController],
providers: []
})
export class AppModule {}
export class AppModule { }

View file

@ -1,24 +0,0 @@
import { PartialType } from '@nestjs/mapped-types';
export class CreateExpenseDto {
date: Date;
cents: number;
categoryId: string;
merchantId?: string;
subcategoryIds?: string[];
tagIds?: string[];
description?: string;
}
export class UpdateExpenseDto extends PartialType(CreateExpenseDto) {}
export class GetExpenseDto {
id: string;
date: Date;
cents: number;
category: string;
merchant?: string;
subcategories?: string[];
tags?: string[];
description?: string;
}

View file

@ -1,30 +0,0 @@
export class Expense {
id: string;
date: Date;
cents: number;
categoryId: string;
merchantId?: string;
subcategoryIds: string[];
tagIds: string[];
description?: string;
}
export class Category {
id: string;
category: string;
}
export class SubCategory {
id: string;
subcategory: string;
}
export class Merchant {
id: string;
merchant: string;
}
export class Tag {
id: string;
tag: string;
}

View file

@ -1,44 +0,0 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, NotFoundException, HttpCode } from '@nestjs/common';
import { ExpensesService } from '../../services/expenses.service';
import { CreateExpenseDto, UpdateExpenseDto } from './expense.dto';
@Controller('expenses')
export class ExpensesController {
constructor(private readonly expensesService: ExpensesService) {}
@Post()
create(@Body() createExpenseDto: CreateExpenseDto) {
return this.expensesService.create(createExpenseDto);
}
@Get()
findAll() {
return this.expensesService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
const expense = this.expensesService.findOne(id);
if (!expense) {
throw new NotFoundException();
}
return expense;
}
@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);
}
}

View file

@ -0,0 +1,3 @@
export class CreateMerchantDto {
name: string;
}

View file

@ -0,0 +1,4 @@
export class UpdateMerchantDto {
id: string;
name: string;
}

View file

@ -0,0 +1,10 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Merchant {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
}

View file

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Merchant } from './entities/merchant.entity';
import { UpdateMerchantDto } from './dto/update-merchant.dto';
@Injectable()
export class MerchantDataService {
private merchants: Repository<Merchant>;
public constructor(private dataSource: DataSource) {
this.merchants = this.dataSource.getRepository(Merchant);
}
public async getAllMerchants(): Promise<Merchant[]> {
return this.merchants.find();
}
public async getMerchantById(id: string): Promise<Merchant | null> {
return await this.merchants.findOneBy({ id });
}
public async createMerchant(name: string): Promise<Merchant> {
return await this.merchants.save({ name });
}
public async updateMerchant(updateMerchant: UpdateMerchantDto): Promise<Merchant> {
return await this.merchants.save(updateMerchant);
}
public async deleteMerchant(id: string): Promise<void> {
await this.merchants.delete({ id });
}
}

View file

@ -0,0 +1,75 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
HttpCode,
HttpStatus,
NotFoundException,
BadRequestException,
InternalServerErrorException
} from '@nestjs/common';
import { MerchantsService } from './merchants.service';
import { CreateMerchantDto } from './dto/create-merchant.dto';
import { UpdateMerchantDto } from './dto/update-merchant.dto';
import { Merchant } from './entities/merchant.entity';
@Controller('merchants')
export class MerchantsController {
public constructor(private readonly merchantsService: MerchantsService) { }
@Get()
@HttpCode(HttpStatus.OK)
public async findAll(): Promise<Merchant[]> {
return await this.merchantsService.findAll();
}
@Get(':id')
@HttpCode(HttpStatus.OK)
public async findOne(@Param('id') id: string): Promise<Merchant> {
if (!id) {
throw new BadRequestException('No ID provided.');
}
try {
return await this.merchantsService.findById(id);
}
catch (error) {
throw new NotFoundException(error);
}
}
@Post()
@HttpCode(HttpStatus.CREATED)
public async create(@Body() newMerchant: CreateMerchantDto): Promise<Merchant> {
if (!newMerchant.name) {
throw new BadRequestException('Merchant name cannot be empty.');
}
try {
return await this.merchantsService.create(newMerchant);
}
catch (error) {
throw new InternalServerErrorException(error);
}
}
@Put()
@HttpCode(HttpStatus.OK)
public async update(@Body() updateMerchant: UpdateMerchantDto) {
if (!updateMerchant.id) {
return new BadRequestException('Merchant ID cannot be empty.');
}
return this.merchantsService.update(updateMerchant);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
public async remove(@Param('id') id: string): Promise<void> {
return this.merchantsService.remove(id);
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { MerchantsService } from './merchants.service';
import { MerchantsController } from './merchants.controller';
import { MerchantDataService } from './merchant-data.service';
@Module({
controllers: [MerchantsController],
providers: [MerchantsService, MerchantDataService]
})
export class MerchantsModule {}

View file

@ -0,0 +1,35 @@
import { Injectable } from '@nestjs/common';
import { CreateMerchantDto } from './dto/create-merchant.dto';
import { UpdateMerchantDto } from './dto/update-merchant.dto';
import { MerchantDataService } from './merchant-data.service';
import { Merchant } from './entities/merchant.entity';
@Injectable()
export class MerchantsService {
public constructor(private merchantDataService: MerchantDataService) { }
public async findAll(): Promise<Merchant[]> {
return await this.merchantDataService.getAllMerchants();
}
public async findById(id: string): Promise<Merchant> {
const merchant = await this.merchantDataService.getMerchantById(id);
if (!merchant) {
throw new Error('Merchant not found.');
}
return merchant;
}
public async create(newMerchant: CreateMerchantDto): Promise<Merchant> {
return await this.merchantDataService.createMerchant(newMerchant.name);
}
public async update(updateMerchant: UpdateMerchantDto): Promise<Merchant> {
return await this.merchantDataService.updateMerchant(updateMerchant);
}
public async remove(id: string): Promise<void> {
await this.merchantDataService.deleteMerchant(id);
}
}

View file

@ -1,141 +0,0 @@
import { Injectable } from '@nestjs/common';
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 categories: Category[] = [
{
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'
}
];
public create(createExpenseDto: CreateExpenseDto) {
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);
}
public findAll() {
return this.expenses.map((expense) => {
return this.mapExpense(expense);
});
}
public findOne(id: string) {
const expense = this.getExpense(id);
return expense ? this.mapExpense(expense) : undefined;
}
public update(id: string, updateExpenseDto: UpdateExpenseDto) {
let index;
const expense = this.expenses.find((exp, idx) => {
if (exp.id === id) {
index = idx;
return exp;
}
});
if (expense && index) {
this.expenses[index] = {
...expense,
...updateExpenseDto
};
return this.mapExpense(this.expenses[index]);
}
return undefined;
}
public 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;
}
}