[SPRINT-6] feat: Implement transport module controllers
Carta Porte Module (4 controllers): - mercancia.controller.ts: Cargo management endpoints - ubicacion-carta-porte.controller.ts: Location endpoints - figura-transporte.controller.ts: Transport figures endpoints - inspeccion-pre-viaje.controller.ts: Pre-trip inspection endpoints Gestion Flota Module (2 controllers): - documento-flota.controller.ts: Fleet document management - asignacion.controller.ts: Unit-operator assignment endpoints Tarifas Transporte Module (2 controllers): - factura-transporte.controller.ts: Invoice endpoints with IVA 16% - recargos.controller.ts: Surcharge catalog and fuel surcharge endpoints GPS Module (1 controller): - evento-geocerca.controller.ts: Geofence event endpoints Total: 9 new controllers following Express Router pattern Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2134ff98e5
commit
a4b1b2fd34
@ -0,0 +1,467 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import {
|
||||
FiguraTransporteService,
|
||||
CreateFiguraDto,
|
||||
UpdateFiguraDto,
|
||||
} from '../services/figura-transporte.service';
|
||||
import { TipoFigura } from '../entities';
|
||||
|
||||
/**
|
||||
* Controlador para gestion de figuras de transporte de Carta Porte
|
||||
* CFDI 3.1 Compliance - Manejo de operadores, propietarios y arrendadores
|
||||
*/
|
||||
export class FiguraTransporteController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly figuraService: FiguraTransporteService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
// CRUD basico
|
||||
this.router.get('/', this.findAll.bind(this));
|
||||
this.router.get('/:id', this.findOne.bind(this));
|
||||
this.router.post('/', this.create.bind(this));
|
||||
this.router.patch('/:id', this.update.bind(this));
|
||||
this.router.delete('/:id', this.delete.bind(this));
|
||||
|
||||
// Operaciones por carta porte
|
||||
this.router.get('/carta-porte/:cartaPorteId', this.findByCartaParte.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/validar', this.validarFigurasRequeridas.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/resumen', this.getResumenPorTipo.bind(this));
|
||||
|
||||
// Filtros por tipo
|
||||
this.router.get('/tipo/:tipo', this.findByTipo.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/operadores', this.getOperadores.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/propietarios', this.getPropietarios.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/arrendadores', this.getArrendadores.bind(this));
|
||||
|
||||
// Operaciones de alta rapida
|
||||
this.router.post('/carta-porte/:cartaPorteId/operador', this.addOperador.bind(this));
|
||||
this.router.post('/carta-porte/:cartaPorteId/propietario', this.addPropietario.bind(this));
|
||||
this.router.post('/carta-porte/:cartaPorteId/arrendador', this.addArrendador.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET / - Obtiene todas las figuras (requiere cartaPorteId en query)
|
||||
*/
|
||||
private async findAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.query;
|
||||
|
||||
if (!cartaPorteId) {
|
||||
res.status(400).json({ error: 'cartaPorteId es requerido en query params' });
|
||||
return;
|
||||
}
|
||||
|
||||
const figuras = await this.figuraService.findByCartaParte(
|
||||
tenantId,
|
||||
cartaPorteId as string
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: figuras,
|
||||
total: figuras.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /:id - Obtiene una figura por ID
|
||||
*/
|
||||
private async findOne(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const figura = await this.figuraService.findById(tenantId, id);
|
||||
|
||||
if (!figura) {
|
||||
res.status(404).json({ error: 'Figura de transporte no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: figura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST / - Crea una nueva figura de transporte
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId, ...figuraData } = req.body;
|
||||
|
||||
if (!cartaPorteId) {
|
||||
res.status(400).json({ error: 'cartaPorteId es requerido' });
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: CreateFiguraDto = figuraData;
|
||||
const figura = await this.figuraService.create(tenantId, cartaPorteId, dto);
|
||||
|
||||
res.status(201).json({ data: figura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /:id - Actualiza una figura existente
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const dto: UpdateFiguraDto = req.body;
|
||||
const figura = await this.figuraService.update(tenantId, id, dto);
|
||||
|
||||
if (!figura) {
|
||||
res.status(404).json({ error: 'Figura de transporte no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: figura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /:id - Elimina una figura de transporte
|
||||
*/
|
||||
private async delete(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const deleted = await this.figuraService.delete(tenantId, id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({ error: 'Figura de transporte no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId - Obtiene figuras de una Carta Porte
|
||||
*/
|
||||
private async findByCartaParte(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const figuras = await this.figuraService.findByCartaParte(tenantId, cartaPorteId);
|
||||
|
||||
res.json({
|
||||
data: figuras,
|
||||
total: figuras.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /tipo/:tipo - Filtra figuras por tipo (requiere cartaPorteId en query)
|
||||
*/
|
||||
private async findByTipo(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { tipo } = req.params;
|
||||
const { cartaPorteId } = req.query;
|
||||
|
||||
if (!cartaPorteId) {
|
||||
res.status(400).json({ error: 'cartaPorteId es requerido en query params' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar tipo
|
||||
const tiposValidos = Object.values(TipoFigura);
|
||||
if (!tiposValidos.includes(tipo as TipoFigura)) {
|
||||
res.status(400).json({
|
||||
error: `tipo debe ser uno de: ${tiposValidos.join(', ')}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener todas las figuras y filtrar por tipo
|
||||
const todasFiguras = await this.figuraService.findByCartaParte(
|
||||
tenantId,
|
||||
cartaPorteId as string
|
||||
);
|
||||
|
||||
const figurasFiltradas = todasFiguras.filter(f => f.tipoFigura === tipo);
|
||||
|
||||
res.json({
|
||||
data: figurasFiltradas,
|
||||
total: figurasFiltradas.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/validar - Valida figuras requeridas
|
||||
*/
|
||||
private async validarFigurasRequeridas(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const validacion = await this.figuraService.validateFigurasRequeridas(tenantId, cartaPorteId);
|
||||
|
||||
res.json({ data: validacion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/resumen - Obtiene resumen por tipo de figura
|
||||
*/
|
||||
private async getResumenPorTipo(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const resumen = await this.figuraService.getResumenPorTipo(tenantId, cartaPorteId);
|
||||
|
||||
res.json({ data: resumen });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/operadores - Obtiene solo operadores
|
||||
*/
|
||||
private async getOperadores(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const operadores = await this.figuraService.getOperadores(tenantId, cartaPorteId);
|
||||
|
||||
res.json({
|
||||
data: operadores,
|
||||
total: operadores.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/propietarios - Obtiene solo propietarios
|
||||
*/
|
||||
private async getPropietarios(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const propietarios = await this.figuraService.getPropietarios(tenantId, cartaPorteId);
|
||||
|
||||
res.json({
|
||||
data: propietarios,
|
||||
total: propietarios.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/arrendadores - Obtiene solo arrendadores
|
||||
*/
|
||||
private async getArrendadores(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const arrendadores = await this.figuraService.getArrendadores(tenantId, cartaPorteId);
|
||||
|
||||
res.json({
|
||||
data: arrendadores,
|
||||
total: arrendadores.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /carta-porte/:cartaPorteId/operador - Agrega un operador con datos minimos
|
||||
*/
|
||||
private async addOperador(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const { rfcFigura, nombreFigura, numLicencia, pais, estado, codigoPostal } = req.body;
|
||||
|
||||
if (!rfcFigura || !nombreFigura || !numLicencia) {
|
||||
res.status(400).json({
|
||||
error: 'rfcFigura, nombreFigura y numLicencia son requeridos para operador',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const figura = await this.figuraService.addOperador(tenantId, cartaPorteId, {
|
||||
rfcFigura,
|
||||
nombreFigura,
|
||||
numLicencia,
|
||||
pais,
|
||||
estado,
|
||||
codigoPostal,
|
||||
});
|
||||
|
||||
res.status(201).json({ data: figura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /carta-porte/:cartaPorteId/propietario - Agrega un propietario de mercancia
|
||||
*/
|
||||
private async addPropietario(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const { rfcFigura, nombreFigura, pais, estado, codigoPostal, calle, partesTransporte } = req.body;
|
||||
|
||||
if (!rfcFigura || !nombreFigura) {
|
||||
res.status(400).json({
|
||||
error: 'rfcFigura y nombreFigura son requeridos para propietario',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const figura = await this.figuraService.addPropietario(tenantId, cartaPorteId, {
|
||||
rfcFigura,
|
||||
nombreFigura,
|
||||
pais,
|
||||
estado,
|
||||
codigoPostal,
|
||||
calle,
|
||||
partesTransporte,
|
||||
});
|
||||
|
||||
res.status(201).json({ data: figura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /carta-porte/:cartaPorteId/arrendador - Agrega un arrendador
|
||||
*/
|
||||
private async addArrendador(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const { rfcFigura, nombreFigura, pais, estado, codigoPostal, calle, partesTransporte } = req.body;
|
||||
|
||||
if (!rfcFigura || !nombreFigura) {
|
||||
res.status(400).json({
|
||||
error: 'rfcFigura y nombreFigura son requeridos para arrendador',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const figura = await this.figuraService.addArrendador(tenantId, cartaPorteId, {
|
||||
rfcFigura,
|
||||
nombreFigura,
|
||||
pais,
|
||||
estado,
|
||||
codigoPostal,
|
||||
calle,
|
||||
partesTransporte,
|
||||
});
|
||||
|
||||
res.status(201).json({ data: figura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,20 @@
|
||||
/**
|
||||
* Carta Porte Controllers
|
||||
* CFDI con Complemento Carta Porte 3.1
|
||||
*/
|
||||
// TODO: Implement controllers
|
||||
// - carta-porte.controller.ts
|
||||
|
||||
// Mercancia (Cargo/Merchandise) Controller
|
||||
export * from './mercancia.controller';
|
||||
|
||||
// Ubicacion (Locations) Controller
|
||||
export * from './ubicacion-carta-porte.controller';
|
||||
|
||||
// Figura Transporte (Transportation Figures) Controller
|
||||
export * from './figura-transporte.controller';
|
||||
|
||||
// Inspeccion Pre-Viaje (Pre-trip Inspection) Controller
|
||||
export * from './inspeccion-pre-viaje.controller';
|
||||
|
||||
// TODO: Implement additional controllers
|
||||
// - carta-porte.controller.ts (main carta porte CRUD)
|
||||
// - cfdi.controller.ts (timbrado y cancelacion)
|
||||
|
||||
@ -0,0 +1,520 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import {
|
||||
InspeccionPreViajeService,
|
||||
CreateInspeccionDto,
|
||||
UpdateInspeccionDto,
|
||||
DateRange,
|
||||
} from '../services/inspeccion-pre-viaje.service';
|
||||
import { ChecklistItem, FotoInspeccion } from '../entities';
|
||||
|
||||
/**
|
||||
* Controlador para gestion de inspecciones pre-viaje
|
||||
* NOM-087-SCT-2-2017 Compliance - Checklists de seguridad vehicular
|
||||
*/
|
||||
export class InspeccionPreViajeController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly inspeccionService: InspeccionPreViajeService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
// CRUD basico
|
||||
this.router.get('/', this.findAll.bind(this));
|
||||
this.router.get('/:id', this.findOne.bind(this));
|
||||
this.router.post('/', this.create.bind(this));
|
||||
this.router.patch('/:id', this.update.bind(this));
|
||||
|
||||
// Consultas por relaciones
|
||||
this.router.get('/viaje/:viajeId', this.findByViaje.bind(this));
|
||||
this.router.get('/unidad/:unidadId', this.findByUnidad.bind(this));
|
||||
this.router.get('/operador/:operadorId', this.findByOperador.bind(this));
|
||||
|
||||
// Operaciones de checklist
|
||||
this.router.post('/:id/checklist-item', this.addChecklistItem.bind(this));
|
||||
this.router.patch('/:id/checklist-items', this.updateChecklistItems.bind(this));
|
||||
|
||||
// Operaciones de finalizacion
|
||||
this.router.post('/:id/completar', this.completarInspeccion.bind(this));
|
||||
|
||||
// Consultas especificas
|
||||
this.router.get('/:id/defectos', this.getDefectos.bind(this));
|
||||
this.router.get('/unidad/:unidadId/puede-despachar', this.canDespachar.bind(this));
|
||||
this.router.get('/unidad/:unidadId/ultima-aprobada', this.getUltimaAprobada.bind(this));
|
||||
|
||||
// Fotos
|
||||
this.router.post('/:id/fotos', this.addFotos.bind(this));
|
||||
|
||||
// Estadisticas
|
||||
this.router.get('/estadisticas/periodo', this.getEstadisticas.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET / - Obtiene inspecciones (con filtros opcionales)
|
||||
*/
|
||||
private async findAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId, operadorId, viajeId, desde, hasta } = req.query;
|
||||
|
||||
let inspecciones;
|
||||
const dateRange: DateRange | undefined =
|
||||
desde && hasta
|
||||
? { desde: new Date(desde as string), hasta: new Date(hasta as string) }
|
||||
: undefined;
|
||||
|
||||
if (viajeId) {
|
||||
inspecciones = await this.inspeccionService.findByViaje(tenantId, viajeId as string);
|
||||
} else if (unidadId) {
|
||||
inspecciones = await this.inspeccionService.findByUnidad(
|
||||
tenantId,
|
||||
unidadId as string,
|
||||
dateRange
|
||||
);
|
||||
} else if (operadorId) {
|
||||
inspecciones = await this.inspeccionService.findByOperador(
|
||||
tenantId,
|
||||
operadorId as string,
|
||||
dateRange
|
||||
);
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: 'Se requiere al menos uno de: viajeId, unidadId, operadorId',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
data: inspecciones,
|
||||
total: inspecciones.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /:id - Obtiene una inspeccion por ID
|
||||
*/
|
||||
private async findOne(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const inspeccion = await this.inspeccionService.findById(tenantId, id);
|
||||
|
||||
if (!inspeccion) {
|
||||
res.status(404).json({ error: 'Inspeccion pre-viaje no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: inspeccion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST / - Crea una nueva inspeccion pre-viaje
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId, operadorId, viajeId, remolqueId, fechaInspeccion, checklistItems, fotos } =
|
||||
req.body;
|
||||
|
||||
if (!unidadId || !operadorId || !viajeId) {
|
||||
res.status(400).json({
|
||||
error: 'unidadId, operadorId y viajeId son requeridos',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: CreateInspeccionDto = {
|
||||
viajeId,
|
||||
remolqueId,
|
||||
fechaInspeccion: fechaInspeccion ? new Date(fechaInspeccion) : undefined,
|
||||
checklistItems,
|
||||
fotos,
|
||||
};
|
||||
|
||||
const inspeccion = await this.inspeccionService.create(tenantId, unidadId, operadorId, dto);
|
||||
|
||||
res.status(201).json({ data: inspeccion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /:id - Actualiza una inspeccion (checklist items y fotos)
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { checklistItems } = req.body;
|
||||
|
||||
if (!checklistItems || !Array.isArray(checklistItems)) {
|
||||
res.status(400).json({ error: 'checklistItems es requerido y debe ser un array' });
|
||||
return;
|
||||
}
|
||||
|
||||
const inspeccion = await this.inspeccionService.updateChecklistItems(
|
||||
tenantId,
|
||||
id,
|
||||
checklistItems
|
||||
);
|
||||
|
||||
if (!inspeccion) {
|
||||
res.status(404).json({ error: 'Inspeccion pre-viaje no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: inspeccion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /viaje/:viajeId - Obtiene inspecciones de un viaje
|
||||
*/
|
||||
private async findByViaje(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { viajeId } = req.params;
|
||||
const inspecciones = await this.inspeccionService.findByViaje(tenantId, viajeId);
|
||||
|
||||
res.json({
|
||||
data: inspecciones,
|
||||
total: inspecciones.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /unidad/:unidadId - Obtiene inspecciones de una unidad
|
||||
*/
|
||||
private async findByUnidad(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId } = req.params;
|
||||
const { desde, hasta } = req.query;
|
||||
|
||||
const dateRange: DateRange | undefined =
|
||||
desde && hasta
|
||||
? { desde: new Date(desde as string), hasta: new Date(hasta as string) }
|
||||
: undefined;
|
||||
|
||||
const inspecciones = await this.inspeccionService.findByUnidad(tenantId, unidadId, dateRange);
|
||||
|
||||
res.json({
|
||||
data: inspecciones,
|
||||
total: inspecciones.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /operador/:operadorId - Obtiene inspecciones de un operador
|
||||
*/
|
||||
private async findByOperador(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { operadorId } = req.params;
|
||||
const { desde, hasta } = req.query;
|
||||
|
||||
const dateRange: DateRange | undefined =
|
||||
desde && hasta
|
||||
? { desde: new Date(desde as string), hasta: new Date(hasta as string) }
|
||||
: undefined;
|
||||
|
||||
const inspecciones = await this.inspeccionService.findByOperador(
|
||||
tenantId,
|
||||
operadorId,
|
||||
dateRange
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: inspecciones,
|
||||
total: inspecciones.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /:id/checklist-item - Agrega o actualiza un item del checklist
|
||||
*/
|
||||
private async addChecklistItem(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const item: ChecklistItem = req.body;
|
||||
|
||||
if (!item.item || !item.categoria || !item.estado) {
|
||||
res.status(400).json({
|
||||
error: 'item, categoria y estado son requeridos',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const inspeccion = await this.inspeccionService.addChecklistItem(tenantId, id, item);
|
||||
|
||||
if (!inspeccion) {
|
||||
res.status(404).json({ error: 'Inspeccion pre-viaje no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: inspeccion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /:id/checklist-items - Actualiza multiples items del checklist
|
||||
*/
|
||||
private async updateChecklistItems(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { items } = req.body;
|
||||
|
||||
if (!items || !Array.isArray(items)) {
|
||||
res.status(400).json({ error: 'items es requerido y debe ser un array' });
|
||||
return;
|
||||
}
|
||||
|
||||
const inspeccion = await this.inspeccionService.updateChecklistItems(tenantId, id, items);
|
||||
|
||||
if (!inspeccion) {
|
||||
res.status(404).json({ error: 'Inspeccion pre-viaje no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: inspeccion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /:id/completar - Completa una inspeccion
|
||||
*/
|
||||
private async completarInspeccion(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { firmaOperador } = req.body;
|
||||
|
||||
const inspeccion = await this.inspeccionService.completeInspeccion(tenantId, id, firmaOperador);
|
||||
|
||||
if (!inspeccion) {
|
||||
res.status(404).json({ error: 'Inspeccion pre-viaje no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
data: inspeccion,
|
||||
message: inspeccion.aprobada
|
||||
? 'Inspeccion completada y aprobada'
|
||||
: 'Inspeccion completada pero NO aprobada debido a defectos criticos',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /:id/defectos - Obtiene resumen de defectos de una inspeccion
|
||||
*/
|
||||
private async getDefectos(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const defectos = await this.inspeccionService.getDefectos(tenantId, id);
|
||||
|
||||
if (!defectos) {
|
||||
res.status(404).json({ error: 'Inspeccion pre-viaje no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: defectos });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /unidad/:unidadId/puede-despachar - Verifica si una unidad puede ser despachada
|
||||
*/
|
||||
private async canDespachar(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId } = req.params;
|
||||
const resultado = await this.inspeccionService.canDespachar(tenantId, unidadId);
|
||||
|
||||
res.json({ data: resultado });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /unidad/:unidadId/ultima-aprobada - Obtiene la ultima inspeccion aprobada
|
||||
*/
|
||||
private async getUltimaAprobada(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId } = req.params;
|
||||
const inspeccion = await this.inspeccionService.getUltimaInspeccionAprobada(
|
||||
tenantId,
|
||||
unidadId
|
||||
);
|
||||
|
||||
if (!inspeccion) {
|
||||
res.status(404).json({ error: 'No hay inspecciones aprobadas para esta unidad' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: inspeccion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /:id/fotos - Agrega fotos a una inspeccion
|
||||
*/
|
||||
private async addFotos(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { fotos } = req.body;
|
||||
|
||||
if (!fotos || !Array.isArray(fotos)) {
|
||||
res.status(400).json({ error: 'fotos es requerido y debe ser un array' });
|
||||
return;
|
||||
}
|
||||
|
||||
const inspeccion = await this.inspeccionService.addFotos(tenantId, id, fotos as FotoInspeccion[]);
|
||||
|
||||
if (!inspeccion) {
|
||||
res.status(404).json({ error: 'Inspeccion pre-viaje no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: inspeccion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /estadisticas/periodo - Obtiene estadisticas de inspecciones por periodo
|
||||
*/
|
||||
private async getEstadisticas(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { desde, hasta } = req.query;
|
||||
|
||||
if (!desde || !hasta) {
|
||||
res.status(400).json({ error: 'desde y hasta son requeridos' });
|
||||
return;
|
||||
}
|
||||
|
||||
const dateRange: DateRange = {
|
||||
desde: new Date(desde as string),
|
||||
hasta: new Date(hasta as string),
|
||||
};
|
||||
|
||||
const estadisticas = await this.inspeccionService.getEstadisticas(tenantId, dateRange);
|
||||
|
||||
res.json({ data: estadisticas });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
282
src/modules/carta-porte/controllers/mercancia.controller.ts
Normal file
282
src/modules/carta-porte/controllers/mercancia.controller.ts
Normal file
@ -0,0 +1,282 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import {
|
||||
MercanciaService,
|
||||
CreateMercanciaDto,
|
||||
UpdateMercanciaDto,
|
||||
} from '../services/mercancia.service';
|
||||
import { MercanciaCartaPorte } from '../entities';
|
||||
|
||||
/**
|
||||
* Controlador para gestion de mercancias de Carta Porte
|
||||
* CFDI 3.1 Compliance - Manejo de bienes transportados
|
||||
*/
|
||||
export class MercanciaController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly mercanciaService: MercanciaService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
// CRUD basico
|
||||
this.router.get('/', this.findAll.bind(this));
|
||||
this.router.get('/:id', this.findOne.bind(this));
|
||||
this.router.post('/', this.create.bind(this));
|
||||
this.router.patch('/:id', this.update.bind(this));
|
||||
this.router.delete('/:id', this.delete.bind(this));
|
||||
|
||||
// Operaciones por carta porte
|
||||
this.router.get('/carta-porte/:cartaPorteId', this.findByCartaParte.bind(this));
|
||||
|
||||
// Validaciones y calculos
|
||||
this.router.get('/carta-porte/:cartaPorteId/validar-pesos', this.validarPesos.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/valor-total', this.getValorTotal.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/peligrosas', this.getMercanciasPeligrosas.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/estadisticas-peligrosas', this.getEstadisticasPeligrosas.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET / - Obtiene todas las mercancias (requiere cartaPorteId en query)
|
||||
*/
|
||||
private async findAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.query;
|
||||
|
||||
if (!cartaPorteId) {
|
||||
res.status(400).json({ error: 'cartaPorteId es requerido en query params' });
|
||||
return;
|
||||
}
|
||||
|
||||
const mercancias = await this.mercanciaService.findByCartaParte(
|
||||
tenantId,
|
||||
cartaPorteId as string
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: mercancias,
|
||||
total: mercancias.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /:id - Obtiene una mercancia por ID
|
||||
*/
|
||||
private async findOne(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const mercancia = await this.mercanciaService.findById(tenantId, id);
|
||||
|
||||
if (!mercancia) {
|
||||
res.status(404).json({ error: 'Mercancia no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: mercancia });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST / - Crea una nueva mercancia
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId, ...mercanciaData } = req.body;
|
||||
|
||||
if (!cartaPorteId) {
|
||||
res.status(400).json({ error: 'cartaPorteId es requerido' });
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: CreateMercanciaDto = mercanciaData;
|
||||
const mercancia = await this.mercanciaService.create(tenantId, cartaPorteId, dto);
|
||||
|
||||
res.status(201).json({ data: mercancia });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /:id - Actualiza una mercancia existente
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const dto: UpdateMercanciaDto = req.body;
|
||||
const mercancia = await this.mercanciaService.update(tenantId, id, dto);
|
||||
|
||||
if (!mercancia) {
|
||||
res.status(404).json({ error: 'Mercancia no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: mercancia });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /:id - Elimina una mercancia
|
||||
*/
|
||||
private async delete(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const deleted = await this.mercanciaService.delete(tenantId, id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({ error: 'Mercancia no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId - Obtiene mercancias de una Carta Porte
|
||||
*/
|
||||
private async findByCartaParte(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const mercancias = await this.mercanciaService.findByCartaParte(tenantId, cartaPorteId);
|
||||
|
||||
res.json({
|
||||
data: mercancias,
|
||||
total: mercancias.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/validar-pesos - Valida pesos de mercancias vs declarado
|
||||
*/
|
||||
private async validarPesos(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const validacion = await this.mercanciaService.validatePesosTotales(tenantId, cartaPorteId);
|
||||
|
||||
res.json({ data: validacion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/valor-total - Calcula valor total de mercancias
|
||||
*/
|
||||
private async getValorTotal(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const resumen = await this.mercanciaService.calculateValorMercancia(tenantId, cartaPorteId);
|
||||
|
||||
res.json({ data: resumen });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/peligrosas - Obtiene solo mercancias peligrosas
|
||||
*/
|
||||
private async getMercanciasPeligrosas(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const mercancias = await this.mercanciaService.getMercanciasPeligrosas(tenantId, cartaPorteId);
|
||||
|
||||
res.json({
|
||||
data: mercancias,
|
||||
total: mercancias.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/estadisticas-peligrosas - Estadisticas de mercancias peligrosas
|
||||
*/
|
||||
private async getEstadisticasPeligrosas(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const estadisticas = await this.mercanciaService.getEstadisticasMercanciasPeligrosas(
|
||||
tenantId,
|
||||
cartaPorteId
|
||||
);
|
||||
|
||||
res.json({ data: estadisticas });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,320 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import {
|
||||
UbicacionCartaPorteService,
|
||||
CreateUbicacionDto,
|
||||
UpdateUbicacionDto,
|
||||
} from '../services/ubicacion-carta-porte.service';
|
||||
|
||||
/**
|
||||
* Controlador para gestion de ubicaciones de Carta Porte
|
||||
* CFDI 3.1 Compliance - Manejo de origenes, destinos y puntos intermedios
|
||||
*/
|
||||
export class UbicacionCartaPorteController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly ubicacionService: UbicacionCartaPorteService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
// CRUD basico
|
||||
this.router.get('/', this.findAll.bind(this));
|
||||
this.router.get('/:id', this.findOne.bind(this));
|
||||
this.router.post('/', this.create.bind(this));
|
||||
this.router.patch('/:id', this.update.bind(this));
|
||||
this.router.delete('/:id', this.delete.bind(this));
|
||||
|
||||
// Operaciones por carta porte
|
||||
this.router.get('/carta-porte/:cartaPorteId', this.findByCartaParte.bind(this));
|
||||
this.router.post('/carta-porte/:cartaPorteId/reorder', this.reorder.bind(this));
|
||||
|
||||
// Validaciones y consultas especificas
|
||||
this.router.get('/carta-porte/:cartaPorteId/validar', this.validarSecuencia.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/origen', this.getOrigen.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/destino', this.getDestino.bind(this));
|
||||
this.router.get('/carta-porte/:cartaPorteId/distancia-total', this.getDistanciaTotal.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET / - Obtiene todas las ubicaciones (requiere cartaPorteId en query)
|
||||
*/
|
||||
private async findAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.query;
|
||||
|
||||
if (!cartaPorteId) {
|
||||
res.status(400).json({ error: 'cartaPorteId es requerido en query params' });
|
||||
return;
|
||||
}
|
||||
|
||||
const ubicaciones = await this.ubicacionService.findByCartaParte(
|
||||
tenantId,
|
||||
cartaPorteId as string
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: ubicaciones,
|
||||
total: ubicaciones.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /:id - Obtiene una ubicacion por ID
|
||||
*/
|
||||
private async findOne(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const ubicacion = await this.ubicacionService.findById(tenantId, id);
|
||||
|
||||
if (!ubicacion) {
|
||||
res.status(404).json({ error: 'Ubicacion no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: ubicacion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST / - Crea una nueva ubicacion
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId, ...ubicacionData } = req.body;
|
||||
|
||||
if (!cartaPorteId) {
|
||||
res.status(400).json({ error: 'cartaPorteId es requerido' });
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: CreateUbicacionDto = ubicacionData;
|
||||
const ubicacion = await this.ubicacionService.create(tenantId, cartaPorteId, dto);
|
||||
|
||||
res.status(201).json({ data: ubicacion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /:id - Actualiza una ubicacion existente
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const dto: UpdateUbicacionDto = req.body;
|
||||
const ubicacion = await this.ubicacionService.update(tenantId, id, dto);
|
||||
|
||||
if (!ubicacion) {
|
||||
res.status(404).json({ error: 'Ubicacion no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: ubicacion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /:id - Elimina una ubicacion
|
||||
*/
|
||||
private async delete(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const deleted = await this.ubicacionService.delete(tenantId, id);
|
||||
|
||||
if (!deleted) {
|
||||
res.status(404).json({ error: 'Ubicacion no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId - Obtiene ubicaciones de una Carta Porte
|
||||
*/
|
||||
private async findByCartaParte(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const ubicaciones = await this.ubicacionService.findByCartaParte(tenantId, cartaPorteId);
|
||||
|
||||
res.json({
|
||||
data: ubicaciones,
|
||||
total: ubicaciones.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /carta-porte/:cartaPorteId/reorder - Reordena las ubicaciones
|
||||
*/
|
||||
private async reorder(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const { orden } = req.body;
|
||||
|
||||
if (!Array.isArray(orden) || orden.length === 0) {
|
||||
res.status(400).json({ error: 'orden es requerido y debe ser un array de IDs' });
|
||||
return;
|
||||
}
|
||||
|
||||
const ubicaciones = await this.ubicacionService.reorder(tenantId, cartaPorteId, orden);
|
||||
|
||||
res.json({
|
||||
data: ubicaciones,
|
||||
total: ubicaciones.length,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/validar - Valida secuencia de ubicaciones
|
||||
*/
|
||||
private async validarSecuencia(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const validacion = await this.ubicacionService.validateSecuencia(tenantId, cartaPorteId);
|
||||
|
||||
res.json({ data: validacion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/origen - Obtiene la ubicacion de origen
|
||||
*/
|
||||
private async getOrigen(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const origen = await this.ubicacionService.getOrigen(tenantId, cartaPorteId);
|
||||
|
||||
if (!origen) {
|
||||
res.status(404).json({ error: 'Origen no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: origen });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/destino - Obtiene la ubicacion de destino final
|
||||
*/
|
||||
private async getDestino(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const destino = await this.ubicacionService.getDestinoFinal(tenantId, cartaPorteId);
|
||||
|
||||
if (!destino) {
|
||||
res.status(404).json({ error: 'Destino no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: destino });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /carta-porte/:cartaPorteId/distancia-total - Obtiene distancia total del recorrido
|
||||
*/
|
||||
private async getDistanciaTotal(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { cartaPorteId } = req.params;
|
||||
const distancia = await this.ubicacionService.getDistanciaTotal(tenantId, cartaPorteId);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
distanciaTotalKm: distancia,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
410
src/modules/gestion-flota/controllers/asignacion.controller.ts
Normal file
410
src/modules/gestion-flota/controllers/asignacion.controller.ts
Normal file
@ -0,0 +1,410 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import { AsignacionService, CreateAsignacionDto } from '../services/asignacion.service';
|
||||
|
||||
/**
|
||||
* Controller for managing unit-operator assignments (asignaciones)
|
||||
* Handles assignment of operators to fleet units
|
||||
*/
|
||||
export class AsignacionController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly asignacionService: AsignacionService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
// CRUD and query routes
|
||||
this.router.get('/', this.findAll.bind(this));
|
||||
this.router.get('/activa/unidad/:unidadId', this.getAsignacionActivaUnidad.bind(this));
|
||||
this.router.get('/activa/operador/:operadorId', this.getAsignacionActivaOperador.bind(this));
|
||||
this.router.get('/historial/unidad/:unidadId', this.getHistorialUnidad.bind(this));
|
||||
this.router.get('/historial/operador/:operadorId', this.getHistorialOperador.bind(this));
|
||||
this.router.get('/disponibilidad', this.validateDisponibilidad.bind(this));
|
||||
this.router.get('/:id', this.findOne.bind(this));
|
||||
this.router.post('/', this.create.bind(this));
|
||||
|
||||
// Operations
|
||||
this.router.post('/:id/finalizar', this.finalizar.bind(this));
|
||||
this.router.post('/transferir', this.transferir.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /asignaciones
|
||||
* Find all assignments with optional filters
|
||||
* Query params: unidadId, operadorId, activa
|
||||
*/
|
||||
private async findAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId, operadorId, activa } = req.query;
|
||||
|
||||
let asignaciones;
|
||||
|
||||
if (activa === 'true') {
|
||||
asignaciones = await this.asignacionService.findActivas(tenantId);
|
||||
} else if (unidadId) {
|
||||
asignaciones = await this.asignacionService.findByUnidad(tenantId, unidadId as string);
|
||||
} else if (operadorId) {
|
||||
asignaciones = await this.asignacionService.findByOperador(tenantId, operadorId as string);
|
||||
} else {
|
||||
// Default: return all active assignments
|
||||
asignaciones = await this.asignacionService.findActivas(tenantId);
|
||||
}
|
||||
|
||||
res.json({ data: asignaciones, total: asignaciones.length });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /asignaciones/:id
|
||||
* Find an assignment by ID
|
||||
*/
|
||||
private async findOne(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const asignacion = await this.asignacionService.findById(tenantId, id);
|
||||
|
||||
if (!asignacion) {
|
||||
res.status(404).json({ error: 'Asignación no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: asignacion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /asignaciones/activa/unidad/:unidadId
|
||||
* Get the current active assignment for a unit
|
||||
*/
|
||||
private async getAsignacionActivaUnidad(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId } = req.params;
|
||||
const asignacion = await this.asignacionService.getAsignacionActual(tenantId, unidadId);
|
||||
|
||||
if (!asignacion) {
|
||||
res.status(404).json({
|
||||
error: 'No hay asignación activa para esta unidad',
|
||||
unidadId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: asignacion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /asignaciones/activa/operador/:operadorId
|
||||
* Get the current active assignment for an operator
|
||||
*/
|
||||
private async getAsignacionActivaOperador(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { operadorId } = req.params;
|
||||
const asignaciones = await this.asignacionService.findByOperador(tenantId, operadorId);
|
||||
const asignacionActiva = asignaciones.find(a => a.activa);
|
||||
|
||||
if (!asignacionActiva) {
|
||||
res.status(404).json({
|
||||
error: 'No hay asignación activa para este operador',
|
||||
operadorId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: asignacionActiva });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /asignaciones/historial/unidad/:unidadId
|
||||
* Get assignment history for a unit
|
||||
*/
|
||||
private async getHistorialUnidad(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId } = req.params;
|
||||
const historial = await this.asignacionService.getHistorial(tenantId, unidadId);
|
||||
res.json({ data: historial, total: historial.length });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /asignaciones/historial/operador/:operadorId
|
||||
* Get assignment history for an operator
|
||||
*/
|
||||
private async getHistorialOperador(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { operadorId } = req.params;
|
||||
const asignaciones = await this.asignacionService.findByOperador(tenantId, operadorId);
|
||||
|
||||
// Calculate duration for each assignment
|
||||
const historial = asignaciones.map(asignacion => {
|
||||
const fechaFin = asignacion.fechaFin || new Date();
|
||||
const duracionMs = fechaFin.getTime() - asignacion.fechaInicio.getTime();
|
||||
const duracionDias = Math.ceil(duracionMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
return {
|
||||
asignacion,
|
||||
duracionDias,
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ data: historial, total: historial.length });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /asignaciones
|
||||
* Create a new assignment (assign operator to unit)
|
||||
* Body: { unidadId, operadorId, remolqueId?, motivo?, fechaInicio? }
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId, operadorId, remolqueId, motivo, fechaInicio } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!unidadId) {
|
||||
res.status(400).json({ error: 'ID de unidad es requerido' });
|
||||
return;
|
||||
}
|
||||
if (!operadorId) {
|
||||
res.status(400).json({ error: 'ID de operador es requerido' });
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: CreateAsignacionDto = {
|
||||
remolqueId,
|
||||
motivo,
|
||||
fechaInicio: fechaInicio ? new Date(fechaInicio) : undefined,
|
||||
};
|
||||
|
||||
const asignacion = await this.asignacionService.create(
|
||||
tenantId,
|
||||
unidadId,
|
||||
operadorId,
|
||||
dto,
|
||||
userId
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
data: asignacion,
|
||||
message: 'Asignación creada exitosamente',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('No se puede crear la asignacion')) {
|
||||
res.status(400).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error.message.includes('ya tiene')) {
|
||||
res.status(409).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error.message.includes('no encontrad')) {
|
||||
res.status(404).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /asignaciones/:id/finalizar
|
||||
* End an active assignment
|
||||
* Body: { motivo }
|
||||
*/
|
||||
private async finalizar(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { motivo } = req.body;
|
||||
|
||||
if (!motivo) {
|
||||
res.status(400).json({ error: 'Motivo de finalización es requerido' });
|
||||
return;
|
||||
}
|
||||
|
||||
const asignacion = await this.asignacionService.terminar(tenantId, id, motivo, userId);
|
||||
res.json({
|
||||
data: asignacion,
|
||||
message: 'Asignación finalizada exitosamente',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('no encontrada')) {
|
||||
res.status(404).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error.message.includes('ya esta terminada')) {
|
||||
res.status(400).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error.message.includes('en viaje')) {
|
||||
res.status(409).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /asignaciones/transferir
|
||||
* Transfer a unit to a new operator (ends current assignment and creates new one)
|
||||
* Body: { unidadId, nuevoOperadorId, motivo }
|
||||
*/
|
||||
private async transferir(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId, nuevoOperadorId, motivo } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!unidadId) {
|
||||
res.status(400).json({ error: 'ID de unidad es requerido' });
|
||||
return;
|
||||
}
|
||||
if (!nuevoOperadorId) {
|
||||
res.status(400).json({ error: 'ID del nuevo operador es requerido' });
|
||||
return;
|
||||
}
|
||||
if (!motivo) {
|
||||
res.status(400).json({ error: 'Motivo de transferencia es requerido' });
|
||||
return;
|
||||
}
|
||||
|
||||
const asignacion = await this.asignacionService.transferir(
|
||||
tenantId,
|
||||
unidadId,
|
||||
nuevoOperadorId,
|
||||
motivo,
|
||||
userId
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
data: asignacion,
|
||||
message: 'Unidad transferida exitosamente al nuevo operador',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('no encontrad')) {
|
||||
res.status(404).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error.message.includes('en viaje')) {
|
||||
res.status(409).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error.message.includes('No se puede crear')) {
|
||||
res.status(400).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /asignaciones/disponibilidad
|
||||
* Validate availability of unit and operator for assignment
|
||||
* Query params: unidadId, operadorId
|
||||
*/
|
||||
private async validateDisponibilidad(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId, operadorId } = req.query;
|
||||
|
||||
if (!unidadId || !operadorId) {
|
||||
res.status(400).json({
|
||||
error: 'Se requieren unidadId y operadorId para validar disponibilidad',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const disponibilidad = await this.asignacionService.validateDisponibilidad(
|
||||
tenantId,
|
||||
unidadId as string,
|
||||
operadorId as string
|
||||
);
|
||||
|
||||
res.json({ data: disponibilidad });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,411 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import {
|
||||
DocumentoFlotaService,
|
||||
CreateDocumentoFlotaDto,
|
||||
UpdateDocumentoFlotaDto,
|
||||
} from '../services/documento-flota.service';
|
||||
import { TipoDocumento, TipoEntidadDocumento } from '../entities';
|
||||
|
||||
/**
|
||||
* Controller for managing fleet documents (documentos de flota)
|
||||
* Handles documents for units, trailers, and operators
|
||||
*/
|
||||
export class DocumentoFlotaController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly documentoFlotaService: DocumentoFlotaService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
// CRUD básico
|
||||
this.router.get('/', this.findAll.bind(this));
|
||||
this.router.get('/por-vencer', this.getDocumentosPorVencer.bind(this));
|
||||
this.router.get('/vencidos', this.getDocumentosVencidos.bind(this));
|
||||
this.router.get('/resumen-vencimientos', this.getResumenVencimientos.bind(this));
|
||||
this.router.get('/entidades-bloqueadas', this.getEntidadesBloqueadas.bind(this));
|
||||
this.router.get('/alertas', this.getAlertasVencimiento.bind(this));
|
||||
this.router.get('/unidad/:unidadId', this.findByUnidad.bind(this));
|
||||
this.router.get('/operador/:operadorId', this.findByOperador.bind(this));
|
||||
this.router.get('/:id', this.findOne.bind(this));
|
||||
this.router.post('/', this.create.bind(this));
|
||||
this.router.patch('/:id', this.update.bind(this));
|
||||
this.router.delete('/:id', this.delete.bind(this));
|
||||
|
||||
// Operaciones específicas
|
||||
this.router.post('/:id/renovar', this.renovar.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /documentos-flota
|
||||
* Find all documents with optional filters
|
||||
* Query params: unidadId, operadorId, tipoDocumento, estadoVigencia
|
||||
*/
|
||||
private async findAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId, operadorId, tipoDocumento, estadoVigencia } = req.query;
|
||||
|
||||
// Handle different filter scenarios
|
||||
let documentos;
|
||||
|
||||
if (unidadId) {
|
||||
documentos = await this.documentoFlotaService.findByUnidad(tenantId, unidadId as string);
|
||||
} else if (operadorId) {
|
||||
documentos = await this.documentoFlotaService.findByOperador(tenantId, operadorId as string);
|
||||
} else if (tipoDocumento) {
|
||||
documentos = await this.documentoFlotaService.findByTipo(tenantId, tipoDocumento as TipoDocumento);
|
||||
} else if (estadoVigencia === 'vencido') {
|
||||
documentos = await this.documentoFlotaService.findVencidos(tenantId);
|
||||
} else if (estadoVigencia === 'por_vencer') {
|
||||
documentos = await this.documentoFlotaService.findPorVencer(tenantId, 30);
|
||||
} else {
|
||||
// Default: return documents expiring soon (all types)
|
||||
documentos = await this.documentoFlotaService.findPorVencer(tenantId, 365);
|
||||
}
|
||||
|
||||
res.json({ data: documentos, total: documentos.length });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /documentos-flota/:id
|
||||
* Find a document by ID
|
||||
*/
|
||||
private async findOne(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const documento = await this.documentoFlotaService.findById(tenantId, id);
|
||||
|
||||
if (!documento) {
|
||||
res.status(404).json({ error: 'Documento no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: documento });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /documentos-flota
|
||||
* Create a new fleet document
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const dto: CreateDocumentoFlotaDto = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!dto.entidadTipo) {
|
||||
res.status(400).json({ error: 'Tipo de entidad es requerido' });
|
||||
return;
|
||||
}
|
||||
if (!dto.entidadId) {
|
||||
res.status(400).json({ error: 'ID de entidad es requerido' });
|
||||
return;
|
||||
}
|
||||
if (!dto.tipoDocumento) {
|
||||
res.status(400).json({ error: 'Tipo de documento es requerido' });
|
||||
return;
|
||||
}
|
||||
if (!dto.nombre) {
|
||||
res.status(400).json({ error: 'Nombre del documento es requerido' });
|
||||
return;
|
||||
}
|
||||
|
||||
const documento = await this.documentoFlotaService.create(tenantId, dto, userId);
|
||||
res.status(201).json({ data: documento });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /documentos-flota/:id
|
||||
* Update a fleet document
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const dto: UpdateDocumentoFlotaDto = req.body;
|
||||
|
||||
const documento = await this.documentoFlotaService.update(tenantId, id, dto, userId);
|
||||
res.json({ data: documento });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('no encontrado')) {
|
||||
res.status(404).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /documentos-flota/:id
|
||||
* Soft delete a fleet document
|
||||
*/
|
||||
private async delete(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
await this.documentoFlotaService.delete(tenantId, id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('no encontrado')) {
|
||||
res.status(404).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /documentos-flota/unidad/:unidadId
|
||||
* Get all documents for a specific unit
|
||||
*/
|
||||
private async findByUnidad(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { unidadId } = req.params;
|
||||
const documentos = await this.documentoFlotaService.findByUnidad(tenantId, unidadId);
|
||||
res.json({ data: documentos, total: documentos.length });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('no encontrada')) {
|
||||
res.status(404).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /documentos-flota/operador/:operadorId
|
||||
* Get all documents for a specific operator
|
||||
*/
|
||||
private async findByOperador(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { operadorId } = req.params;
|
||||
const documentos = await this.documentoFlotaService.findByOperador(tenantId, operadorId);
|
||||
res.json({ data: documentos, total: documentos.length });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('no encontrado')) {
|
||||
res.status(404).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /documentos-flota/por-vencer
|
||||
* Get documents expiring soon
|
||||
* Query param: dias (default: 30)
|
||||
*/
|
||||
private async getDocumentosPorVencer(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { dias } = req.query;
|
||||
const diasAntelacion = dias ? parseInt(dias as string, 10) : 30;
|
||||
|
||||
if (isNaN(diasAntelacion) || diasAntelacion < 1) {
|
||||
res.status(400).json({ error: 'El parámetro días debe ser un número positivo' });
|
||||
return;
|
||||
}
|
||||
|
||||
const documentos = await this.documentoFlotaService.findPorVencer(tenantId, diasAntelacion);
|
||||
res.json({ data: documentos, total: documentos.length, diasConsultados: diasAntelacion });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /documentos-flota/vencidos
|
||||
* Get all expired documents
|
||||
*/
|
||||
private async getDocumentosVencidos(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const documentos = await this.documentoFlotaService.findVencidos(tenantId);
|
||||
res.json({ data: documentos, total: documentos.length });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /documentos-flota/:id/renovar
|
||||
* Renew a document with a new expiration date
|
||||
* Body: { nuevaFechaVencimiento: Date }
|
||||
*/
|
||||
private async renovar(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
const userId = req.headers['x-user-id'] as string;
|
||||
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { nuevaFechaVencimiento } = req.body;
|
||||
|
||||
if (!nuevaFechaVencimiento) {
|
||||
res.status(400).json({ error: 'Nueva fecha de vencimiento es requerida' });
|
||||
return;
|
||||
}
|
||||
|
||||
const fechaVencimiento = new Date(nuevaFechaVencimiento);
|
||||
if (isNaN(fechaVencimiento.getTime())) {
|
||||
res.status(400).json({ error: 'Fecha de vencimiento inválida' });
|
||||
return;
|
||||
}
|
||||
|
||||
const documento = await this.documentoFlotaService.renovar(tenantId, id, fechaVencimiento, userId);
|
||||
res.json({ data: documento, message: 'Documento renovado exitosamente' });
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('no encontrado')) {
|
||||
res.status(404).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
if (error.message.includes('fecha futura')) {
|
||||
res.status(400).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /documentos-flota/resumen-vencimientos
|
||||
* Get summary of document expirations by category
|
||||
*/
|
||||
private async getResumenVencimientos(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resumen = await this.documentoFlotaService.getResumenVencimientos(tenantId);
|
||||
res.json({ data: resumen });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /documentos-flota/entidades-bloqueadas
|
||||
* Get units and operators blocked due to expired critical documents
|
||||
*/
|
||||
private async getEntidadesBloqueadas(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const entidadesBloqueadas = await this.documentoFlotaService.bloquearPorDocumentosVencidos(tenantId);
|
||||
res.json({ data: entidadesBloqueadas, total: entidadesBloqueadas.length });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /documentos-flota/alertas
|
||||
* Get document expiration alerts
|
||||
* Query param: dias (default: 30)
|
||||
*/
|
||||
private async getAlertasVencimiento(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
res.status(400).json({ error: 'Tenant ID is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { dias } = req.query;
|
||||
const diasAnticipacion = dias ? parseInt(dias as string, 10) : 30;
|
||||
|
||||
if (isNaN(diasAnticipacion) || diasAnticipacion < 1) {
|
||||
res.status(400).json({ error: 'El parámetro días debe ser un número positivo' });
|
||||
return;
|
||||
}
|
||||
|
||||
const alertas = await this.documentoFlotaService.sendAlertasVencimiento(tenantId, diasAnticipacion);
|
||||
res.json({
|
||||
data: alertas,
|
||||
total: alertas.length,
|
||||
criticos: alertas.filter(a => a.esCritico).length,
|
||||
diasConsultados: diasAnticipacion,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,3 +4,5 @@
|
||||
export { ProductsController, CategoriesController } from './products.controller';
|
||||
export * from './unidades.controller';
|
||||
export * from './operadores.controller';
|
||||
export * from './documento-flota.controller';
|
||||
export * from './asignacion.controller';
|
||||
|
||||
426
src/modules/gps/controllers/evento-geocerca.controller.ts
Normal file
426
src/modules/gps/controllers/evento-geocerca.controller.ts
Normal file
@ -0,0 +1,426 @@
|
||||
/**
|
||||
* EventoGeocerca Controller
|
||||
* ERP Transportistas
|
||||
*
|
||||
* REST API endpoints for geofence events (entry/exit/permanence).
|
||||
* Adapted from erp-mecanicas-diesel MMD-014 GPS Integration
|
||||
* Module: MAI-006 Tracking
|
||||
*/
|
||||
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { DataSource } from 'typeorm';
|
||||
import {
|
||||
EventoGeocercaService,
|
||||
EventoGeocercaFilters,
|
||||
DateRange,
|
||||
} from '../services/evento-geocerca.service';
|
||||
import { TipoEventoGeocerca } from '../entities/evento-geocerca.entity';
|
||||
|
||||
interface TenantRequest extends Request {
|
||||
tenantId?: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function createEventoGeocercaController(dataSource: DataSource): Router {
|
||||
const router = Router();
|
||||
const service = new EventoGeocercaService(dataSource);
|
||||
|
||||
const extractTenant = (req: TenantRequest, res: Response, next: NextFunction) => {
|
||||
const tenantId = req.headers['x-tenant-id'] as string;
|
||||
if (!tenantId) {
|
||||
return res.status(400).json({ error: 'Tenant ID es requerido' });
|
||||
}
|
||||
req.tenantId = tenantId;
|
||||
req.userId = req.headers['x-user-id'] as string;
|
||||
next();
|
||||
};
|
||||
|
||||
router.use(extractTenant);
|
||||
|
||||
/**
|
||||
* Helper to parse date range from query parameters
|
||||
*/
|
||||
const parseDateRange = (query: any): DateRange => {
|
||||
const now = new Date();
|
||||
const defaultStart = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago
|
||||
return {
|
||||
fechaInicio: query.fechaDesde ? new Date(query.fechaDesde as string) : defaultStart,
|
||||
fechaFin: query.fechaHasta ? new Date(query.fechaHasta as string) : now,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* List geofence events with filters
|
||||
* GET /api/gps/eventos-geocerca
|
||||
* Query params: geocercaId, unidadId, viajeId, tipoEvento, fechaDesde, fechaHasta, page, limit
|
||||
*/
|
||||
router.get('/', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const filters: EventoGeocercaFilters = {
|
||||
geocercaId: req.query.geocercaId as string,
|
||||
unidadId: req.query.unidadId as string,
|
||||
viajeId: req.query.viajeId as string,
|
||||
tipoEvento: req.query.tipoEvento as TipoEventoGeocerca,
|
||||
dispositivoId: req.query.dispositivoId as string,
|
||||
};
|
||||
|
||||
const pagination = {
|
||||
page: parseInt(req.query.page as string, 10) || 1,
|
||||
limit: Math.min(parseInt(req.query.limit as string, 10) || 50, 200),
|
||||
};
|
||||
|
||||
const result = await service.findAll(req.tenantId!, filters, pagination);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get geofence alerts (events marked with alerts)
|
||||
* GET /api/gps/eventos-geocerca/alertas
|
||||
*/
|
||||
router.get('/alertas', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const dateRange = parseDateRange(req.query);
|
||||
const limit = parseInt(req.query.limit as string, 10) || 50;
|
||||
|
||||
// Get recent events and filter those with alerts
|
||||
const result = await service.findAll(
|
||||
req.tenantId!,
|
||||
{},
|
||||
{ page: 1, limit: 500 }
|
||||
);
|
||||
|
||||
const alertas = result.data.filter(
|
||||
(evento) =>
|
||||
evento.metadata?.alertaTriggered === true &&
|
||||
evento.tiempoEvento >= dateRange.fechaInicio &&
|
||||
evento.tiempoEvento <= dateRange.fechaFin
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: alertas.slice(0, limit),
|
||||
total: alertas.length,
|
||||
fechaDesde: dateRange.fechaInicio,
|
||||
fechaHasta: dateRange.fechaFin,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get summary statistics by geofence
|
||||
* GET /api/gps/eventos-geocerca/resumen/geocerca/:geocercaId
|
||||
*/
|
||||
router.get('/resumen/geocerca/:geocercaId', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const dateRange = parseDateRange(req.query);
|
||||
const estadisticas = await service.getEstadisticas(
|
||||
req.tenantId!,
|
||||
req.params.geocercaId,
|
||||
dateRange
|
||||
);
|
||||
res.json(estadisticas);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get summary statistics by unit
|
||||
* GET /api/gps/eventos-geocerca/resumen/unidad/:unidadId
|
||||
*/
|
||||
router.get('/resumen/unidad/:unidadId', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const dateRange = parseDateRange(req.query);
|
||||
|
||||
// Get events for this unit
|
||||
const eventos = await service.findByUnidad(
|
||||
req.tenantId!,
|
||||
req.params.unidadId,
|
||||
dateRange
|
||||
);
|
||||
|
||||
// Calculate summary statistics
|
||||
const geocercasVisitadas = new Set<string>();
|
||||
let totalEntradas = 0;
|
||||
let totalSalidas = 0;
|
||||
let totalPermanencias = 0;
|
||||
|
||||
for (const evento of eventos) {
|
||||
geocercasVisitadas.add(evento.geocercaId);
|
||||
switch (evento.tipoEvento) {
|
||||
case TipoEventoGeocerca.ENTRADA:
|
||||
totalEntradas++;
|
||||
break;
|
||||
case TipoEventoGeocerca.SALIDA:
|
||||
totalSalidas++;
|
||||
break;
|
||||
case TipoEventoGeocerca.PERMANENCIA:
|
||||
totalPermanencias++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
unidadId: req.params.unidadId,
|
||||
totalEventos: eventos.length,
|
||||
totalEntradas,
|
||||
totalSalidas,
|
||||
totalPermanencias,
|
||||
geocercasVisitadas: geocercasVisitadas.size,
|
||||
geocercaIds: Array.from(geocercasVisitadas),
|
||||
periodoInicio: dateRange.fechaInicio,
|
||||
periodoFin: dateRange.fechaFin,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get dwell time (permanence) statistics by geofence
|
||||
* GET /api/gps/eventos-geocerca/permanencia/geocerca/:geocercaId
|
||||
*/
|
||||
router.get('/permanencia/geocerca/:geocercaId', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const dateRange = parseDateRange(req.query);
|
||||
|
||||
// Get events for this geofence
|
||||
const eventos = await service.findByGeocerca(
|
||||
req.tenantId!,
|
||||
req.params.geocercaId,
|
||||
dateRange
|
||||
);
|
||||
|
||||
// Group events by unit and calculate dwell times
|
||||
const unidadEventos: Record<string, typeof eventos> = {};
|
||||
for (const evento of eventos) {
|
||||
if (!unidadEventos[evento.unidadId]) {
|
||||
unidadEventos[evento.unidadId] = [];
|
||||
}
|
||||
unidadEventos[evento.unidadId].push(evento);
|
||||
}
|
||||
|
||||
const tiemposPorUnidad: Array<{
|
||||
unidadId: string;
|
||||
tiempoTotalMinutos: number;
|
||||
visitas: number;
|
||||
promedioMinutosPorVisita: number;
|
||||
}> = [];
|
||||
|
||||
for (const [unidadId, eventosUnidad] of Object.entries(unidadEventos)) {
|
||||
const tiempoInfo = await service.getTiempoEnGeocerca(
|
||||
req.tenantId!,
|
||||
unidadId,
|
||||
req.params.geocercaId
|
||||
);
|
||||
tiemposPorUnidad.push({
|
||||
unidadId,
|
||||
tiempoTotalMinutos: tiempoInfo.tiempoTotalMinutos,
|
||||
visitas: tiempoInfo.visitasTotales,
|
||||
promedioMinutosPorVisita: tiempoInfo.promedioMinutosPorVisita,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by total time descending
|
||||
tiemposPorUnidad.sort((a, b) => b.tiempoTotalMinutos - a.tiempoTotalMinutos);
|
||||
|
||||
const tiempoTotalGeocerca = tiemposPorUnidad.reduce(
|
||||
(sum, t) => sum + t.tiempoTotalMinutos,
|
||||
0
|
||||
);
|
||||
const visitasTotales = tiemposPorUnidad.reduce((sum, t) => sum + t.visitas, 0);
|
||||
|
||||
res.json({
|
||||
geocercaId: req.params.geocercaId,
|
||||
tiempoTotalMinutos: Math.round(tiempoTotalGeocerca * 100) / 100,
|
||||
visitasTotales,
|
||||
unidadesUnicas: tiemposPorUnidad.length,
|
||||
promedioMinutosPorVisita:
|
||||
visitasTotales > 0
|
||||
? Math.round((tiempoTotalGeocerca / visitasTotales) * 100) / 100
|
||||
: 0,
|
||||
detallesPorUnidad: tiemposPorUnidad,
|
||||
periodoInicio: dateRange.fechaInicio,
|
||||
periodoFin: dateRange.fechaFin,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get events by geofence
|
||||
* GET /api/gps/eventos-geocerca/geocerca/:geocercaId
|
||||
*/
|
||||
router.get('/geocerca/:geocercaId', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const dateRange = parseDateRange(req.query);
|
||||
const eventos = await service.findByGeocerca(
|
||||
req.tenantId!,
|
||||
req.params.geocercaId,
|
||||
dateRange
|
||||
);
|
||||
res.json({
|
||||
data: eventos,
|
||||
total: eventos.length,
|
||||
geocercaId: req.params.geocercaId,
|
||||
fechaDesde: dateRange.fechaInicio,
|
||||
fechaHasta: dateRange.fechaFin,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get events by unit
|
||||
* GET /api/gps/eventos-geocerca/unidad/:unidadId
|
||||
*/
|
||||
router.get('/unidad/:unidadId', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const dateRange = parseDateRange(req.query);
|
||||
const eventos = await service.findByUnidad(
|
||||
req.tenantId!,
|
||||
req.params.unidadId,
|
||||
dateRange
|
||||
);
|
||||
res.json({
|
||||
data: eventos,
|
||||
total: eventos.length,
|
||||
unidadId: req.params.unidadId,
|
||||
fechaDesde: dateRange.fechaInicio,
|
||||
fechaHasta: dateRange.fechaFin,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get events by trip
|
||||
* GET /api/gps/eventos-geocerca/viaje/:viajeId
|
||||
*/
|
||||
router.get('/viaje/:viajeId', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const filters: EventoGeocercaFilters = {
|
||||
viajeId: req.params.viajeId,
|
||||
};
|
||||
|
||||
const result = await service.findAll(req.tenantId!, filters, { page: 1, limit: 500 });
|
||||
res.json({
|
||||
data: result.data,
|
||||
total: result.total,
|
||||
viajeId: req.params.viajeId,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Register a new geofence event
|
||||
* POST /api/gps/eventos-geocerca
|
||||
*/
|
||||
router.post('/', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const evento = await service.create(req.tenantId!, {
|
||||
geocercaId: req.body.geocercaId,
|
||||
dispositivoId: req.body.dispositivoId,
|
||||
unidadId: req.body.unidadId,
|
||||
tipoEvento: req.body.tipoEvento,
|
||||
posicionId: req.body.posicionId,
|
||||
latitud: parseFloat(req.body.latitud),
|
||||
longitud: parseFloat(req.body.longitud),
|
||||
tiempoEvento: new Date(req.body.tiempoEvento),
|
||||
viajeId: req.body.viajeId,
|
||||
metadata: req.body.metadata,
|
||||
});
|
||||
res.status(201).json(evento);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Trigger alert for an event
|
||||
* POST /api/gps/eventos-geocerca/:id/alerta
|
||||
*/
|
||||
router.post('/:id/alerta', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const evento = await service.triggerAlerta(req.tenantId!, req.params.id);
|
||||
if (!evento) {
|
||||
return res.status(404).json({ error: 'Evento de geocerca no encontrado' });
|
||||
}
|
||||
res.json(evento);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get units currently inside a geofence
|
||||
* GET /api/gps/eventos-geocerca/geocerca/:geocercaId/unidades-dentro
|
||||
*/
|
||||
router.get('/geocerca/:geocercaId/unidades-dentro', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const unidades = await service.getUnidadesEnGeocerca(
|
||||
req.tenantId!,
|
||||
req.params.geocercaId
|
||||
);
|
||||
res.json({
|
||||
data: unidades,
|
||||
total: unidades.length,
|
||||
geocercaId: req.params.geocercaId,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get event by ID
|
||||
* GET /api/gps/eventos-geocerca/:id
|
||||
*/
|
||||
router.get('/:id', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const result = await service.findAll(
|
||||
req.tenantId!,
|
||||
{},
|
||||
{ page: 1, limit: 1 }
|
||||
);
|
||||
|
||||
// Find by ID in result
|
||||
const evento = result.data.find((e) => e.id === req.params.id);
|
||||
if (!evento) {
|
||||
return res.status(404).json({ error: 'Evento de geocerca no encontrado' });
|
||||
}
|
||||
res.json(evento);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete old events (data retention)
|
||||
* DELETE /api/gps/eventos-geocerca/antiguos
|
||||
*/
|
||||
router.delete('/antiguos', async (req: TenantRequest, res: Response) => {
|
||||
try {
|
||||
const antesDe = new Date(req.query.antesDe as string);
|
||||
if (isNaN(antesDe.getTime())) {
|
||||
return res.status(400).json({ error: 'Fecha inválida. Use formato ISO 8601.' });
|
||||
}
|
||||
const eliminados = await service.eliminarEventosAntiguos(req.tenantId!, antesDe);
|
||||
res.json({ eliminados });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@ -7,3 +7,4 @@
|
||||
export { createDispositivoGpsController } from './dispositivo-gps.controller';
|
||||
export { createPosicionGpsController } from './posicion-gps.controller';
|
||||
export { createSegmentoRutaController } from './segmento-ruta.controller';
|
||||
export { createEventoGeocercaController } from './evento-geocerca.controller';
|
||||
|
||||
@ -0,0 +1,632 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import {
|
||||
FacturaTransporteService,
|
||||
FacturaSearchParams,
|
||||
CreateFacturaDto,
|
||||
UpdateFacturaDto,
|
||||
CreateLineaFacturaDto,
|
||||
RecargoAplicarDto,
|
||||
DescuentoAplicarDto,
|
||||
} from '../services/factura-transporte.service';
|
||||
import { EstadoFactura } from '../entities';
|
||||
|
||||
/**
|
||||
* Service Context
|
||||
*/
|
||||
export interface ServiceContext {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FacturaTransporteController
|
||||
*
|
||||
* Handles transport invoice operations including:
|
||||
* - CRUD operations for invoices
|
||||
* - CFDI timbrado (SAT stamping)
|
||||
* - Payment registration
|
||||
* - PDF/XML download
|
||||
* - Pending collection tracking
|
||||
*
|
||||
* IVA Rate: 16% (Mexico)
|
||||
*/
|
||||
export class FacturaTransporteController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly facturaService: FacturaTransporteService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
// List and search
|
||||
this.router.get('/', this.findAll.bind(this));
|
||||
this.router.get('/pendientes-cobro', this.getPendientesCobro.bind(this));
|
||||
|
||||
// Find by specific identifiers
|
||||
this.router.get('/folio/:folio', this.findByFolio.bind(this));
|
||||
this.router.get('/cliente/:clienteId', this.findByCliente.bind(this));
|
||||
this.router.get('/viaje/:viajeId', this.findByViaje.bind(this));
|
||||
|
||||
// Single invoice operations
|
||||
this.router.get('/:id', this.findOne.bind(this));
|
||||
this.router.get('/:id/pdf', this.downloadPdf.bind(this));
|
||||
this.router.get('/:id/xml', this.downloadXml.bind(this));
|
||||
|
||||
// Create and update
|
||||
this.router.post('/', this.create.bind(this));
|
||||
this.router.patch('/:id', this.update.bind(this));
|
||||
|
||||
// Line items
|
||||
this.router.post('/:id/lineas', this.addLinea.bind(this));
|
||||
this.router.delete('/:id/lineas/:lineaId', this.removeLinea.bind(this));
|
||||
|
||||
// Apply surcharges and discounts
|
||||
this.router.post('/:id/recargos', this.applyRecargos.bind(this));
|
||||
this.router.post('/:id/descuentos', this.applyDescuentos.bind(this));
|
||||
|
||||
// Status operations
|
||||
this.router.post('/:id/timbrar', this.timbrar.bind(this));
|
||||
this.router.post('/:id/cancelar', this.cancelar.bind(this));
|
||||
this.router.post('/:id/enviar', this.enviar.bind(this));
|
||||
this.router.post('/:id/registrar-pago', this.registrarPago.bind(this));
|
||||
|
||||
// Recalculate totals
|
||||
this.router.post('/:id/recalcular', this.recalcularTotales.bind(this));
|
||||
|
||||
// Account statement
|
||||
this.router.get('/cliente/:clienteId/estado-cuenta', this.getEstadoCuenta.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract service context from request headers
|
||||
*/
|
||||
private getContext(req: Request): ServiceContext {
|
||||
return {
|
||||
tenantId: req.headers['x-tenant-id'] as string,
|
||||
userId: req.headers['x-user-id'] as string,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LIST AND SEARCH
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /
|
||||
* Find all invoices with filters
|
||||
*/
|
||||
private async findAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
|
||||
const params: FacturaSearchParams = {
|
||||
tenantId: ctx.tenantId,
|
||||
search: req.query.search as string,
|
||||
clienteId: req.query.clienteId as string,
|
||||
estado: req.query.estado as EstadoFactura,
|
||||
moneda: req.query.moneda as string,
|
||||
limit: parseInt(req.query.limit as string) || 50,
|
||||
offset: parseInt(req.query.offset as string) || 0,
|
||||
};
|
||||
|
||||
// Parse date filters
|
||||
if (req.query.fechaDesde) {
|
||||
params.fechaDesde = new Date(req.query.fechaDesde as string);
|
||||
}
|
||||
if (req.query.fechaHasta) {
|
||||
params.fechaHasta = new Date(req.query.fechaHasta as string);
|
||||
}
|
||||
|
||||
const result = await this.facturaService.findAll(params);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /pendientes-cobro
|
||||
* Get invoices pending collection
|
||||
*/
|
||||
private async getPendientesCobro(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const limit = parseInt(req.query.limit as string) || 100;
|
||||
const offset = parseInt(req.query.offset as string) || 0;
|
||||
|
||||
const result = await this.facturaService.getFacturasPendientes(ctx.tenantId, limit, offset);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FIND BY IDENTIFIERS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /:id
|
||||
* Find invoice by ID
|
||||
*/
|
||||
private async findOne(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const factura = await this.facturaService.findById(ctx.tenantId, id);
|
||||
if (!factura) {
|
||||
res.status(404).json({ error: 'Factura no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: factura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /folio/:folio
|
||||
* Find invoice by folio
|
||||
*/
|
||||
private async findByFolio(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { folio } = req.params;
|
||||
const serie = req.query.serie as string;
|
||||
|
||||
// Search by folio (and optionally serie)
|
||||
const params: FacturaSearchParams = {
|
||||
tenantId: ctx.tenantId,
|
||||
search: folio,
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
const result = await this.facturaService.findAll(params);
|
||||
|
||||
// Filter for exact folio match
|
||||
const factura = result.data.find(f => {
|
||||
if (serie) {
|
||||
return f.folio === folio && f.serie === serie;
|
||||
}
|
||||
return f.folio === folio || f.folioCompleto === folio;
|
||||
});
|
||||
|
||||
if (!factura) {
|
||||
res.status(404).json({ error: 'Factura no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: factura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /cliente/:clienteId
|
||||
* Get invoices by client
|
||||
*/
|
||||
private async findByCliente(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { clienteId } = req.params;
|
||||
const limit = parseInt(req.query.limit as string) || 50;
|
||||
const offset = parseInt(req.query.offset as string) || 0;
|
||||
|
||||
const result = await this.facturaService.findByCliente(ctx.tenantId, clienteId, limit, offset);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /viaje/:viajeId
|
||||
* Get invoice for a trip
|
||||
*/
|
||||
private async findByViaje(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { viajeId } = req.params;
|
||||
|
||||
// Search for invoices containing this trip
|
||||
const params: FacturaSearchParams = {
|
||||
tenantId: ctx.tenantId,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
const result = await this.facturaService.findAll(params);
|
||||
|
||||
// Filter for invoices containing this viajeId
|
||||
const facturas = result.data.filter(f =>
|
||||
f.viajeIds && f.viajeIds.includes(viajeId)
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: facturas,
|
||||
total: facturas.length,
|
||||
viajeId,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CREATE AND UPDATE
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /
|
||||
* Create a new invoice
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const dto: CreateFacturaDto = {
|
||||
...req.body,
|
||||
fechaEmision: new Date(req.body.fechaEmision),
|
||||
fechaVencimiento: req.body.fechaVencimiento
|
||||
? new Date(req.body.fechaVencimiento)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const factura = await this.facturaService.create(ctx.tenantId, dto, ctx.userId);
|
||||
res.status(201).json({ data: factura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /:id
|
||||
* Update invoice (only if in BORRADOR state)
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const dto: UpdateFacturaDto = {
|
||||
...req.body,
|
||||
fechaVencimiento: req.body.fechaVencimiento
|
||||
? new Date(req.body.fechaVencimiento)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const factura = await this.facturaService.update(ctx.tenantId, id, dto);
|
||||
if (!factura) {
|
||||
res.status(404).json({ error: 'Factura no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: factura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LINE ITEMS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /:id/lineas
|
||||
* Add line item to invoice
|
||||
*/
|
||||
private async addLinea(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const dto: CreateLineaFacturaDto = req.body;
|
||||
|
||||
const linea = await this.facturaService.addLinea(ctx.tenantId, id, dto);
|
||||
res.status(201).json({ data: linea });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /:id/lineas/:lineaId
|
||||
* Remove line item from invoice
|
||||
*/
|
||||
private async removeLinea(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id, lineaId } = req.params;
|
||||
|
||||
const deleted = await this.facturaService.removeLinea(ctx.tenantId, id, lineaId);
|
||||
if (!deleted) {
|
||||
res.status(404).json({ error: 'Linea no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SURCHARGES AND DISCOUNTS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /:id/recargos
|
||||
* Apply surcharges to invoice (IVA 16%)
|
||||
*/
|
||||
private async applyRecargos(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const recargos: RecargoAplicarDto[] = req.body.recargos;
|
||||
|
||||
const factura = await this.facturaService.applyRecargos(ctx.tenantId, id, recargos);
|
||||
res.json({ data: factura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /:id/descuentos
|
||||
* Apply discounts to invoice
|
||||
*/
|
||||
private async applyDescuentos(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const descuentos: DescuentoAplicarDto[] = req.body.descuentos;
|
||||
|
||||
const factura = await this.facturaService.applyDescuentos(ctx.tenantId, id, descuentos);
|
||||
res.json({ data: factura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// STATUS OPERATIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /:id/timbrar
|
||||
* Stamp invoice with CFDI (SAT Mexico)
|
||||
*/
|
||||
private async timbrar(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const { uuidCfdi, xmlCfdi, pdfUrl } = req.body;
|
||||
|
||||
if (!uuidCfdi || !xmlCfdi) {
|
||||
res.status(400).json({ error: 'Se requiere uuidCfdi y xmlCfdi' });
|
||||
return;
|
||||
}
|
||||
|
||||
const factura = await this.facturaService.timbrar(ctx.tenantId, id, {
|
||||
uuidCfdi,
|
||||
xmlCfdi,
|
||||
pdfUrl,
|
||||
});
|
||||
|
||||
res.json({ data: factura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /:id/cancelar
|
||||
* Cancel invoice
|
||||
*/
|
||||
private async cancelar(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const { motivo } = req.body;
|
||||
|
||||
if (!motivo) {
|
||||
res.status(400).json({ error: 'Se requiere motivo de cancelacion' });
|
||||
return;
|
||||
}
|
||||
|
||||
const factura = await this.facturaService.cancelar(ctx.tenantId, id, motivo);
|
||||
res.json({ data: factura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /:id/enviar
|
||||
* Send invoice by email
|
||||
*/
|
||||
private async enviar(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const { email, mensaje } = req.body;
|
||||
|
||||
const factura = await this.facturaService.findById(ctx.tenantId, id);
|
||||
if (!factura) {
|
||||
res.status(404).json({ error: 'Factura no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (factura.estado === EstadoFactura.BORRADOR) {
|
||||
res.status(400).json({ error: 'No se puede enviar una factura en estado BORRADOR' });
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement email sending service integration
|
||||
// For now, just return success with the email details
|
||||
res.json({
|
||||
data: {
|
||||
facturaId: id,
|
||||
folioCompleto: factura.folioCompleto,
|
||||
email: email || factura.clienteRfc,
|
||||
mensaje: mensaje || 'Factura enviada correctamente',
|
||||
enviado: true,
|
||||
fechaEnvio: new Date(),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /:id/registrar-pago
|
||||
* Register payment for invoice
|
||||
*/
|
||||
private async registrarPago(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const { montoPago, fechaPago, referencia, formaPago } = req.body;
|
||||
|
||||
if (!montoPago || montoPago <= 0) {
|
||||
res.status(400).json({ error: 'El monto de pago debe ser mayor a cero' });
|
||||
return;
|
||||
}
|
||||
|
||||
const factura = await this.facturaService.registrarPago(
|
||||
ctx.tenantId,
|
||||
id,
|
||||
montoPago,
|
||||
fechaPago ? new Date(fechaPago) : new Date()
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
factura,
|
||||
pago: {
|
||||
monto: montoPago,
|
||||
fecha: fechaPago || new Date(),
|
||||
referencia,
|
||||
formaPago,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DOCUMENT DOWNLOAD
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /:id/pdf
|
||||
* Download invoice PDF
|
||||
*/
|
||||
private async downloadPdf(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const factura = await this.facturaService.findById(ctx.tenantId, id);
|
||||
if (!factura) {
|
||||
res.status(404).json({ error: 'Factura no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!factura.pdfUrl) {
|
||||
res.status(404).json({ error: 'PDF no disponible para esta factura' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Return PDF URL or redirect
|
||||
res.json({
|
||||
data: {
|
||||
facturaId: id,
|
||||
folioCompleto: factura.folioCompleto,
|
||||
pdfUrl: factura.pdfUrl,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /:id/xml
|
||||
* Download invoice XML (CFDI)
|
||||
*/
|
||||
private async downloadXml(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const factura = await this.facturaService.findById(ctx.tenantId, id);
|
||||
if (!factura) {
|
||||
res.status(404).json({ error: 'Factura no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!factura.xmlCfdi) {
|
||||
res.status(404).json({ error: 'XML CFDI no disponible para esta factura' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Return XML content
|
||||
res.setHeader('Content-Type', 'application/xml');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${factura.folioCompleto}.xml"`);
|
||||
res.send(factura.xmlCfdi);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ACCOUNT STATEMENTS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /cliente/:clienteId/estado-cuenta
|
||||
* Get client account statement
|
||||
*/
|
||||
private async getEstadoCuenta(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { clienteId } = req.params;
|
||||
|
||||
const estadoCuenta = await this.facturaService.getEstadoCuenta(ctx.tenantId, clienteId);
|
||||
res.json({ data: estadoCuenta });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// UTILITY OPERATIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /:id/recalcular
|
||||
* Recalculate invoice totals (subtotal, IVA 16%, total)
|
||||
*/
|
||||
private async recalcularTotales(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const factura = await this.facturaService.calculateTotals(ctx.tenantId, id);
|
||||
if (!factura) {
|
||||
res.status(404).json({ error: 'Factura no encontrada' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: factura });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
/**
|
||||
* Tarifas Controllers
|
||||
* Tarifas-Transporte Controllers
|
||||
*/
|
||||
export * from './tarifas.controller';
|
||||
export * from './factura-transporte.controller';
|
||||
export * from './recargos.controller';
|
||||
|
||||
@ -0,0 +1,568 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import {
|
||||
RecargosService,
|
||||
RecargoSearchParams,
|
||||
CreateRecargoDto,
|
||||
UpdateRecargoDto,
|
||||
CreateFuelSurchargeDto,
|
||||
UpdateFuelSurchargeDto,
|
||||
ServiceContext,
|
||||
} from '../services/recargos.service';
|
||||
import { TipoRecargo } from '../entities';
|
||||
|
||||
/**
|
||||
* RecargosController
|
||||
*
|
||||
* Handles surcharge catalog and fuel surcharge operations including:
|
||||
* - CRUD operations for RecargoCatalogo
|
||||
* - CRUD operations for FuelSurcharge
|
||||
* - Surcharge calculations
|
||||
* - Fuel surcharge calculations
|
||||
* - Detention calculations
|
||||
*/
|
||||
export class RecargosController {
|
||||
public router: Router;
|
||||
|
||||
constructor(private readonly recargosService: RecargosService) {
|
||||
this.router = Router();
|
||||
this.initializeRoutes();
|
||||
}
|
||||
|
||||
private initializeRoutes(): void {
|
||||
// ============================================
|
||||
// RECARGOS CATALOGO ROUTES
|
||||
// ============================================
|
||||
|
||||
// List and search
|
||||
this.router.get('/', this.findAll.bind(this));
|
||||
this.router.get('/vigentes', this.getRecargosVigentes.bind(this));
|
||||
this.router.get('/automaticos', this.getRecargosAutomaticos.bind(this));
|
||||
|
||||
// Find by specific identifiers
|
||||
this.router.get('/codigo/:codigo', this.findByCodigo.bind(this));
|
||||
this.router.get('/tipo/:tipo', this.findByTipo.bind(this));
|
||||
|
||||
// ============================================
|
||||
// FUEL SURCHARGE ROUTES
|
||||
// ============================================
|
||||
|
||||
this.router.get('/fuel-surcharge', this.findAllFuelSurcharges.bind(this));
|
||||
this.router.get('/fuel-surcharge/vigente', this.getFuelSurchargeVigente.bind(this));
|
||||
this.router.get('/fuel-surcharge/:id', this.findFuelSurcharge.bind(this));
|
||||
this.router.post('/fuel-surcharge', this.createFuelSurcharge.bind(this));
|
||||
this.router.patch('/fuel-surcharge/:id', this.updateFuelSurcharge.bind(this));
|
||||
this.router.delete('/fuel-surcharge/:id', this.deleteFuelSurcharge.bind(this));
|
||||
|
||||
// ============================================
|
||||
// CALCULATION ROUTES
|
||||
// ============================================
|
||||
|
||||
this.router.post('/calcular', this.calcularRecargos.bind(this));
|
||||
this.router.post('/calcular-fuel', this.calculateFuelSurcharge.bind(this));
|
||||
this.router.post('/calcular-detention', this.calculateDetention.bind(this));
|
||||
this.router.post('/calcular-todos', this.calculateAllSurcharges.bind(this));
|
||||
|
||||
// ============================================
|
||||
// SINGLE RECARGO OPERATIONS (must be after other routes)
|
||||
// ============================================
|
||||
|
||||
this.router.get('/:id', this.findOne.bind(this));
|
||||
this.router.post('/', this.create.bind(this));
|
||||
this.router.patch('/:id', this.update.bind(this));
|
||||
this.router.post('/:id/activar', this.activar.bind(this));
|
||||
this.router.post('/:id/desactivar', this.desactivar.bind(this));
|
||||
this.router.delete('/:id', this.delete.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract service context from request headers
|
||||
*/
|
||||
private getContext(req: Request): ServiceContext {
|
||||
return {
|
||||
tenantId: req.headers['x-tenant-id'] as string,
|
||||
userId: req.headers['x-user-id'] as string,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RECARGOS CATALOGO - LIST AND SEARCH
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /
|
||||
* Find all surcharges with filters
|
||||
*/
|
||||
private async findAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
|
||||
const params: RecargoSearchParams = {
|
||||
tenantId: ctx.tenantId,
|
||||
search: req.query.search as string,
|
||||
tipo: req.query.tipo as TipoRecargo,
|
||||
activo: req.query.activo === 'true' ? true : req.query.activo === 'false' ? false : undefined,
|
||||
aplicaAutomatico:
|
||||
req.query.aplicaAutomatico === 'true'
|
||||
? true
|
||||
: req.query.aplicaAutomatico === 'false'
|
||||
? false
|
||||
: undefined,
|
||||
limit: parseInt(req.query.limit as string) || 50,
|
||||
offset: parseInt(req.query.offset as string) || 0,
|
||||
};
|
||||
|
||||
const result = await this.recargosService.findAll(params);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /vigentes
|
||||
* Get all active surcharges
|
||||
*/
|
||||
private async getRecargosVigentes(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const recargos = await this.recargosService.getRecargosVigentes(ctx);
|
||||
res.json({ data: recargos, total: recargos.length });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /automaticos
|
||||
* Get surcharges that apply automatically
|
||||
*/
|
||||
private async getRecargosAutomaticos(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const recargos = await this.recargosService.getRecargosAutomaticos(ctx);
|
||||
res.json({ data: recargos, total: recargos.length });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RECARGOS CATALOGO - FIND BY IDENTIFIERS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /:id
|
||||
* Find surcharge by ID
|
||||
*/
|
||||
private async findOne(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const recargo = await this.recargosService.findOne(id, ctx.tenantId);
|
||||
if (!recargo) {
|
||||
res.status(404).json({ error: 'Recargo no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: recargo });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /codigo/:codigo
|
||||
* Find surcharge by code
|
||||
*/
|
||||
private async findByCodigo(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { codigo } = req.params;
|
||||
|
||||
const recargo = await this.recargosService.findByCodigo(codigo, ctx.tenantId);
|
||||
if (!recargo) {
|
||||
res.status(404).json({ error: 'Recargo no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: recargo });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /tipo/:tipo
|
||||
* Find surcharges by type
|
||||
*/
|
||||
private async findByTipo(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const tipo = req.params.tipo as TipoRecargo;
|
||||
|
||||
// Validate tipo
|
||||
if (!Object.values(TipoRecargo).includes(tipo)) {
|
||||
res.status(400).json({
|
||||
error: 'Tipo de recargo invalido',
|
||||
tiposValidos: Object.values(TipoRecargo),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const recargos = await this.recargosService.findByTipo(tipo, ctx.tenantId);
|
||||
res.json({ data: recargos, total: recargos.length, tipo });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RECARGOS CATALOGO - CREATE AND UPDATE
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /
|
||||
* Create a new surcharge
|
||||
*/
|
||||
private async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const dto: CreateRecargoDto = req.body;
|
||||
|
||||
const recargo = await this.recargosService.create(dto, ctx);
|
||||
res.status(201).json({ data: recargo });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /:id
|
||||
* Update surcharge
|
||||
*/
|
||||
private async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const dto: UpdateRecargoDto = req.body;
|
||||
|
||||
const recargo = await this.recargosService.update(id, dto, ctx);
|
||||
if (!recargo) {
|
||||
res.status(404).json({ error: 'Recargo no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: recargo });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /:id/activar
|
||||
* Activate surcharge
|
||||
*/
|
||||
private async activar(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const recargo = await this.recargosService.activar(id, ctx);
|
||||
if (!recargo) {
|
||||
res.status(404).json({ error: 'Recargo no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: recargo });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /:id/desactivar
|
||||
* Deactivate surcharge
|
||||
*/
|
||||
private async desactivar(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const recargo = await this.recargosService.desactivar(id, ctx);
|
||||
if (!recargo) {
|
||||
res.status(404).json({ error: 'Recargo no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: recargo });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /:id
|
||||
* Delete surcharge
|
||||
*/
|
||||
private async delete(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const deleted = await this.recargosService.delete(id, ctx);
|
||||
if (!deleted) {
|
||||
res.status(404).json({ error: 'Recargo no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// FUEL SURCHARGE OPERATIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* GET /fuel-surcharge
|
||||
* Find all fuel surcharges
|
||||
*/
|
||||
private async findAllFuelSurcharges(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const limit = parseInt(req.query.limit as string) || 50;
|
||||
const offset = parseInt(req.query.offset as string) || 0;
|
||||
|
||||
const result = await this.recargosService.findAllFuelSurcharges(ctx.tenantId, limit, offset);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /fuel-surcharge/vigente
|
||||
* Get current active fuel surcharge
|
||||
*/
|
||||
private async getFuelSurchargeVigente(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const fuelSurcharge = await this.recargosService.getFuelSurchargeVigente(ctx.tenantId);
|
||||
|
||||
if (!fuelSurcharge) {
|
||||
res.json({ data: null, message: 'No hay fuel surcharge vigente configurado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: fuelSurcharge });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /fuel-surcharge/:id
|
||||
* Find fuel surcharge by ID
|
||||
*/
|
||||
private async findFuelSurcharge(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const fuelSurcharge = await this.recargosService.findFuelSurcharge(id, ctx.tenantId);
|
||||
if (!fuelSurcharge) {
|
||||
res.status(404).json({ error: 'Fuel surcharge no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: fuelSurcharge });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /fuel-surcharge
|
||||
* Create a new fuel surcharge period
|
||||
*/
|
||||
private async createFuelSurcharge(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const dto: CreateFuelSurchargeDto = {
|
||||
...req.body,
|
||||
fechaInicio: new Date(req.body.fechaInicio),
|
||||
fechaFin: new Date(req.body.fechaFin),
|
||||
};
|
||||
|
||||
const fuelSurcharge = await this.recargosService.createFuelSurcharge(dto, ctx);
|
||||
res.status(201).json({ data: fuelSurcharge });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /fuel-surcharge/:id
|
||||
* Update fuel surcharge
|
||||
*/
|
||||
private async updateFuelSurcharge(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
const dto: UpdateFuelSurchargeDto = {
|
||||
...req.body,
|
||||
fechaInicio: req.body.fechaInicio ? new Date(req.body.fechaInicio) : undefined,
|
||||
fechaFin: req.body.fechaFin ? new Date(req.body.fechaFin) : undefined,
|
||||
};
|
||||
|
||||
const fuelSurcharge = await this.recargosService.updateFuelSurcharge(id, dto, ctx);
|
||||
if (!fuelSurcharge) {
|
||||
res.status(404).json({ error: 'Fuel surcharge no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ data: fuelSurcharge });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /fuel-surcharge/:id
|
||||
* Delete fuel surcharge
|
||||
*/
|
||||
private async deleteFuelSurcharge(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { id } = req.params;
|
||||
|
||||
const deleted = await this.recargosService.deleteFuelSurcharge(id, ctx);
|
||||
if (!deleted) {
|
||||
res.status(404).json({ error: 'Fuel surcharge no encontrado' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// CALCULATION OPERATIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* POST /calcular
|
||||
* Calculate surcharges for a base amount
|
||||
*/
|
||||
private async calcularRecargos(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { montoBase, valorDeclarado, incluirFuelSurcharge } = req.body;
|
||||
|
||||
if (!montoBase || montoBase <= 0) {
|
||||
res.status(400).json({ error: 'El monto base debe ser mayor a cero' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resultado = await this.recargosService.calcularRecargos(montoBase, ctx, {
|
||||
valorDeclarado,
|
||||
incluirFuelSurcharge: incluirFuelSurcharge !== false,
|
||||
});
|
||||
|
||||
res.json({ data: resultado });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /calcular-fuel
|
||||
* Calculate fuel surcharge based on distance and fuel price
|
||||
*/
|
||||
private async calculateFuelSurcharge(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { distanciaKm, precioLitro, rendimientoKmPorLitro } = req.body;
|
||||
|
||||
if (!distanciaKm || distanciaKm <= 0) {
|
||||
res.status(400).json({ error: 'La distancia debe ser mayor a cero' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!precioLitro || precioLitro <= 0) {
|
||||
res.status(400).json({ error: 'El precio por litro debe ser mayor a cero' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resultado = await this.recargosService.calculateFuelSurcharge(
|
||||
ctx.tenantId,
|
||||
distanciaKm,
|
||||
precioLitro,
|
||||
rendimientoKmPorLitro
|
||||
);
|
||||
|
||||
res.json({ data: resultado });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /calcular-detention
|
||||
* Calculate detention charge based on waiting hours
|
||||
*/
|
||||
private async calculateDetention(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { horasEspera, tarifaBase } = req.body;
|
||||
|
||||
if (horasEspera === undefined || horasEspera < 0) {
|
||||
res.status(400).json({ error: 'Las horas de espera deben ser un numero positivo' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resultado = await this.recargosService.calculateDetention(
|
||||
ctx.tenantId,
|
||||
horasEspera,
|
||||
tarifaBase
|
||||
);
|
||||
|
||||
res.json({ data: resultado });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /calcular-todos
|
||||
* Calculate all applicable surcharges for a shipment
|
||||
*/
|
||||
private async calculateAllSurcharges(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const ctx = this.getContext(req);
|
||||
const { tarifaBase, distanciaKm, precioLitro, horasEspera, tiposAdicionales } = req.body;
|
||||
|
||||
if (!tarifaBase || tarifaBase <= 0) {
|
||||
res.status(400).json({ error: 'La tarifa base debe ser mayor a cero' });
|
||||
return;
|
||||
}
|
||||
|
||||
const resultado = await this.recargosService.calculateAllSurcharges(ctx.tenantId, {
|
||||
tarifaBase,
|
||||
distanciaKm,
|
||||
precioLitro,
|
||||
horasEspera,
|
||||
tiposAdicionales,
|
||||
});
|
||||
|
||||
res.json({ data: resultado });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user