Add lock files for package management and update architecture documentation

- Introduced bun.lock and package-lock.json to manage dependencies for the project.
- Enhanced backend API architecture documentation with additional security and documentation guidelines.
- Made minor formatting adjustments across various files for consistency and clarity.
This commit is contained in:
2025-12-11 02:11:43 -05:00
parent 4911b5d125
commit 40210c454e
74 changed files with 2599 additions and 1386 deletions

View File

@@ -7,6 +7,7 @@ This backend API is built following **SOLID principles** and **clean architectur
## SOLID Principles Implementation
### 1. Single Responsibility Principle (SRP)
Each class has one well-defined responsibility:
- **Controllers** - Handle HTTP requests/responses only
@@ -15,23 +16,25 @@ Each class has one well-defined responsibility:
- **Middleware** - Handle cross-cutting concerns (auth, errors)
Example:
```typescript
// AssetService - ONLY handles asset business logic
export class AssetService {
async create(userId: string, data: CreateAssetDTO): Promise<Asset>
async update(id: string, userId: string, data: UpdateAssetDTO): Promise<Asset>
async create(userId: string, data: CreateAssetDTO): Promise<Asset>;
async update(id: string, userId: string, data: UpdateAssetDTO): Promise<Asset>;
// ...
}
// AssetRepository - ONLY handles database operations
export class AssetRepository {
async findById(id: string): Promise<Asset | null>
async create(data: Prisma.AssetCreateInput): Promise<Asset>
async findById(id: string): Promise<Asset | null>;
async create(data: Prisma.AssetCreateInput): Promise<Asset>;
// ...
}
```
### 2. Open/Closed Principle (OCP)
The system is open for extension but closed for modification:
- **Custom Error Classes** - Extend `AppError` base class for new error types
@@ -39,6 +42,7 @@ The system is open for extension but closed for modification:
- **Service Pattern** - Add new services without modifying existing ones
Example:
```typescript
// Extensible error hierarchy
export abstract class AppError extends Error {
@@ -55,6 +59,7 @@ export class ValidationError extends AppError {
```
### 3. Liskov Substitution Principle (LSP)
Derived classes can substitute their base classes:
```typescript
@@ -71,6 +76,7 @@ export interface IUserScopedRepository<T> extends Omit<IRepository<T>, 'findAll'
```
### 4. Interface Segregation Principle (ISP)
Clients depend only on interfaces they use:
- `IRepository<T>` - Base CRUD operations
@@ -78,6 +84,7 @@ Clients depend only on interfaces they use:
- Specific methods in services (e.g., `getTotalValue()` in AssetService)
### 5. Dependency Inversion Principle (DIP)
High-level modules depend on abstractions:
```typescript
@@ -99,6 +106,7 @@ class DatabaseConnection {
## Architecture Layers
### 1. Presentation Layer (Controllers & Routes)
- **Location**: `src/controllers/`, `src/routes/`
- **Purpose**: Handle HTTP requests/responses
- **Responsibilities**:
@@ -119,6 +127,7 @@ export class AssetController {
```
### 2. Business Logic Layer (Services)
- **Location**: `src/services/`
- **Purpose**: Implement business rules
- **Responsibilities**:
@@ -139,6 +148,7 @@ export class InvoiceService {
```
### 3. Data Access Layer (Repositories)
- **Location**: `src/repositories/`
- **Purpose**: Abstract database operations
- **Responsibilities**:
@@ -151,13 +161,14 @@ export class AssetRepository implements IUserScopedRepository<Asset> {
async findAllByUser(userId: string): Promise<Asset[]> {
return prisma.asset.findMany({
where: {userId},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
}
```
### 4. Cross-Cutting Concerns (Middleware & Utils)
- **Location**: `src/middleware/`, `src/utils/`
- **Purpose**: Handle common functionality
- **Components**:
@@ -221,21 +232,25 @@ User
## Security Features
### 1. Authentication
- JWT tokens with configurable expiration
- Secure password hashing (bcrypt with 10 rounds)
- Password complexity requirements
### 2. Authorization
- User-scoped data access
- Repository methods verify ownership
- Middleware extracts authenticated user
### 3. Input Validation
- Zod schemas for runtime validation
- Type-safe request/response handling
- SQL injection prevention (Prisma ORM)
### 4. Error Handling
- Custom error classes
- No sensitive information in error messages
- Proper HTTP status codes
@@ -243,6 +258,7 @@ User
## API Design
### RESTful Conventions
- `GET /api/resources` - List all
- `GET /api/resources/:id` - Get one
- `POST /api/resources` - Create
@@ -251,11 +267,16 @@ User
- `DELETE /api/resources/:id` - Delete
### Response Format
```json
{
"resource": { /* data */ },
"resource": {
/* data */
},
// or
"resources": [ /* array */ ],
"resources": [
/* array */
],
// or on error
"error": "ErrorType",
"message": "Human-readable message"
@@ -263,6 +284,7 @@ User
```
### HTTP Status Codes
- `200 OK` - Successful GET/PUT/PATCH
- `201 Created` - Successful POST
- `204 No Content` - Successful DELETE
@@ -276,16 +298,19 @@ User
## Testing Strategy
### Unit Tests
- Test services in isolation
- Mock repository dependencies
- Test business logic thoroughly
### Integration Tests
- Test API endpoints
- Use test database
- Verify request/response flow
### E2E Tests
- Test complete user flows
- Verify authentication
- Test error scenarios
@@ -293,16 +318,19 @@ User
## Performance Considerations
### Database
- Indexes on frequently queried fields
- Connection pooling (Prisma)
- Efficient query composition
### Caching
- JWT tokens cached in client
- Consider Redis for session management
- Database query result caching
### Scalability
- Stateless API (horizontal scaling)
- Database migrations for schema changes
- Environment-based configuration
@@ -328,18 +356,21 @@ User
## Best Practices
### Code Organization
✅ One class per file
✅ Group related files in directories
✅ Use barrel exports (index.ts)
✅ Consistent naming conventions
### Error Handling
✅ Use custom error classes
✅ Validate at boundaries
✅ Log errors appropriately
✅ Return user-friendly messages
### Security
✅ Never log sensitive data
✅ Validate all inputs
✅ Use parameterized queries (Prisma)
@@ -347,6 +378,7 @@ User
✅ Keep dependencies updated
### Documentation
✅ JSDoc comments for public APIs
✅ README for setup instructions
✅ API documentation (Swagger)

View File

@@ -1,6 +1,6 @@
---
description: Use Bun instead of Node.js, npm, pnpm, or vite.
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
globs: '*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json'
alwaysApply: false
---

View File

@@ -1 +1 @@
console.log("Hello via Bun!");
console.log('Hello via Bun!');

View File

@@ -12,7 +12,7 @@ class DatabaseConnection {
public static getInstance(): PrismaClient {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
});
}
return DatabaseConnection.instance;

View File

@@ -10,7 +10,7 @@ const envSchema = z.object({
DATABASE_URL: z.string().min(1),
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default('7d'),
CORS_ORIGIN: z.string().default('http://localhost:5174'),
CORS_ORIGIN: z.string().default('http://localhost:5174')
});
type EnvConfig = z.infer<typeof envSchema>;

View File

