feat(carta-porte): Add CFDI Carta Porte 3.1 compliance entities

- CartaPorte: Main CFDI entity with emisor/receptor, totals, XML/PDF
- UbicacionCartaPorte: Origin/destination locations with SAT codes
- MercanciaCartaPorte: Goods being transported with hazmat support
- FiguraTransporte: Operator/owner/lessor figures
- AutotransporteCartaPorte: Vehicle configuration and trailers
- HosLog: Hours of Service compliance (NOM-087)
- InspeccionPreViaje: Pre-trip inspection checklists

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 14:19:21 -06:00
parent 2722920e12
commit 2120d6e8b0
8 changed files with 846 additions and 6 deletions

View File

@ -0,0 +1,77 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { CartaPorte } from './carta-porte.entity';
/**
* Remolque para Carta Porte
*/
export interface RemolqueCartaPorte {
subTipoRem: string; // Clave SAT del subtipo de remolque
placa: string;
}
@Entity({ schema: 'compliance', name: 'autotransporte_carta_porte' })
@Index('idx_autotransporte_carta', ['cartaPorteId'])
export class AutotransporteCartaPorte {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'carta_porte_id', type: 'uuid' })
cartaPorteId: string;
@ManyToOne(() => CartaPorte, (cp) => cp.autotransporte)
@JoinColumn({ name: 'carta_porte_id' })
cartaPorte: CartaPorte;
// Permiso
@Column({ name: 'perm_sct', type: 'varchar', length: 10 })
permSct: string;
@Column({ name: 'num_permiso_sct', type: 'varchar', length: 50 })
numPermisoSct: string;
// Identificación vehicular tractora
@Column({ name: 'config_vehicular', type: 'varchar', length: 10 })
configVehicular: string;
@Column({ name: 'placa_vm', type: 'varchar', length: 15 })
placaVm: string;
@Column({ name: 'anio_modelo_vm', type: 'int', nullable: true })
anioModeloVm: number | null;
// Remolques
@Column({ type: 'jsonb', nullable: true })
remolques: RemolqueCartaPorte[] | null;
// Helpers
get tieneRemolques(): boolean {
return Array.isArray(this.remolques) && this.remolques.length > 0;
}
get cantidadRemolques(): number {
return this.remolques?.length || 0;
}
get placasRemolques(): string[] {
return this.remolques?.map(r => r.placa) || [];
}
get configCompleta(): string {
const base = `${this.configVehicular} (${this.placaVm})`;
if (this.tieneRemolques) {
const remolquesStr = this.placasRemolques.join(', ');
return `${base} + Remolques: ${remolquesStr}`;
}
return base;
}
}

View File

