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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
console.log("Hello via Bun!");
|
||||
console.log('Hello via Bun!');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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'}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')}`;
|
||||
}
|
||||
|
||||
@@ -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[]>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ export class ClientService {
|
||||
address: data.address,
|
||||
notes: data.notes,
|
||||
user: {
|
||||
connect: {id: userId},
|
||||
},
|
||||
connect: {id: userId}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@ export class DebtAccountService {
|
||||
dueDate: data.dueDate,
|
||||
notes: data.notes,
|
||||
category: {
|
||||
connect: {id: data.categoryId},
|
||||
},
|
||||
connect: {id: data.categoryId}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -48,8 +48,8 @@ export class LiabilityService {
|
||||
creditor: data.creditor,
|
||||
notes: data.notes,
|
||||
user: {
|
||||
connect: {id: userId},
|
||||
},
|
||||
connect: {id: userId}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
1
backend-api/src/types/fastify.d.ts
vendored
1
backend-api/src/types/fastify.d.ts
vendored
@@ -12,4 +12,3 @@ declare module '@fastify/jwt' {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export class PasswordService {
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
errors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user