@@ -9,13 +9,13 @@ const ASSET_TYPES = ['cash', 'investment', 'property', 'vehicle', 'other'] as co
const createAssetSchema = z.object({
name: z.string().min(1),
type: z.enum(ASSET_TYPES),
value: z.number().min(0),
value: z.number().min(0)
});
const updateAssetSchema = z.object({
name: z.string().min(1).optional(),
type: z.enum(ASSET_TYPES).optional(),
value: z.number().min(0).optional(),
value: z.number().min(0).optional()
});
/**
@@ -32,12 +32,14 @@ export class AssetController {
async getAll(request: FastifyRequest, reply: FastifyReply) {
const userId = getUserId(request);
const assets = await this.assetService.getAll(userId);
return reply.send({assets});
}
async getById(request: FastifyRequest<{Params: {id: string}}>, reply: FastifyReply) {
const userId = getUserId(request);
const asset = await this.assetService.getById(request.params.id, userId);
return reply.send({asset});
}
@@ -45,6 +47,7 @@ export class AssetController {
const userId = getUserId(request);
const data = createAssetSchema.parse(request.body);
const asset = await this.assetService.create(userId, data);
return reply.status(201).send({asset});
}
@@ -52,12 +55,14 @@ export class AssetController {
const userId = getUserId(request);
const data = updateAssetSchema.parse(request.body);
const asset = await this.assetService.update(request.params.id, userId, data);
return reply.send({asset});
}
async delete(request: FastifyRequest<{Params: {id: string}}>, reply: FastifyReply) {
const userId = getUserId(request);
await this.assetService.delete(request.params.id, userId);
return reply.status(204).send();
}
}

View File

@@ -8,12 +8,12 @@ import {DebtCategoryRepository} from '../repositories/DebtCategoryRepository';
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(1),
name: z.string().min(1)
});
const loginSchema = z.object({
email: z.string().email(),
password: z.string(),
password: z.string()
});
/**
@@ -37,12 +37,12 @@ export class AuthController {
const token = request.server.jwt.sign({
id: user.id,
email: user.email,
email: user.email
});
return reply.status(201).send({
user,
token,
token
});
}
@@ -52,14 +52,14 @@ export class AuthController {
const token = request.server.jwt.sign({
id: user.id,
email: user.email,
email: user.email
});
const {password: _, ...userWithoutPassword} = user;
return reply.send({
user: userWithoutPassword,
token,
token
});
}

View File

@@ -7,7 +7,7 @@ const createIncomeSchema = z.object({
name: z.string().min(1).max(255),
amount: z.number().min(0.01),
frequency: z.string(),
notes: z.string().optional(),
notes: z.string().optional()
});
const updateIncomeSchema = createIncomeSchema.partial();
@@ -17,8 +17,11 @@ const createExpenseSchema = z.object({
amount: z.number().min(0.01),
category: z.string(),
frequency: z.string(),
dueDate: z.string().transform(str => new Date(str)).optional(),
notes: z.string().optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
notes: z.string().optional()
});
const updateExpenseSchema = createExpenseSchema.partial();
@@ -29,7 +32,7 @@ const createTransactionSchema = z.object({
amount: z.number().min(0.01),
date: z.string().transform(str => new Date(str)),
description: z.string().optional(),
notes: z.string().optional(),
notes: z.string().optional()
});
/**
@@ -151,11 +154,7 @@ export class CashflowController {
}
if (startDate && endDate) {
const transactions = await this.cashflowService.getTransactionsByDateRange(
userId,
new Date(startDate),
new Date(endDate)
);
const transactions = await this.cashflowService.getTransactionsByDateRange(userId, new Date(startDate), new Date(endDate));
return reply.send({transactions});
}
@@ -181,11 +180,7 @@ export class CashflowController {
const userId = getUserId(request);
const {startDate, endDate} = request.query as {startDate: string; endDate: string};
const summary = await this.cashflowService.getCashflowSummary(
userId,
new Date(startDate),
new Date(endDate)
);
const summary = await this.cashflowService.getCashflowSummary(userId, new Date(startDate), new Date(endDate));
return reply.send(summary);
}

View File

@@ -8,7 +8,7 @@ const createClientSchema = z.object({
email: z.string().email(),
phone: z.string().max(50).optional(),
address: z.string().optional(),
notes: z.string().optional(),
notes: z.string().optional()
});
const updateClientSchema = z.object({
@@ -16,7 +16,7 @@ const updateClientSchema = z.object({
email: z.string().email().optional(),
phone: z.string().max(50).optional(),
address: z.string().optional(),
notes: z.string().optional(),
notes: z.string().optional()
});
/**

View File

@@ -12,8 +12,11 @@ const createAccountSchema = z.object({
currentBalance: z.number().min(0),
interestRate: z.number().min(0).max(100).optional(),
minimumPayment: z.number().min(0).optional(),
dueDate: z.string().transform(str => new Date(str)).optional(),
notes: z.string().optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
notes: z.string().optional()
});
const updateAccountSchema = z.object({
@@ -23,8 +26,11 @@ const updateAccountSchema = z.object({
currentBalance: z.number().min(0).optional(),
interestRate: z.number().min(0).max(100).optional(),
minimumPayment: z.number().min(0).optional(),
dueDate: z.string().transform(str => new Date(str)).optional(),
notes: z.string().optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
notes: z.string().optional()
});
/**

View File

@@ -6,13 +6,19 @@ import {z} from 'zod';
const createCategorySchema = z.object({
name: z.string().min(1).max(255),
description: z.string().optional(),
color: z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/).optional(),
color: z
.string()
.regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)
.optional()
});
const updateCategorySchema = z.object({
name: z.string().min(1).max(255).optional(),
description: z.string().optional(),
color: z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/).optional(),
color: z
.string()
.regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/)
.optional()
});
/**

View File

@@ -7,7 +7,7 @@ const createPaymentSchema = z.object({
accountId: z.string().uuid(),
amount: z.number().min(0.01),
paymentDate: z.string().transform(str => new Date(str)),
notes: z.string().optional(),
notes: z.string().optional()
});
/**
@@ -46,11 +46,7 @@ export class DebtPaymentController {
}
if (startDate && endDate) {
const payments = await this.paymentService.getByDateRange(
userId,
new Date(startDate),
new Date(endDate)
);
const payments = await this.paymentService.getByDateRange(userId, new Date(startDate), new Date(endDate));
return reply.send({payments});
}

View File

@@ -7,7 +7,7 @@ const lineItemSchema = z.object({
description: z.string().min(1),
quantity: z.number().min(1),
unitPrice: z.number().min(0),
amount: z.number().min(0),
amount: z.number().min(0)
});
const createInvoiceSchema = z.object({
@@ -16,19 +16,25 @@ const createInvoiceSchema = z.object({
dueDate: z.string().transform(str => new Date(str)),
lineItems: z.array(lineItemSchema).min(1),
notes: z.string().optional(),
terms: z.string().optional(),
terms: z.string().optional()
});
const updateInvoiceSchema = z.object({
issueDate: z.string().transform(str => new Date(str)).optional(),
dueDate: z.string().transform(str => new Date(str)).optional(),
issueDate: z
.string()
.transform(str => new Date(str))
.optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
lineItems: z.array(lineItemSchema).min(1).optional(),
notes: z.string().optional(),
terms: z.string().optional(),
terms: z.string().optional()
});
const updateStatusSchema = z.object({
status: z.enum(['DRAFT', 'SENT', 'PAID', 'OVERDUE', 'CANCELLED']),
status: z.enum(['DRAFT', 'SENT', 'PAID', 'OVERDUE', 'CANCELLED'])
});
/**
@@ -59,7 +65,7 @@ export class InvoiceController {
const invoices = await this.invoiceService.getAllByUser(userId, {
clientId,
status,
status
});
return reply.send({invoices});

View File

@@ -9,9 +9,12 @@ const createLiabilitySchema = z.object({
currentBalance: z.number().min(0),
interestRate: z.number().min(0).max(100).optional(),
minimumPayment: z.number().min(0).optional(),
dueDate: z.string().transform(str => new Date(str)).optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
creditor: z.string().max(255).optional(),
notes: z.string().optional(),
notes: z.string().optional()
});
const updateLiabilitySchema = z.object({
@@ -20,9 +23,12 @@ const updateLiabilitySchema = z.object({
currentBalance: z.number().min(0).optional(),
interestRate: z.number().min(0).max(100).optional(),
minimumPayment: z.number().min(0).optional(),
dueDate: z.string().transform(str => new Date(str)).optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
creditor: z.string().max(255).optional(),
notes: z.string().optional(),
notes: z.string().optional()
});
/**

View File

@@ -8,16 +8,16 @@ const createSnapshotSchema = z.object({
totalAssets: z.number().min(0),
totalLiabilities: z.number().min(0),
netWorth: z.number(),
notes: z.string().optional(),
notes: z.string().optional()
});
const createFromCurrentSchema = z.object({
notes: z.string().optional(),
notes: z.string().optional()
});
const dateRangeSchema = z.object({
startDate: z.string().transform(str => new Date(str)),
endDate: z.string().transform(str => new Date(str)),
endDate: z.string().transform(str => new Date(str))
});
/**
@@ -55,11 +55,7 @@ export class NetWorthController {
const {startDate, endDate} = request.query as {startDate: string; endDate: string};
const parsed = dateRangeSchema.parse({startDate, endDate});
const snapshots = await this.netWorthService.getSnapshotsByDateRange(
userId,
parsed.startDate,
parsed.endDate
);
const snapshots = await this.netWorthService.getSnapshotsByDateRange(userId, parsed.startDate, parsed.endDate);
return reply.send({snapshots});
}
@@ -119,10 +115,7 @@ export class NetWorthController {
const userId = getUserId(request);
const {limit} = request.query as {limit?: string};
const stats = await this.netWorthService.getGrowthStats(
userId,
limit ? parseInt(limit) : undefined
);
const stats = await this.netWorthService.getGrowthStats(userId, limit ? parseInt(limit) : undefined);
return reply.send({stats});
}

View File

@@ -12,7 +12,7 @@ async function main() {
// Start server
await server.listen({
port: env.PORT,
host: '0.0.0.0',
host: '0.0.0.0'
});
server.log.info(`🚀 Server listening on http://localhost:${env.PORT}`);

View File

@@ -14,7 +14,7 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
if (error instanceof AppError) {
return reply.status(error.statusCode).send({
error: error.name,
message: error.message,
message: error.message
});
}
@@ -23,7 +23,7 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
return reply.status(400).send({
error: 'ValidationError',
message: 'Invalid request data',
details: error.errors,
details: error.errors
});
}
@@ -32,7 +32,7 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
return reply.status(400).send({
error: 'ValidationError',
message: error.message,
details: error.validation,
details: error.validation
});
}
@@ -42,13 +42,13 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
if (prismaError.code === 'P2002') {
return reply.status(409).send({
error: 'ConflictError',
message: 'A record with this value already exists',
message: 'A record with this value already exists'
});
}
if (prismaError.code === 'P2025') {
return reply.status(404).send({
error: 'NotFoundError',
message: 'Record not found',
message: 'Record not found'
});
}
}
@@ -59,6 +59,6 @@ export async function errorHandler(error: FastifyError, request: FastifyRequest,
return reply.status(statusCode).send({
error: 'ServerError',
message,
message
});
}

View File

@@ -12,14 +12,14 @@ export class AssetRepository {
async findByIdAndUser(id: string, userId: string): Promise<Asset | null> {
return prisma.asset.findFirst({
where: {id, userId},
where: {id, userId}
});
}
async findAllByUser(userId: string, filters?: Record<string, any>): Promise<Asset[]> {
return prisma.asset.findMany({
where: {userId, ...filters},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
@@ -30,7 +30,7 @@ export class AssetRepository {
async update(id: string, data: Prisma.AssetUpdateInput): Promise<Asset> {
return prisma.asset.update({
where: {id},
data,
data
});
}
@@ -41,7 +41,7 @@ export class AssetRepository {
async getTotalValue(userId: string): Promise<number> {
const result = await prisma.asset.aggregate({
where: {userId},
_sum: {value: true},
_sum: {value: true}
});
return result._sum.value || 0;
}

View File

@@ -18,7 +18,7 @@ export class IncomeSourceRepository {
async findAllByUser(userId: string): Promise<IncomeSource[]> {
return prisma.incomeSource.findMany({
where: {userId},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
@@ -37,7 +37,7 @@ export class IncomeSourceRepository {
async getTotalMonthlyIncome(userId: string): Promise<number> {
const result = await prisma.incomeSource.aggregate({
where: {userId},
_sum: {amount: true},
_sum: {amount: true}
});
return result._sum.amount || 0;
}
@@ -58,7 +58,7 @@ export class ExpenseRepository {
async findAllByUser(userId: string): Promise<Expense[]> {
return prisma.expense.findMany({
where: {userId},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
@@ -77,18 +77,21 @@ export class ExpenseRepository {
async getTotalMonthlyExpenses(userId: string): Promise<number> {
const result = await prisma.expense.aggregate({
where: {userId},
_sum: {amount: true},
_sum: {amount: true}
});
return result._sum.amount || 0;
}
async getByCategory(userId: string): Promise<Record<string, Expense[]>> {
const expenses = await this.findAllByUser(userId);
return expenses.reduce((acc, expense) => {
if (!acc[expense.category]) acc[expense.category] = [];
acc[expense.category].push(expense);
return acc;
}, {} as Record<string, Expense[]>);
return expenses.reduce(
(acc, expense) => {
if (!acc[expense.category]) acc[expense.category] = [];
acc[expense.category].push(expense);
return acc;
},
{} as Record<string, Expense[]>
);
}
}
@@ -107,7 +110,7 @@ export class TransactionRepository {
async findAllByUser(userId: string): Promise<Transaction[]> {
return prisma.transaction.findMany({
where: {userId},
orderBy: {date: 'desc'},
orderBy: {date: 'desc'}
});
}
@@ -127,38 +130,38 @@ export class TransactionRepository {
return prisma.transaction.findMany({
where: {
userId,
date: {gte: startDate, lte: endDate},
date: {gte: startDate, lte: endDate}
},
orderBy: {date: 'desc'},
orderBy: {date: 'desc'}
});
}
async getByType(userId: string, type: string): Promise<Transaction[]> {
return prisma.transaction.findMany({
where: {userId, type},
orderBy: {date: 'desc'},
orderBy: {date: 'desc'}
});
}
async getCashflowSummary(userId: string, startDate: Date, endDate: Date): Promise<{
async getCashflowSummary(
userId: string,
startDate: Date,
endDate: Date
): Promise<{
totalIncome: number;
totalExpenses: number;
netCashflow: number;
}> {
const transactions = await this.getByDateRange(userId, startDate, endDate);
const totalIncome = transactions
.filter(t => t.type === 'income')
.reduce((sum, t) => sum + t.amount, 0);
const totalIncome = transactions.filter(t => t.type === 'income').reduce((sum, t) => sum + t.amount, 0);
const totalExpenses = transactions
.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + t.amount, 0);
const totalExpenses = transactions.filter(t => t.type === 'expense').reduce((sum, t) => sum + t.amount, 0);
return {
totalIncome,
totalExpenses,
netCashflow: totalIncome - totalExpenses,
netCashflow: totalIncome - totalExpenses
};
}
}

View File

@@ -12,8 +12,8 @@ export class ClientRepository {
return prisma.client.findUnique({
where: {id},
include: {
invoices: true,
},
invoices: true
}
});
}
@@ -21,8 +21,8 @@ export class ClientRepository {
return prisma.client.findFirst({
where: {id, userId},
include: {
invoices: true,
},
invoices: true
}
});
}
@@ -31,10 +31,10 @@ export class ClientRepository {
where: {userId},
include: {
invoices: {
orderBy: {createdAt: 'desc'},
},
orderBy: {createdAt: 'desc'}
}
},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
@@ -42,8 +42,8 @@ export class ClientRepository {
return prisma.client.create({
data,
include: {
invoices: true,
},
invoices: true
}
});
}
@@ -52,14 +52,14 @@ export class ClientRepository {
where: {id},
data,
include: {
invoices: true,
},
invoices: true
}
});
}
async delete(id: string): Promise<void> {
await prisma.client.delete({
where: {id},
where: {id}
});
}
@@ -70,8 +70,8 @@ export class ClientRepository {
return prisma.client.findFirst({
where: {
userId,
email,
},
email
}
});
}
@@ -82,13 +82,13 @@ export class ClientRepository {
const result = await prisma.invoice.aggregate({
where: {
client: {
userId,
userId
},
status: 'paid',
status: 'paid'
},
_sum: {
total: true,
},
total: true
}
});
return result._sum.total || 0;
@@ -105,11 +105,11 @@ export class ClientRepository {
select: {
id: true,
total: true,
status: true,
},
},
status: true
}
}
},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
return clients.map(client => ({
@@ -117,13 +117,9 @@ export class ClientRepository {
stats: {
totalInvoices: client.invoices.length,
paidInvoices: client.invoices.filter(inv => inv.status === 'paid').length,
totalRevenue: client.invoices
.filter(inv => inv.status === 'paid')
.reduce((sum, inv) => sum + inv.total, 0),
outstandingAmount: client.invoices
.filter(inv => inv.status !== 'paid')
.reduce((sum, inv) => sum + inv.total, 0),
},
totalRevenue: client.invoices.filter(inv => inv.status === 'paid').reduce((sum, inv) => sum + inv.total, 0),
outstandingAmount: client.invoices.filter(inv => inv.status !== 'paid').reduce((sum, inv) => sum + inv.total, 0)
}
}));
}
}

View File

@@ -14,9 +14,9 @@ export class DebtAccountRepository {
include: {
category: true,
payments: {
orderBy: {date: 'desc'},
},
},
orderBy: {date: 'desc'}
}
}
});
}
@@ -26,9 +26,9 @@ export class DebtAccountRepository {
include: {
category: true,
payments: {
orderBy: {date: 'desc'},
},
},
orderBy: {date: 'desc'}
}
}
});
}
@@ -38,10 +38,10 @@ export class DebtAccountRepository {
include: {
category: true,
payments: {
orderBy: {date: 'desc'},
},
orderBy: {date: 'desc'}
}
},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
@@ -50,10 +50,10 @@ export class DebtAccountRepository {
where: {categoryId},
include: {
payments: {
orderBy: {date: 'desc'},
},
orderBy: {date: 'desc'}
}
},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
@@ -62,8 +62,8 @@ export class DebtAccountRepository {
data,
include: {
category: true,
payments: true,
},
payments: true
}
});
}
@@ -73,14 +73,14 @@ export class DebtAccountRepository {
data,
include: {
category: true,
payments: true,
},
payments: true
}
});
}
async delete(id: string): Promise<void> {
await prisma.debtAccount.delete({
where: {id},
where: {id}
});
}
@@ -91,8 +91,8 @@ export class DebtAccountRepository {
const result = await prisma.debtAccount.aggregate({
where: {userId},
_sum: {
currentBalance: true,
},
currentBalance: true
}
});
return result._sum.currentBalance || 0;
@@ -107,9 +107,9 @@ export class DebtAccountRepository {
include: {
category: true,
payments: {
orderBy: {date: 'desc'},
},
},
orderBy: {date: 'desc'}
}
}
});
return accounts.map(account => {
@@ -122,8 +122,8 @@ export class DebtAccountRepository {
totalPaid,
numberOfPayments: account.payments.length,
lastPaymentDate: lastPayment?.date || null,
lastPaymentAmount: lastPayment?.amount || null,
},
lastPaymentAmount: lastPayment?.amount || null
}
};
});
}

View File

@@ -15,10 +15,10 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
include: {
accounts: {
include: {
payments: true,
},
},
},
payments: true
}
}
}
});
}
@@ -28,12 +28,12 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
include: {
accounts: {
include: {
payments: true,
payments: true
},
orderBy: {createdAt: 'desc'},
},
orderBy: {createdAt: 'desc'}
}
},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
@@ -41,8 +41,8 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
return prisma.debtCategory.create({
data,
include: {
accounts: true,
},
accounts: true
}
});
}
@@ -51,14 +51,14 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
where: {id},
data,
include: {
accounts: true,
},
accounts: true
}
});
}
async delete(id: string): Promise<void> {
await prisma.debtCategory.delete({
where: {id},
where: {id}
});
}
@@ -69,8 +69,8 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
return prisma.debtCategory.findFirst({
where: {
userId,
name,
},
name
}
});
}
@@ -81,8 +81,8 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
const result = await prisma.debtAccount.aggregate({
where: {categoryId},
_sum: {
currentBalance: true,
},
currentBalance: true
}
});
return result._sum.currentBalance || 0;
@@ -97,19 +97,15 @@ export class DebtCategoryRepository implements IUserScopedRepository<DebtCategor
return Promise.all(
categories.map(async category => {
const totalDebt = await this.getTotalDebt(category.id);
const totalPayments = category.accounts.reduce(
(sum, account) =>
sum + account.payments.reduce((pSum, payment) => pSum + payment.amount, 0),
0
);
const totalPayments = category.accounts.reduce((sum, account) => sum + account.payments.reduce((pSum, payment) => pSum + payment.amount, 0), 0);
return {
...category,
stats: {
totalAccounts: category.accounts.length,
totalDebt,
totalPayments,
},
totalPayments
}
};
})
);

View File

@@ -14,17 +14,17 @@ export class DebtPaymentRepository {
include: {
account: {
include: {
category: true,
},
},
},
category: true
}
}
}
});
}
async findByAccount(accountId: string): Promise<DebtPayment[]> {
return prisma.debtPayment.findMany({
where: {accountId},
orderBy: {date: 'desc'},
orderBy: {date: 'desc'}
});
}
@@ -33,18 +33,18 @@ export class DebtPaymentRepository {
where: {
account: {
category: {
userId,
},
},
userId
}
}
},
include: {
account: {
include: {
category: true,
},
},
category: true
}
}
},
orderBy: {date: 'desc'},
orderBy: {date: 'desc'}
});
}
@@ -54,16 +54,16 @@ export class DebtPaymentRepository {
include: {
account: {
include: {
category: true,
},
},
},
category: true
}
}
}
});
}
async delete(id: string): Promise<void> {
await prisma.debtPayment.delete({
where: {id},
where: {id}
});
}
@@ -74,8 +74,8 @@ export class DebtPaymentRepository {
const result = await prisma.debtPayment.aggregate({
where: {accountId},
_sum: {
amount: true,
},
amount: true
}
});
return result._sum.amount || 0;
@@ -89,13 +89,13 @@ export class DebtPaymentRepository {
where: {
account: {
category: {
userId,
},
},
userId
}
}
},
_sum: {
amount: true,
},
amount: true
}
});
return result._sum.amount || 0;
@@ -109,22 +109,22 @@ export class DebtPaymentRepository {
where: {
account: {
category: {
userId,
},
userId
}
},
date: {
gte: startDate,
lte: endDate,
},
lte: endDate
}
},
include: {
account: {
include: {
category: true,
},
},
category: true
}
}
},
orderBy: {date: 'desc'},
orderBy: {date: 'desc'}
});
}
}

View File

@@ -15,14 +15,14 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
async findById(id: string): Promise<Invoice | null> {
return prisma.invoice.findUnique({
where: {id},
include: {lineItems: true, client: true},
include: {lineItems: true, client: true}
}) as unknown as Invoice;
}
async findByIdAndUser(id: string, userId: string): Promise<InvoiceWithLineItems | null> {
return prisma.invoice.findFirst({
where: {id, userId},
include: {lineItems: true, client: true},
include: {lineItems: true, client: true}
});
}
@@ -30,14 +30,14 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
return prisma.invoice.findMany({
where: {userId, ...filters},
include: {lineItems: true, client: true},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
async create(data: Prisma.InvoiceCreateInput): Promise<Invoice> {
return prisma.invoice.create({
data,
include: {lineItems: true, client: true},
include: {lineItems: true, client: true}
}) as unknown as Invoice;
}
@@ -45,7 +45,7 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
return prisma.invoice.update({
where: {id},
data,
include: {lineItems: true, client: true},
include: {lineItems: true, client: true}
}) as unknown as Invoice;
}
@@ -58,8 +58,8 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
where: {
userId,
invoiceNumber,
...(excludeId && {id: {not: excludeId}}),
},
...(excludeId && {id: {not: excludeId}})
}
});
return count > 0;
}
@@ -69,8 +69,8 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
const count = await prisma.invoice.count({
where: {
userId,
invoiceNumber: {startsWith: `INV-${year}-`},
},
invoiceNumber: {startsWith: `INV-${year}-`}
}
});
return `INV-${year}-${String(count + 1).padStart(3, '0')}`;
}

View File

@@ -10,39 +10,39 @@ const prisma = DatabaseConnection.getInstance();
export class LiabilityRepository {
async findById(id: string): Promise<Liability | null> {
return prisma.liability.findUnique({
where: {id},
where: {id}
});
}
async findByIdAndUser(id: string, userId: string): Promise<Liability | null> {
return prisma.liability.findFirst({
where: {id, userId},
where: {id, userId}
});
}
async findAllByUser(userId: string): Promise<Liability[]> {
return prisma.liability.findMany({
where: {userId},
orderBy: {createdAt: 'desc'},
orderBy: {createdAt: 'desc'}
});
}
async create(data: Prisma.LiabilityCreateInput): Promise<Liability> {
return prisma.liability.create({
data,
data
});
}
async update(id: string, data: Prisma.LiabilityUpdateInput): Promise<Liability> {
return prisma.liability.update({
where: {id},
data,
data
});
}
async delete(id: string): Promise<void> {
await prisma.liability.delete({
where: {id},
where: {id}
});
}
@@ -53,8 +53,8 @@ export class LiabilityRepository {
const result = await prisma.liability.aggregate({
where: {userId},
_sum: {
balance: true,
},
balance: true
}
});
return result._sum.balance || 0;
@@ -66,13 +66,16 @@ export class LiabilityRepository {
async getByType(userId: string): Promise<Record<string, Liability[]>> {
const liabilities = await this.findAllByUser(userId);
return liabilities.reduce((acc, liability) => {
const type = liability.type;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push(liability);
return acc;
}, {} as Record<string, Liability[]>);
return liabilities.reduce(
(acc, liability) => {
const type = liability.type;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push(liability);
return acc;
},
{} as Record<string, Liability[]>
);
}
}

View File

@@ -11,33 +11,33 @@ const prisma = DatabaseConnection.getInstance();
export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWorthSnapshot> {
async findById(id: string): Promise<NetWorthSnapshot | null> {
return prisma.netWorthSnapshot.findUnique({
where: {id},
where: {id}
});
}
async findAllByUser(userId: string): Promise<NetWorthSnapshot[]> {
return prisma.netWorthSnapshot.findMany({
where: {userId},
orderBy: {date: 'desc'},
orderBy: {date: 'desc'}
});
}
async create(data: Prisma.NetWorthSnapshotCreateInput): Promise<NetWorthSnapshot> {
return prisma.netWorthSnapshot.create({
data,
data
});
}
async update(id: string, data: Prisma.NetWorthSnapshotUpdateInput): Promise<NetWorthSnapshot> {
return prisma.netWorthSnapshot.update({
where: {id},
data,
data
});
}
async delete(id: string): Promise<void> {
await prisma.netWorthSnapshot.delete({
where: {id},
where: {id}
});
}
@@ -47,7 +47,7 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
async getLatest(userId: string): Promise<NetWorthSnapshot | null> {
return prisma.netWorthSnapshot.findFirst({
where: {userId},
orderBy: {date: 'desc'},
orderBy: {date: 'desc'}
});
}
@@ -60,10 +60,10 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
userId,
date: {
gte: startDate,
lte: endDate,
},
lte: endDate
}
},
orderBy: {date: 'asc'},
orderBy: {date: 'asc'}
});
}
@@ -74,8 +74,8 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
const count = await prisma.netWorthSnapshot.count({
where: {
userId,
date,
},
date
}
});
return count > 0;
@@ -88,7 +88,7 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
const snapshots = await prisma.netWorthSnapshot.findMany({
where: {userId},
orderBy: {date: 'desc'},
take: limit,
take: limit
});
const stats = [];
@@ -96,14 +96,13 @@ export class NetWorthSnapshotRepository implements IUserScopedRepository<NetWort
const current = snapshots[i];
const previous = snapshots[i + 1];
const growthAmount = current.netWorth - previous.netWorth;
const growthPercent =
previous.netWorth !== 0 ? (growthAmount / previous.netWorth) * 100 : 0;
const growthPercent = previous.netWorth !== 0 ? (growthAmount / previous.netWorth) * 100 : 0;
stats.push({
date: current.date,
netWorth: current.netWorth,
growthAmount,
growthPercent: parseFloat(growthPercent.toFixed(2)),
growthPercent: parseFloat(growthPercent.toFixed(2))
});
}

View File

@@ -24,8 +24,8 @@ export class UserRepository implements IRepository<User> {
name: true,
createdAt: true,
updatedAt: true,
password: false, // Never return password
},
password: false // Never return password
}
}) as unknown as User[];
}
@@ -36,7 +36,7 @@ export class UserRepository implements IRepository<User> {
async update(id: string, data: Prisma.UserUpdateInput): Promise<User> {
return prisma.user.update({
where: {id},
data,
data
});
}

View File

@@ -15,8 +15,7 @@ export interface IRepository<T, CreateInput = unknown, UpdateInput = unknown> {
* User-scoped repository interface
* For entities that belong to a specific user
*/
export interface IUserScopedRepository<T, CreateInput = unknown, UpdateInput = unknown>
extends Omit<IRepository<T, CreateInput, UpdateInput>, 'findAll'> {
export interface IUserScopedRepository<T, CreateInput = unknown, UpdateInput = unknown> extends Omit<IRepository<T, CreateInput, UpdateInput>, 'findAll'> {
findAllByUser(userId: string, filters?: Record<string, unknown>): Promise<T[]>;
findByIdAndUser(id: string, userId: string): Promise<T | null>;
}

View File

@@ -16,9 +16,9 @@ export async function assetRoutes(fastify: FastifyInstance) {
schema: {
tags: ['Assets'],
description: 'Get all user assets',
security: [{bearerAuth: []}],
security: [{bearerAuth: []}]
},
handler: controller.getAll.bind(controller),
handler: controller.getAll.bind(controller)
});
fastify.get('/:id', {
@@ -29,11 +29,11 @@ export async function assetRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string', format: 'uuid'},
},
},
id: {type: 'string', format: 'uuid'}
}
}
},
handler: controller.getById.bind(controller),
handler: controller.getById.bind(controller)
});
fastify.post('/', {
@@ -47,11 +47,11 @@ export async function assetRoutes(fastify: FastifyInstance) {
properties: {
name: {type: 'string'},
type: {type: 'string', enum: ['CASH', 'INVESTMENT', 'PROPERTY', 'VEHICLE', 'OTHER']},
value: {type: 'number', minimum: 0},
},
},
value: {type: 'number', minimum: 0}
}
}
},
handler: controller.create.bind(controller),
handler: controller.create.bind(controller)
});
fastify.put('/:id', {
@@ -62,19 +62,19 @@ export async function assetRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string', format: 'uuid'},
},
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},
},
},
value: {type: 'number', minimum: 0}
}
}
},
handler: controller.update.bind(controller),
handler: controller.update.bind(controller)
});
fastify.delete('/:id', {
@@ -85,10 +85,10 @@ export async function assetRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string', format: 'uuid'},
},
},
id: {type: 'string', format: 'uuid'}
}
}
},
handler: controller.delete.bind(controller),
handler: controller.delete.bind(controller)
});
}