@ -0,0 +1,220 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
OneToMany,
} from 'typeorm';
import { UbicacionCartaPorte } from './ubicacion-carta-porte.entity';
import { MercanciaCartaPorte } from './mercancia-carta-porte.entity';
import { FiguraTransporte } from './figura-transporte.entity';
import { AutotransporteCartaPorte } from './autotransporte-carta-porte.entity';
/**
* Estado de Carta Porte
*/
export enum EstadoCartaPorte {
BORRADOR = 'BORRADOR',
VALIDADA = 'VALIDADA',
TIMBRADA = 'TIMBRADA',
CANCELADA = 'CANCELADA',
}
/**
* Tipo de CFDI para Carta Porte
*/
export enum TipoCfdiCartaPorte {
INGRESO = 'INGRESO', // Servicio de transporte (factura)
TRASLADO = 'TRASLADO', // Traslado propio (sin cobro)
}
@Entity({ schema: 'compliance', name: 'cartas_porte' })
@Index('idx_carta_porte_tenant', ['tenantId'])
@Index('idx_carta_porte_viaje', ['viajeId'])
@Index('idx_carta_porte_uuid', ['uuidCfdi'])
@Index('idx_carta_porte_estado', ['tenantId', 'estado'])
export class CartaPorte {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// Viaje relacionado
@Column({ name: 'viaje_id', type: 'uuid' })
viajeId: string;
// CFDI
@Column({ name: 'tipo_cfdi', type: 'enum', enum: TipoCfdiCartaPorte })
tipoCfdi: TipoCfdiCartaPorte;
@Column({ name: 'version_carta_porte', type: 'varchar', length: 10, default: '3.1' })
versionCartaPorte: string;
// Identificación CFDI
@Column({ type: 'varchar', length: 10, nullable: true })
serie: string | null;
@Column({ type: 'varchar', length: 20, nullable: true })
folio: string | null;
@Column({ name: 'uuid_cfdi', type: 'uuid', nullable: true })
uuidCfdi: string | null;
@Column({ name: 'fecha_timbrado', type: 'timestamptz', nullable: true })
fechaTimbrado: Date | null;
// Emisor
@Column({ name: 'emisor_rfc', type: 'varchar', length: 13 })
emisorRfc: string;
@Column({ name: 'emisor_nombre', type: 'varchar', length: 200 })
emisorNombre: string;
@Column({ name: 'emisor_regimen_fiscal', type: 'varchar', length: 10, nullable: true })
emisorRegimenFiscal: string | null;
// Receptor
@Column({ name: 'receptor_rfc', type: 'varchar', length: 13 })
receptorRfc: string;
@Column({ name: 'receptor_nombre', type: 'varchar', length: 200 })
receptorNombre: string;
@Column({ name: 'receptor_uso_cfdi', type: 'varchar', length: 10, nullable: true })
receptorUsoCfdi: string | null;
@Column({ name: 'receptor_domicilio_fiscal_cp', type: 'varchar', length: 10, nullable: true })
receptorDomicilioFiscalCp: string | null;
// Totales CFDI
@Column({ type: 'decimal', precision: 15, scale: 2, nullable: true })
subtotal: number | null;
@Column({ type: 'decimal', precision: 15, scale: 2, nullable: true })
total: number | null;
@Column({ type: 'varchar', length: 3, default: 'MXN' })
moneda: string;
// Datos transporte federal
@Column({ name: 'transporte_internacional', type: 'boolean', default: false })
transporteInternacional: boolean;
@Column({ name: 'entrada_salida_merc', type: 'varchar', length: 10, nullable: true })
entradaSalidaMerc: string | null;
@Column({ name: 'pais_origen_destino', type: 'varchar', length: 3, nullable: true })
paisOrigenDestino: string | null;
// Datos autotransporte
@Column({ name: 'permiso_sct', type: 'varchar', length: 50, nullable: true })
permisoSct: string | null;
@Column({ name: 'num_permiso_sct', type: 'varchar', length: 50, nullable: true })
numPermisoSct: string | null;
@Column({ name: 'config_vehicular', type: 'varchar', length: 10, nullable: true })
configVehicular: string | null;
@Column({ name: 'peso_bruto_total', type: 'decimal', precision: 12, scale: 3, nullable: true })
pesoBrutoTotal: number | null;
@Column({ name: 'unidad_peso', type: 'varchar', length: 10, default: 'KGM' })
unidadPeso: string;
@Column({ name: 'num_total_mercancias', type: 'int', nullable: true })
numTotalMercancias: number | null;
// Seguro
@Column({ name: 'asegura_resp_civil', type: 'varchar', length: 100, nullable: true })
aseguraRespCivil: string | null;
@Column({ name: 'poliza_resp_civil', type: 'varchar', length: 50, nullable: true })
polizaRespCivil: string | null;
@Column({ name: 'asegura_med_ambiente', type: 'varchar', length: 100, nullable: true })
aseguraMedAmbiente: string | null;
@Column({ name: 'poliza_med_ambiente', type: 'varchar', length: 50, nullable: true })
polizaMedAmbiente: string | null;
@Column({ name: 'asegura_carga', type: 'varchar', length: 100, nullable: true })
aseguraCarga: string | null;
@Column({ name: 'poliza_carga', type: 'varchar', length: 50, nullable: true })
polizaCarga: string | null;
@Column({ name: 'prima_seguro', type: 'decimal', precision: 15, scale: 2, nullable: true })
primaSeguro: number | null;
// Estado
@Column({ type: 'enum', enum: EstadoCartaPorte, default: EstadoCartaPorte.BORRADOR })
estado: EstadoCartaPorte;
// XML y PDF
@Column({ name: 'xml_cfdi', type: 'text', nullable: true })
xmlCfdi: string | null;
@Column({ name: 'xml_carta_porte', type: 'text', nullable: true })
xmlCartaPorte: string | null;
@Column({ name: 'pdf_url', type: 'text', nullable: true })
pdfUrl: string | null;
@Column({ name: 'qr_url', type: 'text', nullable: true })
qrUrl: string | 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;
@Column({ name: 'uuid_sustitucion', type: 'uuid', nullable: true })
uuidSustitucion: string | null;
// Relations
@OneToMany(() => UbicacionCartaPorte, (ubicacion) => ubicacion.cartaPorte)
ubicaciones: UbicacionCartaPorte[];
@OneToMany(() => MercanciaCartaPorte, (mercancia) => mercancia.cartaPorte)
mercancias: MercanciaCartaPorte[];
@OneToMany(() => FiguraTransporte, (figura) => figura.cartaPorte)
figuras: FiguraTransporte[];
@OneToMany(() => AutotransporteCartaPorte, (auto) => auto.cartaPorte)
autotransporte: AutotransporteCartaPorte[];
// 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 estaTimbrada(): boolean {
return this.estado === EstadoCartaPorte.TIMBRADA && !!this.uuidCfdi;
}
get estaCancelada(): boolean {
return this.estado === EstadoCartaPorte.CANCELADA;
}
get folioCompleto(): string {
if (!this.folio) return '';
return this.serie ? `${this.serie}-${this.folio}` : this.folio;
}
get esIngreso(): boolean {
return this.tipoCfdi === TipoCfdiCartaPorte.INGRESO;
}
}

