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:
parent
ffc82bf99e
commit
06d79e1c52
103
src/app.ts
Normal file
103
src/app.ts
Normal 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;
|
||||
47
src/modules/fiscal/entities/cfdi-use.entity.ts
Normal file
47
src/modules/fiscal/entities/cfdi-use.entity.ts
Normal 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;
|
||||
}
|
||||
49
src/modules/fiscal/entities/fiscal-regime.entity.ts
Normal file
49
src/modules/fiscal/entities/fiscal-regime.entity.ts
Normal 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;
|
||||
}
|
||||
6
src/modules/fiscal/entities/index.ts
Normal file
6
src/modules/fiscal/entities/index.ts
Normal 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';
|
||||
36
src/modules/fiscal/entities/payment-method.entity.ts
Normal file
36
src/modules/fiscal/entities/payment-method.entity.ts
Normal 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;
|
||||
}
|
||||
33
src/modules/fiscal/entities/payment-type.entity.ts
Normal file
33
src/modules/fiscal/entities/payment-type.entity.ts
Normal 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;
|
||||
}
|
||||
52
src/modules/fiscal/entities/tax-category.entity.ts
Normal file
52
src/modules/fiscal/entities/tax-category.entity.ts
Normal 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;
|
||||
}
|
||||
54
src/modules/fiscal/entities/withholding-type.entity.ts
Normal file
54
src/modules/fiscal/entities/withholding-type.entity.ts
Normal 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;
|
||||
}
|
||||
426
src/modules/fiscal/fiscal-catalogs.service.ts
Normal file
426
src/modules/fiscal/fiscal-catalogs.service.ts
Normal 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();
|
||||
281
src/modules/fiscal/fiscal.controller.ts
Normal file
281
src/modules/fiscal/fiscal.controller.ts
Normal 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();
|
||||
45
src/modules/fiscal/fiscal.routes.ts
Normal file
45
src/modules/fiscal/fiscal.routes.ts
Normal 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;
|
||||
4
src/modules/fiscal/index.ts
Normal file
4
src/modules/fiscal/index.ts
Normal 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';
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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]: [],
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user