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 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 14:15:26 -06:00
parent 99d18bb340
commit 2722920e12
13 changed files with 1214 additions and 11 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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}`;
}
}

View File

@ -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';

View File

@ -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}`;
}
}

View File

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

View File

@ -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}`;
}
}

View File

@ -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;
}
}