View File

@@ -18,11 +18,11 @@ export async function authRoutes(fastify: FastifyInstance) {
properties: {
email: {type: 'string', format: 'email'},
password: {type: 'string', minLength: 8},
name: {type: 'string', minLength: 1},
},
},
name: {type: 'string', minLength: 1}
}
}
},
handler: controller.register.bind(controller),
handler: controller.register.bind(controller)
});
fastify.post('/login', {
@@ -34,20 +34,20 @@ export async function authRoutes(fastify: FastifyInstance) {
required: ['email', 'password'],
properties: {
email: {type: 'string', format: 'email'},
password: {type: 'string'},
},
},
password: {type: 'string'}
}
}
},
handler: controller.login.bind(controller),
handler: controller.login.bind(controller)
});
fastify.get('/profile', {
schema: {
tags: ['Authentication'],
description: 'Get current user profile',
security: [{bearerAuth: []}],
security: [{bearerAuth: []}]
},
preHandler: authenticate,
handler: controller.getProfile.bind(controller),
handler: controller.getProfile.bind(controller)
});
}

View File

@@ -1,11 +1,7 @@
import {FastifyInstance} from 'fastify';
import {CashflowController} from '../controllers/CashflowController';
import {CashflowService} from '../services/CashflowService';
import {
IncomeSourceRepository,
ExpenseRepository,
TransactionRepository,
} from '../repositories/CashflowRepository';
import {IncomeSourceRepository, ExpenseRepository, TransactionRepository} from '../repositories/CashflowRepository';
import {authenticate} from '../middleware/auth';
const incomeRepository = new IncomeSourceRepository();
@@ -19,199 +15,267 @@ export async function cashflowRoutes(fastify: FastifyInstance) {
// ===== Income Source Routes =====
fastify.get('/income', {
schema: {
description: 'Get all income sources',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.get(
'/income',
{
schema: {
description: 'Get all income sources',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.getAllIncome.bind(cashflowController));
cashflowController.getAllIncome.bind(cashflowController)
);
fastify.get('/income/total', {
schema: {
description: 'Get total monthly income',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.get(
'/income/total',
{
schema: {
description: 'Get total monthly income',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.getTotalMonthlyIncome.bind(cashflowController));
cashflowController.getTotalMonthlyIncome.bind(cashflowController)
);
fastify.get('/income/:id', {
schema: {
description: 'Get income source by ID',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.get(
'/income/:id',
{
schema: {
description: 'Get income source by ID',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.getOneIncome.bind(cashflowController));
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'},
},
},
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));
cashflowController.createIncome.bind(cashflowController)
);
fastify.put('/income/:id', {
schema: {
description: 'Update income source',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.put(
'/income/:id',
{
schema: {
description: 'Update income source',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.updateIncome.bind(cashflowController));
cashflowController.updateIncome.bind(cashflowController)
);
fastify.delete('/income/:id', {
schema: {
description: 'Delete income source',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.delete(
'/income/:id',
{
schema: {
description: 'Delete income source',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.deleteIncome.bind(cashflowController));
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']},
},
},
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));
cashflowController.getAllExpenses.bind(cashflowController)
);
fastify.get('/expenses/total', {
schema: {
description: 'Get total monthly expenses',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.get(
'/expenses/total',
{
schema: {
description: 'Get total monthly expenses',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.getTotalMonthlyExpenses.bind(cashflowController));
cashflowController.getTotalMonthlyExpenses.bind(cashflowController)
);
fastify.get('/expenses/:id', {
schema: {
description: 'Get expense by ID',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.get(
'/expenses/:id',
{
schema: {
description: 'Get expense by ID',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.getOneExpense.bind(cashflowController));
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'},
},
},
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));
cashflowController.createExpense.bind(cashflowController)
);
fastify.put('/expenses/:id', {
schema: {
description: 'Update expense',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.put(
'/expenses/:id',
{
schema: {
description: 'Update expense',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.updateExpense.bind(cashflowController));
cashflowController.updateExpense.bind(cashflowController)
);
fastify.delete('/expenses/:id', {
schema: {
description: 'Delete expense',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.delete(
'/expenses/:id',
{
schema: {
description: 'Delete expense',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.deleteExpense.bind(cashflowController));
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'},
},
},
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));
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'},
},
},
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));
cashflowController.getCashflowSummary.bind(cashflowController)
);
fastify.get('/transactions/:id', {
schema: {
description: 'Get transaction by ID',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.get(
'/transactions/:id',
{
schema: {
description: 'Get transaction by ID',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.getOneTransaction.bind(cashflowController));
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'},
},
},
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));
cashflowController.createTransaction.bind(cashflowController)
);
fastify.delete('/transactions/:id', {
schema: {
description: 'Delete transaction',
tags: ['Cashflow'],
security: [{bearerAuth: []}],
fastify.delete(
'/transactions/:id',
{
schema: {
description: 'Delete transaction',
tags: ['Cashflow'],
security: [{bearerAuth: []}]
}
},
}, cashflowController.deleteTransaction.bind(cashflowController));
cashflowController.deleteTransaction.bind(cashflowController)
);
}

View File

@@ -28,9 +28,9 @@ export async function clientRoutes(fastify: FastifyInstance) {
withStats: {
type: 'string',
enum: ['true', 'false'],
description: 'Include invoice statistics for each client',
},
},
description: 'Include invoice statistics for each client'
}
}
},
response: {
200: {
@@ -49,14 +49,14 @@ export async function clientRoutes(fastify: FastifyInstance) {
address: {type: 'string', nullable: true},
notes: {type: 'string', nullable: true},
createdAt: {type: 'string'},
updatedAt: {type: 'string'},
},
},
},
},
},
},
},
updatedAt: {type: 'string'}
}
}
}
}
}
}
}
},
clientController.getAll.bind(clientController)
);
@@ -76,11 +76,11 @@ export async function clientRoutes(fastify: FastifyInstance) {
description: 'Total revenue',
type: 'object',
properties: {
totalRevenue: {type: 'number'},
},
},
},
},
totalRevenue: {type: 'number'}
}
}
}
}
},
clientController.getTotalRevenue.bind(clientController)
);
@@ -98,8 +98,8 @@ export async function clientRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
200: {
@@ -116,13 +116,13 @@ export async function clientRoutes(fastify: FastifyInstance) {
address: {type: 'string', nullable: true},
notes: {type: 'string', nullable: true},
createdAt: {type: 'string'},
updatedAt: {type: 'string'},
},
},
},
},
},
},
updatedAt: {type: 'string'}
}
}
}
}
}
}
},
clientController.getOne.bind(clientController)
);
@@ -145,19 +145,19 @@ export async function clientRoutes(fastify: FastifyInstance) {
email: {type: 'string', format: 'email'},
phone: {type: 'string', maxLength: 50},
address: {type: 'string'},
notes: {type: 'string'},
},
notes: {type: 'string'}
}
},
response: {
201: {
description: 'Client created successfully',
type: 'object',
properties: {
client: {type: 'object'},
},
},
},
},
client: {type: 'object'}
}
}
}
}
},
clientController.create.bind(clientController)
);
@@ -175,8 +175,8 @@ export async function clientRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
body: {
type: 'object',
@@ -185,19 +185,19 @@ export async function clientRoutes(fastify: FastifyInstance) {
email: {type: 'string', format: 'email'},
phone: {type: 'string', maxLength: 50},
address: {type: 'string'},
notes: {type: 'string'},
},
notes: {type: 'string'}
}
},
response: {
200: {
description: 'Client updated successfully',
type: 'object',
properties: {
client: {type: 'object'},
},
},
},
},
client: {type: 'object'}
}
}
}
}
},
clientController.update.bind(clientController)
);
@@ -215,16 +215,16 @@ export async function clientRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
204: {
description: 'Client deleted successfully',
type: 'null',
},
},
},
type: 'null'
}
}
}
},
clientController.delete.bind(clientController)
);

