Files
motovaultpro/backend/src/features/ownership-costs/api/ownership-costs.controller.ts
Eric Gullickson a8c4eba8d1 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>
2026-01-12 21:28:25 -06:00

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'
});
}
}
}