mirror of
https://github.com/netlight/my-finance-pal-backend.git
synced 2024-11-10 08:57:45 +01:00
Refactor Transaction to Expense
This commit is contained in:
parent
7df93b4ee9
commit
27565e3eff
30 changed files with 246 additions and 265 deletions
|
@ -4,8 +4,8 @@ info:
|
|||
title: My Finance Pal
|
||||
description: API of the personal finance budgeting app My Finance Pal
|
||||
tags:
|
||||
- name: transactions
|
||||
description: Transaction of budgets
|
||||
- name: expenses
|
||||
description: Expenses of budgets
|
||||
- name: budgets
|
||||
description: Managing budgets
|
||||
paths:
|
||||
|
@ -48,7 +48,7 @@ paths:
|
|||
get:
|
||||
tags:
|
||||
- budgets
|
||||
description: Get a budget summary including all transactions for a given ID
|
||||
description: Get a budget summary including all expenses for a given ID
|
||||
operationId: getBudgetSummary
|
||||
parameters:
|
||||
- in: path
|
||||
|
@ -70,7 +70,7 @@ paths:
|
|||
delete:
|
||||
tags:
|
||||
- budgets
|
||||
description: Delete a budget including all transactions
|
||||
description: Delete a budget including all expenses
|
||||
operationId: deleteBudget
|
||||
parameters:
|
||||
- in: path
|
||||
|
@ -84,12 +84,12 @@ paths:
|
|||
description: Budget successfully deleted
|
||||
404:
|
||||
description: Budget not found
|
||||
/budgets/{budgetId}/transactions:
|
||||
/budgets/{budgetId}/expenses:
|
||||
post:
|
||||
tags:
|
||||
- transactions
|
||||
description: Register a new transaction for a budget
|
||||
operationId: createTransaction
|
||||
- expenses
|
||||
description: Register a new expense for a budget
|
||||
operationId: createExpense
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/BudgetIdParam"
|
||||
requestBody:
|
||||
|
@ -97,14 +97,14 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NewTransaction"
|
||||
$ref: "#/components/schemas/NewExpense"
|
||||
responses:
|
||||
201:
|
||||
description: Transaction created
|
||||
description: Expense created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Transaction"
|
||||
$ref: "#/components/schemas/Expense"
|
||||
404:
|
||||
description: Budget not found
|
||||
|
||||
|
@ -117,13 +117,13 @@ components:
|
|||
description: The UUID of the budget
|
||||
schema:
|
||||
$ref: "#/components/schemas/BudgetId"
|
||||
TransactionIdParam:
|
||||
ExpenseIdParam:
|
||||
in: path
|
||||
name: transactionId
|
||||
name: expenseId
|
||||
required: true
|
||||
description: The UUID of the transaction
|
||||
description: The UUID of the expense
|
||||
schema:
|
||||
$ref: "#/components/schemas/TransactionId"
|
||||
$ref: "#/components/schemas/ExpenseId"
|
||||
schemas:
|
||||
BudgetId:
|
||||
type: string
|
||||
|
@ -153,7 +153,7 @@ components:
|
|||
spent:
|
||||
type: number
|
||||
minimum: 0
|
||||
description: The summed up spent amount of all transaction of that budget
|
||||
description: The summed up spent amount of all expenses of that budget
|
||||
startDate:
|
||||
type: string
|
||||
format: date
|
||||
|
@ -166,13 +166,13 @@ components:
|
|||
example: 2023-04-30
|
||||
BudgetSummary:
|
||||
type: object
|
||||
description: Summary of the whole budget including all transactions
|
||||
description: Summary of the whole budget including all expenses
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- limit
|
||||
- spent
|
||||
- transactions
|
||||
- expenses
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/BudgetId"
|
||||
|
@ -199,11 +199,11 @@ components:
|
|||
format: date
|
||||
description: Date marking the end of the budget period
|
||||
example: 2023-04-30
|
||||
transactions:
|
||||
expenses:
|
||||
type: array
|
||||
description: All transactions of the budget
|
||||
description: All expenses of the budget
|
||||
items:
|
||||
$ref: "#/components/schemas/Transaction"
|
||||
$ref: "#/components/schemas/Expense"
|
||||
NewBudget:
|
||||
type: object
|
||||
description: Budget to be created
|
||||
|
@ -230,14 +230,14 @@ components:
|
|||
format: date
|
||||
description: Date marking the end of the budget period
|
||||
example: 2023-04-30
|
||||
TransactionId:
|
||||
ExpenseId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique identifier of a transaction
|
||||
description: Unique identifier of a expense
|
||||
example: cfd3b0b7-bf5e-43d4-8341-940d5e07487d
|
||||
Transaction:
|
||||
Expense:
|
||||
type: object
|
||||
description: An expense or income related to a single budget
|
||||
description: An expense related to a single budget
|
||||
required:
|
||||
- id
|
||||
- description
|
||||
|
@ -245,21 +245,21 @@ components:
|
|||
- date
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/TransactionId"
|
||||
$ref: "#/components/schemas/ExpenseId"
|
||||
description:
|
||||
type: string
|
||||
description: Description of the transaction
|
||||
description: Description of the expense
|
||||
example: Healthy breakfast at Five Guys
|
||||
amount:
|
||||
type: number
|
||||
description: Amount of money contained in the transaction
|
||||
description: Amount of money contained in the expense
|
||||
example: 23.94
|
||||
date:
|
||||
type: string
|
||||
format: date-time
|
||||
NewTransaction:
|
||||
NewExpense:
|
||||
type: object
|
||||
description: A new transaction to be created
|
||||
description: A new expense to be created
|
||||
required:
|
||||
- description
|
||||
- amount
|
||||
|
@ -267,11 +267,11 @@ components:
|
|||
properties:
|
||||
description:
|
||||
type: string
|
||||
description: Description of the transaction
|
||||
description: Description of the expense
|
||||
example: Healthy breakfast at Five Guys
|
||||
amount:
|
||||
type: number
|
||||
description: Amount of money contained in the transaction
|
||||
description: Amount of money contained in the expense
|
||||
example: 23.94
|
||||
date:
|
||||
type: string
|
||||
|
|
|
@ -7,8 +7,8 @@ import BudgetService from "./usecase/budget/budgetService";
|
|||
import { errorHandler } from "./middleware/errorHandler";
|
||||
import BudgetMongoRepository from "./repository/budget/mongo/budgetMongoRepository";
|
||||
import BudgetSummaryMongoRepository from "./repository/budget/mongo/budgetSummaryMongoRepository";
|
||||
import TransactionService from "./usecase/transaction/transactionService";
|
||||
import TransactionMongoRepository from "./repository/transaction/mongo/TransactionMongoRepository";
|
||||
import ExpenseService from "./usecase/expense/expenseService";
|
||||
import ExpenseMongoRepository from "./repository/expense/mongo/ExpenseMongoRepository";
|
||||
import * as OpenApiValidator from "express-openapi-validator";
|
||||
import * as path from "path";
|
||||
|
||||
|
@ -34,8 +34,8 @@ const budgetUseCases = BudgetService(
|
|||
BudgetSummaryMongoRepository(),
|
||||
BudgetMongoRepository()
|
||||
);
|
||||
const transactionUseCases = TransactionService(TransactionMongoRepository());
|
||||
app.use(ApiRouter(budgetUseCases, transactionUseCases));
|
||||
const expenseUseCases = ExpenseService(ExpenseMongoRepository());
|
||||
app.use(ApiRouter(budgetUseCases, expenseUseCases));
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type UUID from "./uuid";
|
||||
import type Limit from "./limit";
|
||||
import { type Transaction } from "./transaction";
|
||||
import { type Expense } from "./expense";
|
||||
|
||||
export type BudgetId = UUID;
|
||||
|
||||
|
@ -17,5 +17,5 @@ export interface Budget extends NewBudget {
|
|||
}
|
||||
|
||||
export interface BudgetSummary extends Budget {
|
||||
transactions: Transaction[];
|
||||
expenses: Expense[];
|
||||
}
|
||||
|
|
13
src/domain/expense.ts
Normal file
13
src/domain/expense.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import type UUID from "./uuid";
|
||||
|
||||
export type ExpenseId = UUID;
|
||||
|
||||
export interface NewExpense {
|
||||
description: string;
|
||||
amount: number;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface Expense extends NewExpense {
|
||||
id: ExpenseId;
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
import type UUID from "./uuid";
|
||||
|
||||
export type TransactionId = UUID;
|
||||
|
||||
export interface NewTransaction {
|
||||
description: string;
|
||||
amount: number;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export interface Transaction extends NewTransaction {
|
||||
id: TransactionId;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import type TransactionEntity from "../../transaction/entity/transactionEntity";
|
||||
import type ExpenseEntity from "../../expense/entity/expenseEntity";
|
||||
|
||||
interface BudgetSummaryEntity {
|
||||
id: string;
|
||||
|
@ -7,7 +7,7 @@ interface BudgetSummaryEntity {
|
|||
spent: number;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
transactions: TransactionEntity[];
|
||||
expenses: ExpenseEntity[];
|
||||
}
|
||||
|
||||
export default BudgetSummaryEntity;
|
||||
|
|
|
@ -3,7 +3,7 @@ import type BudgetSummaryEntity from "./budgetSummaryEntity";
|
|||
import UUID from "../../../domain/uuid";
|
||||
import Limit from "../../../domain/limit";
|
||||
import type BudgetEntity from "./budgetEntity";
|
||||
import { TransactionEntityConverter } from "../../transaction/entity/converters";
|
||||
import { ExpenseEntityConverter } from "../../expense/entity/converters";
|
||||
|
||||
export const BudgetEntityConverter = {
|
||||
toDomain: (entity: BudgetEntity): Budget => ({
|
||||
|
@ -24,7 +24,7 @@ export const BudgetSummaryEntityConverter = {
|
|||
spent: domain.spent,
|
||||
startDate: domain.startDate,
|
||||
endDate: domain.endDate,
|
||||
transactions: domain.transactions.map(TransactionEntityConverter.toEntity),
|
||||
expenses: domain.expenses.map(ExpenseEntityConverter.toEntity),
|
||||
}),
|
||||
toDomain: (entity: BudgetSummaryEntity): BudgetSummary => ({
|
||||
id: new UUID(entity.id),
|
||||
|
@ -33,6 +33,6 @@ export const BudgetSummaryEntityConverter = {
|
|||
spent: entity.spent,
|
||||
startDate: entity.startDate,
|
||||
endDate: entity.endDate,
|
||||
transactions: entity.transactions.map(TransactionEntityConverter.toDomain),
|
||||
expenses: entity.expenses.map(ExpenseEntityConverter.toDomain),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { BudgetEntityConverter } from "../entity/converters";
|
|||
import { BudgetSummaryModel } from "./models";
|
||||
|
||||
export const findBudgets: BudgetRepository["findAll"] = async () => {
|
||||
const found = await BudgetSummaryModel.find().select("-transactions");
|
||||
const found = await BudgetSummaryModel.find().select("-expenses");
|
||||
return found.map(BudgetEntityConverter.toDomain);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import mongoose, { Schema } from "mongoose";
|
||||
import type BudgetSummaryEntity from "../../entity/budgetSummaryEntity";
|
||||
import transactionSchema from "../../../transaction/mongo/schema/transactionSchema";
|
||||
import expenseSchema from "../../../expense/mongo/schema/expenseSchema";
|
||||
|
||||
const Types = Schema.Types;
|
||||
|
||||
|
@ -12,7 +12,7 @@ const budgetSummarySchema = new mongoose.Schema<BudgetSummaryEntity>(
|
|||
spent: Types.Number,
|
||||
startDate: Types.Date,
|
||||
endDate: Types.Date,
|
||||
transactions: [transactionSchema],
|
||||
expenses: [expenseSchema],
|
||||
},
|
||||
{ strict: true, timestamps: true, versionKey: false }
|
||||
);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { type Transaction } from "../../../domain/transaction";
|
||||
import { type Expense } from "../../../domain/expense";
|
||||
import UUID from "../../../domain/uuid";
|
||||
import type TransactionEntity from "./transactionEntity";
|
||||
import type ExpenseEntity from "./expenseEntity";
|
||||
|
||||
export const TransactionEntityConverter = {
|
||||
toEntity: (domain: Transaction): TransactionEntity => ({
|
||||
export const ExpenseEntityConverter = {
|
||||
toEntity: (domain: Expense): ExpenseEntity => ({
|
||||
id: domain.id.value,
|
||||
description: domain.description,
|
||||
amount: domain.amount,
|
||||
date: domain.date,
|
||||
}),
|
||||
toDomain: (entity: TransactionEntity): Transaction => ({
|
||||
toDomain: (entity: ExpenseEntity): Expense => ({
|
||||
id: new UUID(entity.id),
|
||||
description: entity.description,
|
||||
amount: entity.amount,
|
|
@ -1,8 +1,8 @@
|
|||
interface TransactionEntity {
|
||||
interface ExpenseEntity {
|
||||
id: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
date: Date;
|
||||
}
|
||||
|
||||
export default TransactionEntity;
|
||||
export default ExpenseEntity;
|
13
src/repository/expense/expenseRepository.ts
Normal file
13
src/repository/expense/expenseRepository.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { type Expense } from "../../domain/expense";
|
||||
import { type BudgetId } from "../../domain/budget";
|
||||
|
||||
interface ExpenseRepository {
|
||||
findAllForBudget: (budgetId: BudgetId) => Promise<Expense[] | undefined>;
|
||||
insert: (
|
||||
budgetId: BudgetId,
|
||||
spent: number,
|
||||
expense: Expense
|
||||
) => Promise<Expense | undefined>;
|
||||
}
|
||||
|
||||
export default ExpenseRepository;
|
44
src/repository/expense/mongo/ExpenseMongoRepository.ts
Normal file
44
src/repository/expense/mongo/ExpenseMongoRepository.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import type ExpenseRepository from "../expenseRepository";
|
||||
import { ExpenseEntityConverter } from "../entity/converters";
|
||||
import { BudgetSummaryModel } from "../../budget/mongo/models";
|
||||
import type BudgetSummaryEntity from "../../budget/entity/budgetSummaryEntity";
|
||||
|
||||
export const findAllExpensesForBudget: ExpenseRepository["findAllForBudget"] =
|
||||
async (budgetId) => {
|
||||
const budgetExpenses: Pick<BudgetSummaryEntity, "expenses"> | null =
|
||||
await BudgetSummaryModel.findOne({
|
||||
id: budgetId.value,
|
||||
}).select("expenses");
|
||||
if (budgetExpenses !== null) {
|
||||
return budgetExpenses.expenses.map(ExpenseEntityConverter.toDomain);
|
||||
}
|
||||
};
|
||||
|
||||
export const insertExpense: ExpenseRepository["insert"] = async (
|
||||
budgetId,
|
||||
spent,
|
||||
expense
|
||||
) => {
|
||||
const expenseEntity = ExpenseEntityConverter.toEntity(expense);
|
||||
const updatedSummary = await BudgetSummaryModel.findOneAndUpdate(
|
||||
{ id: budgetId.value },
|
||||
{
|
||||
$set: { spent },
|
||||
$push: { expenses: expenseEntity },
|
||||
},
|
||||
{ returnDocument: "after" }
|
||||
);
|
||||
const updatedExpense = updatedSummary?.expenses.find(
|
||||
(t) => t.id === expense.id.value
|
||||
);
|
||||
if (updatedExpense !== undefined) {
|
||||
return ExpenseEntityConverter.toDomain(updatedExpense);
|
||||
}
|
||||
};
|
||||
|
||||
const ExpenseMongoRepository = (): ExpenseRepository => ({
|
||||
findAllForBudget: findAllExpensesForBudget,
|
||||
insert: insertExpense,
|
||||
});
|
||||
|
||||
export default ExpenseMongoRepository;
|
|
@ -1,9 +1,9 @@
|
|||
import mongoose, { Schema } from "mongoose";
|
||||
import type TransactionEntity from "../../entity/transactionEntity";
|
||||
import type ExpenseEntity from "../../entity/expenseEntity";
|
||||
|
||||
const Types = Schema.Types;
|
||||
|
||||
const transactionSchema = new mongoose.Schema<TransactionEntity>(
|
||||
const expenseSchema = new mongoose.Schema<ExpenseEntity>(
|
||||
{
|
||||
id: Types.UUID,
|
||||
description: Types.String,
|
||||
|
@ -13,4 +13,4 @@ const transactionSchema = new mongoose.Schema<TransactionEntity>(
|
|||
{ strict: true, _id: false }
|
||||
);
|
||||
|
||||
export default transactionSchema;
|
||||
export default expenseSchema;
|
|
@ -1,46 +0,0 @@
|
|||
import type TransactionRepository from "../transactionRepository";
|
||||
import { TransactionEntityConverter } from "../entity/converters";
|
||||
import { BudgetSummaryModel } from "../../budget/mongo/models";
|
||||
import type BudgetSummaryEntity from "../../budget/entity/budgetSummaryEntity";
|
||||
|
||||
export const findAllTransactionsForBudget: TransactionRepository["findAllForBudget"] =
|
||||
async (budgetId) => {
|
||||
const budgetTransactions: Pick<BudgetSummaryEntity, "transactions"> | null =
|
||||
await BudgetSummaryModel.findOne({
|
||||
id: budgetId.value,
|
||||
}).select("transactions");
|
||||
if (budgetTransactions !== null) {
|
||||
return budgetTransactions.transactions.map(
|
||||
TransactionEntityConverter.toDomain
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const insertTransaction: TransactionRepository["insert"] = async (
|
||||
budgetId,
|
||||
spent,
|
||||
transaction
|
||||
) => {
|
||||
const transactionEntity = TransactionEntityConverter.toEntity(transaction);
|
||||
const updatedSummary = await BudgetSummaryModel.findOneAndUpdate(
|
||||
{ id: budgetId.value },
|
||||
{
|
||||
$set: { spent },
|
||||
$push: { transactions: transactionEntity },
|
||||
},
|
||||
{ returnDocument: "after" }
|
||||
);
|
||||
const updatedTransaction = updatedSummary?.transactions.find(
|
||||
(t) => t.id === transaction.id.value
|
||||
);
|
||||
if (updatedTransaction !== undefined) {
|
||||
return TransactionEntityConverter.toDomain(updatedTransaction);
|
||||
}
|
||||
};
|
||||
|
||||
const TransactionMongoRepository = (): TransactionRepository => ({
|
||||
findAllForBudget: findAllTransactionsForBudget,
|
||||
insert: insertTransaction,
|
||||
});
|
||||
|
||||
export default TransactionMongoRepository;
|
|
@ -1,13 +0,0 @@
|
|||
import { type Transaction } from "../../domain/transaction";
|
||||
import { type BudgetId } from "../../domain/budget";
|
||||
|
||||
interface TransactionRepository {
|
||||
findAllForBudget: (budgetId: BudgetId) => Promise<Transaction[] | undefined>;
|
||||
insert: (
|
||||
budgetId: BudgetId,
|
||||
spent: number,
|
||||
transaction: Transaction
|
||||
) => Promise<Transaction | undefined>;
|
||||
}
|
||||
|
||||
export default TransactionRepository;
|
|
@ -5,7 +5,7 @@ const apiPaths: Record<keyof operations, keyof paths> = {
|
|||
getBudgets: "/budgets",
|
||||
getBudgetSummary: "/budgets/{budgetId}/summary",
|
||||
deleteBudget: "/budgets/{budgetId}",
|
||||
createTransaction: "/budgets/{budgetId}/transactions",
|
||||
createExpense: "/budgets/{budgetId}/expenses",
|
||||
};
|
||||
|
||||
export default apiPaths;
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { Router } from "express";
|
||||
import BudgetRouter from "./budget/budgetRouter";
|
||||
import type BudgetUseCases from "../usecase/budget/budgetUseCases";
|
||||
import TransactionRouter from "./transaction/transactionRouter";
|
||||
import type TransactionUseCases from "../usecase/transaction/transactionUseCases";
|
||||
import ExpenseRouter from "./expense/expenseRouter";
|
||||
import type ExpenseUseCases from "../usecase/expense/expenseUseCases";
|
||||
|
||||
const ApiRouter = (
|
||||
budgetUseCases: BudgetUseCases,
|
||||
transactionUseCases: TransactionUseCases
|
||||
expenseUseCases: ExpenseUseCases
|
||||
): Router => {
|
||||
const router = Router();
|
||||
router.use(BudgetRouter(budgetUseCases));
|
||||
router.use(TransactionRouter(transactionUseCases));
|
||||
router.use(ExpenseRouter(expenseUseCases));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
type BudgetSummaryDto,
|
||||
type NewBudgetDto,
|
||||
} from "./budget";
|
||||
import { TransactionDtoConverter } from "../../transaction/dto/converters";
|
||||
import { ExpenseDtoConverter } from "../../expense/dto/converters";
|
||||
|
||||
export const NewBudgetDtoConverter = {
|
||||
toDomain: (dto: NewBudgetDto): NewBudget => ({
|
||||
|
@ -40,6 +40,6 @@ export const BudgetSummaryDtoConverter = {
|
|||
name: domain.name,
|
||||
startDate: toIsoDate(domain.startDate),
|
||||
endDate: toIsoDate(domain.endDate),
|
||||
transactions: domain.transactions.map(TransactionDtoConverter.toDto),
|
||||
expenses: domain.expenses.map(ExpenseDtoConverter.toDto),
|
||||
}),
|
||||
};
|
||||
|
|
19
src/routes/expense/dto/converters.ts
Normal file
19
src/routes/expense/dto/converters.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { type NewExpenseDto, type ExpenseDto } from "./expense";
|
||||
import { type NewExpense, type Expense } from "../../../domain/expense";
|
||||
|
||||
export const NewExpenseDtoConverter = {
|
||||
toDomain: (dto: NewExpenseDto): NewExpense => ({
|
||||
description: dto.description,
|
||||
date: new Date(dto.date),
|
||||
amount: dto.amount,
|
||||
}),
|
||||
};
|
||||
|
||||
export const ExpenseDtoConverter = {
|
||||
toDto: (domain: Expense): ExpenseDto => ({
|
||||
id: domain.id.value,
|
||||
amount: domain.amount,
|
||||
description: domain.description,
|
||||
date: domain.date.toISOString(),
|
||||
}),
|
||||
};
|
4
src/routes/expense/dto/expense.ts
Normal file
4
src/routes/expense/dto/expense.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { type components } from "../../../../generated/api";
|
||||
|
||||
export type NewExpenseDto = components["schemas"]["NewExpense"];
|
||||
export type ExpenseDto = components["schemas"]["Expense"];
|
39
src/routes/expense/expenseRouter.ts
Normal file
39
src/routes/expense/expenseRouter.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type ExpenseUseCases from "../../usecase/expense/expenseUseCases";
|
||||
import { type Request, type Response, Router } from "express";
|
||||
import { NewExpenseDtoConverter, ExpenseDtoConverter } from "./dto/converters";
|
||||
import UUID from "../../domain/uuid";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import toExpressPath from "../toExpressPath";
|
||||
import apiPaths from "../apiPaths";
|
||||
import asyncHandler from "express-async-handler";
|
||||
|
||||
export const createExpense =
|
||||
(addExpenseToBudget: ExpenseUseCases["addToBudget"]) =>
|
||||
async (req: Request, res: Response): Promise<void> => {
|
||||
const newExpenseDto = req.body;
|
||||
const budgetId: string = req.params.budgetId;
|
||||
const newExpense = NewExpenseDtoConverter.toDomain(newExpenseDto);
|
||||
const expense = await addExpenseToBudget(
|
||||
new UUID(budgetId),
|
||||
newExpense
|
||||
);
|
||||
if (expense === undefined) {
|
||||
res.sendStatus(StatusCodes.NOT_FOUND);
|
||||
} else {
|
||||
res
|
||||
.status(StatusCodes.CREATED)
|
||||
.json(ExpenseDtoConverter.toDto(expense));
|
||||
}
|
||||
};
|
||||
|
||||
const ExpenseRouter = (expenseUseCases: ExpenseUseCases): Router => {
|
||||
const router = Router();
|
||||
router.post(
|
||||
toExpressPath(apiPaths.createExpense),
|
||||
asyncHandler(createExpense(expenseUseCases.addToBudget))
|
||||
);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
export default ExpenseRouter;
|
|
@ -1,22 +0,0 @@
|
|||
import { type NewTransactionDto, type TransactionDto } from "./transaction";
|
||||
import {
|
||||
type NewTransaction,
|
||||
type Transaction,
|
||||
} from "../../../domain/transaction";
|
||||
|
||||
export const NewTransactionDtoConverter = {
|
||||
toDomain: (dto: NewTransactionDto): NewTransaction => ({
|
||||
description: dto.description,
|
||||
date: new Date(dto.date),
|
||||
amount: dto.amount,
|
||||
}),
|
||||
};
|
||||
|
||||
export const TransactionDtoConverter = {
|
||||
toDto: (domain: Transaction): TransactionDto => ({
|
||||
id: domain.id.value,
|
||||
amount: domain.amount,
|
||||
description: domain.description,
|
||||
date: domain.date.toISOString(),
|
||||
}),
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
import { type components } from "../../../../generated/api";
|
||||
|
||||
export type NewTransactionDto = components["schemas"]["NewTransaction"];
|
||||
export type TransactionDto = components["schemas"]["Transaction"];
|
|
@ -1,47 +0,0 @@
|
|||
import type TransactionUseCases from "../../usecase/transaction/transactionUseCases";
|
||||
import { type Request, type Response, Router } from "express";
|
||||
import {
|
||||
NewTransactionDtoConverter,
|
||||
TransactionDtoConverter,
|
||||
} from "./dto/converters";
|
||||
import UUID from "../../domain/uuid";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import toExpressPath from "../toExpressPath";
|
||||
import apiPaths from "../apiPaths";
|
||||
import asyncHandler from "express-async-handler";
|
||||
|
||||
export const createTransaction =
|
||||
(addTransactionToBudget: TransactionUseCases["addNewTransactionToBudget"]) =>
|
||||
async (req: Request, res: Response): Promise<void> => {
|
||||
const newTransactionDto = req.body;
|
||||
const budgetId: string = req.params.budgetId;
|
||||
const newTransaction =
|
||||
NewTransactionDtoConverter.toDomain(newTransactionDto);
|
||||
const transaction = await addTransactionToBudget(
|
||||
new UUID(budgetId),
|
||||
newTransaction
|
||||
);
|
||||
if (transaction === undefined) {
|
||||
res.sendStatus(StatusCodes.NOT_FOUND);
|
||||
} else {
|
||||
res
|
||||
.status(StatusCodes.CREATED)
|
||||
.json(TransactionDtoConverter.toDto(transaction));
|
||||
}
|
||||
};
|
||||
|
||||
const TransactionRouter = (
|
||||
transactionUseCases: TransactionUseCases
|
||||
): Router => {
|
||||
const router = Router();
|
||||
router.post(
|
||||
toExpressPath(apiPaths.createTransaction),
|
||||
asyncHandler(
|
||||
createTransaction(transactionUseCases.addNewTransactionToBudget)
|
||||
)
|
||||
);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
export default TransactionRouter;
|
|
@ -11,7 +11,7 @@ export const createBudget: (
|
|||
...newBudget,
|
||||
id: new UUID(),
|
||||
spent: 0,
|
||||
transactions: [],
|
||||
expenses: [],
|
||||
};
|
||||
return await insertBudget(budgetSummary);
|
||||
};
|
||||
|
|
39
src/usecase/expense/expenseService.ts
Normal file
39
src/usecase/expense/expenseService.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type ExpenseRepository from "../../repository/expense/expenseRepository";
|
||||
import type ExpenseUseCases from "./expenseUseCases";
|
||||
import { type Expense } from "../../domain/expense";
|
||||
import UUID from "../../domain/uuid";
|
||||
import currency from "currency.js";
|
||||
|
||||
const addNewExpenseToBudget: (
|
||||
findAll: ExpenseRepository["findAllForBudget"],
|
||||
insert: ExpenseRepository["insert"]
|
||||
) => ExpenseUseCases["addToBudget"] =
|
||||
(findAll, insert) => async (budgetId, newExpense) => {
|
||||
const existingExpenses = await findAll(budgetId);
|
||||
if (existingExpenses === undefined) {
|
||||
return;
|
||||
}
|
||||
const expense: Expense = {
|
||||
...newExpense,
|
||||
id: new UUID(),
|
||||
};
|
||||
const updatedSpent = calculateSpent(existingExpenses, expense);
|
||||
return await insert(budgetId, updatedSpent, expense);
|
||||
};
|
||||
|
||||
const calculateSpent = (expenses: Expense[], newExpense: Expense): number =>
|
||||
expenses.reduce(
|
||||
(prev, curr) => prev.add(curr.amount),
|
||||
currency(newExpense.amount)
|
||||
).value;
|
||||
|
||||
const ExpenseService: (
|
||||
expenseRepository: ExpenseRepository
|
||||
) => ExpenseUseCases = (expenseRepository) => ({
|
||||
addToBudget: addNewExpenseToBudget(
|
||||
expenseRepository.findAllForBudget,
|
||||
expenseRepository.insert
|
||||
),
|
||||
});
|
||||
|
||||
export default ExpenseService;
|
11
src/usecase/expense/expenseUseCases.ts
Normal file
11
src/usecase/expense/expenseUseCases.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { type BudgetId } from "../../domain/budget";
|
||||
import { type NewExpense, type Expense } from "../../domain/expense";
|
||||
|
||||
interface ExpenseUseCases {
|
||||
addToBudget: (
|
||||
budgetId: BudgetId,
|
||||
newExpense: NewExpense
|
||||
) => Promise<Expense | undefined>;
|
||||
}
|
||||
|
||||
export default ExpenseUseCases;
|
|
@ -1,42 +0,0 @@
|
|||
import type TransactionRepository from "../../repository/transaction/transactionRepository";
|
||||
import type TransactionUseCases from "./transactionUseCases";
|
||||
import { type Transaction } from "../../domain/transaction";
|
||||
import UUID from "../../domain/uuid";
|
||||
import currency from "currency.js";
|
||||
|
||||
const addNewTransactionToBudget: (
|
||||
findAll: TransactionRepository["findAllForBudget"],
|
||||
insert: TransactionRepository["insert"]
|
||||
) => TransactionUseCases["addNewTransactionToBudget"] =
|
||||
(findAll, insert) => async (budgetId, newTransaction) => {
|
||||
const existingTransactions = await findAll(budgetId);
|
||||
if (existingTransactions === undefined) {
|
||||
return;
|
||||
}
|
||||
const transaction: Transaction = {
|
||||
...newTransaction,
|
||||
id: new UUID(),
|
||||
};
|
||||
const updatedSpent = calculateSpent(existingTransactions, transaction);
|
||||
return await insert(budgetId, updatedSpent, transaction);
|
||||
};
|
||||
|
||||
const calculateSpent = (
|
||||
transactions: Transaction[],
|
||||
newTransaction: Transaction
|
||||
): number =>
|
||||
transactions.reduce(
|
||||
(prev, curr) => prev.add(curr.amount),
|
||||
currency(newTransaction.amount)
|
||||
).value;
|
||||
|
||||
const TransactionService: (
|
||||
transactionRepo: TransactionRepository
|
||||
) => TransactionUseCases = (transactionRepo) => ({
|
||||
addNewTransactionToBudget: addNewTransactionToBudget(
|
||||
transactionRepo.findAllForBudget,
|
||||
transactionRepo.insert
|
||||
),
|
||||
});
|
||||
|
||||
export default TransactionService;
|
|
@ -1,14 +0,0 @@
|
|||
import { type BudgetId } from "../../domain/budget";
|
||||
import {
|
||||
type NewTransaction,
|
||||
type Transaction,
|
||||
} from "../../domain/transaction";
|
||||
|
||||
interface TransactionUseCases {
|
||||
addNewTransactionToBudget: (
|
||||
budgetId: BudgetId,
|
||||
newTransaction: NewTransaction
|
||||
) => Promise<Transaction | undefined>;
|
||||
}
|
||||
|
||||
export default TransactionUseCases;
|
Loading…
Reference in a new issue