View File

@@ -5,11 +5,7 @@ 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 {IncomeSourceRepository, ExpenseRepository, TransactionRepository} from '../repositories/CashflowRepository';
import {NetWorthSnapshotRepository} from '../repositories/NetWorthSnapshotRepository';
import {authenticate} from '../middleware/auth';
@@ -60,8 +56,8 @@ export async function dashboardRoutes(fastify: FastifyInstance) {
assets: {type: 'number'},
liabilities: {type: 'number'},
change: {type: 'number'},
lastUpdated: {type: 'string'},
},
lastUpdated: {type: 'string'}
}
},
invoices: {
type: 'object',
@@ -69,15 +65,15 @@ export async function dashboardRoutes(fastify: FastifyInstance) {
total: {type: 'number'},
paid: {type: 'number'},
outstanding: {type: 'number'},
overdue: {type: 'number'},
},
overdue: {type: 'number'}
}
},
debts: {
type: 'object',
properties: {
total: {type: 'number'},
accounts: {type: 'number'},
},
accounts: {type: 'number'}
}
},
cashflow: {
type: 'object',
@@ -85,21 +81,21 @@ export async function dashboardRoutes(fastify: FastifyInstance) {
monthlyIncome: {type: 'number'},
monthlyExpenses: {type: 'number'},
monthlyNet: {type: 'number'},
last30Days: {type: 'object'},
},
last30Days: {type: 'object'}
}
},
assets: {
type: 'object',
properties: {
total: {type: 'number'},
count: {type: 'number'},
allocation: {type: 'array'},
},
},
},
},
},
},
allocation: {type: 'array'}
}
}
}
}
}
}
},
dashboardController.getSummary.bind(dashboardController)
);