View File

@ -0,0 +1,97 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { CartaPorte } from './carta-porte.entity';
/**
* Tipo de Figura de Transporte (catálogo SAT)
*/
export enum TipoFigura {
OPERADOR = '01', // Operador/Conductor
PROPIETARIO = '02', // Propietario de la mercancía
ARRENDADOR = '03', // Arrendador del vehículo
NOTIFICADO = '04', // Notificado
}
/**
* Partes de Transporte
*/
export interface ParteTransporte {
parteTransporte: string;
}
@Entity({ schema: 'compliance', name: 'figuras_transporte' })
@Index('idx_figura_carta', ['cartaPorteId'])
export class FiguraTransporte {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'carta_porte_id', type: 'uuid' })
cartaPorteId: string;
@ManyToOne(() => CartaPorte, (cp) => cp.figuras)
@JoinColumn({ name: 'carta_porte_id' })
cartaPorte: CartaPorte;
// Tipo de figura
@Column({ name: 'tipo_figura', type: 'varchar', length: 10 })
tipoFigura: string;
// Datos
@Column({ name: 'rfc_figura', type: 'varchar', length: 13, nullable: true })
rfcFigura: string | null;
@Column({ name: 'nombre_figura', type: 'varchar', length: 200, nullable: true })
nombreFigura: string | null;
@Column({ name: 'num_licencia', type: 'varchar', length: 50, nullable: true })
numLicencia: string | null;
// Domicilio (opcional)
@Column({ type: 'varchar', length: 3, nullable: true })
pais: string | null;
@Column({ type: 'varchar', length: 10, nullable: true })
estado: string | null;
@Column({ name: 'codigo_postal', type: 'varchar', length: 10, nullable: true })
codigoPostal: string | null;
@Column({ type: 'varchar', length: 200, nullable: true })
calle: string | null;
// Partes transporte (solo para Propietario/Arrendador)
@Column({ name: 'partes_transporte', type: 'jsonb', nullable: true })
partesTransporte: ParteTransporte[] | null;
// Helpers
get esOperador(): boolean {
return this.tipoFigura === TipoFigura.OPERADOR;
}
get esPropietario(): boolean {
return this.tipoFigura === TipoFigura.PROPIETARIO;
}
get esArrendador(): boolean {
return this.tipoFigura === TipoFigura.ARRENDADOR;
}
get tipoFiguraDescripcion(): string {
switch (this.tipoFigura) {
case TipoFigura.OPERADOR: return 'Operador';
case TipoFigura.PROPIETARIO: return 'Propietario';
case TipoFigura.ARRENDADOR: return 'Arrendador';
case TipoFigura.NOTIFICADO: return 'Notificado';
default: return 'Desconocido';
}
}
}

View File

