From 2722920e127aabba09a788bdba1b7094ad062ff5 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 14:15:26 -0600 Subject: [PATCH] feat: Add combustible-gastos and tarifas-transporte entities New entities for combustible-gastos module: - anticipo-viatico.entity.ts - carga-combustible.entity.ts - control-rendimiento.entity.ts - cruce-peaje.entity.ts - gasto-viaje.entity.ts New entities for tarifas-transporte module: - factura-transporte.entity.ts - fuel-surcharge.entity.ts - lane.entity.ts - linea-factura.entity.ts - recargo-catalogo.entity.ts - tarifa.entity.ts Co-Authored-By: Claude Opus 4.5 --- .../entities/anticipo-viatico.entity.ts | 114 ++++++++++++ .../entities/carga-combustible.entity.ts | 141 ++++++++++++++ .../entities/control-rendimiento.entity.ts | 93 ++++++++++ .../entities/cruce-peaje.entity.ts | 86 +++++++++ .../entities/gasto-viaje.entity.ts | 109 +++++++++++ .../combustible-gastos/entities/index.ts | 11 +- .../entities/factura-transporte.entity.ts | 173 ++++++++++++++++++ .../entities/fuel-surcharge.entity.ts | 69 +++++++ .../tarifas-transporte/entities/index.ts | 14 +- .../entities/lane.entity.ts | 72 ++++++++ .../entities/linea-factura.entity.ts | 92 ++++++++++ .../entities/recargo-catalogo.entity.ts | 90 +++++++++ .../entities/tarifa.entity.ts | 161 ++++++++++++++++ 13 files changed, 1214 insertions(+), 11 deletions(-) create mode 100644 src/modules/combustible-gastos/entities/anticipo-viatico.entity.ts create mode 100644 src/modules/combustible-gastos/entities/carga-combustible.entity.ts create mode 100644 src/modules/combustible-gastos/entities/control-rendimiento.entity.ts create mode 100644 src/modules/combustible-gastos/entities/cruce-peaje.entity.ts create mode 100644 src/modules/combustible-gastos/entities/gasto-viaje.entity.ts create mode 100644 src/modules/tarifas-transporte/entities/factura-transporte.entity.ts create mode 100644 src/modules/tarifas-transporte/entities/fuel-surcharge.entity.ts create mode 100644 src/modules/tarifas-transporte/entities/lane.entity.ts create mode 100644 src/modules/tarifas-transporte/entities/linea-factura.entity.ts create mode 100644 src/modules/tarifas-transporte/entities/recargo-catalogo.entity.ts create mode 100644 src/modules/tarifas-transporte/entities/tarifa.entity.ts diff --git a/src/modules/combustible-gastos/entities/anticipo-viatico.entity.ts b/src/modules/combustible-gastos/entities/anticipo-viatico.entity.ts new file mode 100644 index 0000000..2756a94 --- /dev/null +++ b/src/modules/combustible-gastos/entities/anticipo-viatico.entity.ts @@ -0,0 +1,114 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * Estado del Anticipo + */ +export enum EstadoAnticipo { + SOLICITADO = 'SOLICITADO', + APROBADO = 'APROBADO', + ENTREGADO = 'ENTREGADO', + COMPROBANDO = 'COMPROBANDO', + LIQUIDADO = 'LIQUIDADO', + RECHAZADO = 'RECHAZADO', +} + +@Entity({ schema: 'fuel', name: 'anticipos_viaticos' }) +@Index('idx_anticipo_viaje', ['viajeId']) +@Index('idx_anticipo_operador', ['operadorId']) +@Index('idx_anticipo_estado', ['tenantId', 'estado']) +export class AnticipoViatico { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Viaje y operador + @Column({ name: 'viaje_id', type: 'uuid' }) + viajeId: string; + + @Column({ name: 'operador_id', type: 'uuid' }) + operadorId: string; + + // Anticipo + @Column({ name: 'monto_solicitado', type: 'decimal', precision: 12, scale: 2 }) + montoSolicitado: number; + + @Column({ name: 'monto_aprobado', type: 'decimal', precision: 12, scale: 2, nullable: true }) + montoAprobado: number | null; + + @Column({ name: 'monto_comprobado', type: 'decimal', precision: 12, scale: 2, default: 0 }) + montoComprobado: number; + + @Column({ name: 'monto_reintegro', type: 'decimal', precision: 12, scale: 2, default: 0 }) + montoReintegro: number; + + // Conceptos desglosados + @Column({ name: 'combustible_estimado', type: 'decimal', precision: 12, scale: 2, nullable: true }) + combustibleEstimado: number | null; + + @Column({ name: 'peajes_estimado', type: 'decimal', precision: 12, scale: 2, nullable: true }) + peajesEstimado: number | null; + + @Column({ name: 'viaticos_estimado', type: 'decimal', precision: 12, scale: 2, nullable: true }) + viaticosEstimado: number | null; + + // Estado + @Column({ type: 'varchar', length: 20, default: EstadoAnticipo.SOLICITADO }) + estado: string; + + // Fechas + @Column({ name: 'fecha_solicitud', type: 'timestamptz', default: () => 'NOW()' }) + fechaSolicitud: Date; + + @Column({ name: 'fecha_aprobacion', type: 'timestamptz', nullable: true }) + fechaAprobacion: Date | null; + + @Column({ name: 'fecha_entrega', type: 'timestamptz', nullable: true }) + fechaEntrega: Date | null; + + @Column({ name: 'fecha_liquidacion', type: 'timestamptz', nullable: true }) + fechaLiquidacion: Date | null; + + // Aprobaciones + @Column({ name: 'aprobado_por', type: 'uuid', nullable: true }) + aprobadoPor: string | null; + + @Column({ name: 'entregado_por', type: 'uuid', nullable: true }) + entregadoPor: string | null; + + @Column({ name: 'liquidado_por', type: 'uuid', nullable: true }) + liquidadoPor: string | null; + + // Observaciones + @Column({ type: 'text', nullable: true }) + observaciones: string | null; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid' }) + createdById: string; + + // Helpers + get saldoPendiente(): number { + const aprobado = this.montoAprobado || 0; + return aprobado - this.montoComprobado; + } + + get porcentajeComprobado(): number { + const aprobado = this.montoAprobado || 0; + return aprobado > 0 ? (this.montoComprobado / aprobado) * 100 : 0; + } + + get requiereReintegro(): boolean { + return this.montoReintegro > 0; + } +} diff --git a/src/modules/combustible-gastos/entities/carga-combustible.entity.ts b/src/modules/combustible-gastos/entities/carga-combustible.entity.ts new file mode 100644 index 0000000..c465032 --- /dev/null +++ b/src/modules/combustible-gastos/entities/carga-combustible.entity.ts @@ -0,0 +1,141 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; + +/** + * Tipo de Carga de Combustible + */ +export enum TipoCargaCombustible { + VALE = 'VALE', + TARJETA = 'TARJETA', + EFECTIVO = 'EFECTIVO', + FACTURA_DIRECTA = 'FACTURA_DIRECTA', +} + +/** + * Tipo de Combustible + */ +export enum TipoCombustible { + DIESEL = 'DIESEL', + GASOLINA_MAGNA = 'GASOLINA_MAGNA', + GASOLINA_PREMIUM = 'GASOLINA_PREMIUM', + GAS_LP = 'GAS_LP', + GAS_NATURAL = 'GAS_NATURAL', +} + +/** + * Estado del Gasto + */ +export enum EstadoGasto { + PENDIENTE = 'PENDIENTE', + APROBADO = 'APROBADO', + RECHAZADO = 'RECHAZADO', + PAGADO = 'PAGADO', +} + +@Entity({ schema: 'fuel', name: 'cargas_combustible' }) +@Index('idx_carga_unidad', ['unidadId']) +@Index('idx_carga_viaje', ['viajeId']) +@Index('idx_carga_fecha', ['tenantId', 'fechaCarga']) +export class CargaCombustible { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Unidad y viaje + @Column({ name: 'unidad_id', type: 'uuid' }) + unidadId: string; + + @Column({ name: 'viaje_id', type: 'uuid', nullable: true }) + viajeId: string | null; + + @Column({ name: 'operador_id', type: 'uuid' }) + operadorId: string; + + // Carga + @Column({ name: 'tipo_carga', type: 'enum', enum: TipoCargaCombustible }) + tipoCarga: TipoCargaCombustible; + + @Column({ name: 'tipo_combustible', type: 'varchar', length: 20 }) + tipoCombustible: string; + + @Column({ type: 'decimal', precision: 10, scale: 3 }) + litros: number; + + @Column({ name: 'precio_litro', type: 'decimal', precision: 10, scale: 4 }) + precioLitro: number; + + @Column({ type: 'decimal', precision: 12, scale: 2 }) + total: number; + + // Odómetro + @Column({ name: 'odometro_carga', type: 'int', nullable: true }) + odometroCarga: number | null; + + @Column({ name: 'rendimiento_calculado', type: 'decimal', precision: 6, scale: 2, nullable: true }) + rendimientoCalculado: number | null; + + // Ubicación + @Column({ name: 'estacion_id', type: 'uuid', nullable: true }) + estacionId: string | null; + + @Column({ name: 'estacion_nombre', type: 'varchar', length: 200, nullable: true }) + estacionNombre: string | null; + + @Column({ name: 'estacion_direccion', type: 'text', nullable: true }) + estacionDireccion: string | null; + + @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true }) + latitud: number | null; + + @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true }) + longitud: number | null; + + // Vale/Factura + @Column({ name: 'numero_vale', type: 'varchar', length: 50, nullable: true }) + numeroVale: string | null; + + @Column({ name: 'numero_factura', type: 'varchar', length: 50, nullable: true }) + numeroFactura: string | null; + + @Column({ name: 'folio_ticket', type: 'varchar', length: 50, nullable: true }) + folioTicket: string | null; + + // Fecha + @Column({ name: 'fecha_carga', type: 'timestamptz' }) + fechaCarga: Date; + + // Aprobación + @Column({ type: 'enum', enum: EstadoGasto, default: EstadoGasto.PENDIENTE }) + estado: EstadoGasto; + + @Column({ name: 'aprobado_por', type: 'uuid', nullable: true }) + aprobadoPor: string | null; + + @Column({ name: 'aprobado_fecha', type: 'timestamptz', nullable: true }) + aprobadoFecha: Date | null; + + // Evidencia + @Column({ name: 'foto_ticket_url', type: 'text', nullable: true }) + fotoTicketUrl: string | null; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid' }) + createdById: string; + + // Helpers + get costoPorLitro(): number { + return this.litros > 0 ? this.total / this.litros : 0; + } +} diff --git a/src/modules/combustible-gastos/entities/control-rendimiento.entity.ts b/src/modules/combustible-gastos/entities/control-rendimiento.entity.ts new file mode 100644 index 0000000..0e8566e --- /dev/null +++ b/src/modules/combustible-gastos/entities/control-rendimiento.entity.ts @@ -0,0 +1,93 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * Tipo de Anomalía en Rendimiento + */ +export enum TipoAnomaliaRendimiento { + BAJO_RENDIMIENTO = 'BAJO_RENDIMIENTO', + CONSUMO_EXCESIVO = 'CONSUMO_EXCESIVO', + POSIBLE_ROBO = 'POSIBLE_ROBO', + FALLA_MECANICA = 'FALLA_MECANICA', + RUTA_INEFICIENTE = 'RUTA_INEFICIENTE', +} + +@Entity({ schema: 'fuel', name: 'control_rendimiento' }) +@Index('idx_rendimiento_unidad', ['unidadId']) +@Index('idx_rendimiento_fecha', ['tenantId', 'fechaInicio']) +export class ControlRendimiento { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Unidad + @Column({ name: 'unidad_id', type: 'uuid' }) + unidadId: string; + + // Período + @Column({ name: 'fecha_inicio', type: 'date' }) + fechaInicio: Date; + + @Column({ name: 'fecha_fin', type: 'date' }) + fechaFin: Date; + + // Métricas + @Column({ name: 'km_recorridos', type: 'int' }) + kmRecorridos: number; + + @Column({ name: 'litros_consumidos', type: 'decimal', precision: 12, scale: 3 }) + litrosConsumidos: number; + + @Column({ name: 'rendimiento_real', type: 'decimal', precision: 6, scale: 2 }) + rendimientoReal: number; + + @Column({ name: 'rendimiento_esperado', type: 'decimal', precision: 6, scale: 2, nullable: true }) + rendimientoEsperado: number | null; + + @Column({ name: 'variacion_porcentaje', type: 'decimal', precision: 5, scale: 2, nullable: true }) + variacionPorcentaje: number | null; + + // Costos + @Column({ name: 'costo_total_combustible', type: 'decimal', precision: 15, scale: 2, nullable: true }) + costoTotalCombustible: number | null; + + @Column({ name: 'costo_por_km', type: 'decimal', precision: 8, scale: 4, nullable: true }) + costoPorKm: number | null; + + // Alertas + @Column({ name: 'tiene_anomalia', type: 'boolean', default: false }) + tieneAnomalia: boolean; + + @Column({ name: 'tipo_anomalia', type: 'varchar', length: 50, nullable: true }) + tipoAnomalia: string | null; + + @Column({ name: 'descripcion_anomalia', type: 'text', nullable: true }) + descripcionAnomalia: string | null; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + // Helpers + get eficiencia(): string { + if (!this.rendimientoEsperado) return 'SIN_REFERENCIA'; + const ratio = this.rendimientoReal / this.rendimientoEsperado; + if (ratio >= 0.95) return 'OPTIMO'; + if (ratio >= 0.85) return 'ACEPTABLE'; + if (ratio >= 0.75) return 'BAJO'; + return 'CRITICO'; + } + + get diasPeriodo(): number { + const inicio = new Date(this.fechaInicio); + const fin = new Date(this.fechaFin); + return Math.ceil((fin.getTime() - inicio.getTime()) / (1000 * 60 * 60 * 24)) + 1; + } +} diff --git a/src/modules/combustible-gastos/entities/cruce-peaje.entity.ts b/src/modules/combustible-gastos/entities/cruce-peaje.entity.ts new file mode 100644 index 0000000..b9d3f21 --- /dev/null +++ b/src/modules/combustible-gastos/entities/cruce-peaje.entity.ts @@ -0,0 +1,86 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; +import { EstadoGasto } from './carga-combustible.entity'; + +/** + * Tipo de Pago de Peaje + */ +export enum TipoPagoPeaje { + EFECTIVO = 'EFECTIVO', + TAG = 'TAG', + PREPAGO = 'PREPAGO', +} + +@Entity({ schema: 'fuel', name: 'cruces_peaje' }) +@Index('idx_peaje_unidad', ['unidadId']) +@Index('idx_peaje_viaje', ['viajeId']) +@Index('idx_peaje_fecha', ['tenantId', 'fechaCruce']) +export class CrucePeaje { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Unidad y viaje + @Column({ name: 'unidad_id', type: 'uuid' }) + unidadId: string; + + @Column({ name: 'viaje_id', type: 'uuid', nullable: true }) + viajeId: string | null; + + @Column({ name: 'operador_id', type: 'uuid', nullable: true }) + operadorId: string | null; + + // Peaje + @Column({ name: 'caseta_nombre', type: 'varchar', length: 200 }) + casetaNombre: string; + + @Column({ name: 'caseta_codigo', type: 'varchar', length: 50, nullable: true }) + casetaCodigo: string | null; + + @Column({ type: 'varchar', length: 200, nullable: true }) + carretera: string | null; + + // Monto + @Column({ type: 'decimal', precision: 10, scale: 2 }) + monto: number; + + @Column({ name: 'tipo_pago', type: 'varchar', length: 20, nullable: true }) + tipoPago: string | null; + + // TAG + @Column({ name: 'tag_numero', type: 'varchar', length: 50, nullable: true }) + tagNumero: string | null; + + // Ubicación + @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true }) + latitud: number | null; + + @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true }) + longitud: number | null; + + // Fecha + @Column({ name: 'fecha_cruce', type: 'timestamptz' }) + fechaCruce: Date; + + // Comprobante + @Column({ name: 'numero_ticket', type: 'varchar', length: 50, nullable: true }) + numeroTicket: string | null; + + @Column({ name: 'foto_ticket_url', type: 'text', nullable: true }) + fotoTicketUrl: string | null; + + // Estado + @Column({ type: 'enum', enum: EstadoGasto, default: EstadoGasto.APROBADO }) + estado: EstadoGasto; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; +} diff --git a/src/modules/combustible-gastos/entities/gasto-viaje.entity.ts b/src/modules/combustible-gastos/entities/gasto-viaje.entity.ts new file mode 100644 index 0000000..596b516 --- /dev/null +++ b/src/modules/combustible-gastos/entities/gasto-viaje.entity.ts @@ -0,0 +1,109 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; +import { EstadoGasto } from './carga-combustible.entity'; + +/** + * Tipo de Gasto de Viaje + */ +export enum TipoGasto { + COMBUSTIBLE = 'COMBUSTIBLE', + PEAJE = 'PEAJE', + VIATICO = 'VIATICO', + HOSPEDAJE = 'HOSPEDAJE', + ALIMENTOS = 'ALIMENTOS', + ESTACIONAMIENTO = 'ESTACIONAMIENTO', + MULTA = 'MULTA', + MANIOBRA = 'MANIOBRA', + REPARACION_MENOR = 'REPARACION_MENOR', + OTRO = 'OTRO', +} + +@Entity({ schema: 'fuel', name: 'gastos_viaje' }) +@Index('idx_gasto_viaje', ['viajeId']) +@Index('idx_gasto_operador', ['operadorId']) +@Index('idx_gasto_estado', ['tenantId', 'estado']) +export class GastoViaje { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Viaje y operador + @Column({ name: 'viaje_id', type: 'uuid' }) + viajeId: string; + + @Column({ name: 'operador_id', type: 'uuid' }) + operadorId: string; + + // Gasto + @Column({ name: 'tipo_gasto', type: 'enum', enum: TipoGasto }) + tipoGasto: TipoGasto; + + @Column({ type: 'varchar', length: 500 }) + descripcion: string; + + @Column({ type: 'decimal', precision: 12, scale: 2 }) + monto: number; + + // Comprobante + @Column({ name: 'tiene_factura', type: 'boolean', default: false }) + tieneFactura: boolean; + + @Column({ name: 'numero_factura', type: 'varchar', length: 50, nullable: true }) + numeroFactura: string | null; + + @Column({ name: 'numero_ticket', type: 'varchar', length: 50, nullable: true }) + numeroTicket: string | null; + + @Column({ name: 'foto_comprobante_url', type: 'text', nullable: true }) + fotoComprobanteUrl: string | null; + + // Ubicación + @Column({ type: 'varchar', length: 200, nullable: true }) + lugar: string | null; + + @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true }) + latitud: number | null; + + @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true }) + longitud: number | null; + + // Fecha + @Column({ name: 'fecha_gasto', type: 'timestamptz' }) + fechaGasto: Date; + + // Estado + @Column({ type: 'enum', enum: EstadoGasto, default: EstadoGasto.PENDIENTE }) + estado: EstadoGasto; + + @Column({ name: 'aprobado_por', type: 'uuid', nullable: true }) + aprobadoPor: string | null; + + @Column({ name: 'aprobado_fecha', type: 'timestamptz', nullable: true }) + aprobadoFecha: Date | null; + + @Column({ name: 'motivo_rechazo', type: 'text', nullable: true }) + motivoRechazo: string | null; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid' }) + createdById: string; + + // Helpers + get esDeducible(): boolean { + return this.tieneFactura; + } + + get requiereAprobacion(): boolean { + return this.estado === EstadoGasto.PENDIENTE; + } +} diff --git a/src/modules/combustible-gastos/entities/index.ts b/src/modules/combustible-gastos/entities/index.ts index f8701cf..81eda88 100644 --- a/src/modules/combustible-gastos/entities/index.ts +++ b/src/modules/combustible-gastos/entities/index.ts @@ -1,8 +1,9 @@ /** * Combustible y Gastos Entities + * Schema: fuel */ -// TODO: Implement entities -// - carga-combustible.entity.ts -// - cruce-peaje.entity.ts -// - gasto-viaje.entity.ts -// - viatico.entity.ts +export * from './carga-combustible.entity'; +export * from './cruce-peaje.entity'; +export * from './gasto-viaje.entity'; +export * from './anticipo-viatico.entity'; +export * from './control-rendimiento.entity'; diff --git a/src/modules/tarifas-transporte/entities/factura-transporte.entity.ts b/src/modules/tarifas-transporte/entities/factura-transporte.entity.ts new file mode 100644 index 0000000..a9ff324 --- /dev/null +++ b/src/modules/tarifas-transporte/entities/factura-transporte.entity.ts @@ -0,0 +1,173 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + OneToMany, +} from 'typeorm'; +import { LineaFactura } from './linea-factura.entity'; + +/** + * Estado de Factura + */ +export enum EstadoFactura { + BORRADOR = 'BORRADOR', + EMITIDA = 'EMITIDA', + ENVIADA = 'ENVIADA', + PAGADA = 'PAGADA', + PARCIAL = 'PARCIAL', + VENCIDA = 'VENCIDA', + CANCELADA = 'CANCELADA', +} + +@Entity({ schema: 'billing', name: 'facturas_transporte' }) +@Index('idx_factura_tenant', ['tenantId']) +@Index('idx_factura_cliente', ['clienteId']) +@Index('idx_factura_estado', ['tenantId', 'estado']) +@Index('idx_factura_fecha', ['fechaEmision']) +@Index('idx_factura_folio', ['tenantId', 'serie', 'folio'], { unique: true }) +export class FacturaTransporte { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Identificación + @Column({ type: 'varchar', length: 10, nullable: true }) + serie: string | null; + + @Column({ type: 'varchar', length: 20 }) + folio: string; + + @Column({ name: 'uuid_cfdi', type: 'uuid', nullable: true }) + uuidCfdi: string | null; + + // Cliente + @Column({ name: 'cliente_id', type: 'uuid' }) + clienteId: string; + + @Column({ name: 'cliente_rfc', type: 'varchar', length: 13 }) + clienteRfc: string; + + @Column({ name: 'cliente_razon_social', type: 'varchar', length: 200 }) + clienteRazonSocial: string; + + @Column({ name: 'cliente_uso_cfdi', type: 'varchar', length: 10, nullable: true }) + clienteUsoCfdi: string | null; + + // Fechas + @Column({ name: 'fecha_emision', type: 'timestamptz' }) + fechaEmision: Date; + + @Column({ name: 'fecha_vencimiento', type: 'date', nullable: true }) + fechaVencimiento: Date | null; + + // Totales + @Column({ type: 'decimal', precision: 15, scale: 2 }) + subtotal: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 }) + descuento: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 }) + iva: number; + + @Column({ name: 'retencion_iva', type: 'decimal', precision: 15, scale: 2, default: 0 }) + retencionIva: number; + + @Column({ name: 'retencion_isr', type: 'decimal', precision: 15, scale: 2, default: 0 }) + retencionIsr: number; + + @Column({ type: 'decimal', precision: 15, scale: 2 }) + total: number; + + @Column({ type: 'varchar', length: 3, default: 'MXN' }) + moneda: string; + + @Column({ name: 'tipo_cambio', type: 'decimal', precision: 10, scale: 4, default: 1 }) + tipoCambio: number; + + // Pago + @Column({ name: 'forma_pago', type: 'varchar', length: 10, nullable: true }) + formaPago: string | null; + + @Column({ name: 'metodo_pago', type: 'varchar', length: 10, nullable: true }) + metodoPago: string | null; + + @Column({ name: 'condiciones_pago', type: 'varchar', length: 200, nullable: true }) + condicionesPago: string | null; + + // Relacionados + @Column({ name: 'viaje_ids', type: 'uuid', array: true, nullable: true }) + viajeIds: string[] | null; + + @Column({ name: 'ot_ids', type: 'uuid', array: true, nullable: true }) + otIds: string[] | null; + + // CFDI + @Column({ name: 'xml_cfdi', type: 'text', nullable: true }) + xmlCfdi: string | null; + + @Column({ name: 'pdf_url', type: 'text', nullable: true }) + pdfUrl: string | null; + + // Estado + @Column({ type: 'enum', enum: EstadoFactura, default: EstadoFactura.BORRADOR }) + estado: EstadoFactura; + + // Pago + @Column({ name: 'monto_pagado', type: 'decimal', precision: 15, scale: 2, default: 0 }) + montoPagado: number; + + @Column({ name: 'fecha_pago', type: 'timestamptz', nullable: true }) + fechaPago: Date | null; + + // Cancelación + @Column({ name: 'fecha_cancelacion', type: 'timestamptz', nullable: true }) + fechaCancelacion: Date | null; + + @Column({ name: 'motivo_cancelacion', type: 'text', nullable: true }) + motivoCancelacion: string | null; + + // Relations + @OneToMany(() => LineaFactura, (linea) => linea.factura) + lineas: LineaFactura[]; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid' }) + createdById: string; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + // Helpers + get saldoPendiente(): number { + return this.total - this.montoPagado; + } + + get estaPagada(): boolean { + return this.estado === EstadoFactura.PAGADA || this.montoPagado >= this.total; + } + + get estaVencida(): boolean { + if (!this.fechaVencimiento || this.estaPagada) return false; + return new Date() > new Date(this.fechaVencimiento); + } + + get diasVencida(): number { + if (!this.fechaVencimiento || !this.estaVencida) return 0; + const hoy = new Date(); + const vencimiento = new Date(this.fechaVencimiento); + return Math.ceil((hoy.getTime() - vencimiento.getTime()) / (1000 * 60 * 60 * 24)); + } + + get folioCompleto(): string { + return this.serie ? `${this.serie}-${this.folio}` : this.folio; + } +} diff --git a/src/modules/tarifas-transporte/entities/fuel-surcharge.entity.ts b/src/modules/tarifas-transporte/entities/fuel-surcharge.entity.ts new file mode 100644 index 0000000..6969f67 --- /dev/null +++ b/src/modules/tarifas-transporte/entities/fuel-surcharge.entity.ts @@ -0,0 +1,69 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +@Entity({ schema: 'billing', name: 'fuel_surcharge' }) +@Index('idx_fuel_surcharge_fecha', ['tenantId', 'fechaInicio', 'fechaFin']) +export class FuelSurcharge { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Período + @Column({ name: 'fecha_inicio', type: 'date' }) + fechaInicio: Date; + + @Column({ name: 'fecha_fin', type: 'date' }) + fechaFin: Date; + + // Precios de referencia + @Column({ name: 'precio_diesel_referencia', type: 'decimal', precision: 10, scale: 4, nullable: true }) + precioDieselReferencia: number | null; + + @Column({ name: 'precio_diesel_actual', type: 'decimal', precision: 10, scale: 4, nullable: true }) + precioDieselActual: number | null; + + // Surcharge + @Column({ name: 'porcentaje_surcharge', type: 'decimal', precision: 5, scale: 2 }) + porcentajeSurcharge: number; + + // Estado + @Column({ type: 'boolean', default: true }) + activo: boolean; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid' }) + createdById: string; + + // Helpers + get vigente(): boolean { + const hoy = new Date(); + const inicio = new Date(this.fechaInicio); + const fin = new Date(this.fechaFin); + return this.activo && hoy >= inicio && hoy <= fin; + } + + get variacionPrecio(): number | null { + if (!this.precioDieselReferencia || !this.precioDieselActual) return null; + return ((this.precioDieselActual - this.precioDieselReferencia) / this.precioDieselReferencia) * 100; + } + + calcularRecargo(montoBase: number): number { + return montoBase * (this.porcentajeSurcharge / 100); + } + + get descripcionPeriodo(): string { + const inicio = new Date(this.fechaInicio).toLocaleDateString('es-MX'); + const fin = new Date(this.fechaFin).toLocaleDateString('es-MX'); + return `${inicio} - ${fin}`; + } +} diff --git a/src/modules/tarifas-transporte/entities/index.ts b/src/modules/tarifas-transporte/entities/index.ts index 87489a3..be62e4c 100644 --- a/src/modules/tarifas-transporte/entities/index.ts +++ b/src/modules/tarifas-transporte/entities/index.ts @@ -1,8 +1,10 @@ /** - * Tarifas Entities + * Tarifas de Transporte Entities + * Schema: billing */ -// TODO: Implement entities -// - tarifa.entity.ts -// - recargo.entity.ts -// - contrato-tarifa.entity.ts -// - lane.entity.ts +export * from './lane.entity'; +export * from './tarifa.entity'; +export * from './recargo-catalogo.entity'; +export * from './factura-transporte.entity'; +export * from './linea-factura.entity'; +export * from './fuel-surcharge.entity'; diff --git a/src/modules/tarifas-transporte/entities/lane.entity.ts b/src/modules/tarifas-transporte/entities/lane.entity.ts new file mode 100644 index 0000000..81c607f --- /dev/null +++ b/src/modules/tarifas-transporte/entities/lane.entity.ts @@ -0,0 +1,72 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +@Entity({ schema: 'billing', name: 'lanes' }) +@Index('idx_lane_tenant', ['tenantId']) +@Index('idx_lane_codigo', ['tenantId', 'codigo'], { unique: true }) +export class Lane { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Identificación + @Column({ type: 'varchar', length: 50 }) + codigo: string; + + @Column({ type: 'varchar', length: 200 }) + nombre: string; + + // Origen + @Column({ name: 'origen_ciudad', type: 'varchar', length: 100 }) + origenCiudad: string; + + @Column({ name: 'origen_estado', type: 'varchar', length: 100 }) + origenEstado: string; + + @Column({ name: 'origen_codigo_postal', type: 'varchar', length: 10, nullable: true }) + origenCodigoPostal: string | null; + + // Destino + @Column({ name: 'destino_ciudad', type: 'varchar', length: 100 }) + destinoCiudad: string; + + @Column({ name: 'destino_estado', type: 'varchar', length: 100 }) + destinoEstado: string; + + @Column({ name: 'destino_codigo_postal', type: 'varchar', length: 10, nullable: true }) + destinoCodigoPostal: string | null; + + // Distancia + @Column({ name: 'distancia_km', type: 'decimal', precision: 10, scale: 2, nullable: true }) + distanciaKm: number | null; + + @Column({ name: 'tiempo_estimado_horas', type: 'decimal', precision: 6, scale: 2, nullable: true }) + tiempoEstimadoHoras: number | null; + + // Estado + @Column({ type: 'boolean', default: true }) + activo: boolean; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid' }) + createdById: string; + + // Helpers + get descripcionCorta(): string { + return `${this.origenCiudad} → ${this.destinoCiudad}`; + } + + get descripcionCompleta(): string { + return `${this.origenCiudad}, ${this.origenEstado} → ${this.destinoCiudad}, ${this.destinoEstado}`; + } +} diff --git a/src/modules/tarifas-transporte/entities/linea-factura.entity.ts b/src/modules/tarifas-transporte/entities/linea-factura.entity.ts new file mode 100644 index 0000000..e5828b5 --- /dev/null +++ b/src/modules/tarifas-transporte/entities/linea-factura.entity.ts @@ -0,0 +1,92 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { FacturaTransporte } from './factura-transporte.entity'; +import { RecargoCatalogo } from './recargo-catalogo.entity'; + +@Entity({ schema: 'billing', name: 'lineas_factura' }) +@Index('idx_linea_factura', ['facturaId']) +export class LineaFactura { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ name: 'factura_id', type: 'uuid' }) + facturaId: string; + + @ManyToOne(() => FacturaTransporte, (factura) => factura.lineas) + @JoinColumn({ name: 'factura_id' }) + factura: FacturaTransporte; + + // Secuencia + @Column({ type: 'int' }) + linea: number; + + // Concepto + @Column({ type: 'text' }) + descripcion: string; + + @Column({ name: 'clave_producto_sat', type: 'varchar', length: 10, nullable: true }) + claveProductoSat: string | null; + + @Column({ name: 'unidad_sat', type: 'varchar', length: 10, nullable: true }) + unidadSat: string | null; + + // Cantidades + @Column({ type: 'decimal', precision: 12, scale: 4 }) + cantidad: number; + + @Column({ name: 'precio_unitario', type: 'decimal', precision: 15, scale: 4 }) + precioUnitario: number; + + @Column({ type: 'decimal', precision: 15, scale: 2, default: 0 }) + descuento: number; + + @Column({ type: 'decimal', precision: 15, scale: 2 }) + importe: number; + + // Impuestos + @Column({ name: 'iva_tasa', type: 'decimal', precision: 5, scale: 2, default: 16 }) + ivaTasa: number; + + @Column({ name: 'iva_monto', type: 'decimal', precision: 15, scale: 2, nullable: true }) + ivaMonto: number | null; + + // Referencia + @Column({ name: 'viaje_id', type: 'uuid', nullable: true }) + viajeId: string | null; + + @Column({ name: 'ot_id', type: 'uuid', nullable: true }) + otId: string | null; + + @Column({ name: 'recargo_id', type: 'uuid', nullable: true }) + recargoId: string | null; + + @ManyToOne(() => RecargoCatalogo) + @JoinColumn({ name: 'recargo_id' }) + recargo: RecargoCatalogo | null; + + // Helpers + get importeNeto(): number { + return this.importe - this.descuento; + } + + get importeConIva(): number { + return this.importeNeto + (this.ivaMonto || 0); + } + + calcularImporte(): number { + return this.cantidad * this.precioUnitario; + } + + calcularIva(): number { + return this.importeNeto * (this.ivaTasa / 100); + } +} diff --git a/src/modules/tarifas-transporte/entities/recargo-catalogo.entity.ts b/src/modules/tarifas-transporte/entities/recargo-catalogo.entity.ts new file mode 100644 index 0000000..e6bbf74 --- /dev/null +++ b/src/modules/tarifas-transporte/entities/recargo-catalogo.entity.ts @@ -0,0 +1,90 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * Tipo de Recargo + */ +export enum TipoRecargo { + FUEL_SURCHARGE = 'FUEL_SURCHARGE', + DETENTION = 'DETENTION', + MANIOBRAS = 'MANIOBRAS', + CUSTODIA = 'CUSTODIA', + ESCOLTA = 'ESCOLTA', + PERNOCTA = 'PERNOCTA', + ESTADIAS = 'ESTADIAS', + FALSO_FLETE = 'FALSO_FLETE', + SEGURO_ADICIONAL = 'SEGURO_ADICIONAL', + OTRO = 'OTRO', +} + +@Entity({ schema: 'billing', name: 'recargos_catalogo' }) +@Index('idx_recargo_tenant', ['tenantId']) +@Index('idx_recargo_codigo', ['tenantId', 'codigo'], { unique: true }) +export class RecargoCatalogo { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Identificación + @Column({ type: 'varchar', length: 50 }) + codigo: string; + + @Column({ type: 'varchar', length: 200 }) + nombre: string; + + @Column({ type: 'enum', enum: TipoRecargo }) + tipo: TipoRecargo; + + @Column({ type: 'text', nullable: true }) + descripcion: string | null; + + // Monto + @Column({ name: 'es_porcentaje', type: 'boolean', default: false }) + esPorcentaje: boolean; + + @Column({ type: 'decimal', precision: 15, scale: 4 }) + monto: number; + + @Column({ type: 'varchar', length: 3, default: 'MXN' }) + moneda: string; + + // Aplicación + @Column({ name: 'aplica_automatico', type: 'boolean', default: false }) + aplicaAutomatico: boolean; + + @Column({ name: 'condicion_aplicacion', type: 'text', nullable: true }) + condicionAplicacion: string | null; + + // Estado + @Column({ type: 'boolean', default: true }) + activo: boolean; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid' }) + createdById: string; + + // Helpers + calcularRecargo(base: number): number { + if (this.esPorcentaje) { + return base * (this.monto / 100); + } + return this.monto; + } + + get descripcionMonto(): string { + if (this.esPorcentaje) { + return `${this.monto}%`; + } + return `$${this.monto.toFixed(2)} ${this.moneda}`; + } +} diff --git a/src/modules/tarifas-transporte/entities/tarifa.entity.ts b/src/modules/tarifas-transporte/entities/tarifa.entity.ts new file mode 100644 index 0000000..f4c3a85 --- /dev/null +++ b/src/modules/tarifas-transporte/entities/tarifa.entity.ts @@ -0,0 +1,161 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Lane } from './lane.entity'; + +/** + * Tipo de Tarifa + */ +export enum TipoTarifa { + POR_VIAJE = 'POR_VIAJE', + POR_KM = 'POR_KM', + POR_TONELADA = 'POR_TONELADA', + POR_M3 = 'POR_M3', + POR_PALLET = 'POR_PALLET', + POR_HORA = 'POR_HORA', + MIXTA = 'MIXTA', +} + +@Entity({ schema: 'billing', name: 'tarifas' }) +@Index('idx_tarifa_tenant', ['tenantId']) +@Index('idx_tarifa_cliente', ['clienteId']) +@Index('idx_tarifa_lane', ['laneId']) +@Index('idx_tarifa_activa', ['tenantId', 'activa', 'fechaInicio']) +export class Tarifa { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Identificación + @Column({ type: 'varchar', length: 50 }) + codigo: string; + + @Column({ type: 'varchar', length: 200 }) + nombre: string; + + @Column({ type: 'text', nullable: true }) + descripcion: string | null; + + // Cliente (opcional para tarifas generales) + @Column({ name: 'cliente_id', type: 'uuid', nullable: true }) + clienteId: string | null; + + // Lane (opcional) + @Column({ name: 'lane_id', type: 'uuid', nullable: true }) + laneId: string | null; + + @ManyToOne(() => Lane) + @JoinColumn({ name: 'lane_id' }) + lane: Lane | null; + + // Tipo de servicio + @Column({ name: 'modalidad_servicio', type: 'varchar', length: 50, nullable: true }) + modalidadServicio: string | null; + + @Column({ name: 'tipo_equipo', type: 'varchar', length: 50, nullable: true }) + tipoEquipo: string | null; + + // Tipo de tarifa + @Column({ name: 'tipo_tarifa', type: 'enum', enum: TipoTarifa }) + tipoTarifa: TipoTarifa; + + // Precios + @Column({ name: 'tarifa_base', type: 'decimal', precision: 15, scale: 2 }) + tarifaBase: number; + + @Column({ name: 'tarifa_km', type: 'decimal', precision: 10, scale: 4, nullable: true }) + tarifaKm: number | null; + + @Column({ name: 'tarifa_tonelada', type: 'decimal', precision: 10, scale: 4, nullable: true }) + tarifaTonelada: number | null; + + @Column({ name: 'tarifa_m3', type: 'decimal', precision: 10, scale: 4, nullable: true }) + tarifaM3: number | null; + + @Column({ name: 'tarifa_pallet', type: 'decimal', precision: 10, scale: 4, nullable: true }) + tarifaPallet: number | null; + + @Column({ name: 'tarifa_hora', type: 'decimal', precision: 10, scale: 4, nullable: true }) + tarifaHora: number | null; + + // Mínimos + @Column({ name: 'minimo_facturar', type: 'decimal', precision: 15, scale: 2, nullable: true }) + minimoFacturar: number | null; + + @Column({ name: 'peso_minimo_kg', type: 'decimal', precision: 10, scale: 2, nullable: true }) + pesoMinimoKg: number | null; + + // Moneda + @Column({ type: 'varchar', length: 3, default: 'MXN' }) + moneda: string; + + // Vigencia + @Column({ name: 'fecha_inicio', type: 'date' }) + fechaInicio: Date; + + @Column({ name: 'fecha_fin', type: 'date', nullable: true }) + fechaFin: Date | null; + + // Estado + @Column({ type: 'boolean', default: true }) + activa: boolean; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid' }) + createdById: string; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + // Helpers + get vigente(): boolean { + const hoy = new Date(); + const inicio = new Date(this.fechaInicio); + const fin = this.fechaFin ? new Date(this.fechaFin) : null; + return this.activa && hoy >= inicio && (!fin || hoy <= fin); + } + + get esEspecifica(): boolean { + return !!this.clienteId; + } + + calcularMonto(params: { km?: number; toneladas?: number; m3?: number; pallets?: number; horas?: number }): number { + let total = this.tarifaBase; + + switch (this.tipoTarifa) { + case TipoTarifa.POR_KM: + total += (params.km || 0) * (this.tarifaKm || 0); + break; + case TipoTarifa.POR_TONELADA: + total += (params.toneladas || 0) * (this.tarifaTonelada || 0); + break; + case TipoTarifa.POR_M3: + total += (params.m3 || 0) * (this.tarifaM3 || 0); + break; + case TipoTarifa.POR_PALLET: + total += (params.pallets || 0) * (this.tarifaPallet || 0); + break; + case TipoTarifa.POR_HORA: + total += (params.horas || 0) * (this.tarifaHora || 0); + break; + case TipoTarifa.MIXTA: + total += (params.km || 0) * (this.tarifaKm || 0); + total += (params.toneladas || 0) * (this.tarifaTonelada || 0); + break; + } + + return this.minimoFacturar ? Math.max(total, this.minimoFacturar) : total; + } +}