View File

@@ -42,9 +42,9 @@ export async function debtRoutes(fastify: FastifyInstance) {
withStats: {
type: 'string',
enum: ['true', 'false'],
description: 'Include statistics for each category',
},
},
description: 'Include statistics for each category'
}
}
},
response: {
200: {
@@ -61,14 +61,14 @@ export async function debtRoutes(fastify: FastifyInstance) {
description: {type: 'string', nullable: true},
color: {type: 'string', nullable: true},
createdAt: {type: 'string'},
updatedAt: {type: 'string'},
},
},
},
},
},
},
},
updatedAt: {type: 'string'}
}
}
}
}
}
}
}
},
categoryController.getAll.bind(categoryController)
);
@@ -86,8 +86,8 @@ export async function debtRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
200: {
@@ -102,13 +102,13 @@ export async function debtRoutes(fastify: FastifyInstance) {
description: {type: 'string', nullable: true},
color: {type: 'string', nullable: true},
createdAt: {type: 'string'},
updatedAt: {type: 'string'},
},
},
},
},
},
},
updatedAt: {type: 'string'}
}
}
}
}
}
}
},
categoryController.getOne.bind(categoryController)
);
@@ -129,19 +129,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
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})$'},
},
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'},
},
},
},
},
category: {type: 'object'}
}
}
}
}
},
categoryController.create.bind(categoryController)
);
@@ -159,27 +159,27 @@ export async function debtRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
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})$'},
},
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'},
},
},
},
},
category: {type: 'object'}
}
}
}
}
},
categoryController.update.bind(categoryController)
);
@@ -197,16 +197,16 @@ export async function debtRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
204: {
description: 'Debt category deleted successfully',
type: 'null',
},
},
},
type: 'null'
}
}
}
},
categoryController.delete.bind(categoryController)
);
@@ -227,19 +227,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
type: 'object',
properties: {
withStats: {type: 'string', enum: ['true', 'false']},
categoryId: {type: 'string', description: 'Filter by category ID'},
},
categoryId: {type: 'string', description: 'Filter by category ID'}
}
},
response: {
200: {
description: 'List of debt accounts',
type: 'object',
properties: {
accounts: {type: 'array', items: {type: 'object'}},
},
},
},
},
accounts: {type: 'array', items: {type: 'object'}}
}
}
}
}
},
accountController.getAll.bind(accountController)
);
@@ -259,11 +259,11 @@ export async function debtRoutes(fastify: FastifyInstance) {
description: 'Total debt',
type: 'object',
properties: {
totalDebt: {type: 'number'},
},
},
},
},
totalDebt: {type: 'number'}
}
}
}
}
},
accountController.getTotalDebt.bind(accountController)
);
@@ -281,19 +281,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
200: {
description: 'Debt account details',
type: 'object',
properties: {
account: {type: 'object'},
},
},
},
},
account: {type: 'object'}
}
}
}
}
},
accountController.getOne.bind(accountController)
);
@@ -321,19 +321,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
interestRate: {type: 'number', minimum: 0, maximum: 100},
minimumPayment: {type: 'number', minimum: 0},
dueDate: {type: 'string', format: 'date-time'},
notes: {type: 'string'},
},
notes: {type: 'string'}
}
},
response: {
201: {
description: 'Debt account created successfully',
type: 'object',
properties: {
account: {type: 'object'},
},
},
},
},
account: {type: 'object'}
}
}
}
}
},
accountController.create.bind(accountController)
);
@@ -351,8 +351,8 @@ export async function debtRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
body: {
type: 'object',
@@ -364,19 +364,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
interestRate: {type: 'number', minimum: 0, maximum: 100},
minimumPayment: {type: 'number', minimum: 0},
dueDate: {type: 'string', format: 'date-time'},
notes: {type: 'string'},
},
notes: {type: 'string'}
}
},
response: {
200: {
description: 'Debt account updated successfully',
type: 'object',
properties: {
account: {type: 'object'},
},
},
},
},
account: {type: 'object'}
}
}
}
}
},
accountController.update.bind(accountController)
);
@@ -394,16 +394,16 @@ export async function debtRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
204: {
description: 'Debt account deleted successfully',
type: 'null',
},
},
},
type: 'null'
}
}
}
},
accountController.delete.bind(accountController)
);
@@ -425,19 +425,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
properties: {
accountId: {type: 'string', description: 'Filter by account ID'},
startDate: {type: 'string', format: 'date-time'},
endDate: {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'}},
},
},
},
},
payments: {type: 'array', items: {type: 'object'}}
}
}
}
}
},
paymentController.getAll.bind(paymentController)
);
@@ -457,11 +457,11 @@ export async function debtRoutes(fastify: FastifyInstance) {
description: 'Total payments',
type: 'object',
properties: {
totalPayments: {type: 'number'},
},
},
},
},
totalPayments: {type: 'number'}
}
}
}
}
},
paymentController.getTotalPayments.bind(paymentController)
);
@@ -479,19 +479,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
200: {
description: 'Debt payment details',
type: 'object',
properties: {
payment: {type: 'object'},
},
},
},
},
payment: {type: 'object'}
}
}
}
}
},
paymentController.getOne.bind(paymentController)
);
@@ -513,19 +513,19 @@ export async function debtRoutes(fastify: FastifyInstance) {
accountId: {type: 'string', format: 'uuid'},
amount: {type: 'number', minimum: 0.01},
paymentDate: {type: 'string', format: 'date-time'},
notes: {type: 'string'},
},
notes: {type: 'string'}
}
},
response: {
201: {
description: 'Debt payment created successfully',
type: 'object',
properties: {
payment: {type: 'object'},
},
},
},
},
payment: {type: 'object'}
}
}
}
}
},
paymentController.create.bind(paymentController)
);
@@ -543,16 +543,16 @@ export async function debtRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
204: {
description: 'Debt payment deleted successfully',
type: 'null',
},
},
},
type: 'null'
}
}
}
},
paymentController.delete.bind(paymentController)
);