@ -0,0 +1,106 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
/**
* Estado HOS (Hours of Service)
* NOM-087-SCT-2-2017
*/
export enum EstadoHos {
DRIVING = 'DRIVING', // Conduciendo
ON_DUTY = 'ON_DUTY', // En servicio (no conduciendo)
SLEEPER = 'SLEEPER', // En litera
OFF_DUTY = 'OFF_DUTY', // Fuera de servicio
}
@Entity({ schema: 'compliance', name: 'hos_logs' })
@Index('idx_hos_operador', ['operadorId'])
@Index('idx_hos_fecha', ['tenantId', 'fecha'])
@Index('idx_hos_viaje', ['viajeId'])
export class HosLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// Operador y viaje
@Column({ name: 'operador_id', type: 'uuid' })
operadorId: string;
@Column({ name: 'viaje_id', type: 'uuid', nullable: true })
viajeId: string | null;
// Log
@Column({ type: 'date' })
fecha: Date;
@Column({ name: 'hora_inicio', type: 'time' })
horaInicio: string;
@Column({ name: 'hora_fin', type: 'time', nullable: true })
horaFin: string | null;
@Column({ name: 'duracion_minutos', type: 'int', nullable: true })
duracionMinutos: number | null;
// Estado
@Column({ type: 'enum', enum: EstadoHos })
estado: EstadoHos;
// 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;
@Column({ name: 'ubicacion_descripcion', type: 'varchar', length: 200, nullable: true })
ubicacionDescripcion: string | null;
// Odómetro
@Column({ name: 'odometro_inicio', type: 'int', nullable: true })
odometroInicio: number | null;
@Column({ name: 'odometro_fin', type: 'int', nullable: true })
odometroFin: number | null;
// Observaciones
@Column({ type: 'text', nullable: true })
observaciones: string | null;
// Certificado
@Column({ name: 'certificado_por_operador', type: 'boolean', default: false })
certificadoPorOperador: boolean;
@Column({ name: 'certificado_fecha', type: 'timestamptz', nullable: true })
certificadoFecha: Date | null;
// Auditoría
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Helpers
get duracionHoras(): number {
return (this.duracionMinutos || 0) / 60;
}
get kmRecorridos(): number | null {
if (this.odometroInicio && this.odometroFin) {
return this.odometroFin - this.odometroInicio;
}
return null;
}
get esConduccion(): boolean {
return this.estado === EstadoHos.DRIVING;
}
get esDescanso(): boolean {
return this.estado === EstadoHos.OFF_DUTY || this.estado === EstadoHos.SLEEPER;
}
}

View File

@ -1,9 +1,12 @@
/**
* Carta Porte Entities
* Schema: compliance
* CFDI con Complemento Carta Porte 3.1
*/
// TODO: Implement entities
// - carta-porte.entity.ts
// - ubicacion-carta-porte.entity.ts
// - mercancia-carta-porte.entity.ts
// - autotransporte-carta-porte.entity.ts
// - figura-transporte.entity.ts
export * from './carta-porte.entity';
export * from './ubicacion-carta-porte.entity';
export * from './mercancia-carta-porte.entity';
export * from './figura-transporte.entity';
export * from './autotransporte-carta-porte.entity';
export * from './hos-log.entity';
export * from './inspeccion-pre-viaje.entity';

View File

