diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..5d85a12 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,103 @@ +import express, { Application, Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import compression from 'compression'; +import morgan from 'morgan'; +import { config } from './config/index.js'; +import { logger } from './shared/utils/logger.js'; +import { AppError, ApiResponse } from './shared/types/index.js'; +import { setupSwagger } from './config/swagger.config.js'; +import authRoutes from './modules/auth/auth.routes.js'; +import apiKeysRoutes from './modules/auth/apiKeys.routes.js'; +import usersRoutes from './modules/users/users.routes.js'; +import { tenantsRoutes } from './modules/tenants/index.js'; +import companiesRoutes from './modules/companies/companies.routes.js'; +import coreRoutes from './modules/core/core.routes.js'; +import partnersRoutes from './modules/partners/partners.routes.js'; +import inventoryRoutes from './modules/inventory/inventory.routes.js'; +import financialRoutes from './modules/financial/financial.routes.js'; +import salesRoutes from './modules/ordenes-transporte/sales.routes.js'; +import productsRoutes from './modules/gestion-flota/products.routes.js'; +import projectsRoutes from './modules/viajes/projects.routes.js'; + +const app: Application = express(); + +// Security middleware +app.use(helmet()); +app.use(cors({ + origin: config.cors.origin, + credentials: true, +})); + +// Request parsing +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); +app.use(compression()); + +// Logging +const morganFormat = config.env === 'production' ? 'combined' : 'dev'; +app.use(morgan(morganFormat, { + stream: { write: (message) => logger.http(message.trim()) } +})); + +// Swagger documentation +const apiPrefix = config.apiPrefix; +setupSwagger(app, apiPrefix); + +// Health check +app.get('/health', (_req: Request, res: Response) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// API routes - Core +app.use(`${apiPrefix}/auth`, authRoutes); +app.use(`${apiPrefix}/auth/api-keys`, apiKeysRoutes); +app.use(`${apiPrefix}/users`, usersRoutes); +app.use(`${apiPrefix}/tenants`, tenantsRoutes); +app.use(`${apiPrefix}/companies`, companiesRoutes); +app.use(`${apiPrefix}/core`, coreRoutes); +app.use(`${apiPrefix}/partners`, partnersRoutes); +app.use(`${apiPrefix}/inventory`, inventoryRoutes); +app.use(`${apiPrefix}/financial`, financialRoutes); + +// API routes - Transport +app.use(`${apiPrefix}/sales`, salesRoutes); +app.use(`${apiPrefix}/products`, productsRoutes); +app.use(`${apiPrefix}/projects`, projectsRoutes); + +// 404 handler +app.use((_req: Request, res: Response) => { + const response: ApiResponse = { + success: false, + error: 'Endpoint no encontrado' + }; + res.status(404).json(response); +}); + +// Global error handler +app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { + logger.error('Unhandled error', { + error: err.message, + stack: err.stack, + name: err.name + }); + + if (err instanceof AppError) { + const response: ApiResponse = { + success: false, + error: err.message, + }; + return res.status(err.statusCode).json(response); + } + + // Generic error + const response: ApiResponse = { + success: false, + error: config.env === 'production' + ? 'Error interno del servidor' + : err.message, + }; + res.status(500).json(response); +}); + +export default app; diff --git a/src/modules/fiscal/entities/cfdi-use.entity.ts b/src/modules/fiscal/entities/cfdi-use.entity.ts new file mode 100644 index 0000000..6b09b0f --- /dev/null +++ b/src/modules/fiscal/entities/cfdi-use.entity.ts @@ -0,0 +1,47 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; +import { PersonType } from './fiscal-regime.entity.js'; + +@Entity({ schema: 'fiscal', name: 'cfdi_uses' }) +@Index('idx_cfdi_uses_code', ['code'], { unique: true }) +@Index('idx_cfdi_uses_applies', ['appliesTo']) +export class CfdiUse { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 10, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ + type: 'enum', + enum: PersonType, + nullable: false, + default: PersonType.BOTH, + name: 'applies_to', + }) + appliesTo: PersonType; + + @Column({ type: 'simple-array', nullable: true, name: 'allowed_regimes' }) + allowedRegimes: string[] | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/fiscal-regime.entity.ts b/src/modules/fiscal/entities/fiscal-regime.entity.ts new file mode 100644 index 0000000..b24ee36 --- /dev/null +++ b/src/modules/fiscal/entities/fiscal-regime.entity.ts @@ -0,0 +1,49 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +export enum PersonType { + NATURAL = 'natural', // Persona fisica + LEGAL = 'legal', // Persona moral + BOTH = 'both', +} + +@Entity({ schema: 'fiscal', name: 'fiscal_regimes' }) +@Index('idx_fiscal_regimes_code', ['code'], { unique: true }) +@Index('idx_fiscal_regimes_applies', ['appliesTo']) +export class FiscalRegime { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 10, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ + type: 'enum', + enum: PersonType, + nullable: false, + default: PersonType.BOTH, + name: 'applies_to', + }) + appliesTo: PersonType; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/index.ts b/src/modules/fiscal/entities/index.ts new file mode 100644 index 0000000..0c2a75b --- /dev/null +++ b/src/modules/fiscal/entities/index.ts @@ -0,0 +1,6 @@ +export * from './tax-category.entity.js'; +export * from './fiscal-regime.entity.js'; +export * from './cfdi-use.entity.js'; +export * from './payment-method.entity.js'; +export * from './payment-type.entity.js'; +export * from './withholding-type.entity.js'; diff --git a/src/modules/fiscal/entities/payment-method.entity.ts b/src/modules/fiscal/entities/payment-method.entity.ts new file mode 100644 index 0000000..d9fa946 --- /dev/null +++ b/src/modules/fiscal/entities/payment-method.entity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +@Entity({ schema: 'fiscal', name: 'payment_methods' }) +@Index('idx_payment_methods_code', ['code'], { unique: true }) +export class PaymentMethod { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 10, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'boolean', default: false, nullable: false, name: 'requires_bank_info' }) + requiresBankInfo: boolean; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/payment-type.entity.ts b/src/modules/fiscal/entities/payment-type.entity.ts new file mode 100644 index 0000000..31e9fbc --- /dev/null +++ b/src/modules/fiscal/entities/payment-type.entity.ts @@ -0,0 +1,33 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +@Entity({ schema: 'fiscal', name: 'payment_types' }) +@Index('idx_payment_types_code', ['code'], { unique: true }) +export class PaymentType { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 10, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/tax-category.entity.ts b/src/modules/fiscal/entities/tax-category.entity.ts new file mode 100644 index 0000000..9841c68 --- /dev/null +++ b/src/modules/fiscal/entities/tax-category.entity.ts @@ -0,0 +1,52 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +export enum TaxNature { + TAX = 'tax', + WITHHOLDING = 'withholding', + BOTH = 'both', +} + +@Entity({ schema: 'fiscal', name: 'tax_categories' }) +@Index('idx_tax_categories_code', ['code'], { unique: true }) +@Index('idx_tax_categories_sat', ['satCode']) +export class TaxCategory { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 20, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ + type: 'enum', + enum: TaxNature, + nullable: false, + default: TaxNature.TAX, + name: 'tax_nature', + }) + taxNature: TaxNature; + + @Column({ type: 'varchar', length: 10, nullable: true, name: 'sat_code' }) + satCode: string | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/withholding-type.entity.ts b/src/modules/fiscal/entities/withholding-type.entity.ts new file mode 100644 index 0000000..7305544 --- /dev/null +++ b/src/modules/fiscal/entities/withholding-type.entity.ts @@ -0,0 +1,54 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { TaxCategory } from './tax-category.entity.js'; + +@Entity({ schema: 'fiscal', name: 'withholding_types' }) +@Index('idx_withholding_types_code', ['code'], { unique: true }) +@Index('idx_withholding_types_category', ['taxCategoryId']) +export class WithholdingType { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 20, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ + type: 'decimal', + precision: 5, + scale: 2, + nullable: false, + default: 0, + name: 'default_rate', + }) + defaultRate: number; + + @Column({ type: 'uuid', nullable: true, name: 'tax_category_id' }) + taxCategoryId: string | null; + + @ManyToOne(() => TaxCategory, { nullable: true }) + @JoinColumn({ name: 'tax_category_id' }) + taxCategory: TaxCategory | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/fiscal-catalogs.service.ts b/src/modules/fiscal/fiscal-catalogs.service.ts new file mode 100644 index 0000000..cc37e4a --- /dev/null +++ b/src/modules/fiscal/fiscal-catalogs.service.ts @@ -0,0 +1,426 @@ +import { Repository } from 'typeorm'; +import { AppDataSource } from '../../config/typeorm.js'; +import { + TaxCategory, + FiscalRegime, + CfdiUse, + PaymentMethod, + PaymentType, + WithholdingType, + PersonType, +} from './entities/index.js'; +import { NotFoundError } from '../../shared/errors/index.js'; +import { logger } from '../../shared/utils/logger.js'; + +// ========================================== +// TAX CATEGORIES SERVICE +// ========================================== + +export interface TaxCategoryFilter { + taxNature?: string; + active?: boolean; +} + +class TaxCategoriesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(TaxCategory); + } + + async findAll(filter: TaxCategoryFilter = {}): Promise { + logger.debug('Finding all tax categories', { filter }); + + const qb = this.repository.createQueryBuilder('tc'); + + if (filter.taxNature) { + qb.andWhere('tc.tax_nature = :taxNature', { taxNature: filter.taxNature }); + } + + if (filter.active !== undefined) { + qb.andWhere('tc.is_active = :active', { active: filter.active }); + } + + qb.orderBy('tc.name', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding tax category by id', { id }); + + const category = await this.repository.findOne({ where: { id } }); + + if (!category) { + throw new NotFoundError('Categoría de impuesto no encontrada'); + } + + return category; + } + + async findByCode(code: string): Promise { + logger.debug('Finding tax category by code', { code }); + + return this.repository.findOne({ + where: { code: code.toUpperCase() }, + }); + } + + async findBySatCode(satCode: string): Promise { + logger.debug('Finding tax category by SAT code', { satCode }); + + return this.repository.findOne({ + where: { satCode }, + }); + } +} + +// ========================================== +// FISCAL REGIMES SERVICE +// ========================================== + +export interface FiscalRegimeFilter { + appliesTo?: PersonType; + active?: boolean; +} + +class FiscalRegimesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(FiscalRegime); + } + + async findAll(filter: FiscalRegimeFilter = {}): Promise { + logger.debug('Finding all fiscal regimes', { filter }); + + const qb = this.repository.createQueryBuilder('fr'); + + if (filter.appliesTo) { + qb.andWhere('(fr.applies_to = :appliesTo OR fr.applies_to = :both)', { + appliesTo: filter.appliesTo, + both: PersonType.BOTH, + }); + } + + if (filter.active !== undefined) { + qb.andWhere('fr.is_active = :active', { active: filter.active }); + } + + qb.orderBy('fr.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding fiscal regime by id', { id }); + + const regime = await this.repository.findOne({ where: { id } }); + + if (!regime) { + throw new NotFoundError('Régimen fiscal no encontrado'); + } + + return regime; + } + + async findByCode(code: string): Promise { + logger.debug('Finding fiscal regime by code', { code }); + + return this.repository.findOne({ + where: { code }, + }); + } + + async findForPersonType(personType: PersonType): Promise { + logger.debug('Finding fiscal regimes for person type', { personType }); + + return this.repository + .createQueryBuilder('fr') + .where('fr.applies_to = :personType OR fr.applies_to = :both', { + personType, + both: PersonType.BOTH, + }) + .andWhere('fr.is_active = true') + .orderBy('fr.code', 'ASC') + .getMany(); + } +} + +// ========================================== +// CFDI USES SERVICE +// ========================================== + +export interface CfdiUseFilter { + appliesTo?: PersonType; + active?: boolean; +} + +class CfdiUsesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(CfdiUse); + } + + async findAll(filter: CfdiUseFilter = {}): Promise { + logger.debug('Finding all CFDI uses', { filter }); + + const qb = this.repository.createQueryBuilder('cu'); + + if (filter.appliesTo) { + qb.andWhere('(cu.applies_to = :appliesTo OR cu.applies_to = :both)', { + appliesTo: filter.appliesTo, + both: PersonType.BOTH, + }); + } + + if (filter.active !== undefined) { + qb.andWhere('cu.is_active = :active', { active: filter.active }); + } + + qb.orderBy('cu.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding CFDI use by id', { id }); + + const cfdiUse = await this.repository.findOne({ where: { id } }); + + if (!cfdiUse) { + throw new NotFoundError('Uso de CFDI no encontrado'); + } + + return cfdiUse; + } + + async findByCode(code: string): Promise { + logger.debug('Finding CFDI use by code', { code }); + + return this.repository.findOne({ + where: { code: code.toUpperCase() }, + }); + } + + async findForPersonType(personType: PersonType): Promise { + logger.debug('Finding CFDI uses for person type', { personType }); + + return this.repository + .createQueryBuilder('cu') + .where('cu.applies_to = :personType OR cu.applies_to = :both', { + personType, + both: PersonType.BOTH, + }) + .andWhere('cu.is_active = true') + .orderBy('cu.code', 'ASC') + .getMany(); + } + + async findForRegime(regimeCode: string): Promise { + logger.debug('Finding CFDI uses for regime', { regimeCode }); + + // Get all active CFDI uses and filter by allowed regimes + const all = await this.repository + .createQueryBuilder('cu') + .where('cu.is_active = true') + .orderBy('cu.code', 'ASC') + .getMany(); + + return all.filter( + (cu) => !cu.allowedRegimes || cu.allowedRegimes.length === 0 || cu.allowedRegimes.includes(regimeCode) + ); + } +} + +// ========================================== +// PAYMENT METHODS SERVICE +// ========================================== + +export interface PaymentMethodFilter { + requiresBankInfo?: boolean; + active?: boolean; +} + +class PaymentMethodsService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(PaymentMethod); + } + + async findAll(filter: PaymentMethodFilter = {}): Promise { + logger.debug('Finding all payment methods', { filter }); + + const qb = this.repository.createQueryBuilder('pm'); + + if (filter.requiresBankInfo !== undefined) { + qb.andWhere('pm.requires_bank_info = :requiresBankInfo', { + requiresBankInfo: filter.requiresBankInfo, + }); + } + + if (filter.active !== undefined) { + qb.andWhere('pm.is_active = :active', { active: filter.active }); + } + + qb.orderBy('pm.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding payment method by id', { id }); + + const method = await this.repository.findOne({ where: { id } }); + + if (!method) { + throw new NotFoundError('Forma de pago no encontrada'); + } + + return method; + } + + async findByCode(code: string): Promise { + logger.debug('Finding payment method by code', { code }); + + return this.repository.findOne({ + where: { code }, + }); + } +} + +// ========================================== +// PAYMENT TYPES SERVICE +// ========================================== + +export interface PaymentTypeFilter { + active?: boolean; +} + +class PaymentTypesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(PaymentType); + } + + async findAll(filter: PaymentTypeFilter = {}): Promise { + logger.debug('Finding all payment types', { filter }); + + const qb = this.repository.createQueryBuilder('pt'); + + if (filter.active !== undefined) { + qb.andWhere('pt.is_active = :active', { active: filter.active }); + } + + qb.orderBy('pt.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding payment type by id', { id }); + + const type = await this.repository.findOne({ where: { id } }); + + if (!type) { + throw new NotFoundError('Método de pago no encontrado'); + } + + return type; + } + + async findByCode(code: string): Promise { + logger.debug('Finding payment type by code', { code }); + + return this.repository.findOne({ + where: { code: code.toUpperCase() }, + }); + } +} + +// ========================================== +// WITHHOLDING TYPES SERVICE +// ========================================== + +export interface WithholdingTypeFilter { + taxCategoryId?: string; + active?: boolean; +} + +class WithholdingTypesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(WithholdingType); + } + + async findAll(filter: WithholdingTypeFilter = {}): Promise { + logger.debug('Finding all withholding types', { filter }); + + const qb = this.repository + .createQueryBuilder('wt') + .leftJoinAndSelect('wt.taxCategory', 'taxCategory'); + + if (filter.taxCategoryId) { + qb.andWhere('wt.tax_category_id = :taxCategoryId', { + taxCategoryId: filter.taxCategoryId, + }); + } + + if (filter.active !== undefined) { + qb.andWhere('wt.is_active = :active', { active: filter.active }); + } + + qb.orderBy('wt.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding withholding type by id', { id }); + + const type = await this.repository.findOne({ + where: { id }, + relations: ['taxCategory'], + }); + + if (!type) { + throw new NotFoundError('Tipo de retención no encontrado'); + } + + return type; + } + + async findByCode(code: string): Promise { + logger.debug('Finding withholding type by code', { code }); + + return this.repository.findOne({ + where: { code }, + relations: ['taxCategory'], + }); + } + + async findByTaxCategory(taxCategoryId: string): Promise { + logger.debug('Finding withholding types by tax category', { taxCategoryId }); + + return this.repository.find({ + where: { taxCategoryId, isActive: true }, + relations: ['taxCategory'], + order: { code: 'ASC' }, + }); + } +} + +// ========================================== +// SERVICE EXPORTS +// ========================================== + +export const taxCategoriesService = new TaxCategoriesService(); +export const fiscalRegimesService = new FiscalRegimesService(); +export const cfdiUsesService = new CfdiUsesService(); +export const paymentMethodsService = new PaymentMethodsService(); +export const paymentTypesService = new PaymentTypesService(); +export const withholdingTypesService = new WithholdingTypesService(); diff --git a/src/modules/fiscal/fiscal.controller.ts b/src/modules/fiscal/fiscal.controller.ts new file mode 100644 index 0000000..a03d6ff --- /dev/null +++ b/src/modules/fiscal/fiscal.controller.ts @@ -0,0 +1,281 @@ +import { Response, NextFunction } from 'express'; +import { + taxCategoriesService, + fiscalRegimesService, + cfdiUsesService, + paymentMethodsService, + paymentTypesService, + withholdingTypesService, +} from './fiscal-catalogs.service.js'; +import { PersonType } from './entities/fiscal-regime.entity.js'; +import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; + +class FiscalController { + // ========== TAX CATEGORIES ========== + async getTaxCategories(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + taxNature: req.query.tax_nature as string | undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const categories = await taxCategoriesService.findAll(filter); + res.json({ success: true, data: categories }); + } catch (error) { + next(error); + } + } + + async getTaxCategory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const category = await taxCategoriesService.findById(req.params.id); + res.json({ success: true, data: category }); + } catch (error) { + next(error); + } + } + + async getTaxCategoryByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const category = await taxCategoriesService.findByCode(req.params.code); + if (!category) { + res.status(404).json({ success: false, message: 'Categoría de impuesto no encontrada' }); + return; + } + res.json({ success: true, data: category }); + } catch (error) { + next(error); + } + } + + async getTaxCategoryBySatCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const category = await taxCategoriesService.findBySatCode(req.params.satCode); + if (!category) { + res.status(404).json({ success: false, message: 'Categoría de impuesto no encontrada' }); + return; + } + res.json({ success: true, data: category }); + } catch (error) { + next(error); + } + } + + // ========== FISCAL REGIMES ========== + async getFiscalRegimes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + appliesTo: req.query.applies_to as PersonType | undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const regimes = await fiscalRegimesService.findAll(filter); + res.json({ success: true, data: regimes }); + } catch (error) { + next(error); + } + } + + async getFiscalRegime(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const regime = await fiscalRegimesService.findById(req.params.id); + res.json({ success: true, data: regime }); + } catch (error) { + next(error); + } + } + + async getFiscalRegimeByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const regime = await fiscalRegimesService.findByCode(req.params.code); + if (!regime) { + res.status(404).json({ success: false, message: 'Régimen fiscal no encontrado' }); + return; + } + res.json({ success: true, data: regime }); + } catch (error) { + next(error); + } + } + + async getFiscalRegimesForPersonType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const personType = req.params.personType as PersonType; + const regimes = await fiscalRegimesService.findForPersonType(personType); + res.json({ success: true, data: regimes }); + } catch (error) { + next(error); + } + } + + // ========== CFDI USES ========== + async getCfdiUses(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + appliesTo: req.query.applies_to as PersonType | undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const uses = await cfdiUsesService.findAll(filter); + res.json({ success: true, data: uses }); + } catch (error) { + next(error); + } + } + + async getCfdiUse(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const use = await cfdiUsesService.findById(req.params.id); + res.json({ success: true, data: use }); + } catch (error) { + next(error); + } + } + + async getCfdiUseByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const use = await cfdiUsesService.findByCode(req.params.code); + if (!use) { + res.status(404).json({ success: false, message: 'Uso de CFDI no encontrado' }); + return; + } + res.json({ success: true, data: use }); + } catch (error) { + next(error); + } + } + + async getCfdiUsesForPersonType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const personType = req.params.personType as PersonType; + const uses = await cfdiUsesService.findForPersonType(personType); + res.json({ success: true, data: uses }); + } catch (error) { + next(error); + } + } + + async getCfdiUsesForRegime(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const uses = await cfdiUsesService.findForRegime(req.params.regimeCode); + res.json({ success: true, data: uses }); + } catch (error) { + next(error); + } + } + + // ========== PAYMENT METHODS ========== + async getPaymentMethods(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + requiresBankInfo: req.query.requires_bank_info === 'true' ? true : undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const methods = await paymentMethodsService.findAll(filter); + res.json({ success: true, data: methods }); + } catch (error) { + next(error); + } + } + + async getPaymentMethod(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const method = await paymentMethodsService.findById(req.params.id); + res.json({ success: true, data: method }); + } catch (error) { + next(error); + } + } + + async getPaymentMethodByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const method = await paymentMethodsService.findByCode(req.params.code); + if (!method) { + res.status(404).json({ success: false, message: 'Forma de pago no encontrada' }); + return; + } + res.json({ success: true, data: method }); + } catch (error) { + next(error); + } + } + + // ========== PAYMENT TYPES ========== + async getPaymentTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + active: req.query.active === 'true' ? true : undefined, + }; + const types = await paymentTypesService.findAll(filter); + res.json({ success: true, data: types }); + } catch (error) { + next(error); + } + } + + async getPaymentType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const type = await paymentTypesService.findById(req.params.id); + res.json({ success: true, data: type }); + } catch (error) { + next(error); + } + } + + async getPaymentTypeByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const type = await paymentTypesService.findByCode(req.params.code); + if (!type) { + res.status(404).json({ success: false, message: 'Método de pago no encontrado' }); + return; + } + res.json({ success: true, data: type }); + } catch (error) { + next(error); + } + } + + // ========== WITHHOLDING TYPES ========== + async getWithholdingTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + taxCategoryId: req.query.tax_category_id as string | undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const types = await withholdingTypesService.findAll(filter); + res.json({ success: true, data: types }); + } catch (error) { + next(error); + } + } + + async getWithholdingType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const type = await withholdingTypesService.findById(req.params.id); + res.json({ success: true, data: type }); + } catch (error) { + next(error); + } + } + + async getWithholdingTypeByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const type = await withholdingTypesService.findByCode(req.params.code); + if (!type) { + res.status(404).json({ success: false, message: 'Tipo de retención no encontrado' }); + return; + } + res.json({ success: true, data: type }); + } catch (error) { + next(error); + } + } + + async getWithholdingTypesByCategory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const types = await withholdingTypesService.findByTaxCategory(req.params.categoryId); + res.json({ success: true, data: types }); + } catch (error) { + next(error); + } + } +} + +export const fiscalController = new FiscalController(); diff --git a/src/modules/fiscal/fiscal.routes.ts b/src/modules/fiscal/fiscal.routes.ts new file mode 100644 index 0000000..ac90f4e --- /dev/null +++ b/src/modules/fiscal/fiscal.routes.ts @@ -0,0 +1,45 @@ +import { Router } from 'express'; +import { fiscalController } from './fiscal.controller.js'; +import { authenticate } from '../../shared/middleware/auth.middleware.js'; + +const router = Router(); + +// All routes require authentication +router.use(authenticate); + +// ========== TAX CATEGORIES ========== +router.get('/tax-categories', (req, res, next) => fiscalController.getTaxCategories(req, res, next)); +router.get('/tax-categories/by-code/:code', (req, res, next) => fiscalController.getTaxCategoryByCode(req, res, next)); +router.get('/tax-categories/by-sat-code/:satCode', (req, res, next) => fiscalController.getTaxCategoryBySatCode(req, res, next)); +router.get('/tax-categories/:id', (req, res, next) => fiscalController.getTaxCategory(req, res, next)); + +// ========== FISCAL REGIMES ========== +router.get('/fiscal-regimes', (req, res, next) => fiscalController.getFiscalRegimes(req, res, next)); +router.get('/fiscal-regimes/by-code/:code', (req, res, next) => fiscalController.getFiscalRegimeByCode(req, res, next)); +router.get('/fiscal-regimes/person-type/:personType', (req, res, next) => fiscalController.getFiscalRegimesForPersonType(req, res, next)); +router.get('/fiscal-regimes/:id', (req, res, next) => fiscalController.getFiscalRegime(req, res, next)); + +// ========== CFDI USES ========== +router.get('/cfdi-uses', (req, res, next) => fiscalController.getCfdiUses(req, res, next)); +router.get('/cfdi-uses/by-code/:code', (req, res, next) => fiscalController.getCfdiUseByCode(req, res, next)); +router.get('/cfdi-uses/person-type/:personType', (req, res, next) => fiscalController.getCfdiUsesForPersonType(req, res, next)); +router.get('/cfdi-uses/regime/:regimeCode', (req, res, next) => fiscalController.getCfdiUsesForRegime(req, res, next)); +router.get('/cfdi-uses/:id', (req, res, next) => fiscalController.getCfdiUse(req, res, next)); + +// ========== PAYMENT METHODS (SAT Forms of Payment) ========== +router.get('/payment-methods', (req, res, next) => fiscalController.getPaymentMethods(req, res, next)); +router.get('/payment-methods/by-code/:code', (req, res, next) => fiscalController.getPaymentMethodByCode(req, res, next)); +router.get('/payment-methods/:id', (req, res, next) => fiscalController.getPaymentMethod(req, res, next)); + +// ========== PAYMENT TYPES (SAT Payment Methods - PUE/PPD) ========== +router.get('/payment-types', (req, res, next) => fiscalController.getPaymentTypes(req, res, next)); +router.get('/payment-types/by-code/:code', (req, res, next) => fiscalController.getPaymentTypeByCode(req, res, next)); +router.get('/payment-types/:id', (req, res, next) => fiscalController.getPaymentType(req, res, next)); + +// ========== WITHHOLDING TYPES ========== +router.get('/withholding-types', (req, res, next) => fiscalController.getWithholdingTypes(req, res, next)); +router.get('/withholding-types/by-code/:code', (req, res, next) => fiscalController.getWithholdingTypeByCode(req, res, next)); +router.get('/withholding-types/by-category/:categoryId', (req, res, next) => fiscalController.getWithholdingTypesByCategory(req, res, next)); +router.get('/withholding-types/:id', (req, res, next) => fiscalController.getWithholdingType(req, res, next)); + +export default router; diff --git a/src/modules/fiscal/index.ts b/src/modules/fiscal/index.ts new file mode 100644 index 0000000..ef65dc9 --- /dev/null +++ b/src/modules/fiscal/index.ts @@ -0,0 +1,4 @@ +export * from './entities/index.js'; +export * from './fiscal-catalogs.service.js'; +export { fiscalController } from './fiscal.controller.js'; +export { default as fiscalRoutes } from './fiscal.routes.js'; diff --git a/src/modules/gestion-flota/entities/operador.entity.ts b/src/modules/gestion-flota/entities/operador.entity.ts index c0efe82..dd0afd7 100644 --- a/src/modules/gestion-flota/entities/operador.entity.ts +++ b/src/modules/gestion-flota/entities/operador.entity.ts @@ -27,7 +27,9 @@ export enum TipoLicencia { */ export enum EstadoOperador { ACTIVO = 'ACTIVO', + DISPONIBLE = 'DISPONIBLE', EN_VIAJE = 'EN_VIAJE', + EN_RUTA = 'EN_RUTA', DESCANSO = 'DESCANSO', VACACIONES = 'VACACIONES', INCAPACIDAD = 'INCAPACIDAD', @@ -45,6 +47,10 @@ export class Operador { @Column({ name: 'tenant_id', type: 'uuid' }) tenantId: string; + // Sucursal + @Column({ name: 'sucursal_id', type: 'uuid', nullable: true }) + sucursalId: string; + // Identificación @Column({ name: 'numero_empleado', type: 'varchar', length: 20 }) numeroEmpleado: string; diff --git a/src/modules/gestion-flota/entities/unidad.entity.ts b/src/modules/gestion-flota/entities/unidad.entity.ts index dceac38..b2ffb71 100644 --- a/src/modules/gestion-flota/entities/unidad.entity.ts +++ b/src/modules/gestion-flota/entities/unidad.entity.ts @@ -30,6 +30,7 @@ export enum TipoUnidad { export enum EstadoUnidad { DISPONIBLE = 'DISPONIBLE', EN_VIAJE = 'EN_VIAJE', + EN_RUTA = 'EN_RUTA', EN_TALLER = 'EN_TALLER', BLOQUEADA = 'BLOQUEADA', BAJA = 'BAJA', @@ -72,10 +73,17 @@ export class Unidad { @Column({ name: 'numero_motor', type: 'varchar', length: 50, nullable: true }) numeroMotor: string; + // Sucursal + @Column({ name: 'sucursal_id', type: 'uuid', nullable: true }) + sucursalId: string; + // Placas @Column({ type: 'varchar', length: 15, nullable: true }) placa: string; + @Column({ type: 'varchar', length: 15, nullable: true }) + placas: string; + @Column({ name: 'placa_estado', type: 'varchar', length: 50, nullable: true }) placaEstado: string; diff --git a/src/modules/ordenes-transporte/entities/orden-transporte.entity.ts b/src/modules/ordenes-transporte/entities/orden-transporte.entity.ts index f0f5d96..0aafc85 100644 --- a/src/modules/ordenes-transporte/entities/orden-transporte.entity.ts +++ b/src/modules/ordenes-transporte/entities/orden-transporte.entity.ts @@ -12,16 +12,34 @@ import { /** * Estado de la Orden de Transporte */ -export enum EstadoOrden { +export enum EstadoOrdenTransporte { BORRADOR = 'BORRADOR', + PENDIENTE = 'PENDIENTE', + SOLICITADA = 'SOLICITADA', CONFIRMADA = 'CONFIRMADA', ASIGNADA = 'ASIGNADA', EN_PROCESO = 'EN_PROCESO', + EN_TRANSITO = 'EN_TRANSITO', COMPLETADA = 'COMPLETADA', + ENTREGADA = 'ENTREGADA', FACTURADA = 'FACTURADA', CANCELADA = 'CANCELADA', } +/** + * Tipo de Equipo Requerido + */ +export enum TipoEquipo { + CAJA_SECA = 'CAJA_SECA', + CAJA_REFRIGERADA = 'CAJA_REFRIGERADA', + PLATAFORMA = 'PLATAFORMA', + TANQUE = 'TANQUE', + PORTACONTENEDOR = 'PORTACONTENEDOR', + TORTON = 'TORTON', + RABON = 'RABON', + CAMIONETA = 'CAMIONETA', +} + /** * Tipo de Carga */ @@ -62,11 +80,17 @@ export class OrdenTransporte { @Column({ type: 'varchar', length: 50 }) codigo: string; + @Column({ name: 'numero_ot', type: 'varchar', length: 50, nullable: true }) + numeroOt: string; + @Column({ name: 'referencia_cliente', type: 'varchar', length: 100, nullable: true }) referenciaCliente: string; - // Cliente (Shipper) - @Column({ name: 'shipper_id', type: 'uuid' }) + // Cliente (Shipper/clienteId) + @Column({ name: 'cliente_id', type: 'uuid' }) + clienteId: string; + + @Column({ name: 'shipper_id', type: 'uuid', nullable: true }) shipperId: string; @Column({ name: 'shipper_nombre', type: 'varchar', length: 200 }) @@ -130,12 +154,19 @@ export class OrdenTransporte { destinoTelefono: string; // Fechas programadas + @Column({ name: 'fecha_recoleccion', type: 'timestamptz', nullable: true }) + fechaRecoleccion: Date; + @Column({ name: 'fecha_recoleccion_programada', type: 'timestamptz', nullable: true }) fechaRecoleccionProgramada: Date; @Column({ name: 'fecha_entrega_programada', type: 'timestamptz', nullable: true }) fechaEntregaProgramada: Date; + // Observaciones + @Column({ type: 'text', nullable: true }) + observaciones: string; + // Carga @Column({ name: 'tipo_carga', type: 'enum', enum: TipoCarga, default: TipoCarga.GENERAL }) tipoCarga: TipoCarga; @@ -203,8 +234,8 @@ export class OrdenTransporte { total: number; // Estado - @Column({ type: 'enum', enum: EstadoOrden, default: EstadoOrden.BORRADOR }) - estado: EstadoOrden; + @Column({ type: 'enum', enum: EstadoOrdenTransporte, default: EstadoOrdenTransporte.BORRADOR }) + estado: EstadoOrdenTransporte; // Asignación @Column({ name: 'viaje_id', type: 'uuid', nullable: true }) diff --git a/src/modules/ordenes-transporte/services/ordenes-transporte.service.ts b/src/modules/ordenes-transporte/services/ordenes-transporte.service.ts index 41ba7b6..7df11fd 100644 --- a/src/modules/ordenes-transporte/services/ordenes-transporte.service.ts +++ b/src/modules/ordenes-transporte/services/ordenes-transporte.service.ts @@ -291,10 +291,14 @@ export class OrdenesTransporteService { private isValidTransition(from: EstadoOrdenTransporte, to: EstadoOrdenTransporte): boolean { const transitions: Record = { + [EstadoOrdenTransporte.BORRADOR]: [EstadoOrdenTransporte.PENDIENTE, EstadoOrdenTransporte.SOLICITADA, EstadoOrdenTransporte.CANCELADA], + [EstadoOrdenTransporte.PENDIENTE]: [EstadoOrdenTransporte.SOLICITADA, EstadoOrdenTransporte.CONFIRMADA, EstadoOrdenTransporte.CANCELADA], [EstadoOrdenTransporte.SOLICITADA]: [EstadoOrdenTransporte.CONFIRMADA, EstadoOrdenTransporte.CANCELADA], [EstadoOrdenTransporte.CONFIRMADA]: [EstadoOrdenTransporte.ASIGNADA, EstadoOrdenTransporte.CANCELADA], - [EstadoOrdenTransporte.ASIGNADA]: [EstadoOrdenTransporte.EN_TRANSITO, EstadoOrdenTransporte.CANCELADA], - [EstadoOrdenTransporte.EN_TRANSITO]: [EstadoOrdenTransporte.ENTREGADA], + [EstadoOrdenTransporte.ASIGNADA]: [EstadoOrdenTransporte.EN_PROCESO, EstadoOrdenTransporte.EN_TRANSITO, EstadoOrdenTransporte.CANCELADA], + [EstadoOrdenTransporte.EN_PROCESO]: [EstadoOrdenTransporte.EN_TRANSITO, EstadoOrdenTransporte.CANCELADA], + [EstadoOrdenTransporte.EN_TRANSITO]: [EstadoOrdenTransporte.COMPLETADA, EstadoOrdenTransporte.ENTREGADA], + [EstadoOrdenTransporte.COMPLETADA]: [EstadoOrdenTransporte.ENTREGADA], [EstadoOrdenTransporte.ENTREGADA]: [EstadoOrdenTransporte.FACTURADA], [EstadoOrdenTransporte.FACTURADA]: [], [EstadoOrdenTransporte.CANCELADA]: [], diff --git a/src/modules/tracking/entities/evento-tracking.entity.ts b/src/modules/tracking/entities/evento-tracking.entity.ts index f2091b2..c649085 100644 --- a/src/modules/tracking/entities/evento-tracking.entity.ts +++ b/src/modules/tracking/entities/evento-tracking.entity.ts @@ -9,7 +9,8 @@ import { /** * Tipo de Evento de Tracking */ -export enum TipoEvento { +export enum TipoEventoTracking { + POSICION = 'POSICION', SALIDA = 'SALIDA', ARRIBO_ORIGEN = 'ARRIBO_ORIGEN', INICIO_CARGA = 'INICIO_CARGA', @@ -22,6 +23,8 @@ export enum TipoEvento { PARADA = 'PARADA', INCIDENTE = 'INCIDENTE', GPS_POSICION = 'GPS_POSICION', + GEOCERCA_ENTRADA = 'GEOCERCA_ENTRADA', + GEOCERCA_SALIDA = 'GEOCERCA_SALIDA', } /** @@ -47,12 +50,20 @@ export class EventoTracking { tenantId: string; // Viaje - @Column({ name: 'viaje_id', type: 'uuid' }) + @Column({ name: 'viaje_id', type: 'uuid', nullable: true }) viajeId: string; + // Unidad + @Column({ name: 'unidad_id', type: 'uuid', nullable: true }) + unidadId: string; + + // Operador + @Column({ name: 'operador_id', type: 'uuid', nullable: true }) + operadorId: string; + // Tipo y fuente - @Column({ name: 'tipo_evento', type: 'enum', enum: TipoEvento }) - tipoEvento: TipoEvento; + @Column({ name: 'tipo_evento', type: 'enum', enum: TipoEventoTracking }) + tipoEvento: TipoEventoTracking; @Column({ type: 'enum', enum: FuenteEvento }) fuente: FuenteEvento; @@ -68,12 +79,43 @@ export class EventoTracking { direccion: string; // Timestamp - @Column({ name: 'timestamp_evento', type: 'timestamptz' }) + @Column({ name: 'timestamp', type: 'timestamptz' }) + timestamp: Date; + + @Column({ name: 'timestamp_evento', type: 'timestamptz', nullable: true }) timestampEvento: Date; @CreateDateColumn({ name: 'timestamp_registro', type: 'timestamptz' }) timestampRegistro: Date; + // Datos GPS adicionales + @Column({ type: 'decimal', precision: 6, scale: 2, nullable: true }) + velocidad: number; + + @Column({ type: 'decimal', precision: 5, scale: 2, nullable: true }) + rumbo: number; + + @Column({ type: 'decimal', precision: 8, scale: 2, nullable: true }) + altitud: number; + + @Column({ type: 'decimal', precision: 6, scale: 2, nullable: true }) + precision: number; + + @Column({ type: 'int', nullable: true }) + odometro: number; + + @Column({ name: 'nivel_combustible', type: 'decimal', precision: 5, scale: 2, nullable: true }) + nivelCombustible: number; + + @Column({ name: 'motor_encendido', type: 'boolean', nullable: true }) + motorEncendido: boolean; + + @Column({ type: 'text', nullable: true }) + descripcion: string; + + @Column({ name: 'datos_adicionales', type: 'jsonb', nullable: true }) + datosAdicionales: Record; + // Datos específicos del evento @Column({ type: 'jsonb', nullable: true }) datos: Record; diff --git a/src/modules/tracking/entities/geocerca.entity.ts b/src/modules/tracking/entities/geocerca.entity.ts index f7c8cac..37345ca 100644 --- a/src/modules/tracking/entities/geocerca.entity.ts +++ b/src/modules/tracking/entities/geocerca.entity.ts @@ -11,6 +11,8 @@ import { * Tipo de Geocerca */ export enum TipoGeocerca { + CIRCULAR = 'CIRCULAR', + POLIGONAL = 'POLIGONAL', CLIENTE = 'CLIENTE', PROVEEDOR = 'PROVEEDOR', PATIO = 'PATIO', @@ -55,10 +57,17 @@ export class Geocerca { @Column({ name: 'radio_metros', type: 'decimal', precision: 10, scale: 2, nullable: true }) radioMetros: number; + @Column({ type: 'decimal', precision: 10, scale: 2, nullable: true }) + radio: number; + // Para geocerca poligonal (GeoJSON como string) @Column({ type: 'text', nullable: true }) poligono: string; + // GeoJSON geometry + @Column({ type: 'jsonb', nullable: true }) + geometria: Record; + // Asociación @Column({ name: 'cliente_id', type: 'uuid', nullable: true }) clienteId: string; diff --git a/src/modules/viajes/entities/viaje.entity.ts b/src/modules/viajes/entities/viaje.entity.ts index c4806d3..e88a160 100644 --- a/src/modules/viajes/entities/viaje.entity.ts +++ b/src/modules/viajes/entities/viaje.entity.ts @@ -54,6 +54,13 @@ export class Viaje { @Column({ type: 'varchar', length: 50 }) codigo: string; + @Column({ name: 'numero_viaje', type: 'varchar', length: 50, nullable: true }) + numeroViaje: string; + + // Cliente + @Column({ name: 'cliente_id', type: 'uuid', nullable: true }) + clienteId: string; + // Unidad y operador (referencias a fleet schema) @Column({ name: 'unidad_id', type: 'uuid' }) unidadId: string; @@ -68,9 +75,15 @@ export class Viaje { @Column({ name: 'origen_principal', type: 'varchar', length: 200, nullable: true }) origenPrincipal: string; + @Column({ name: 'origen_ciudad', type: 'varchar', length: 100, nullable: true }) + origenCiudad: string; + @Column({ name: 'destino_principal', type: 'varchar', length: 200, nullable: true }) destinoPrincipal: string; + @Column({ name: 'destino_ciudad', type: 'varchar', length: 100, nullable: true }) + destinoCiudad: string; + @Column({ name: 'distancia_estimada_km', type: 'decimal', precision: 10, scale: 2, nullable: true }) distanciaEstimadaKm: number; @@ -81,6 +94,9 @@ export class Viaje { @Column({ name: 'fecha_salida_programada', type: 'timestamptz', nullable: true }) fechaSalidaProgramada: Date; + @Column({ name: 'fecha_programada_salida', type: 'timestamptz', nullable: true }) + fechaProgramadaSalida: Date; + @Column({ name: 'fecha_llegada_programada', type: 'timestamptz', nullable: true }) fechaLlegadaProgramada: Date; @@ -88,9 +104,15 @@ export class Viaje { @Column({ name: 'fecha_salida_real', type: 'timestamptz', nullable: true }) fechaSalidaReal: Date; + @Column({ name: 'fecha_real_salida', type: 'timestamptz', nullable: true }) + fechaRealSalida: Date; + @Column({ name: 'fecha_llegada_real', type: 'timestamptz', nullable: true }) fechaLlegadaReal: Date; + @Column({ name: 'fecha_real_llegada', type: 'timestamptz', nullable: true }) + fechaRealLlegada: Date; + // Kilometraje @Column({ name: 'km_inicio', type: 'int', nullable: true }) kmInicio: number;