View File

@@ -28,8 +28,8 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
type: 'object',
properties: {
clientId: {type: 'string', description: 'Filter by client ID'},
status: {type: 'string', description: 'Filter by status'},
},
status: {type: 'string', description: 'Filter by status'}
}
},
response: {
200: {
@@ -52,14 +52,14 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
notes: {type: 'string', nullable: true},
terms: {type: 'string', nullable: true},
createdAt: {type: 'string'},
updatedAt: {type: 'string'},
},
},
},
},
},
},
},
updatedAt: {type: 'string'}
}
}
}
}
}
}
}
},
invoiceController.getAll.bind(invoiceController)
);
@@ -85,13 +85,13 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
total: {type: 'number'},
paid: {type: 'number'},
outstanding: {type: 'number'},
overdue: {type: 'number'},
},
},
},
},
},
},
overdue: {type: 'number'}
}
}
}
}
}
}
},
invoiceController.getStats.bind(invoiceController)
);
@@ -111,11 +111,11 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
description: 'List of overdue invoices',
type: 'object',
properties: {
invoices: {type: 'array', items: {type: 'object'}},
},
},
},
},
invoices: {type: 'array', items: {type: 'object'}}
}
}
}
}
},
invoiceController.getOverdue.bind(invoiceController)
);
@@ -133,8 +133,8 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
200: {
@@ -155,13 +155,13 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
notes: {type: 'string', nullable: true},
terms: {type: 'string', nullable: true},
createdAt: {type: 'string'},
updatedAt: {type: 'string'},
},
},
},
},
},
},
updatedAt: {type: 'string'}
}
}
}
}
}
}
},
invoiceController.getOne.bind(invoiceController)
);
@@ -193,24 +193,24 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
description: {type: 'string', minLength: 1},
quantity: {type: 'number', minimum: 1},
unitPrice: {type: 'number', minimum: 0},
amount: {type: 'number', minimum: 0},
},
},
amount: {type: 'number', minimum: 0}
}
}
},
notes: {type: 'string'},
terms: {type: 'string'},
},
terms: {type: 'string'}
}
},
response: {
201: {
description: 'Invoice created successfully',
type: 'object',
properties: {
invoice: {type: 'object'},
},
},
},
},
invoice: {type: 'object'}
}
}
}
}
},
invoiceController.create.bind(invoiceController)
);
@@ -228,8 +228,8 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
body: {
type: 'object',
@@ -246,24 +246,24 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
description: {type: 'string', minLength: 1},
quantity: {type: 'number', minimum: 1},
unitPrice: {type: 'number', minimum: 0},
amount: {type: 'number', minimum: 0},
},
},
amount: {type: 'number', minimum: 0}
}
}
},
notes: {type: 'string'},
terms: {type: 'string'},
},
terms: {type: 'string'}
}
},
response: {
200: {
description: 'Invoice updated successfully',
type: 'object',
properties: {
invoice: {type: 'object'},
},
},
},
},
invoice: {type: 'object'}
}
}
}
}
},
invoiceController.update.bind(invoiceController)
);
@@ -281,8 +281,8 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
body: {
type: 'object',
@@ -290,20 +290,20 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
properties: {
status: {
type: 'string',
enum: ['DRAFT', 'SENT', 'PAID', 'OVERDUE', 'CANCELLED'],
},
},
enum: ['DRAFT', 'SENT', 'PAID', 'OVERDUE', 'CANCELLED']
}
}
},
response: {
200: {
description: 'Invoice status updated successfully',
type: 'object',
properties: {
invoice: {type: 'object'},
},
},
},
},
invoice: {type: 'object'}
}
}
}
}
},
invoiceController.updateStatus.bind(invoiceController)
);
@@ -321,16 +321,16 @@ export async function invoiceRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
204: {
description: 'Invoice deleted successfully',
type: 'null',
},
},
},
type: 'null'
}
}
}
},
invoiceController.delete.bind(invoiceController)
);

