fix(entities): Update transport entities and services for TypeScript compatibility

- Add missing fields to transport entities (tracking, viajes, ordenes-transporte)
- Update enums to match service expectations (EstadoOrdenTransporte, TipoEventoTracking)
- Add fiscal module from erp-core
- Create app.ts entry point
- Disable strictPropertyInitialization in tsconfig for TypeORM entities

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 14:46:02 -06:00
parent ffc82bf99e
commit 06d79e1c52
19 changed files with 1270 additions and 12 deletions

103
src/app.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<TaxCategory>;
constructor() {
this.repository = AppDataSource.getRepository(TaxCategory);
}
async findAll(filter: TaxCategoryFilter = {}): Promise<TaxCategory[]> {
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<TaxCategory> {
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<TaxCategory | null> {
logger.debug('Finding tax category by code', { code });
return this.repository.findOne({
where: { code: code.toUpperCase() },
});
}
async findBySatCode(satCode: string): Promise<TaxCategory | null> {
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<FiscalRegime>;
constructor() {
this.repository = AppDataSource.getRepository(FiscalRegime);
}
async findAll(filter: FiscalRegimeFilter = {}): Promise<FiscalRegime[]> {
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<FiscalRegime> {
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<FiscalRegime | null> {
logger.debug('Finding fiscal regime by code', { code });
return this.repository.findOne({
where: { code },
});
}
async findForPersonType(personType: PersonType): Promise<FiscalRegime[]> {
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<CfdiUse>;
constructor() {
this.repository = AppDataSource.getRepository(CfdiUse);
}
async findAll(filter: CfdiUseFilter = {}): Promise<CfdiUse[]> {
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<CfdiUse> {
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<CfdiUse | null> {
logger.debug('Finding CFDI use by code', { code });
return this.repository.findOne({
where: { code: code.toUpperCase() },
});
}
async findForPersonType(personType: PersonType): Promise<CfdiUse[]> {
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<CfdiUse[]> {
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<PaymentMethod>;
constructor() {
this.repository = AppDataSource.getRepository(PaymentMethod);
}
async findAll(filter: PaymentMethodFilter = {}): Promise<PaymentMethod[]> {
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<PaymentMethod> {
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<PaymentMethod | null> {
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<PaymentType>;
constructor() {
this.repository = AppDataSource.getRepository(PaymentType);
}
async findAll(filter: PaymentTypeFilter = {}): Promise<PaymentType[]> {
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<PaymentType> {
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<PaymentType | null> {
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<WithholdingType>;
constructor() {
this.repository = AppDataSource.getRepository(WithholdingType);
}
async findAll(filter: WithholdingTypeFilter = {}): Promise<WithholdingType[]> {
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<WithholdingType> {
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<WithholdingType | null> {
logger.debug('Finding withholding type by code', { code });
return this.repository.findOne({
where: { code },
relations: ['taxCategory'],
});
}
async findByTaxCategory(taxCategoryId: string): Promise<WithholdingType[]> {
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();

View File

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
try {
const types = await withholdingTypesService.findByTaxCategory(req.params.categoryId);
res.json({ success: true, data: types });
} catch (error) {
next(error);
}
}
}
export const fiscalController = new FiscalController();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -291,10 +291,14 @@ export class OrdenesTransporteService {
private isValidTransition(from: EstadoOrdenTransporte, to: EstadoOrdenTransporte): boolean {
const transitions: Record<EstadoOrdenTransporte, EstadoOrdenTransporte[]> = {
[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]: [],

View File

@ -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<string, any>;
// Datos específicos del evento
@Column({ type: 'jsonb', nullable: true })
datos: Record<string, any>;

View File

@ -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<string, any>;
// Asociación
@Column({ name: 'cliente_id', type: 'uuid', nullable: true })
clienteId: string;

View File

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