From 2120d6e8b05c4feefd98e4dfac6cc7b7154826c7 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 14:19:21 -0600 Subject: [PATCH] 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 --- .../autotransporte-carta-porte.entity.ts | 77 ++++++ .../entities/carta-porte.entity.ts | 220 ++++++++++++++++++ .../entities/figura-transporte.entity.ts | 97 ++++++++ .../carta-porte/entities/hos-log.entity.ts | 106 +++++++++ src/modules/carta-porte/entities/index.ts | 15 +- .../entities/inspeccion-pre-viaje.entity.ts | 117 ++++++++++ .../entities/mercancia-carta-porte.entity.ts | 108 +++++++++ .../entities/ubicacion-carta-porte.entity.ts | 112 +++++++++ 8 files changed, 846 insertions(+), 6 deletions(-) create mode 100644 src/modules/carta-porte/entities/autotransporte-carta-porte.entity.ts create mode 100644 src/modules/carta-porte/entities/carta-porte.entity.ts create mode 100644 src/modules/carta-porte/entities/figura-transporte.entity.ts create mode 100644 src/modules/carta-porte/entities/hos-log.entity.ts create mode 100644 src/modules/carta-porte/entities/inspeccion-pre-viaje.entity.ts create mode 100644 src/modules/carta-porte/entities/mercancia-carta-porte.entity.ts create mode 100644 src/modules/carta-porte/entities/ubicacion-carta-porte.entity.ts diff --git a/src/modules/carta-porte/entities/autotransporte-carta-porte.entity.ts b/src/modules/carta-porte/entities/autotransporte-carta-porte.entity.ts new file mode 100644 index 0000000..7098b67 --- /dev/null +++ b/src/modules/carta-porte/entities/autotransporte-carta-porte.entity.ts @@ -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; + } +} diff --git a/src/modules/carta-porte/entities/carta-porte.entity.ts b/src/modules/carta-porte/entities/carta-porte.entity.ts new file mode 100644 index 0000000..355969c --- /dev/null +++ b/src/modules/carta-porte/entities/carta-porte.entity.ts @@ -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; + } +} diff --git a/src/modules/carta-porte/entities/figura-transporte.entity.ts b/src/modules/carta-porte/entities/figura-transporte.entity.ts new file mode 100644 index 0000000..6bde721 --- /dev/null +++ b/src/modules/carta-porte/entities/figura-transporte.entity.ts @@ -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'; + } + } +} diff --git a/src/modules/carta-porte/entities/hos-log.entity.ts b/src/modules/carta-porte/entities/hos-log.entity.ts new file mode 100644 index 0000000..46e1ccf --- /dev/null +++ b/src/modules/carta-porte/entities/hos-log.entity.ts @@ -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; + } +} diff --git a/src/modules/carta-porte/entities/index.ts b/src/modules/carta-porte/entities/index.ts index ea7c039..622a4f6 100644 --- a/src/modules/carta-porte/entities/index.ts +++ b/src/modules/carta-porte/entities/index.ts @@ -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'; diff --git a/src/modules/carta-porte/entities/inspeccion-pre-viaje.entity.ts b/src/modules/carta-porte/entities/inspeccion-pre-viaje.entity.ts new file mode 100644 index 0000000..5875ba1 --- /dev/null +++ b/src/modules/carta-porte/entities/inspeccion-pre-viaje.entity.ts @@ -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; + } +} diff --git a/src/modules/carta-porte/entities/mercancia-carta-porte.entity.ts b/src/modules/carta-porte/entities/mercancia-carta-porte.entity.ts new file mode 100644 index 0000000..8ae23bf --- /dev/null +++ b/src/modules/carta-porte/entities/mercancia-carta-porte.entity.ts @@ -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; + } +} diff --git a/src/modules/carta-porte/entities/ubicacion-carta-porte.entity.ts b/src/modules/carta-porte/entities/ubicacion-carta-porte.entity.ts new file mode 100644 index 0000000..1f688ec --- /dev/null +++ b/src/modules/carta-porte/entities/ubicacion-carta-porte.entity.ts @@ -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(', '); + } +}