View File

@@ -42,14 +42,14 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
creditor: {type: 'string', nullable: true},
notes: {type: 'string', nullable: true},
createdAt: {type: 'string'},
updatedAt: {type: 'string'},
},
},
},
},
},
},
},
updatedAt: {type: 'string'}
}
}
}
}
}
}
}
},
liabilityController.getAll.bind(liabilityController)
);
@@ -69,11 +69,11 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
description: 'Total liability value',
type: 'object',
properties: {
totalValue: {type: 'number'},
},
},
},
},
totalValue: {type: 'number'}
}
}
}
}
},
liabilityController.getTotalValue.bind(liabilityController)
);
@@ -97,13 +97,13 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
type: 'object',
additionalProperties: {
type: 'array',
items: {type: 'object'},
},
},
},
},
},
},
items: {type: 'object'}
}
}
}
}
}
}
},
liabilityController.getByType.bind(liabilityController)
);
@@ -121,8 +121,8 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
200: {
@@ -142,13 +142,13 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
creditor: {type: 'string', nullable: true},
notes: {type: 'string', nullable: true},
createdAt: {type: 'string'},
updatedAt: {type: 'string'},
},
},
},
},
},
},
updatedAt: {type: 'string'}
}
}
}
}
}
}
},
liabilityController.getOne.bind(liabilityController)
);
@@ -174,19 +174,19 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
minimumPayment: {type: 'number', minimum: 0},
dueDate: {type: 'string', format: 'date-time'},
creditor: {type: 'string', maxLength: 255},
notes: {type: 'string'},
},
notes: {type: 'string'}
}
},
response: {
201: {
description: 'Liability created successfully',
type: 'object',
properties: {
liability: {type: 'object'},
},
},
},
},
liability: {type: 'object'}
}
}
}
}
},
liabilityController.create.bind(liabilityController)
);
@@ -204,8 +204,8 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
body: {
type: 'object',
@@ -217,19 +217,19 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
minimumPayment: {type: 'number', minimum: 0},
dueDate: {type: 'string', format: 'date-time'},
creditor: {type: 'string', maxLength: 255},
notes: {type: 'string'},
},
notes: {type: 'string'}
}
},
response: {
200: {
description: 'Liability updated successfully',
type: 'object',
properties: {
liability: {type: 'object'},
},
},
},
},
liability: {type: 'object'}
}
}
}
}
},
liabilityController.update.bind(liabilityController)
);
@@ -247,16 +247,16 @@ export async function liabilityRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
204: {
description: 'Liability deleted successfully',
type: 'null',
},
},
},
type: 'null'
}
}
}
},
liabilityController.delete.bind(liabilityController)
);

View File

@@ -35,11 +35,11 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
totalLiabilities: {type: 'number'},
netWorth: {type: 'number'},
asOf: {type: 'string'},
isCalculated: {type: 'boolean'},
},
},
},
},
isCalculated: {type: 'boolean'}
}
}
}
}
},
netWorthController.getCurrent.bind(netWorthController)
);
@@ -70,14 +70,14 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
totalLiabilities: {type: 'number'},
netWorth: {type: 'number'},
notes: {type: 'string', nullable: true},
createdAt: {type: 'string'},
},
},
},
},
},
},
},
createdAt: {type: 'string'}
}
}
}
}
}
}
}
},
netWorthController.getAllSnapshots.bind(netWorthController)
);
@@ -97,19 +97,19 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
required: ['startDate', 'endDate'],
properties: {
startDate: {type: 'string', format: 'date-time'},
endDate: {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'}},
},
},
},
},
snapshots: {type: 'array', items: {type: 'object'}}
}
}
}
}
},
netWorthController.getByDateRange.bind(netWorthController)
);
@@ -127,8 +127,8 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
querystring: {
type: 'object',
properties: {
limit: {type: 'string', description: 'Number of periods to include (default: 12)'},
},
limit: {type: 'string', description: 'Number of periods to include (default: 12)'}
}
},
response: {
200: {
@@ -143,14 +143,14 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
date: {type: 'string'},
netWorth: {type: 'number'},
growthAmount: {type: 'number'},
growthPercent: {type: 'number'},
},
},
},
},
},
},
},
growthPercent: {type: 'number'}
}
}
}
}
}
}
}
},
netWorthController.getGrowthStats.bind(netWorthController)
);
@@ -168,19 +168,19 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
200: {
description: 'Snapshot details',
type: 'object',
properties: {
snapshot: {type: 'object'},
},
},
},
},
snapshot: {type: 'object'}
}
}
}
}
},
netWorthController.getOne.bind(netWorthController)
);
@@ -203,19 +203,19 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
totalAssets: {type: 'number', minimum: 0},
totalLiabilities: {type: 'number', minimum: 0},
netWorth: {type: 'number'},
notes: {type: 'string'},
},
notes: {type: 'string'}
}
},
response: {
201: {
description: 'Snapshot created successfully',
type: 'object',
properties: {
snapshot: {type: 'object'},
},
},
},
},
snapshot: {type: 'object'}
}
}
}
}
},
netWorthController.createSnapshot.bind(netWorthController)
);
@@ -233,19 +233,19 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
body: {
type: 'object',
properties: {
notes: {type: 'string'},
},
notes: {type: 'string'}
}
},
response: {
201: {
description: 'Snapshot created successfully',
type: 'object',
properties: {
snapshot: {type: 'object'},
},
},
},
},
snapshot: {type: 'object'}
}
}
}
}
},
netWorthController.createFromCurrent.bind(netWorthController)
);
@@ -263,16 +263,16 @@ export async function netWorthRoutes(fastify: FastifyInstance) {
params: {
type: 'object',
properties: {
id: {type: 'string'},
},
id: {type: 'string'}
}
},
response: {
204: {
description: 'Snapshot deleted successfully',
type: 'null',
},
},
},
type: 'null'
}
}
}
},
netWorthController.delete.bind(netWorthController)
);

View File

@@ -20,24 +20,28 @@ import {dashboardRoutes} from './routes/dashboard.routes';
* Implements Single Responsibility: Server configuration
*/
export async function buildServer() {
if (env.NODE_ENV !== 'production') {
console.log('Development mode enabled. Environment variables [%o]', env);
}
const fastify = Fastify({
logger: {
level: env.NODE_ENV === 'development' ? 'info' : 'error',
transport: env.NODE_ENV === 'development' ? {target: 'pino-pretty'} : undefined,
},
transport: env.NODE_ENV === 'development' ? {target: 'pino-pretty'} : undefined
}
});
// Register plugins
await fastify.register(cors, {
origin: env.CORS_ORIGIN,
credentials: true,
credentials: true
});
await fastify.register(jwt, {
secret: env.JWT_SECRET,
sign: {
expiresIn: env.JWT_EXPIRES_IN,
},
expiresIn: env.JWT_EXPIRES_IN
}
});
// Register Swagger for API documentation
@@ -46,32 +50,32 @@ export async function buildServer() {
info: {
title: 'Personal Finances API',
description: 'API for managing personal finances including assets, liabilities, invoices, and more',
version: '1.0.0',
version: '1.0.0'
},
servers: [
{
url: `http://localhost:${env.PORT}`,
description: 'Development server',
},
description: 'Development server'
}
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
},
bearerFormat: 'JWT'
}
}
}
}
});
await fastify.register(swaggerUi, {
routePrefix: '/docs',
uiConfig: {
docExpansion: 'list',
deepLinking: false,
},
deepLinking: false
}
});
// Register error handler
@@ -80,7 +84,7 @@ export async function buildServer() {
// Health check
fastify.get('/health', async () => ({
status: 'ok',
timestamp: new Date().toISOString(),
timestamp: new Date().toISOString()
}));
// Register routes

View File

@@ -44,7 +44,7 @@ export class AssetService {
name: data.name,
type: data.type,
value: data.value,
user: {connect: {id: userId}},
user: {connect: {id: userId}}
});
}
@@ -58,7 +58,7 @@ export class AssetService {
this.validateAssetData({
name: data.name || asset.name,
type: (data.type || asset.type) as AssetType,
value: data.value !== undefined ? data.value : asset.value,
value: data.value !== undefined ? data.value : asset.value
});
}
@@ -80,14 +80,17 @@ export class AssetService {
async getByType(userId: string): Promise<Record<string, Asset[]>> {
const assets = await this.assetRepository.findAllByUser(userId);
return assets.reduce((acc, asset) => {
const type = asset.type;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push(asset);
return acc;
}, {} as Record<string, Asset[]>);
return assets.reduce(
(acc, asset) => {
const type = asset.type;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push(asset);
return acc;
},
{} as Record<string, Asset[]>
);
}
private validateAssetData(data: CreateAssetDTO): void {

View File

@@ -33,7 +33,7 @@ export class AuthService {
const user = await this.userRepository.create({
email,
password: hashedPassword,
name,
name
});
// Create default debt categories for new user

View File

@@ -1,9 +1,5 @@
import {IncomeSource, Expense, Transaction} from '@prisma/client';
import {
IncomeSourceRepository,
ExpenseRepository,
TransactionRepository,
} from '../repositories/CashflowRepository';
import {IncomeSourceRepository, ExpenseRepository, TransactionRepository} from '../repositories/CashflowRepository';
import {NotFoundError, ValidationError, ForbiddenError} from '../utils/errors';
export interface CreateIncomeSourceDTO {
@@ -47,7 +43,7 @@ export class CashflowService {
return this.incomeRepository.create({
...data,
user: {connect: {id: userId}},
user: {connect: {id: userId}}
});
}
@@ -85,7 +81,7 @@ export class CashflowService {
return this.expenseRepository.create({
...data,
user: {connect: {id: userId}},
user: {connect: {id: userId}}
});
}
@@ -128,7 +124,7 @@ export class CashflowService {
return this.transactionRepository.create({
...data,
user: {connect: {id: userId}},
user: {connect: {id: userId}}
});
}

View File

@@ -45,8 +45,8 @@ export class ClientService {
address: data.address,
notes: data.notes,
user: {
connect: {id: userId},
},
connect: {id: userId}
}
});
}

View File

@@ -48,18 +48,14 @@ export class DashboardService {
// Get recent transactions (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentCashflow = await this.transactionRepository.getCashflowSummary(
userId,
thirtyDaysAgo,
new Date()
);
const recentCashflow = await this.transactionRepository.getCashflowSummary(userId, thirtyDaysAgo, new Date());
// Get assets by type
const assetsByType = await this.assetRepository.getByType(userId);
const assetAllocation = Object.entries(assetsByType).map(([type, assets]) => ({
type,
count: assets.length,
totalValue: assets.reduce((sum, asset) => sum + asset.currentValue, 0),
totalValue: assets.reduce((sum, asset) => sum + asset.currentValue, 0)
}));
return {
@@ -68,29 +64,29 @@ export class DashboardService {
assets: totalAssets,
liabilities: totalLiabilities,
change: netWorthChange,
lastUpdated: new Date(),
lastUpdated: new Date()
},
invoices: {
total: invoiceStats.totalInvoices,
paid: invoiceStats.paidInvoices,
outstanding: invoiceStats.outstandingAmount,
overdue: invoiceStats.overdueInvoices,
overdue: invoiceStats.overdueInvoices
},
debts: {
total: totalDebt,
accounts: (await this.debtAccountRepository.findAllByUser(userId)).length,
accounts: (await this.debtAccountRepository.findAllByUser(userId)).length
},
cashflow: {
monthlyIncome: totalMonthlyIncome,
monthlyExpenses: totalMonthlyExpenses,
monthlyNet: monthlyCashflow,
last30Days: recentCashflow,
last30Days: recentCashflow
},
assets: {
total: totalAssets,
count: (await this.assetRepository.findAllByUser(userId)).length,
allocation: assetAllocation,
},
allocation: assetAllocation
}
};
}
}

