feat: add ownership-costs feature capsule (refs #15)
- Create ownership_costs table for recurring vehicle costs - Add backend feature capsule with types, repository, service, routes - Update TCO calculation to use ownership_costs (with fallback to legacy vehicle fields) - Add taxCosts and otherCosts to TCO response - Create frontend ownership-costs feature with form, list, API, hooks - Update TCODisplay to show all cost types This implements a more flexible approach to tracking recurring ownership costs (insurance, registration, tax, other) with explicit date ranges and optional document association. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* @ai-summary Fastify route handlers for ownership costs API
|
||||
* @ai-context HTTP request/response handling with Fastify reply methods
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { OwnershipCostsService } from '../domain/ownership-costs.service';
|
||||
import { pool } from '../../../core/config/database';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
import {
|
||||
OwnershipCostParams,
|
||||
VehicleParams,
|
||||
CreateOwnershipCostBody,
|
||||
UpdateOwnershipCostBody
|
||||
} from '../domain/ownership-costs.types';
|
||||
|
||||
export class OwnershipCostsController {
|
||||
private service: OwnershipCostsService;
|
||||
|
||||
constructor() {
|
||||
this.service = new OwnershipCostsService(pool);
|
||||
}
|
||||
|
||||
async create(request: FastifyRequest<{ Body: CreateOwnershipCostBody }>, reply: FastifyReply) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const userId = (request as any).user.sub;
|
||||
const cost = await this.service.create(request.body, userId);
|
||||
|
||||
return reply.code(201).send(cost);
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
logger.error('Error creating ownership cost', { error: err, userId: (request as any).user?.sub });
|
||||
|
||||
if (err.message.includes('not found')) {
|
||||
return reply.code(404).send({
|
||||
error: 'Not Found',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
return reply.code(403).send({
|
||||
error: 'Forbidden',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(500).send({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to create ownership cost'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getByVehicle(request: FastifyRequest<{ Params: VehicleParams }>, reply: FastifyReply) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const userId = (request as any).user.sub;
|
||||
const { vehicleId } = request.params;
|
||||
|
||||
const costs = await this.service.getByVehicleId(vehicleId, userId);
|
||||
|
||||
return reply.code(200).send(costs);
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
logger.error('Error listing ownership costs', { error: err, vehicleId: request.params.vehicleId, userId: (request as any).user?.sub });
|
||||
|
||||
if (err.message.includes('not found')) {
|
||||
return reply.code(404).send({
|
||||
error: 'Not Found',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
return reply.code(403).send({
|
||||
error: 'Forbidden',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(500).send({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to get ownership costs'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getById(request: FastifyRequest<{ Params: OwnershipCostParams }>, reply: FastifyReply) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const userId = (request as any).user.sub;
|
||||
const { id } = request.params;
|
||||
|
||||
const cost = await this.service.getById(id, userId);
|
||||
|
||||
return reply.code(200).send(cost);
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
logger.error('Error getting ownership cost', { error: err, costId: request.params.id, userId: (request as any).user?.sub });
|
||||
|
||||
if (err.message.includes('not found')) {
|
||||
return reply.code(404).send({
|
||||
error: 'Not Found',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
return reply.code(403).send({
|
||||
error: 'Forbidden',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(500).send({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to get ownership cost'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async update(request: FastifyRequest<{ Params: OwnershipCostParams; Body: UpdateOwnershipCostBody }>, reply: FastifyReply) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const userId = (request as any).user.sub;
|
||||
const { id } = request.params;
|
||||
|
||||
const updatedCost = await this.service.update(id, request.body, userId);
|
||||
|
||||
return reply.code(200).send(updatedCost);
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
logger.error('Error updating ownership cost', { error: err, costId: request.params.id, userId: (request as any).user?.sub });
|
||||
|
||||
if (err.message.includes('not found')) {
|
||||
return reply.code(404).send({
|
||||
error: 'Not Found',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
return reply.code(403).send({
|
||||
error: 'Forbidden',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(500).send({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to update ownership cost'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async delete(request: FastifyRequest<{ Params: OwnershipCostParams }>, reply: FastifyReply) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const userId = (request as any).user.sub;
|
||||
const { id } = request.params;
|
||||
|
||||
await this.service.delete(id, userId);
|
||||
|
||||
return reply.code(204).send();
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
logger.error('Error deleting ownership cost', { error: err, costId: request.params.id, userId: (request as any).user?.sub });
|
||||
|
||||
if (err.message.includes('not found')) {
|
||||
return reply.code(404).send({
|
||||
error: 'Not Found',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
return reply.code(403).send({
|
||||
error: 'Forbidden',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(500).send({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to delete ownership cost'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getVehicleStats(request: FastifyRequest<{ Params: VehicleParams }>, reply: FastifyReply) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const userId = (request as any).user.sub;
|
||||
const { vehicleId } = request.params;
|
||||
|
||||
const stats = await this.service.getVehicleCostStats(vehicleId, userId);
|
||||
|
||||
return reply.code(200).send(stats);
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
logger.error('Error getting ownership cost stats', { error: err, vehicleId: request.params.vehicleId, userId: (request as any).user?.sub });
|
||||
|
||||
if (err.message.includes('not found')) {
|
||||
return reply.code(404).send({
|
||||
error: 'Not Found',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
if (err.message.includes('Unauthorized')) {
|
||||
return reply.code(403).send({
|
||||
error: 'Forbidden',
|
||||
message: err.message
|
||||
});
|
||||
}
|
||||
|
||||
return reply.code(500).send({
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to get ownership cost stats'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user