- Add carta-porte controller with CRUD operations - Add DTOs: create, update, cancelar, timbrar - Add DTOs: autotransporte, mercancia, ubicacion, figura-transporte - Add common utilities folder - Add ordenes-transporte service tests - Update index exports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
696 lines
22 KiB
TypeScript
696 lines
22 KiB
TypeScript
import { Request, Response, NextFunction, Router } from 'express';
|
|
import {
|
|
CartaPorteService,
|
|
CartaPorteSearchParams,
|
|
CreateCartaPorteDto,
|
|
UpdateCartaPorteDto,
|
|
TimbrarCartaPorteDto,
|
|
CancelarCartaPorteDto,
|
|
} from '../services/carta-porte.service';
|
|
import { CartaPorte, EstadoCartaPorte, TipoCfdiCartaPorte } from '../entities';
|
|
|
|
/**
|
|
* Response interfaces for API documentation
|
|
*/
|
|
export interface CartaPorteResponse {
|
|
data: CartaPorte;
|
|
}
|
|
|
|
export interface CartaPorteListResponse {
|
|
data: CartaPorte[];
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
}
|
|
|
|
export interface CartaPorteValidationResponse {
|
|
data: {
|
|
valid: boolean;
|
|
errors: string[];
|
|
};
|
|
}
|
|
|
|
export interface ExpedienteFiscalResponse {
|
|
data: {
|
|
viajeId: string;
|
|
cartasPorte: CartaPorte[];
|
|
total: number;
|
|
timbradas: number;
|
|
pendientes: number;
|
|
canceladas: number;
|
|
};
|
|
}
|
|
|
|
export interface PendientesTimbrarResponse {
|
|
data: CartaPorte[];
|
|
total: number;
|
|
}
|
|
|
|
/**
|
|
* Controlador principal para Carta Porte
|
|
* CFDI 3.1 Compliance - Gestion completa de Cartas Porte
|
|
*
|
|
* @apiTags CartaPorte
|
|
*
|
|
* Endpoints:
|
|
* - POST /carta-porte - Crear nueva Carta Porte desde viaje
|
|
* - GET /carta-porte - Listar con filtros
|
|
* - GET /carta-porte/:id - Obtener detalle con relaciones
|
|
* - PUT /carta-porte/:id - Actualizar (solo BORRADOR)
|
|
* - DELETE /carta-porte/:id - Eliminar (solo BORRADOR)
|
|
* - POST /carta-porte/:id/validar - Validar campos SAT
|
|
* - POST /carta-porte/:id/timbrar - Timbrar con PAC
|
|
* - POST /carta-porte/:id/cancelar - Cancelar con motivo
|
|
* - GET /carta-porte/:id/xml - Descargar XML
|
|
* - GET /carta-porte/:id/pdf - Descargar PDF
|
|
* - GET /carta-porte/expediente/:viajeId - Obtener expediente fiscal por viaje
|
|
* - GET /carta-porte/pendientes-timbrar - Listar listas para timbrar
|
|
*/
|
|
export class CartaPorteController {
|
|
public router: Router;
|
|
|
|
constructor(private readonly cartaPorteService: CartaPorteService) {
|
|
this.router = Router();
|
|
this.initializeRoutes();
|
|
}
|
|
|
|
private initializeRoutes(): void {
|
|
// Special routes (must be before :id routes)
|
|
this.router.get('/pendientes-timbrar', this.getPendientesTimbrar.bind(this));
|
|
this.router.get('/expediente/:viajeId', this.getExpedienteFiscal.bind(this));
|
|
|
|
// CRUD basico
|
|
this.router.post('/', this.create.bind(this));
|
|
this.router.get('/', this.findAll.bind(this));
|
|
this.router.get('/:id', this.findOne.bind(this));
|
|
this.router.put('/:id', this.update.bind(this));
|
|
this.router.delete('/:id', this.delete.bind(this));
|
|
|
|
// Workflow: Validacion, Timbrado, Cancelacion
|
|
this.router.post('/:id/validar', this.validar.bind(this));
|
|
this.router.post('/:id/timbrar', this.timbrar.bind(this));
|
|
this.router.post('/:id/cancelar', this.cancelar.bind(this));
|
|
|
|
// Documentos: XML y PDF
|
|
this.router.get('/:id/xml', this.downloadXml.bind(this));
|
|
this.router.get('/:id/pdf', this.downloadPdf.bind(this));
|
|
}
|
|
|
|
/**
|
|
* POST / - Crear nueva Carta Porte
|
|
* @apiOperation Crear Carta Porte desde viaje
|
|
* @apiResponse 201 Carta Porte creada exitosamente
|
|
* @apiResponse 400 Datos invalidos o tenant no especificado
|
|
* @apiResponse 500 Error interno del servidor
|
|
*/
|
|
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 userId = req.headers['x-user-id'] as string;
|
|
if (!userId) {
|
|
res.status(400).json({ error: 'User ID is required' });
|
|
return;
|
|
}
|
|
|
|
const dto: CreateCartaPorteDto = req.body;
|
|
|
|
// Validar campos requeridos
|
|
if (!dto.viajeId) {
|
|
res.status(400).json({ error: 'viajeId es requerido' });
|
|
return;
|
|
}
|
|
|
|
if (!dto.tipoCfdi) {
|
|
res.status(400).json({ error: 'tipoCfdi es requerido (INGRESO o TRASLADO)' });
|
|
return;
|
|
}
|
|
|
|
if (!dto.emisorRfc || !dto.emisorNombre) {
|
|
res.status(400).json({ error: 'Datos del emisor son requeridos (emisorRfc, emisorNombre)' });
|
|
return;
|
|
}
|
|
|
|
if (!dto.receptorRfc || !dto.receptorNombre) {
|
|
res.status(400).json({ error: 'Datos del receptor son requeridos (receptorRfc, receptorNombre)' });
|
|
return;
|
|
}
|
|
|
|
const cartaPorte = await this.cartaPorteService.create(tenantId, dto, userId);
|
|
|
|
res.status(201).json({ data: cartaPorte } as CartaPorteResponse);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET / - Listar Cartas Porte con filtros
|
|
* @apiOperation Listar Cartas Porte
|
|
* @apiQuery estado - Filtrar por estado (BORRADOR, VALIDADA, TIMBRADA, CANCELADA)
|
|
* @apiQuery tipoCfdi - Filtrar por tipo CFDI (INGRESO, TRASLADO)
|
|
* @apiQuery viajeId - Filtrar por ID de viaje
|
|
* @apiQuery fechaDesde - Fecha inicio (ISO 8601)
|
|
* @apiQuery fechaHasta - Fecha fin (ISO 8601)
|
|
* @apiQuery search - Busqueda en folio, serie, emisor, receptor
|
|
* @apiQuery limit - Limite de resultados (default 50)
|
|
* @apiQuery offset - Offset para paginacion (default 0)
|
|
* @apiResponse 200 Lista de Cartas Porte
|
|
* @apiResponse 400 Tenant no especificado
|
|
*/
|
|
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 {
|
|
estado,
|
|
tipoCfdi,
|
|
viajeId,
|
|
fechaDesde,
|
|
fechaHasta,
|
|
search,
|
|
emisorRfc,
|
|
receptorRfc,
|
|
limit = '50',
|
|
offset = '0',
|
|
} = req.query;
|
|
|
|
// Validar estado si se proporciona
|
|
if (estado && !Object.values(EstadoCartaPorte).includes(estado as EstadoCartaPorte)) {
|
|
res.status(400).json({
|
|
error: `estado debe ser uno de: ${Object.values(EstadoCartaPorte).join(', ')}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validar tipoCfdi si se proporciona
|
|
if (tipoCfdi && !Object.values(TipoCfdiCartaPorte).includes(tipoCfdi as TipoCfdiCartaPorte)) {
|
|
res.status(400).json({
|
|
error: `tipoCfdi debe ser uno de: ${Object.values(TipoCfdiCartaPorte).join(', ')}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const params: CartaPorteSearchParams = {
|
|
tenantId,
|
|
estado: estado as EstadoCartaPorte | undefined,
|
|
tipoCfdi: tipoCfdi as TipoCfdiCartaPorte | undefined,
|
|
viajeId: viajeId as string | undefined,
|
|
emisorRfc: emisorRfc as string | undefined,
|
|
receptorRfc: receptorRfc as string | undefined,
|
|
fechaDesde: fechaDesde ? new Date(fechaDesde as string) : undefined,
|
|
fechaHasta: fechaHasta ? new Date(fechaHasta as string) : undefined,
|
|
search: search as string | undefined,
|
|
limit: parseInt(limit as string, 10),
|
|
offset: parseInt(offset as string, 10),
|
|
};
|
|
|
|
const { data, total } = await this.cartaPorteService.findAll(params);
|
|
|
|
res.json({
|
|
data,
|
|
total,
|
|
limit: params.limit,
|
|
offset: params.offset,
|
|
} as CartaPorteListResponse);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /:id - Obtener Carta Porte por ID con todas las relaciones
|
|
* @apiOperation Obtener detalle de Carta Porte
|
|
* @apiParam id - UUID de la Carta Porte
|
|
* @apiResponse 200 Carta Porte con ubicaciones, mercancias, figuras y autotransporte
|
|
* @apiResponse 404 Carta Porte no encontrada
|
|
*/
|
|
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 cartaPorte = await this.cartaPorteService.findOne(id, tenantId);
|
|
|
|
if (!cartaPorte) {
|
|
res.status(404).json({ error: 'Carta Porte no encontrada' });
|
|
return;
|
|
}
|
|
|
|
res.json({ data: cartaPorte } as CartaPorteResponse);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT /:id - Actualizar Carta Porte
|
|
* @apiOperation Actualizar Carta Porte (solo estado BORRADOR)
|
|
* @apiParam id - UUID de la Carta Porte
|
|
* @apiResponse 200 Carta Porte actualizada
|
|
* @apiResponse 400 Solo se puede actualizar en estado BORRADOR
|
|
* @apiResponse 404 Carta Porte no encontrada
|
|
*/
|
|
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 userId = req.headers['x-user-id'] as string;
|
|
if (!userId) {
|
|
res.status(400).json({ error: 'User ID is required' });
|
|
return;
|
|
}
|
|
|
|
const { id } = req.params;
|
|
const dto: UpdateCartaPorteDto = req.body;
|
|
|
|
// Verificar existencia y estado
|
|
const existing = await this.cartaPorteService.findOne(id, tenantId);
|
|
if (!existing) {
|
|
res.status(404).json({ error: 'Carta Porte no encontrada' });
|
|
return;
|
|
}
|
|
|
|
if (existing.estado !== EstadoCartaPorte.BORRADOR) {
|
|
res.status(400).json({
|
|
error: `Solo se puede actualizar Carta Porte en estado BORRADOR. Estado actual: ${existing.estado}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const cartaPorte = await this.cartaPorteService.update(id, tenantId, dto, userId);
|
|
|
|
res.json({ data: cartaPorte } as CartaPorteResponse);
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('No se puede modificar')) {
|
|
res.status(400).json({ error: error.message });
|
|
return;
|
|
}
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /:id - Eliminar Carta Porte
|
|
* @apiOperation Eliminar Carta Porte (solo estado BORRADOR)
|
|
* @apiParam id - UUID de la Carta Porte
|
|
* @apiResponse 204 Carta Porte eliminada
|
|
* @apiResponse 400 Solo se puede eliminar en estado BORRADOR
|
|
* @apiResponse 404 Carta Porte no encontrada
|
|
*/
|
|
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;
|
|
|
|
// Verificar existencia y estado
|
|
const existing = await this.cartaPorteService.findOne(id, tenantId);
|
|
if (!existing) {
|
|
res.status(404).json({ error: 'Carta Porte no encontrada' });
|
|
return;
|
|
}
|
|
|
|
if (existing.estado !== EstadoCartaPorte.BORRADOR) {
|
|
res.status(400).json({
|
|
error: `Solo se puede eliminar Carta Porte en estado BORRADOR. Estado actual: ${existing.estado}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const deleted = await this.cartaPorteService.delete(id, tenantId);
|
|
|
|
if (!deleted) {
|
|
res.status(404).json({ error: 'Carta Porte no encontrada' });
|
|
return;
|
|
}
|
|
|
|
res.status(204).send();
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('Solo se pueden eliminar')) {
|
|
res.status(400).json({ error: error.message });
|
|
return;
|
|
}
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /:id/validar - Validar campos SAT de Carta Porte
|
|
* @apiOperation Validar Carta Porte para timbrado
|
|
* @apiParam id - UUID de la Carta Porte
|
|
* @apiResponse 200 Resultado de validacion (valid: boolean, errors: string[])
|
|
* @apiResponse 404 Carta Porte no encontrada
|
|
*/
|
|
private async validar(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 { marcarValidada = false } = req.body;
|
|
|
|
// Verificar existencia
|
|
const existing = await this.cartaPorteService.findOne(id, tenantId);
|
|
if (!existing) {
|
|
res.status(404).json({ error: 'Carta Porte no encontrada' });
|
|
return;
|
|
}
|
|
|
|
// Validar campos requeridos
|
|
const validation = await this.cartaPorteService.validar(id, tenantId);
|
|
|
|
// Si se solicita marcar como validada y la validacion es exitosa
|
|
if (marcarValidada && validation.valid) {
|
|
const updated = await this.cartaPorteService.marcarValidada(id, tenantId);
|
|
res.json({
|
|
data: {
|
|
valid: validation.valid,
|
|
errors: validation.errors,
|
|
cartaPorte: updated,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.json({ data: validation } as CartaPorteValidationResponse);
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('no valida')) {
|
|
res.status(400).json({ error: error.message });
|
|
return;
|
|
}
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /:id/timbrar - Timbrar Carta Porte con PAC
|
|
* @apiOperation Timbrar CFDI con Complemento Carta Porte
|
|
* @apiParam id - UUID de la Carta Porte
|
|
* @apiBody uuidCfdi - UUID del CFDI timbrado (retornado por PAC)
|
|
* @apiBody fechaTimbrado - Fecha de timbrado
|
|
* @apiBody xmlCfdi - XML del CFDI completo
|
|
* @apiBody xmlCartaPorte - XML del complemento Carta Porte
|
|
* @apiBody pdfUrl - URL del PDF generado (opcional)
|
|
* @apiBody qrUrl - URL del codigo QR (opcional)
|
|
* @apiResponse 200 Carta Porte timbrada exitosamente
|
|
* @apiResponse 400 Carta Porte debe estar en estado VALIDADA
|
|
* @apiResponse 404 Carta Porte no encontrada
|
|
*
|
|
* NOTA: Esta implementacion es solo la estructura.
|
|
* La integracion real con PAC debe implementarse en el servicio.
|
|
*/
|
|
private async timbrar(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;
|
|
|
|
// Verificar existencia
|
|
const existing = await this.cartaPorteService.findOne(id, tenantId);
|
|
if (!existing) {
|
|
res.status(404).json({ error: 'Carta Porte no encontrada' });
|
|
return;
|
|
}
|
|
|
|
// Verificar estado
|
|
if (existing.estado !== EstadoCartaPorte.VALIDADA) {
|
|
res.status(400).json({
|
|
error: `La Carta Porte debe estar en estado VALIDADA para timbrar. Estado actual: ${existing.estado}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Datos del timbrado (vienen del PAC o de proceso de timbrado)
|
|
const dto: TimbrarCartaPorteDto = req.body;
|
|
|
|
// Validar campos requeridos para timbrado
|
|
if (!dto.uuidCfdi || !dto.fechaTimbrado || !dto.xmlCfdi || !dto.xmlCartaPorte) {
|
|
res.status(400).json({
|
|
error: 'Datos de timbrado incompletos. Se requiere: uuidCfdi, fechaTimbrado, xmlCfdi, xmlCartaPorte',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const cartaPorte = await this.cartaPorteService.timbrar(id, tenantId, dto);
|
|
|
|
res.json({
|
|
data: cartaPorte,
|
|
message: 'Carta Porte timbrada exitosamente',
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('debe estar validada')) {
|
|
res.status(400).json({ error: error.message });
|
|
return;
|
|
}
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /:id/cancelar - Cancelar Carta Porte ante SAT
|
|
* @apiOperation Cancelar CFDI timbrado
|
|
* @apiParam id - UUID de la Carta Porte
|
|
* @apiBody motivoCancelacion - Motivo de cancelacion (requerido)
|
|
* @apiBody uuidSustitucion - UUID del CFDI sustituto (opcional, para tipo 01)
|
|
* @apiResponse 200 Carta Porte cancelada exitosamente
|
|
* @apiResponse 400 Solo se pueden cancelar Cartas Porte timbradas
|
|
* @apiResponse 404 Carta Porte no encontrada
|
|
*/
|
|
private async cancelar(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: CancelarCartaPorteDto = req.body;
|
|
|
|
// Verificar existencia
|
|
const existing = await this.cartaPorteService.findOne(id, tenantId);
|
|
if (!existing) {
|
|
res.status(404).json({ error: 'Carta Porte no encontrada' });
|
|
return;
|
|
}
|
|
|
|
// Verificar estado
|
|
if (existing.estado !== EstadoCartaPorte.TIMBRADA) {
|
|
res.status(400).json({
|
|
error: `Solo se pueden cancelar Cartas Porte timbradas. Estado actual: ${existing.estado}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validar motivo de cancelacion
|
|
if (!dto.motivoCancelacion) {
|
|
res.status(400).json({ error: 'motivoCancelacion es requerido' });
|
|
return;
|
|
}
|
|
|
|
const cartaPorte = await this.cartaPorteService.cancelar(id, tenantId, dto);
|
|
|
|
res.json({
|
|
data: cartaPorte,
|
|
message: 'Carta Porte cancelada exitosamente',
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes('Solo se pueden cancelar')) {
|
|
res.status(400).json({ error: error.message });
|
|
return;
|
|
}
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /:id/xml - Descargar XML del CFDI
|
|
* @apiOperation Obtener XML del CFDI timbrado
|
|
* @apiParam id - UUID de la Carta Porte
|
|
* @apiResponse 200 XML del CFDI (application/xml)
|
|
* @apiResponse 400 Carta Porte no timbrada
|
|
* @apiResponse 404 Carta Porte no encontrada
|
|
*/
|
|
private async downloadXml(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 cartaPorte = await this.cartaPorteService.findOne(id, tenantId);
|
|
|
|
if (!cartaPorte) {
|
|
res.status(404).json({ error: 'Carta Porte no encontrada' });
|
|
return;
|
|
}
|
|
|
|
if (!cartaPorte.xmlCfdi) {
|
|
res.status(400).json({
|
|
error: 'La Carta Porte no tiene XML. Debe estar timbrada para descargar XML.',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Construir nombre del archivo
|
|
const filename = cartaPorte.uuidCfdi
|
|
? `carta-porte-${cartaPorte.uuidCfdi}.xml`
|
|
: `carta-porte-${cartaPorte.id}.xml`;
|
|
|
|
res.setHeader('Content-Type', 'application/xml');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
res.send(cartaPorte.xmlCfdi);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /:id/pdf - Descargar PDF de la Carta Porte
|
|
* @apiOperation Obtener PDF del CFDI timbrado
|
|
* @apiParam id - UUID de la Carta Porte
|
|
* @apiResponse 200 Redireccion a URL del PDF o PDF generado
|
|
* @apiResponse 400 Carta Porte no timbrada o PDF no disponible
|
|
* @apiResponse 404 Carta Porte no encontrada
|
|
*
|
|
* NOTA: Esta implementacion es un placeholder.
|
|
* La generacion real del PDF debe implementarse con una libreria PDF.
|
|
*/
|
|
private async downloadPdf(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 cartaPorte = await this.cartaPorteService.findOne(id, tenantId);
|
|
|
|
if (!cartaPorte) {
|
|
res.status(404).json({ error: 'Carta Porte no encontrada' });
|
|
return;
|
|
}
|
|
|
|
if (cartaPorte.estado !== EstadoCartaPorte.TIMBRADA) {
|
|
res.status(400).json({
|
|
error: 'Solo se puede descargar PDF de Cartas Porte timbradas.',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Si existe URL del PDF, redirigir
|
|
if (cartaPorte.pdfUrl) {
|
|
res.redirect(cartaPorte.pdfUrl);
|
|
return;
|
|
}
|
|
|
|
// Placeholder: En implementacion real, generar PDF aqui
|
|
// Por ahora retornamos mensaje indicando que PDF no esta disponible
|
|
res.status(501).json({
|
|
error: 'Generacion de PDF no implementada',
|
|
message: 'La Carta Porte esta timbrada pero la generacion de PDF no esta disponible. ' +
|
|
'Utilice el XML para generar una representacion impresa.',
|
|
cartaPorteId: cartaPorte.id,
|
|
uuidCfdi: cartaPorte.uuidCfdi,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /expediente/:viajeId - Obtener expediente fiscal del viaje
|
|
* @apiOperation Obtener todas las Cartas Porte de un viaje
|
|
* @apiParam viajeId - UUID del viaje
|
|
* @apiResponse 200 Expediente fiscal con estadisticas
|
|
* @apiResponse 404 No se encontraron Cartas Porte para el viaje
|
|
*/
|
|
private async getExpedienteFiscal(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 cartasPorte = await this.cartaPorteService.findByViaje(viajeId, tenantId);
|
|
|
|
// Calcular estadisticas
|
|
const timbradas = cartasPorte.filter((cp) => cp.estado === EstadoCartaPorte.TIMBRADA).length;
|
|
const pendientes = cartasPorte.filter(
|
|
(cp) => cp.estado === EstadoCartaPorte.BORRADOR || cp.estado === EstadoCartaPorte.VALIDADA
|
|
).length;
|
|
const canceladas = cartasPorte.filter((cp) => cp.estado === EstadoCartaPorte.CANCELADA).length;
|
|
|
|
res.json({
|
|
data: {
|
|
viajeId,
|
|
cartasPorte,
|
|
total: cartasPorte.length,
|
|
timbradas,
|
|
pendientes,
|
|
canceladas,
|
|
},
|
|
} as ExpedienteFiscalResponse);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /pendientes-timbrar - Listar Cartas Porte listas para timbrar
|
|
* @apiOperation Obtener Cartas Porte en estado BORRADOR o VALIDADA
|
|
* @apiResponse 200 Lista de Cartas Porte pendientes de timbrar
|
|
*/
|
|
private async getPendientesTimbrar(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 cartasPorte = await this.cartaPorteService.getPendientesTimbrar(tenantId);
|
|
|
|
res.json({
|
|
data: cartasPorte,
|
|
total: cartasPorte.length,
|
|
} as PendientesTimbrarResponse);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
}
|
|
}
|