View File

@@ -64,8 +64,8 @@ export class DebtAccountService {
dueDate: data.dueDate,
notes: data.notes,
category: {
connect: {id: data.categoryId},
},
connect: {id: data.categoryId}
}
});
}

View File

@@ -32,7 +32,7 @@ export class DebtCategoryService {
{name: 'Auto Loans', description: 'Car and vehicle loans', color: '#10b981'},
{name: 'Mortgages', description: 'Home mortgages', color: '#f59e0b'},
{name: 'Personal Loans', description: 'Personal loan debts', color: '#8b5cf6'},
{name: 'Other', description: 'Other debt types', color: '#6b7280'},
{name: 'Other', description: 'Other debt types', color: '#6b7280'}
];
const categories: DebtCategory[] = [];
@@ -43,8 +43,8 @@ export class DebtCategoryService {
description: category.description,
color: category.color,
user: {
connect: {id: userId},
},
connect: {id: userId}
}
});
categories.push(created);
}
@@ -69,8 +69,8 @@ export class DebtCategoryService {
description: data.description,
color: data.color,
user: {
connect: {id: userId},
},
connect: {id: userId}
}
});
}

View File

@@ -42,14 +42,14 @@ export class DebtPaymentService {
paymentDate: data.paymentDate,
notes: data.notes,
account: {
connect: {id: data.accountId},
},
connect: {id: data.accountId}
}
});
// Update account current balance
const newBalance = account.currentBalance - data.amount;
await this.accountRepository.update(data.accountId, {
currentBalance: Math.max(0, newBalance), // Don't allow negative balance
currentBalance: Math.max(0, newBalance) // Don't allow negative balance
});
return payment;
@@ -114,7 +114,7 @@ export class DebtPaymentService {
if (account) {
const newBalance = account.currentBalance + payment.amount;
await this.accountRepository.update(payment.accountId, {
currentBalance: newBalance,
currentBalance: newBalance
});
}

View File

@@ -66,8 +66,7 @@ export class InvoiceService {
this.validateInvoiceData(data);
// Generate invoice number if not provided
const invoiceNumber =
data.invoiceNumber || (await this.invoiceRepository.generateInvoiceNumber(userId));
const invoiceNumber = data.invoiceNumber || (await this.invoiceRepository.generateInvoiceNumber(userId));
// Check if invoice number already exists
const exists = await this.invoiceRepository.invoiceNumberExists(userId, invoiceNumber);
@@ -80,7 +79,7 @@ export class InvoiceService {
description: item.description,
quantity: item.quantity,
unitPrice: item.unitPrice,
total: item.quantity * item.unitPrice,
total: item.quantity * item.unitPrice
}));
const subtotal = lineItems.reduce((sum, item) => sum + item.total, 0);
@@ -99,8 +98,8 @@ export class InvoiceService {
user: {connect: {id: userId}},
client: {connect: {id: data.clientId}},
lineItems: {
create: lineItems,
},
create: lineItems
}
});
}
@@ -127,7 +126,7 @@ export class InvoiceService {
description: item.description,
quantity: item.quantity,
unitPrice: item.unitPrice,
total: item.quantity * item.unitPrice,
total: item.quantity * item.unitPrice
}));
const subtotal = lineItems.reduce((sum, item) => sum + item.total, 0);
@@ -137,7 +136,7 @@ export class InvoiceService {
updateData.total = total;
updateData.lineItems = {
deleteMany: {},
create: lineItems,
create: lineItems
};
}
@@ -155,7 +154,7 @@ export class InvoiceService {
async getStats(userId: string): Promise<InvoiceStats> {
const invoices = await this.invoiceRepository.findAllByUser(userId);
const stats: InvoiceStats = {
total: invoices.length,
draft: 0,
@@ -164,12 +163,12 @@ export class InvoiceService {
overdue: 0,
totalAmount: 0,
paidAmount: 0,
outstandingAmount: 0,
outstandingAmount: 0
};
for (const inv of invoices) {
stats.totalAmount += inv.total;
switch (inv.status) {
case 'draft':
stats.draft++;

View File

@@ -48,8 +48,8 @@ export class LiabilityService {
creditor: data.creditor,
notes: data.notes,
user: {
connect: {id: userId},
},
connect: {id: userId}
}
});
}

View File

@@ -40,9 +40,7 @@ export class NetWorthService {
const calculatedNetWorth = data.totalAssets - data.totalLiabilities;
if (Math.abs(calculatedNetWorth - data.netWorth) > 0.01) {
// Allow small floating point differences
throw new ValidationError(
`Net worth calculation mismatch. Expected ${calculatedNetWorth}, got ${data.netWorth}`
);
throw new ValidationError(`Net worth calculation mismatch. Expected ${calculatedNetWorth}, got ${data.netWorth}`);
}
return this.snapshotRepository.create({
@@ -52,8 +50,8 @@ export class NetWorthService {
netWorth: data.netWorth,
notes: data.notes,
user: {
connect: {id: userId},
},
connect: {id: userId}
}
});
}
@@ -70,7 +68,7 @@ export class NetWorthService {
totalAssets,
totalLiabilities,
netWorth,
notes,
notes
});
}
@@ -84,11 +82,7 @@ export class NetWorthService {
/**
* Get snapshots within a date range
*/
async getSnapshotsByDateRange(
userId: string,
startDate: Date,
endDate: Date
): Promise<NetWorthSnapshot[]> {
async getSnapshotsByDateRange(userId: string, startDate: Date, endDate: Date): Promise<NetWorthSnapshot[]> {
return this.snapshotRepository.getByDateRange(userId, startDate, endDate);
}
@@ -106,8 +100,7 @@ export class NetWorthService {
// If we have a recent snapshot (within last 24 hours), use it
if (latestSnapshot) {
const hoursSinceSnapshot =
(Date.now() - latestSnapshot.date.getTime()) / (1000 * 60 * 60);
const hoursSinceSnapshot = (Date.now() - latestSnapshot.date.getTime()) / (1000 * 60 * 60);
if (hoursSinceSnapshot < 24) {
return {
@@ -115,7 +108,7 @@ export class NetWorthService {
totalLiabilities: latestSnapshot.totalLiabilities,
netWorth: latestSnapshot.netWorth,
asOf: latestSnapshot.date,
isCalculated: false,
isCalculated: false
};
}
}
@@ -129,7 +122,7 @@ export class NetWorthService {
totalLiabilities,
netWorth: totalAssets - totalLiabilities,
asOf: new Date(),
isCalculated: true,
isCalculated: true
};
}

View File

@@ -12,4 +12,3 @@ declare module '@fastify/jwt' {
};
}
}

View File

@@ -33,7 +33,7 @@ export class PasswordService {
return {
valid: errors.length === 0,
errors,
errors
};
}
}