- 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>
226 lines
7.3 KiB
TypeScript
226 lines
7.3 KiB
TypeScript
/**
|
|
* @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'
|
|
});
|
|
}
|
|
}
|
|
}
|