Add backend API for personal finance management application
- Introduced a comprehensive backend API using TypeScript, Fastify, and PostgreSQL. - Added essential files including architecture documentation, environment configuration, and Docker setup. - Implemented RESTful routes for managing assets, liabilities, clients, invoices, and cashflow. - Established a robust database schema with Prisma for data management. - Integrated middleware for authentication and error handling. - Created service and repository layers to adhere to SOLID principles and clean architecture. - Included example environment variables for development, staging, and production setups.
This commit is contained in:
94
backend-api/src/routes/assets.ts
Normal file
94
backend-api/src/routes/assets.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {AssetController} from '../controllers/AssetController';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
/**
|
||||
* Asset Routes
|
||||
* All routes require authentication
|
||||
*/
|
||||
export async function assetRoutes(fastify: FastifyInstance) {
|
||||
const controller = new AssetController();
|
||||
|
||||
// Apply authentication to all routes
|
||||
fastify.addHook('preHandler', authenticate);
|
||||
|
||||
fastify.get('/', {
|
||||
schema: {
|
||||
tags: ['Assets'],
|
||||
description: 'Get all user assets',
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
handler: controller.getAll.bind(controller),
|
||||
});
|
||||
|
||||
fastify.get('/:id', {
|
||||
schema: {
|
||||
tags: ['Assets'],
|
||||
description: 'Get asset by ID',
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: controller.getById.bind(controller),
|
||||
});
|
||||
|
||||
fastify.post('/', {
|
||||
schema: {
|
||||
tags: ['Assets'],
|
||||
description: 'Create a new asset',
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'type', 'value'],
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
type: {type: 'string', enum: ['CASH', 'INVESTMENT', 'PROPERTY', 'VEHICLE', 'OTHER']},
|
||||
value: {type: 'number', minimum: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: controller.create.bind(controller),
|
||||
});
|
||||
|
||||
fastify.put('/:id', {
|
||||
schema: {
|
||||
tags: ['Assets'],
|
||||
description: 'Update an asset',
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
type: {type: 'string', enum: ['CASH', 'INVESTMENT', 'PROPERTY', 'VEHICLE', 'OTHER']},
|
||||
value: {type: 'number', minimum: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: controller.update.bind(controller),
|
||||
});
|
||||
|
||||
fastify.delete('/:id', {
|
||||
schema: {
|
||||
tags: ['Assets'],
|
||||
description: 'Delete an asset',
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string', format: 'uuid'},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: controller.delete.bind(controller),
|
||||
});
|
||||
}
|
||||
53
backend-api/src/routes/auth.ts
Normal file
53
backend-api/src/routes/auth.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {AuthController} from '../controllers/AuthController';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
/**
|
||||
* Authentication Routes
|
||||
*/
|
||||
export async function authRoutes(fastify: FastifyInstance) {
|
||||
const controller = new AuthController();
|
||||
|
||||
fastify.post('/register', {
|
||||
schema: {
|
||||
tags: ['Authentication'],
|
||||
description: 'Register a new user',
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['email', 'password', 'name'],
|
||||
properties: {
|
||||
email: {type: 'string', format: 'email'},
|
||||
password: {type: 'string', minLength: 8},
|
||||
name: {type: 'string', minLength: 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: controller.register.bind(controller),
|
||||
});
|
||||
|
||||
fastify.post('/login', {
|
||||
schema: {
|
||||
tags: ['Authentication'],
|
||||
description: 'Login with email and password',
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['email', 'password'],
|
||||
properties: {
|
||||
email: {type: 'string', format: 'email'},
|
||||
password: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: controller.login.bind(controller),
|
||||
});
|
||||
|
||||
fastify.get('/profile', {
|
||||
schema: {
|
||||
tags: ['Authentication'],
|
||||
description: 'Get current user profile',
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
preHandler: authenticate,
|
||||
handler: controller.getProfile.bind(controller),
|
||||
});
|
||||
}
|
||||
217
backend-api/src/routes/cashflow.routes.ts
Normal file
217
backend-api/src/routes/cashflow.routes.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {CashflowController} from '../controllers/CashflowController';
|
||||
import {CashflowService} from '../services/CashflowService';
|
||||
import {
|
||||
IncomeSourceRepository,
|
||||
ExpenseRepository,
|
||||
TransactionRepository,
|
||||
} from '../repositories/CashflowRepository';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
const incomeRepository = new IncomeSourceRepository();
|
||||
const expenseRepository = new ExpenseRepository();
|
||||
const transactionRepository = new TransactionRepository();
|
||||
const cashflowService = new CashflowService(incomeRepository, expenseRepository, transactionRepository);
|
||||
const cashflowController = new CashflowController(cashflowService);
|
||||
|
||||
export async function cashflowRoutes(fastify: FastifyInstance) {
|
||||
fastify.addHook('onRequest', authenticate);
|
||||
|
||||
// ===== Income Source Routes =====
|
||||
|
||||
fastify.get('/income', {
|
||||
schema: {
|
||||
description: 'Get all income sources',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.getAllIncome.bind(cashflowController));
|
||||
|
||||
fastify.get('/income/total', {
|
||||
schema: {
|
||||
description: 'Get total monthly income',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.getTotalMonthlyIncome.bind(cashflowController));
|
||||
|
||||
fastify.get('/income/:id', {
|
||||
schema: {
|
||||
description: 'Get income source by ID',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.getOneIncome.bind(cashflowController));
|
||||
|
||||
fastify.post('/income', {
|
||||
schema: {
|
||||
description: 'Create income source',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'amount', 'frequency'],
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
amount: {type: 'number'},
|
||||
frequency: {type: 'string'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, cashflowController.createIncome.bind(cashflowController));
|
||||
|
||||
fastify.put('/income/:id', {
|
||||
schema: {
|
||||
description: 'Update income source',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.updateIncome.bind(cashflowController));
|
||||
|
||||
fastify.delete('/income/:id', {
|
||||
schema: {
|
||||
description: 'Delete income source',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.deleteIncome.bind(cashflowController));
|
||||
|
||||
// ===== Expense Routes =====
|
||||
|
||||
fastify.get('/expenses', {
|
||||
schema: {
|
||||
description: 'Get all expenses',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
byCategory: {type: 'string', enum: ['true', 'false']},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, cashflowController.getAllExpenses.bind(cashflowController));
|
||||
|
||||
fastify.get('/expenses/total', {
|
||||
schema: {
|
||||
description: 'Get total monthly expenses',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.getTotalMonthlyExpenses.bind(cashflowController));
|
||||
|
||||
fastify.get('/expenses/:id', {
|
||||
schema: {
|
||||
description: 'Get expense by ID',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.getOneExpense.bind(cashflowController));
|
||||
|
||||
fastify.post('/expenses', {
|
||||
schema: {
|
||||
description: 'Create expense',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'amount', 'category', 'frequency'],
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
amount: {type: 'number'},
|
||||
category: {type: 'string'},
|
||||
frequency: {type: 'string'},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, cashflowController.createExpense.bind(cashflowController));
|
||||
|
||||
fastify.put('/expenses/:id', {
|
||||
schema: {
|
||||
description: 'Update expense',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.updateExpense.bind(cashflowController));
|
||||
|
||||
fastify.delete('/expenses/:id', {
|
||||
schema: {
|
||||
description: 'Delete expense',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.deleteExpense.bind(cashflowController));
|
||||
|
||||
// ===== Transaction Routes =====
|
||||
|
||||
fastify.get('/transactions', {
|
||||
schema: {
|
||||
description: 'Get all transactions',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, cashflowController.getAllTransactions.bind(cashflowController));
|
||||
|
||||
fastify.get('/transactions/summary', {
|
||||
schema: {
|
||||
description: 'Get cashflow summary for date range',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
required: ['startDate', 'endDate'],
|
||||
properties: {
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, cashflowController.getCashflowSummary.bind(cashflowController));
|
||||
|
||||
fastify.get('/transactions/:id', {
|
||||
schema: {
|
||||
description: 'Get transaction by ID',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.getOneTransaction.bind(cashflowController));
|
||||
|
||||
fastify.post('/transactions', {
|
||||
schema: {
|
||||
description: 'Create transaction',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['type', 'category', 'amount', 'date'],
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
category: {type: 'string'},
|
||||
amount: {type: 'number'},
|
||||
date: {type: 'string', format: 'date-time'},
|
||||
description: {type: 'string'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, cashflowController.createTransaction.bind(cashflowController));
|
||||
|
||||
fastify.delete('/transactions/:id', {
|
||||
schema: {
|
||||
description: 'Delete transaction',
|
||||
tags: ['Cashflow'],
|
||||
security: [{bearerAuth: []}],
|
||||
},
|
||||
}, cashflowController.deleteTransaction.bind(cashflowController));
|
||||
}
|
||||
231
backend-api/src/routes/client.routes.ts
Normal file
231
backend-api/src/routes/client.routes.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {ClientController} from '../controllers/ClientController';
|
||||
import {ClientService} from '../services/ClientService';
|
||||
import {ClientRepository} from '../repositories/ClientRepository';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
const clientRepository = new ClientRepository();
|
||||
const clientService = new ClientService(clientRepository);
|
||||
const clientController = new ClientController(clientService);
|
||||
|
||||
export async function clientRoutes(fastify: FastifyInstance) {
|
||||
// Apply authentication to all routes
|
||||
fastify.addHook('onRequest', authenticate);
|
||||
|
||||
/**
|
||||
* Get all clients
|
||||
*/
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all clients for the authenticated user',
|
||||
tags: ['Clients'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withStats: {
|
||||
type: 'string',
|
||||
enum: ['true', 'false'],
|
||||
description: 'Include invoice statistics for each client',
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of clients',
|
||||
type: 'object',
|
||||
properties: {
|
||||
clients: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
name: {type: 'string'},
|
||||
email: {type: 'string'},
|
||||
phone: {type: 'string', nullable: true},
|
||||
address: {type: 'string', nullable: true},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
clientController.getAll.bind(clientController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get total revenue
|
||||
*/
|
||||
fastify.get(
|
||||
'/revenue/total',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get total revenue from all paid client invoices',
|
||||
tags: ['Clients'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'Total revenue',
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalRevenue: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
clientController.getTotalRevenue.bind(clientController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get single client
|
||||
*/
|
||||
fastify.get(
|
||||
'/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get a single client by ID',
|
||||
tags: ['Clients'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Client details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
client: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
name: {type: 'string'},
|
||||
email: {type: 'string'},
|
||||
phone: {type: 'string', nullable: true},
|
||||
address: {type: 'string', nullable: true},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
clientController.getOne.bind(clientController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create client
|
||||
*/
|
||||
fastify.post(
|
||||
'/',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create a new client',
|
||||
tags: ['Clients'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'email'],
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
email: {type: 'string', format: 'email'},
|
||||
phone: {type: 'string', maxLength: 50},
|
||||
address: {type: 'string'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Client created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
client: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
clientController.create.bind(clientController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Update client
|
||||
*/
|
||||
fastify.put(
|
||||
'/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Update a client',
|
||||
tags: ['Clients'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
email: {type: 'string', format: 'email'},
|
||||
phone: {type: 'string', maxLength: 50},
|
||||
address: {type: 'string'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Client updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
client: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
clientController.update.bind(clientController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete client
|
||||
*/
|
||||
fastify.delete(
|
||||
'/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete a client',
|
||||
tags: ['Clients'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Client deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
clientController.delete.bind(clientController)
|
||||
);
|
||||
}
|
||||
106
backend-api/src/routes/dashboard.routes.ts
Normal file
106
backend-api/src/routes/dashboard.routes.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {DashboardController} from '../controllers/DashboardController';
|
||||
import {DashboardService} from '../services/DashboardService';
|
||||
import {AssetRepository} from '../repositories/AssetRepository';
|
||||
import {LiabilityRepository} from '../repositories/LiabilityRepository';
|
||||
import {InvoiceRepository} from '../repositories/InvoiceRepository';
|
||||
import {DebtAccountRepository} from '../repositories/DebtAccountRepository';
|
||||
import {
|
||||
IncomeSourceRepository,
|
||||
ExpenseRepository,
|
||||
TransactionRepository,
|
||||
} from '../repositories/CashflowRepository';
|
||||
import {NetWorthSnapshotRepository} from '../repositories/NetWorthSnapshotRepository';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
const assetRepository = new AssetRepository();
|
||||
const liabilityRepository = new LiabilityRepository();
|
||||
const invoiceRepository = new InvoiceRepository();
|
||||
const debtAccountRepository = new DebtAccountRepository();
|
||||
const incomeRepository = new IncomeSourceRepository();
|
||||
const expenseRepository = new ExpenseRepository();
|
||||
const transactionRepository = new TransactionRepository();
|
||||
const snapshotRepository = new NetWorthSnapshotRepository();
|
||||
|
||||
const dashboardService = new DashboardService(
|
||||
assetRepository,
|
||||
liabilityRepository,
|
||||
invoiceRepository,
|
||||
debtAccountRepository,
|
||||
incomeRepository,
|
||||
expenseRepository,
|
||||
transactionRepository,
|
||||
snapshotRepository
|
||||
);
|
||||
|
||||
const dashboardController = new DashboardController(dashboardService);
|
||||
|
||||
export async function dashboardRoutes(fastify: FastifyInstance) {
|
||||
fastify.addHook('onRequest', authenticate);
|
||||
|
||||
/**
|
||||
* Get dashboard summary
|
||||
*/
|
||||
fastify.get(
|
||||
'/summary',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get comprehensive financial dashboard summary',
|
||||
tags: ['Dashboard'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'Dashboard summary data',
|
||||
type: 'object',
|
||||
properties: {
|
||||
netWorth: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
current: {type: 'number'},
|
||||
assets: {type: 'number'},
|
||||
liabilities: {type: 'number'},
|
||||
change: {type: 'number'},
|
||||
lastUpdated: {type: 'string'},
|
||||
},
|
||||
},
|
||||
invoices: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total: {type: 'number'},
|
||||
paid: {type: 'number'},
|
||||
outstanding: {type: 'number'},
|
||||
overdue: {type: 'number'},
|
||||
},
|
||||
},
|
||||
debts: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total: {type: 'number'},
|
||||
accounts: {type: 'number'},
|
||||
},
|
||||
},
|
||||
cashflow: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
monthlyIncome: {type: 'number'},
|
||||
monthlyExpenses: {type: 'number'},
|
||||
monthlyNet: {type: 'number'},
|
||||
last30Days: {type: 'object'},
|
||||
},
|
||||
},
|
||||
assets: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total: {type: 'number'},
|
||||
count: {type: 'number'},
|
||||
allocation: {type: 'array'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dashboardController.getSummary.bind(dashboardController)
|
||||
);
|
||||
}
|
||||
559
backend-api/src/routes/debt.routes.ts
Normal file
559
backend-api/src/routes/debt.routes.ts
Normal file
@@ -0,0 +1,559 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {DebtCategoryController} from '../controllers/DebtCategoryController';
|
||||
import {DebtCategoryService} from '../services/DebtCategoryService';
|
||||
import {DebtCategoryRepository} from '../repositories/DebtCategoryRepository';
|
||||
import {DebtAccountController} from '../controllers/DebtAccountController';
|
||||
import {DebtAccountService} from '../services/DebtAccountService';
|
||||
import {DebtAccountRepository} from '../repositories/DebtAccountRepository';
|
||||
import {DebtPaymentController} from '../controllers/DebtPaymentController';
|
||||
import {DebtPaymentService} from '../services/DebtPaymentService';
|
||||
import {DebtPaymentRepository} from '../repositories/DebtPaymentRepository';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
const categoryRepository = new DebtCategoryRepository();
|
||||
const categoryService = new DebtCategoryService(categoryRepository);
|
||||
const categoryController = new DebtCategoryController(categoryService);
|
||||
|
||||
const accountRepository = new DebtAccountRepository();
|
||||
const accountService = new DebtAccountService(accountRepository, categoryRepository);
|
||||
const accountController = new DebtAccountController(accountService);
|
||||
|
||||
const paymentRepository = new DebtPaymentRepository();
|
||||
const paymentService = new DebtPaymentService(paymentRepository, accountRepository);
|
||||
const paymentController = new DebtPaymentController(paymentService);
|
||||
|
||||
export async function debtRoutes(fastify: FastifyInstance) {
|
||||
// Apply authentication to all routes
|
||||
fastify.addHook('onRequest', authenticate);
|
||||
|
||||
/**
|
||||
* Get all debt categories
|
||||
*/
|
||||
fastify.get(
|
||||
'/categories',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all debt categories for the authenticated user',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withStats: {
|
||||
type: 'string',
|
||||
enum: ['true', 'false'],
|
||||
description: 'Include statistics for each category',
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of debt categories',
|
||||
type: 'object',
|
||||
properties: {
|
||||
categories: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
name: {type: 'string'},
|
||||
description: {type: 'string', nullable: true},
|
||||
color: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
categoryController.getAll.bind(categoryController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get single debt category
|
||||
*/
|
||||
fastify.get(
|
||||
'/categories/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get a single debt category by ID',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Debt category details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
name: {type: 'string'},
|
||||
description: {type: 'string', nullable: true},
|
||||
color: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
categoryController.getOne.bind(categoryController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create debt category
|
||||
*/
|
||||
fastify.post(
|
||||
'/categories',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create a new debt category',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name'],
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
description: {type: 'string'},
|
||||
color: {type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Debt category created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
categoryController.create.bind(categoryController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Update debt category
|
||||
*/
|
||||
fastify.put(
|
||||
'/categories/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Update a debt category',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
description: {type: 'string'},
|
||||
color: {type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Debt category updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
categoryController.update.bind(categoryController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete debt category
|
||||
*/
|
||||
fastify.delete(
|
||||
'/categories/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete a debt category',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Debt category deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
categoryController.delete.bind(categoryController)
|
||||
);
|
||||
|
||||
// ===== Debt Account Routes =====
|
||||
|
||||
/**
|
||||
* Get all debt accounts
|
||||
*/
|
||||
fastify.get(
|
||||
'/accounts',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all debt accounts for the authenticated user',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withStats: {type: 'string', enum: ['true', 'false']},
|
||||
categoryId: {type: 'string', description: 'Filter by category ID'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of debt accounts',
|
||||
type: 'object',
|
||||
properties: {
|
||||
accounts: {type: 'array', items: {type: 'object'}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountController.getAll.bind(accountController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get total debt
|
||||
*/
|
||||
fastify.get(
|
||||
'/accounts/total',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get total debt across all accounts',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'Total debt',
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalDebt: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountController.getTotalDebt.bind(accountController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get single debt account
|
||||
*/
|
||||
fastify.get(
|
||||
'/accounts/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get a single debt account by ID',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Debt account details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
account: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountController.getOne.bind(accountController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create debt account
|
||||
*/
|
||||
fastify.post(
|
||||
'/accounts',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create a new debt account',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['categoryId', 'name', 'creditor', 'originalBalance', 'currentBalance'],
|
||||
properties: {
|
||||
categoryId: {type: 'string', format: 'uuid'},
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
creditor: {type: 'string', minLength: 1, maxLength: 255},
|
||||
accountNumber: {type: 'string', maxLength: 100},
|
||||
originalBalance: {type: 'number', minimum: 0},
|
||||
currentBalance: {type: 'number', minimum: 0},
|
||||
interestRate: {type: 'number', minimum: 0, maximum: 100},
|
||||
minimumPayment: {type: 'number', minimum: 0},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Debt account created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
account: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountController.create.bind(accountController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Update debt account
|
||||
*/
|
||||
fastify.put(
|
||||
'/accounts/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Update a debt account',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
creditor: {type: 'string', minLength: 1, maxLength: 255},
|
||||
accountNumber: {type: 'string', maxLength: 100},
|
||||
currentBalance: {type: 'number', minimum: 0},
|
||||
interestRate: {type: 'number', minimum: 0, maximum: 100},
|
||||
minimumPayment: {type: 'number', minimum: 0},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Debt account updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
account: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountController.update.bind(accountController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete debt account
|
||||
*/
|
||||
fastify.delete(
|
||||
'/accounts/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete a debt account',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Debt account deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountController.delete.bind(accountController)
|
||||
);
|
||||
|
||||
// ===== Debt Payment Routes =====
|
||||
|
||||
/**
|
||||
* Get all debt payments
|
||||
*/
|
||||
fastify.get(
|
||||
'/payments',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all debt payments for the authenticated user',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
accountId: {type: 'string', description: 'Filter by account ID'},
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of debt payments',
|
||||
type: 'object',
|
||||
properties: {
|
||||
payments: {type: 'array', items: {type: 'object'}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
paymentController.getAll.bind(paymentController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get total payments
|
||||
*/
|
||||
fastify.get(
|
||||
'/payments/total',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get total payments made across all accounts',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'Total payments',
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalPayments: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
paymentController.getTotalPayments.bind(paymentController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get single debt payment
|
||||
*/
|
||||
fastify.get(
|
||||
'/payments/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get a single debt payment by ID',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Debt payment details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
payment: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
paymentController.getOne.bind(paymentController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create debt payment
|
||||
*/
|
||||
fastify.post(
|
||||
'/payments',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create a new debt payment',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['accountId', 'amount', 'paymentDate'],
|
||||
properties: {
|
||||
accountId: {type: 'string', format: 'uuid'},
|
||||
amount: {type: 'number', minimum: 0.01},
|
||||
paymentDate: {type: 'string', format: 'date-time'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Debt payment created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
payment: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
paymentController.create.bind(paymentController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete debt payment
|
||||
*/
|
||||
fastify.delete(
|
||||
'/payments/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete a debt payment',
|
||||
tags: ['Debts'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Debt payment deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
paymentController.delete.bind(paymentController)
|
||||
);
|
||||
}
|
||||
337
backend-api/src/routes/invoice.routes.ts
Normal file
337
backend-api/src/routes/invoice.routes.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {InvoiceController} from '../controllers/InvoiceController';
|
||||
import {InvoiceService} from '../services/InvoiceService';
|
||||
import {InvoiceRepository} from '../repositories/InvoiceRepository';
|
||||
import {ClientRepository} from '../repositories/ClientRepository';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
const invoiceRepository = new InvoiceRepository();
|
||||
const clientRepository = new ClientRepository();
|
||||
const invoiceService = new InvoiceService(invoiceRepository, clientRepository);
|
||||
const invoiceController = new InvoiceController(invoiceService);
|
||||
|
||||
export async function invoiceRoutes(fastify: FastifyInstance) {
|
||||
// Apply authentication to all routes
|
||||
fastify.addHook('onRequest', authenticate);
|
||||
|
||||
/**
|
||||
* Get all invoices
|
||||
*/
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all invoices for the authenticated user',
|
||||
tags: ['Invoices'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
clientId: {type: 'string', description: 'Filter by client ID'},
|
||||
status: {type: 'string', description: 'Filter by status'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of invoices',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoices: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
invoiceNumber: {type: 'string'},
|
||||
status: {type: 'string'},
|
||||
issueDate: {type: 'string'},
|
||||
dueDate: {type: 'string'},
|
||||
subtotal: {type: 'number'},
|
||||
tax: {type: 'number'},
|
||||
total: {type: 'number'},
|
||||
notes: {type: 'string', nullable: true},
|
||||
terms: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoiceController.getAll.bind(invoiceController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get invoice statistics
|
||||
*/
|
||||
fastify.get(
|
||||
'/stats',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get invoice statistics (total, paid, outstanding, overdue)',
|
||||
tags: ['Invoices'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'Invoice statistics',
|
||||
type: 'object',
|
||||
properties: {
|
||||
stats: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total: {type: 'number'},
|
||||
paid: {type: 'number'},
|
||||
outstanding: {type: 'number'},
|
||||
overdue: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoiceController.getStats.bind(invoiceController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get overdue invoices
|
||||
*/
|
||||
fastify.get(
|
||||
'/overdue',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all overdue invoices',
|
||||
tags: ['Invoices'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of overdue invoices',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoices: {type: 'array', items: {type: 'object'}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoiceController.getOverdue.bind(invoiceController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get single invoice
|
||||
*/
|
||||
fastify.get(
|
||||
'/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get a single invoice by ID',
|
||||
tags: ['Invoices'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Invoice details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
invoiceNumber: {type: 'string'},
|
||||
status: {type: 'string'},
|
||||
issueDate: {type: 'string'},
|
||||
dueDate: {type: 'string'},
|
||||
subtotal: {type: 'number'},
|
||||
tax: {type: 'number'},
|
||||
total: {type: 'number'},
|
||||
notes: {type: 'string', nullable: true},
|
||||
terms: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoiceController.getOne.bind(invoiceController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create invoice
|
||||
*/
|
||||
fastify.post(
|
||||
'/',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create a new invoice',
|
||||
tags: ['Invoices'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['clientId', 'issueDate', 'dueDate', 'lineItems'],
|
||||
properties: {
|
||||
clientId: {type: 'string', format: 'uuid'},
|
||||
issueDate: {type: 'string', format: 'date-time'},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
lineItems: {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['description', 'quantity', 'unitPrice', 'amount'],
|
||||
properties: {
|
||||
description: {type: 'string', minLength: 1},
|
||||
quantity: {type: 'number', minimum: 1},
|
||||
unitPrice: {type: 'number', minimum: 0},
|
||||
amount: {type: 'number', minimum: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
notes: {type: 'string'},
|
||||
terms: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Invoice created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoiceController.create.bind(invoiceController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Update invoice
|
||||
*/
|
||||
fastify.put(
|
||||
'/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Update an invoice',
|
||||
tags: ['Invoices'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
issueDate: {type: 'string', format: 'date-time'},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
lineItems: {
|
||||
type: 'array',
|
||||
minItems: 1,
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['description', 'quantity', 'unitPrice', 'amount'],
|
||||
properties: {
|
||||
description: {type: 'string', minLength: 1},
|
||||
quantity: {type: 'number', minimum: 1},
|
||||
unitPrice: {type: 'number', minimum: 0},
|
||||
amount: {type: 'number', minimum: 0},
|
||||
},
|
||||
},
|
||||
},
|
||||
notes: {type: 'string'},
|
||||
terms: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Invoice updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoiceController.update.bind(invoiceController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Update invoice status
|
||||
*/
|
||||
fastify.patch(
|
||||
'/:id/status',
|
||||
{
|
||||
schema: {
|
||||
description: 'Update invoice status',
|
||||
tags: ['Invoices'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['status'],
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['DRAFT', 'SENT', 'PAID', 'OVERDUE', 'CANCELLED'],
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Invoice status updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoiceController.updateStatus.bind(invoiceController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete invoice
|
||||
*/
|
||||
fastify.delete(
|
||||
'/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete an invoice',
|
||||
tags: ['Invoices'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Invoice deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
invoiceController.delete.bind(invoiceController)
|
||||
);
|
||||
}
|
||||
263
backend-api/src/routes/liability.routes.ts
Normal file
263
backend-api/src/routes/liability.routes.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {LiabilityController} from '../controllers/LiabilityController';
|
||||
import {LiabilityService} from '../services/LiabilityService';
|
||||
import {LiabilityRepository} from '../repositories/LiabilityRepository';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
const liabilityRepository = new LiabilityRepository();
|
||||
const liabilityService = new LiabilityService(liabilityRepository);
|
||||
const liabilityController = new LiabilityController(liabilityService);
|
||||
|
||||
export async function liabilityRoutes(fastify: FastifyInstance) {
|
||||
// Apply authentication to all routes
|
||||
fastify.addHook('onRequest', authenticate);
|
||||
|
||||
/**
|
||||
* Get all liabilities
|
||||
*/
|
||||
fastify.get(
|
||||
'/',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all liabilities for the authenticated user',
|
||||
tags: ['Liabilities'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of liabilities',
|
||||
type: 'object',
|
||||
properties: {
|
||||
liabilities: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
name: {type: 'string'},
|
||||
type: {type: 'string'},
|
||||
currentBalance: {type: 'number'},
|
||||
interestRate: {type: 'number', nullable: true},
|
||||
minimumPayment: {type: 'number', nullable: true},
|
||||
dueDate: {type: 'string', nullable: true},
|
||||
creditor: {type: 'string', nullable: true},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
liabilityController.getAll.bind(liabilityController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get total liability value
|
||||
*/
|
||||
fastify.get(
|
||||
'/total',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get total value of all liabilities',
|
||||
tags: ['Liabilities'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'Total liability value',
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalValue: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
liabilityController.getTotalValue.bind(liabilityController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get liabilities by type
|
||||
*/
|
||||
fastify.get(
|
||||
'/by-type',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get liabilities grouped by type',
|
||||
tags: ['Liabilities'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'Liabilities grouped by type',
|
||||
type: 'object',
|
||||
properties: {
|
||||
liabilitiesByType: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
liabilityController.getByType.bind(liabilityController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get single liability
|
||||
*/
|
||||
fastify.get(
|
||||
'/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get a single liability by ID',
|
||||
tags: ['Liabilities'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Liability details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
liability: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
name: {type: 'string'},
|
||||
type: {type: 'string'},
|
||||
currentBalance: {type: 'number'},
|
||||
interestRate: {type: 'number', nullable: true},
|
||||
minimumPayment: {type: 'number', nullable: true},
|
||||
dueDate: {type: 'string', nullable: true},
|
||||
creditor: {type: 'string', nullable: true},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
updatedAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
liabilityController.getOne.bind(liabilityController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create liability
|
||||
*/
|
||||
fastify.post(
|
||||
'/',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create a new liability',
|
||||
tags: ['Liabilities'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'type', 'currentBalance'],
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
type: {type: 'string'},
|
||||
currentBalance: {type: 'number', minimum: 0},
|
||||
interestRate: {type: 'number', minimum: 0, maximum: 100},
|
||||
minimumPayment: {type: 'number', minimum: 0},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
creditor: {type: 'string', maxLength: 255},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Liability created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
liability: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
liabilityController.create.bind(liabilityController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Update liability
|
||||
*/
|
||||
fastify.put(
|
||||
'/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Update a liability',
|
||||
tags: ['Liabilities'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {type: 'string', minLength: 1, maxLength: 255},
|
||||
type: {type: 'string'},
|
||||
currentBalance: {type: 'number', minimum: 0},
|
||||
interestRate: {type: 'number', minimum: 0, maximum: 100},
|
||||
minimumPayment: {type: 'number', minimum: 0},
|
||||
dueDate: {type: 'string', format: 'date-time'},
|
||||
creditor: {type: 'string', maxLength: 255},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Liability updated successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
liability: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
liabilityController.update.bind(liabilityController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete liability
|
||||
*/
|
||||
fastify.delete(
|
||||
'/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete a liability',
|
||||
tags: ['Liabilities'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Liability deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
liabilityController.delete.bind(liabilityController)
|
||||
);
|
||||
}
|
||||
279
backend-api/src/routes/networth.routes.ts
Normal file
279
backend-api/src/routes/networth.routes.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import {FastifyInstance} from 'fastify';
|
||||
import {NetWorthController} from '../controllers/NetWorthController';
|
||||
import {NetWorthService} from '../services/NetWorthService';
|
||||
import {NetWorthSnapshotRepository} from '../repositories/NetWorthSnapshotRepository';
|
||||
import {AssetRepository} from '../repositories/AssetRepository';
|
||||
import {LiabilityRepository} from '../repositories/LiabilityRepository';
|
||||
import {authenticate} from '../middleware/auth';
|
||||
|
||||
const snapshotRepository = new NetWorthSnapshotRepository();
|
||||
const assetRepository = new AssetRepository();
|
||||
const liabilityRepository = new LiabilityRepository();
|
||||
const netWorthService = new NetWorthService(snapshotRepository, assetRepository, liabilityRepository);
|
||||
const netWorthController = new NetWorthController(netWorthService);
|
||||
|
||||
export async function netWorthRoutes(fastify: FastifyInstance) {
|
||||
// Apply authentication to all routes
|
||||
fastify.addHook('onRequest', authenticate);
|
||||
|
||||
/**
|
||||
* Get current net worth
|
||||
*/
|
||||
fastify.get(
|
||||
'/current',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get current net worth (calculated or from latest snapshot)',
|
||||
tags: ['Net Worth'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'Current net worth',
|
||||
type: 'object',
|
||||
properties: {
|
||||
totalAssets: {type: 'number'},
|
||||
totalLiabilities: {type: 'number'},
|
||||
netWorth: {type: 'number'},
|
||||
asOf: {type: 'string'},
|
||||
isCalculated: {type: 'boolean'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
netWorthController.getCurrent.bind(netWorthController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get all snapshots
|
||||
*/
|
||||
fastify.get(
|
||||
'/snapshots',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get all net worth snapshots',
|
||||
tags: ['Net Worth'],
|
||||
security: [{bearerAuth: []}],
|
||||
response: {
|
||||
200: {
|
||||
description: 'List of snapshots',
|
||||
type: 'object',
|
||||
properties: {
|
||||
snapshots: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
date: {type: 'string'},
|
||||
totalAssets: {type: 'number'},
|
||||
totalLiabilities: {type: 'number'},
|
||||
netWorth: {type: 'number'},
|
||||
notes: {type: 'string', nullable: true},
|
||||
createdAt: {type: 'string'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
netWorthController.getAllSnapshots.bind(netWorthController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get snapshots by date range
|
||||
*/
|
||||
fastify.get(
|
||||
'/snapshots/range',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get snapshots within a date range',
|
||||
tags: ['Net Worth'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
required: ['startDate', 'endDate'],
|
||||
properties: {
|
||||
startDate: {type: 'string', format: 'date-time'},
|
||||
endDate: {type: 'string', format: 'date-time'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Snapshots in date range',
|
||||
type: 'object',
|
||||
properties: {
|
||||
snapshots: {type: 'array', items: {type: 'object'}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
netWorthController.getByDateRange.bind(netWorthController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get growth statistics
|
||||
*/
|
||||
fastify.get(
|
||||
'/growth',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get net worth growth statistics',
|
||||
tags: ['Net Worth'],
|
||||
security: [{bearerAuth: []}],
|
||||
querystring: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: {type: 'string', description: 'Number of periods to include (default: 12)'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Growth statistics',
|
||||
type: 'object',
|
||||
properties: {
|
||||
stats: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: {type: 'string'},
|
||||
netWorth: {type: 'number'},
|
||||
growthAmount: {type: 'number'},
|
||||
growthPercent: {type: 'number'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
netWorthController.getGrowthStats.bind(netWorthController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Get single snapshot
|
||||
*/
|
||||
fastify.get(
|
||||
'/snapshots/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get a single snapshot by ID',
|
||||
tags: ['Net Worth'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
description: 'Snapshot details',
|
||||
type: 'object',
|
||||
properties: {
|
||||
snapshot: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
netWorthController.getOne.bind(netWorthController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create manual snapshot
|
||||
*/
|
||||
fastify.post(
|
||||
'/snapshots',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create a new net worth snapshot manually',
|
||||
tags: ['Net Worth'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['date', 'totalAssets', 'totalLiabilities', 'netWorth'],
|
||||
properties: {
|
||||
date: {type: 'string', format: 'date-time'},
|
||||
totalAssets: {type: 'number', minimum: 0},
|
||||
totalLiabilities: {type: 'number', minimum: 0},
|
||||
netWorth: {type: 'number'},
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Snapshot created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
snapshot: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
netWorthController.createSnapshot.bind(netWorthController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create snapshot from current data
|
||||
*/
|
||||
fastify.post(
|
||||
'/snapshots/record',
|
||||
{
|
||||
schema: {
|
||||
description: 'Create a snapshot from current assets and liabilities',
|
||||
tags: ['Net Worth'],
|
||||
security: [{bearerAuth: []}],
|
||||
body: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
notes: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
201: {
|
||||
description: 'Snapshot created successfully',
|
||||
type: 'object',
|
||||
properties: {
|
||||
snapshot: {type: 'object'},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
netWorthController.createFromCurrent.bind(netWorthController)
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete snapshot
|
||||
*/
|
||||
fastify.delete(
|
||||
'/snapshots/:id',
|
||||
{
|
||||
schema: {
|
||||
description: 'Delete a snapshot',
|
||||
tags: ['Net Worth'],
|
||||
security: [{bearerAuth: []}],
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
204: {
|
||||
description: 'Snapshot deleted successfully',
|
||||
type: 'null',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
netWorthController.delete.bind(netWorthController)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user