@ -0,0 +1,117 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Index,
} from 'typeorm';
/**
* Item del Checklist de Inspección
*/
export interface ChecklistItem {
item: string;
categoria: string;
estado: 'OK' | 'DEFECTO_MENOR' | 'DEFECTO_CRITICO' | 'NO_APLICA';
observacion?: string;
}
/**
* Foto de Inspección
*/
export interface FotoInspeccion {
url: string;
descripcion?: string;
categoria?: string;
}
@Entity({ schema: 'compliance', name: 'inspecciones_pre_viaje' })
@Index('idx_inspeccion_viaje', ['viajeId'])
@Index('idx_inspeccion_unidad', ['unidadId'])
@Index('idx_inspeccion_fecha', ['tenantId', 'fechaInspeccion'])
export class InspeccionPreViaje {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
// Viaje y unidad
@Column({ name: 'viaje_id', type: 'uuid' })
viajeId: string;
@Column({ name: 'unidad_id', type: 'uuid' })
unidadId: string;
@Column({ name: 'remolque_id', type: 'uuid', nullable: true })
remolqueId: string | null;
@Column({ name: 'operador_id', type: 'uuid' })
operadorId: string;
// Fecha
@Column({ name: 'fecha_inspeccion', type: 'timestamptz' })
fechaInspeccion: Date;
// Resultado general
@Column({ type: 'boolean', default: false })
aprobada: boolean;
// Items checklist
@Column({ name: 'checklist_items', type: 'jsonb' })
checklistItems: ChecklistItem[];
// Defectos encontrados
@Column({ name: 'defectos_encontrados', type: 'text', array: true, nullable: true })
defectosEncontrados: string[] | null;
@Column({ name: 'defectos_criticos', type: 'int', default: 0 })
defectosCriticos: number;
@Column({ name: 'defectos_menores', type: 'int', default: 0 })
defectosMenores: number;
// Firma
@Column({ name: 'firma_operador', type: 'text', nullable: true })
firmaOperador: string | null;
@Column({ name: 'firma_fecha', type: 'timestamptz', nullable: true })
firmaFecha: Date | null;
// Evidencias
@Column({ type: 'jsonb', nullable: true })
fotos: FotoInspeccion[] | null;
// Auditoría
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
// Helpers
get totalDefectos(): number {
return this.defectosCriticos + this.defectosMenores;
}
get tieneDefectosCriticos(): boolean {
return this.defectosCriticos > 0;
}
get puedeViajar(): boolean {
return this.aprobada && this.defectosCriticos === 0;
}
get tieneFirma(): boolean {
return !!this.firmaOperador;
}
get itemsConDefecto(): ChecklistItem[] {
return this.checklistItems.filter(
item => item.estado === 'DEFECTO_MENOR' || item.estado === 'DEFECTO_CRITICO'
);
}
get porcentajeAprobacion(): number {
const total = this.checklistItems.filter(i => i.estado !== 'NO_APLICA').length;
const aprobados = this.checklistItems.filter(i => i.estado === 'OK').length;
return total > 0 ? (aprobados / total) * 100 : 0;
}
}

View File

@ -0,0 +1,108 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { CartaPorte } from './carta-porte.entity';
@Entity({ schema: 'compliance', name: 'mercancias_carta_porte' })
@Index('idx_mercancia_carta', ['cartaPorteId'])
export class MercanciaCartaPorte {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'carta_porte_id', type: 'uuid' })
cartaPorteId: string;
@ManyToOne(() => CartaPorte, (cp) => cp.mercancias)
@JoinColumn({ name: 'carta_porte_id' })
cartaPorte: CartaPorte;
// Bienes transportados
@Column({ name: 'bienes_transp', type: 'varchar', length: 10 })
bienesTransp: string;
@Column({ type: 'varchar', length: 1000 })
descripcion: string;
@Column({ type: 'decimal', precision: 14, scale: 3 })
cantidad: number;
@Column({ name: 'clave_unidad', type: 'varchar', length: 10 })
claveUnidad: string;
@Column({ type: 'varchar', length: 50, nullable: true })
unidad: string | null;
// Dimensiones
@Column({ name: 'peso_en_kg', type: 'decimal', precision: 14, scale: 3 })
pesoEnKg: number;
@Column({ name: 'largo_cm', type: 'decimal', precision: 10, scale: 2, nullable: true })
largoCm: number | null;
@Column({ name: 'ancho_cm', type: 'decimal', precision: 10, scale: 2, nullable: true })
anchoCm: number | null;
@Column({ name: 'alto_cm', type: 'decimal', precision: 10, scale: 2, nullable: true })
altoCm: number | null;
// Valor
@Column({ name: 'valor_mercancia', type: 'decimal', precision: 15, scale: 2, nullable: true })
valorMercancia: number | null;
@Column({ type: 'varchar', length: 3, default: 'MXN' })
moneda: string;
// Material peligroso
@Column({ name: 'material_peligroso', type: 'boolean', default: false })
materialPeligroso: boolean;
@Column({ name: 'cve_material_peligroso', type: 'varchar', length: 10, nullable: true })
cveMaterialPeligroso: string | null;
@Column({ name: 'tipo_embalaje', type: 'varchar', length: 10, nullable: true })
tipoEmbalaje: string | null;
@Column({ name: 'descripcion_embalaje', type: 'varchar', length: 200, nullable: true })
descripcionEmbalaje: string | null;
// Fracción arancelaria (comercio exterior)
@Column({ name: 'fraccion_arancelaria', type: 'varchar', length: 10, nullable: true })
fraccionArancelaria: string | null;
@Column({ name: 'uuid_comercio_ext', type: 'uuid', nullable: true })
uuidComercioExt: string | null;
// Pedimentos
@Column({ type: 'text', array: true, nullable: true })
pedimentos: string[] | null;
// Guías
@Column({ type: 'text', array: true, nullable: true })
guias: string[] | null;
// Secuencia
@Column({ type: 'int' })
secuencia: number;
// Helpers
get esPeligroso(): boolean {
return this.materialPeligroso;
}
get volumenM3(): number | null {
if (!this.largoCm || !this.anchoCm || !this.altoCm) return null;
return (this.largoCm * this.anchoCm * this.altoCm) / 1000000; // cm³ to m³
}
get tieneComercioExterior(): boolean {
return !!this.fraccionArancelaria || !!this.uuidComercioExt;
}
}

View File

@ -0,0 +1,112 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { CartaPorte } from './carta-porte.entity';
/**
* Tipo de Ubicación
*/
export enum TipoUbicacionCartaPorte {
ORIGEN = 'Origen',
DESTINO = 'Destino',
}
@Entity({ schema: 'compliance', name: 'ubicaciones_carta_porte' })
@Index('idx_ubicacion_carta', ['cartaPorteId'])
export class UbicacionCartaPorte {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Column({ name: 'carta_porte_id', type: 'uuid' })
cartaPorteId: string;
@ManyToOne(() => CartaPorte, (cp) => cp.ubicaciones)
@JoinColumn({ name: 'carta_porte_id' })
cartaPorte: CartaPorte;
// Tipo
@Column({ name: 'tipo_ubicacion', type: 'varchar', length: 10 })
tipoUbicacion: string;
// ID Ubicación (catálogo SAT)
@Column({ name: 'id_ubicacion', type: 'varchar', length: 10, nullable: true })
idUbicacion: string | null;
// RFC
@Column({ name: 'rfc_remitente_destinatario', type: 'varchar', length: 13, nullable: true })
rfcRemitenteDestinatario: string | null;
@Column({ name: 'nombre_remitente_destinatario', type: 'varchar', length: 200, nullable: true })
nombreRemitenteDestinatario: string | null;
// Domicilio
@Column({ type: 'varchar', length: 3, default: 'MEX' })
pais: string;
@Column({ type: 'varchar', length: 10, nullable: true })
estado: string | null;
@Column({ type: 'varchar', length: 10, nullable: true })
municipio: string | null;
@Column({ type: 'varchar', length: 10, nullable: true })
localidad: string | null;
@Column({ name: 'codigo_postal', type: 'varchar', length: 10 })
codigoPostal: string;
@Column({ type: 'varchar', length: 10, nullable: true })
colonia: string | null;
@Column({ type: 'varchar', length: 200, nullable: true })
calle: string | null;
@Column({ name: 'numero_exterior', type: 'varchar', length: 50, nullable: true })
numeroExterior: string | null;
@Column({ name: 'numero_interior', type: 'varchar', length: 50, nullable: true })
numeroInterior: string | null;
@Column({ type: 'varchar', length: 500, nullable: true })
referencia: string | null;
// Fechas
@Column({ name: 'fecha_hora_salida_llegada', type: 'timestamptz', nullable: true })
fechaHoraSalidaLlegada: Date | null;
// Distancia
@Column({ name: 'distancia_recorrida', type: 'decimal', precision: 10, scale: 2, nullable: true })
distanciaRecorrida: number | null;
// Secuencia
@Column({ type: 'int' })
secuencia: number;
// Helpers
get esOrigen(): boolean {
return this.tipoUbicacion === TipoUbicacionCartaPorte.ORIGEN;
}
get esDestino(): boolean {
return this.tipoUbicacion === TipoUbicacionCartaPorte.DESTINO;
}
get direccionCompleta(): string {
const partes = [
this.calle,
this.numeroExterior ? `#${this.numeroExterior}` : null,
this.numeroInterior ? `Int. ${this.numeroInterior}` : null,
this.colonia,
`CP ${this.codigoPostal}`,
].filter(Boolean);
return partes.join(', ');
}
}