From 5fa451e09f1577478a1916b78b3f7e6d3ee37ecb Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Sun, 18 Jan 2026 09:31:47 -0600 Subject: [PATCH] feat(fiscal): Add fiscal module with catalogs (MGN-005) - Add fiscal entities: - TaxCategory, FiscalRegime, CfdiUse - PaymentMethod, PaymentType, WithholdingType - Add fiscal services with filtering capabilities - Add fiscal controller with REST endpoints - Add fiscal routes at /api/v1/fiscal - Register fiscal routes in app.ts Endpoints: - GET /fiscal/tax-categories - GET /fiscal/fiscal-regimes - GET /fiscal/cfdi-uses - GET /fiscal/payment-methods - GET /fiscal/payment-types - GET /fiscal/withholding-types Co-Authored-By: Claude Opus 4.5 --- src/app.ts | 2 + .../fiscal/entities/cfdi-use.entity.ts | 47 ++ .../fiscal/entities/fiscal-regime.entity.ts | 49 ++ src/modules/fiscal/entities/index.ts | 6 + .../fiscal/entities/payment-method.entity.ts | 36 ++ .../fiscal/entities/payment-type.entity.ts | 33 ++ .../fiscal/entities/tax-category.entity.ts | 52 +++ .../entities/withholding-type.entity.ts | 54 +++ src/modules/fiscal/fiscal-catalogs.service.ts | 426 ++++++++++++++++++ src/modules/fiscal/fiscal.controller.ts | 281 ++++++++++++ src/modules/fiscal/fiscal.routes.ts | 45 ++ src/modules/fiscal/index.ts | 4 + 12 files changed, 1035 insertions(+) create mode 100644 src/modules/fiscal/entities/cfdi-use.entity.ts create mode 100644 src/modules/fiscal/entities/fiscal-regime.entity.ts create mode 100644 src/modules/fiscal/entities/index.ts create mode 100644 src/modules/fiscal/entities/payment-method.entity.ts create mode 100644 src/modules/fiscal/entities/payment-type.entity.ts create mode 100644 src/modules/fiscal/entities/tax-category.entity.ts create mode 100644 src/modules/fiscal/entities/withholding-type.entity.ts create mode 100644 src/modules/fiscal/fiscal-catalogs.service.ts create mode 100644 src/modules/fiscal/fiscal.controller.ts create mode 100644 src/modules/fiscal/fiscal.routes.ts create mode 100644 src/modules/fiscal/index.ts diff --git a/src/app.ts b/src/app.ts index 7abff0a..4e9b568 100644 --- a/src/app.ts +++ b/src/app.ts @@ -27,6 +27,7 @@ import reportsRoutes from './modules/reports/reports.routes.js'; import invoicesRoutes from './modules/invoices/invoices.routes.js'; import productsRoutes from './modules/products/products.routes.js'; import warehousesRoutes from './modules/warehouses/warehouses.routes.js'; +import fiscalRoutes from './modules/fiscal/fiscal.routes.js'; const app: Application = express(); @@ -79,6 +80,7 @@ app.use(`${apiPrefix}/reports`, reportsRoutes); app.use(`${apiPrefix}/invoices`, invoicesRoutes); app.use(`${apiPrefix}/products`, productsRoutes); app.use(`${apiPrefix}/warehouses`, warehousesRoutes); +app.use(`${apiPrefix}/fiscal`, fiscalRoutes); // 404 handler app.use((_req: Request, res: Response) => { diff --git a/src/modules/fiscal/entities/cfdi-use.entity.ts b/src/modules/fiscal/entities/cfdi-use.entity.ts new file mode 100644 index 0000000..6b09b0f --- /dev/null +++ b/src/modules/fiscal/entities/cfdi-use.entity.ts @@ -0,0 +1,47 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; +import { PersonType } from './fiscal-regime.entity.js'; + +@Entity({ schema: 'fiscal', name: 'cfdi_uses' }) +@Index('idx_cfdi_uses_code', ['code'], { unique: true }) +@Index('idx_cfdi_uses_applies', ['appliesTo']) +export class CfdiUse { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 10, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ + type: 'enum', + enum: PersonType, + nullable: false, + default: PersonType.BOTH, + name: 'applies_to', + }) + appliesTo: PersonType; + + @Column({ type: 'simple-array', nullable: true, name: 'allowed_regimes' }) + allowedRegimes: string[] | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/fiscal-regime.entity.ts b/src/modules/fiscal/entities/fiscal-regime.entity.ts new file mode 100644 index 0000000..b24ee36 --- /dev/null +++ b/src/modules/fiscal/entities/fiscal-regime.entity.ts @@ -0,0 +1,49 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +export enum PersonType { + NATURAL = 'natural', // Persona fisica + LEGAL = 'legal', // Persona moral + BOTH = 'both', +} + +@Entity({ schema: 'fiscal', name: 'fiscal_regimes' }) +@Index('idx_fiscal_regimes_code', ['code'], { unique: true }) +@Index('idx_fiscal_regimes_applies', ['appliesTo']) +export class FiscalRegime { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 10, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 255, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ + type: 'enum', + enum: PersonType, + nullable: false, + default: PersonType.BOTH, + name: 'applies_to', + }) + appliesTo: PersonType; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/index.ts b/src/modules/fiscal/entities/index.ts new file mode 100644 index 0000000..0c2a75b --- /dev/null +++ b/src/modules/fiscal/entities/index.ts @@ -0,0 +1,6 @@ +export * from './tax-category.entity.js'; +export * from './fiscal-regime.entity.js'; +export * from './cfdi-use.entity.js'; +export * from './payment-method.entity.js'; +export * from './payment-type.entity.js'; +export * from './withholding-type.entity.js'; diff --git a/src/modules/fiscal/entities/payment-method.entity.ts b/src/modules/fiscal/entities/payment-method.entity.ts new file mode 100644 index 0000000..d9fa946 --- /dev/null +++ b/src/modules/fiscal/entities/payment-method.entity.ts @@ -0,0 +1,36 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +@Entity({ schema: 'fiscal', name: 'payment_methods' }) +@Index('idx_payment_methods_code', ['code'], { unique: true }) +export class PaymentMethod { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 10, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'boolean', default: false, nullable: false, name: 'requires_bank_info' }) + requiresBankInfo: boolean; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/payment-type.entity.ts b/src/modules/fiscal/entities/payment-type.entity.ts new file mode 100644 index 0000000..31e9fbc --- /dev/null +++ b/src/modules/fiscal/entities/payment-type.entity.ts @@ -0,0 +1,33 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +@Entity({ schema: 'fiscal', name: 'payment_types' }) +@Index('idx_payment_types_code', ['code'], { unique: true }) +export class PaymentType { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 10, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/tax-category.entity.ts b/src/modules/fiscal/entities/tax-category.entity.ts new file mode 100644 index 0000000..9841c68 --- /dev/null +++ b/src/modules/fiscal/entities/tax-category.entity.ts @@ -0,0 +1,52 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +export enum TaxNature { + TAX = 'tax', + WITHHOLDING = 'withholding', + BOTH = 'both', +} + +@Entity({ schema: 'fiscal', name: 'tax_categories' }) +@Index('idx_tax_categories_code', ['code'], { unique: true }) +@Index('idx_tax_categories_sat', ['satCode']) +export class TaxCategory { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 20, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ + type: 'enum', + enum: TaxNature, + nullable: false, + default: TaxNature.TAX, + name: 'tax_nature', + }) + taxNature: TaxNature; + + @Column({ type: 'varchar', length: 10, nullable: true, name: 'sat_code' }) + satCode: string | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/entities/withholding-type.entity.ts b/src/modules/fiscal/entities/withholding-type.entity.ts new file mode 100644 index 0000000..7305544 --- /dev/null +++ b/src/modules/fiscal/entities/withholding-type.entity.ts @@ -0,0 +1,54 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { TaxCategory } from './tax-category.entity.js'; + +@Entity({ schema: 'fiscal', name: 'withholding_types' }) +@Index('idx_withholding_types_code', ['code'], { unique: true }) +@Index('idx_withholding_types_category', ['taxCategoryId']) +export class WithholdingType { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 20, nullable: false, unique: true }) + code: string; + + @Column({ type: 'varchar', length: 100, nullable: false }) + name: string; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ + type: 'decimal', + precision: 5, + scale: 2, + nullable: false, + default: 0, + name: 'default_rate', + }) + defaultRate: number; + + @Column({ type: 'uuid', nullable: true, name: 'tax_category_id' }) + taxCategoryId: string | null; + + @ManyToOne(() => TaxCategory, { nullable: true }) + @JoinColumn({ name: 'tax_category_id' }) + taxCategory: TaxCategory | null; + + @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' }) + isActive: boolean; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/src/modules/fiscal/fiscal-catalogs.service.ts b/src/modules/fiscal/fiscal-catalogs.service.ts new file mode 100644 index 0000000..cc37e4a --- /dev/null +++ b/src/modules/fiscal/fiscal-catalogs.service.ts @@ -0,0 +1,426 @@ +import { Repository } from 'typeorm'; +import { AppDataSource } from '../../config/typeorm.js'; +import { + TaxCategory, + FiscalRegime, + CfdiUse, + PaymentMethod, + PaymentType, + WithholdingType, + PersonType, +} from './entities/index.js'; +import { NotFoundError } from '../../shared/errors/index.js'; +import { logger } from '../../shared/utils/logger.js'; + +// ========================================== +// TAX CATEGORIES SERVICE +// ========================================== + +export interface TaxCategoryFilter { + taxNature?: string; + active?: boolean; +} + +class TaxCategoriesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(TaxCategory); + } + + async findAll(filter: TaxCategoryFilter = {}): Promise { + logger.debug('Finding all tax categories', { filter }); + + const qb = this.repository.createQueryBuilder('tc'); + + if (filter.taxNature) { + qb.andWhere('tc.tax_nature = :taxNature', { taxNature: filter.taxNature }); + } + + if (filter.active !== undefined) { + qb.andWhere('tc.is_active = :active', { active: filter.active }); + } + + qb.orderBy('tc.name', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding tax category by id', { id }); + + const category = await this.repository.findOne({ where: { id } }); + + if (!category) { + throw new NotFoundError('Categoría de impuesto no encontrada'); + } + + return category; + } + + async findByCode(code: string): Promise { + logger.debug('Finding tax category by code', { code }); + + return this.repository.findOne({ + where: { code: code.toUpperCase() }, + }); + } + + async findBySatCode(satCode: string): Promise { + logger.debug('Finding tax category by SAT code', { satCode }); + + return this.repository.findOne({ + where: { satCode }, + }); + } +} + +// ========================================== +// FISCAL REGIMES SERVICE +// ========================================== + +export interface FiscalRegimeFilter { + appliesTo?: PersonType; + active?: boolean; +} + +class FiscalRegimesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(FiscalRegime); + } + + async findAll(filter: FiscalRegimeFilter = {}): Promise { + logger.debug('Finding all fiscal regimes', { filter }); + + const qb = this.repository.createQueryBuilder('fr'); + + if (filter.appliesTo) { + qb.andWhere('(fr.applies_to = :appliesTo OR fr.applies_to = :both)', { + appliesTo: filter.appliesTo, + both: PersonType.BOTH, + }); + } + + if (filter.active !== undefined) { + qb.andWhere('fr.is_active = :active', { active: filter.active }); + } + + qb.orderBy('fr.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding fiscal regime by id', { id }); + + const regime = await this.repository.findOne({ where: { id } }); + + if (!regime) { + throw new NotFoundError('Régimen fiscal no encontrado'); + } + + return regime; + } + + async findByCode(code: string): Promise { + logger.debug('Finding fiscal regime by code', { code }); + + return this.repository.findOne({ + where: { code }, + }); + } + + async findForPersonType(personType: PersonType): Promise { + logger.debug('Finding fiscal regimes for person type', { personType }); + + return this.repository + .createQueryBuilder('fr') + .where('fr.applies_to = :personType OR fr.applies_to = :both', { + personType, + both: PersonType.BOTH, + }) + .andWhere('fr.is_active = true') + .orderBy('fr.code', 'ASC') + .getMany(); + } +} + +// ========================================== +// CFDI USES SERVICE +// ========================================== + +export interface CfdiUseFilter { + appliesTo?: PersonType; + active?: boolean; +} + +class CfdiUsesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(CfdiUse); + } + + async findAll(filter: CfdiUseFilter = {}): Promise { + logger.debug('Finding all CFDI uses', { filter }); + + const qb = this.repository.createQueryBuilder('cu'); + + if (filter.appliesTo) { + qb.andWhere('(cu.applies_to = :appliesTo OR cu.applies_to = :both)', { + appliesTo: filter.appliesTo, + both: PersonType.BOTH, + }); + } + + if (filter.active !== undefined) { + qb.andWhere('cu.is_active = :active', { active: filter.active }); + } + + qb.orderBy('cu.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding CFDI use by id', { id }); + + const cfdiUse = await this.repository.findOne({ where: { id } }); + + if (!cfdiUse) { + throw new NotFoundError('Uso de CFDI no encontrado'); + } + + return cfdiUse; + } + + async findByCode(code: string): Promise { + logger.debug('Finding CFDI use by code', { code }); + + return this.repository.findOne({ + where: { code: code.toUpperCase() }, + }); + } + + async findForPersonType(personType: PersonType): Promise { + logger.debug('Finding CFDI uses for person type', { personType }); + + return this.repository + .createQueryBuilder('cu') + .where('cu.applies_to = :personType OR cu.applies_to = :both', { + personType, + both: PersonType.BOTH, + }) + .andWhere('cu.is_active = true') + .orderBy('cu.code', 'ASC') + .getMany(); + } + + async findForRegime(regimeCode: string): Promise { + logger.debug('Finding CFDI uses for regime', { regimeCode }); + + // Get all active CFDI uses and filter by allowed regimes + const all = await this.repository + .createQueryBuilder('cu') + .where('cu.is_active = true') + .orderBy('cu.code', 'ASC') + .getMany(); + + return all.filter( + (cu) => !cu.allowedRegimes || cu.allowedRegimes.length === 0 || cu.allowedRegimes.includes(regimeCode) + ); + } +} + +// ========================================== +// PAYMENT METHODS SERVICE +// ========================================== + +export interface PaymentMethodFilter { + requiresBankInfo?: boolean; + active?: boolean; +} + +class PaymentMethodsService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(PaymentMethod); + } + + async findAll(filter: PaymentMethodFilter = {}): Promise { + logger.debug('Finding all payment methods', { filter }); + + const qb = this.repository.createQueryBuilder('pm'); + + if (filter.requiresBankInfo !== undefined) { + qb.andWhere('pm.requires_bank_info = :requiresBankInfo', { + requiresBankInfo: filter.requiresBankInfo, + }); + } + + if (filter.active !== undefined) { + qb.andWhere('pm.is_active = :active', { active: filter.active }); + } + + qb.orderBy('pm.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding payment method by id', { id }); + + const method = await this.repository.findOne({ where: { id } }); + + if (!method) { + throw new NotFoundError('Forma de pago no encontrada'); + } + + return method; + } + + async findByCode(code: string): Promise { + logger.debug('Finding payment method by code', { code }); + + return this.repository.findOne({ + where: { code }, + }); + } +} + +// ========================================== +// PAYMENT TYPES SERVICE +// ========================================== + +export interface PaymentTypeFilter { + active?: boolean; +} + +class PaymentTypesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(PaymentType); + } + + async findAll(filter: PaymentTypeFilter = {}): Promise { + logger.debug('Finding all payment types', { filter }); + + const qb = this.repository.createQueryBuilder('pt'); + + if (filter.active !== undefined) { + qb.andWhere('pt.is_active = :active', { active: filter.active }); + } + + qb.orderBy('pt.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding payment type by id', { id }); + + const type = await this.repository.findOne({ where: { id } }); + + if (!type) { + throw new NotFoundError('Método de pago no encontrado'); + } + + return type; + } + + async findByCode(code: string): Promise { + logger.debug('Finding payment type by code', { code }); + + return this.repository.findOne({ + where: { code: code.toUpperCase() }, + }); + } +} + +// ========================================== +// WITHHOLDING TYPES SERVICE +// ========================================== + +export interface WithholdingTypeFilter { + taxCategoryId?: string; + active?: boolean; +} + +class WithholdingTypesService { + private repository: Repository; + + constructor() { + this.repository = AppDataSource.getRepository(WithholdingType); + } + + async findAll(filter: WithholdingTypeFilter = {}): Promise { + logger.debug('Finding all withholding types', { filter }); + + const qb = this.repository + .createQueryBuilder('wt') + .leftJoinAndSelect('wt.taxCategory', 'taxCategory'); + + if (filter.taxCategoryId) { + qb.andWhere('wt.tax_category_id = :taxCategoryId', { + taxCategoryId: filter.taxCategoryId, + }); + } + + if (filter.active !== undefined) { + qb.andWhere('wt.is_active = :active', { active: filter.active }); + } + + qb.orderBy('wt.code', 'ASC'); + + return qb.getMany(); + } + + async findById(id: string): Promise { + logger.debug('Finding withholding type by id', { id }); + + const type = await this.repository.findOne({ + where: { id }, + relations: ['taxCategory'], + }); + + if (!type) { + throw new NotFoundError('Tipo de retención no encontrado'); + } + + return type; + } + + async findByCode(code: string): Promise { + logger.debug('Finding withholding type by code', { code }); + + return this.repository.findOne({ + where: { code }, + relations: ['taxCategory'], + }); + } + + async findByTaxCategory(taxCategoryId: string): Promise { + logger.debug('Finding withholding types by tax category', { taxCategoryId }); + + return this.repository.find({ + where: { taxCategoryId, isActive: true }, + relations: ['taxCategory'], + order: { code: 'ASC' }, + }); + } +} + +// ========================================== +// SERVICE EXPORTS +// ========================================== + +export const taxCategoriesService = new TaxCategoriesService(); +export const fiscalRegimesService = new FiscalRegimesService(); +export const cfdiUsesService = new CfdiUsesService(); +export const paymentMethodsService = new PaymentMethodsService(); +export const paymentTypesService = new PaymentTypesService(); +export const withholdingTypesService = new WithholdingTypesService(); diff --git a/src/modules/fiscal/fiscal.controller.ts b/src/modules/fiscal/fiscal.controller.ts new file mode 100644 index 0000000..a03d6ff --- /dev/null +++ b/src/modules/fiscal/fiscal.controller.ts @@ -0,0 +1,281 @@ +import { Response, NextFunction } from 'express'; +import { + taxCategoriesService, + fiscalRegimesService, + cfdiUsesService, + paymentMethodsService, + paymentTypesService, + withholdingTypesService, +} from './fiscal-catalogs.service.js'; +import { PersonType } from './entities/fiscal-regime.entity.js'; +import { AuthenticatedRequest } from '../../shared/middleware/auth.middleware.js'; + +class FiscalController { + // ========== TAX CATEGORIES ========== + async getTaxCategories(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + taxNature: req.query.tax_nature as string | undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const categories = await taxCategoriesService.findAll(filter); + res.json({ success: true, data: categories }); + } catch (error) { + next(error); + } + } + + async getTaxCategory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const category = await taxCategoriesService.findById(req.params.id); + res.json({ success: true, data: category }); + } catch (error) { + next(error); + } + } + + async getTaxCategoryByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const category = await taxCategoriesService.findByCode(req.params.code); + if (!category) { + res.status(404).json({ success: false, message: 'Categoría de impuesto no encontrada' }); + return; + } + res.json({ success: true, data: category }); + } catch (error) { + next(error); + } + } + + async getTaxCategoryBySatCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const category = await taxCategoriesService.findBySatCode(req.params.satCode); + if (!category) { + res.status(404).json({ success: false, message: 'Categoría de impuesto no encontrada' }); + return; + } + res.json({ success: true, data: category }); + } catch (error) { + next(error); + } + } + + // ========== FISCAL REGIMES ========== + async getFiscalRegimes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + appliesTo: req.query.applies_to as PersonType | undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const regimes = await fiscalRegimesService.findAll(filter); + res.json({ success: true, data: regimes }); + } catch (error) { + next(error); + } + } + + async getFiscalRegime(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const regime = await fiscalRegimesService.findById(req.params.id); + res.json({ success: true, data: regime }); + } catch (error) { + next(error); + } + } + + async getFiscalRegimeByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const regime = await fiscalRegimesService.findByCode(req.params.code); + if (!regime) { + res.status(404).json({ success: false, message: 'Régimen fiscal no encontrado' }); + return; + } + res.json({ success: true, data: regime }); + } catch (error) { + next(error); + } + } + + async getFiscalRegimesForPersonType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const personType = req.params.personType as PersonType; + const regimes = await fiscalRegimesService.findForPersonType(personType); + res.json({ success: true, data: regimes }); + } catch (error) { + next(error); + } + } + + // ========== CFDI USES ========== + async getCfdiUses(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + appliesTo: req.query.applies_to as PersonType | undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const uses = await cfdiUsesService.findAll(filter); + res.json({ success: true, data: uses }); + } catch (error) { + next(error); + } + } + + async getCfdiUse(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const use = await cfdiUsesService.findById(req.params.id); + res.json({ success: true, data: use }); + } catch (error) { + next(error); + } + } + + async getCfdiUseByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const use = await cfdiUsesService.findByCode(req.params.code); + if (!use) { + res.status(404).json({ success: false, message: 'Uso de CFDI no encontrado' }); + return; + } + res.json({ success: true, data: use }); + } catch (error) { + next(error); + } + } + + async getCfdiUsesForPersonType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const personType = req.params.personType as PersonType; + const uses = await cfdiUsesService.findForPersonType(personType); + res.json({ success: true, data: uses }); + } catch (error) { + next(error); + } + } + + async getCfdiUsesForRegime(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const uses = await cfdiUsesService.findForRegime(req.params.regimeCode); + res.json({ success: true, data: uses }); + } catch (error) { + next(error); + } + } + + // ========== PAYMENT METHODS ========== + async getPaymentMethods(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + requiresBankInfo: req.query.requires_bank_info === 'true' ? true : undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const methods = await paymentMethodsService.findAll(filter); + res.json({ success: true, data: methods }); + } catch (error) { + next(error); + } + } + + async getPaymentMethod(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const method = await paymentMethodsService.findById(req.params.id); + res.json({ success: true, data: method }); + } catch (error) { + next(error); + } + } + + async getPaymentMethodByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const method = await paymentMethodsService.findByCode(req.params.code); + if (!method) { + res.status(404).json({ success: false, message: 'Forma de pago no encontrada' }); + return; + } + res.json({ success: true, data: method }); + } catch (error) { + next(error); + } + } + + // ========== PAYMENT TYPES ========== + async getPaymentTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + active: req.query.active === 'true' ? true : undefined, + }; + const types = await paymentTypesService.findAll(filter); + res.json({ success: true, data: types }); + } catch (error) { + next(error); + } + } + + async getPaymentType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const type = await paymentTypesService.findById(req.params.id); + res.json({ success: true, data: type }); + } catch (error) { + next(error); + } + } + + async getPaymentTypeByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const type = await paymentTypesService.findByCode(req.params.code); + if (!type) { + res.status(404).json({ success: false, message: 'Método de pago no encontrado' }); + return; + } + res.json({ success: true, data: type }); + } catch (error) { + next(error); + } + } + + // ========== WITHHOLDING TYPES ========== + async getWithholdingTypes(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const filter = { + taxCategoryId: req.query.tax_category_id as string | undefined, + active: req.query.active === 'true' ? true : undefined, + }; + const types = await withholdingTypesService.findAll(filter); + res.json({ success: true, data: types }); + } catch (error) { + next(error); + } + } + + async getWithholdingType(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const type = await withholdingTypesService.findById(req.params.id); + res.json({ success: true, data: type }); + } catch (error) { + next(error); + } + } + + async getWithholdingTypeByCode(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const type = await withholdingTypesService.findByCode(req.params.code); + if (!type) { + res.status(404).json({ success: false, message: 'Tipo de retención no encontrado' }); + return; + } + res.json({ success: true, data: type }); + } catch (error) { + next(error); + } + } + + async getWithholdingTypesByCategory(req: AuthenticatedRequest, res: Response, next: NextFunction): Promise { + try { + const types = await withholdingTypesService.findByTaxCategory(req.params.categoryId); + res.json({ success: true, data: types }); + } catch (error) { + next(error); + } + } +} + +export const fiscalController = new FiscalController(); diff --git a/src/modules/fiscal/fiscal.routes.ts b/src/modules/fiscal/fiscal.routes.ts new file mode 100644 index 0000000..ac90f4e --- /dev/null +++ b/src/modules/fiscal/fiscal.routes.ts @@ -0,0 +1,45 @@ +import { Router } from 'express'; +import { fiscalController } from './fiscal.controller.js'; +import { authenticate } from '../../shared/middleware/auth.middleware.js'; + +const router = Router(); + +// All routes require authentication +router.use(authenticate); + +// ========== TAX CATEGORIES ========== +router.get('/tax-categories', (req, res, next) => fiscalController.getTaxCategories(req, res, next)); +router.get('/tax-categories/by-code/:code', (req, res, next) => fiscalController.getTaxCategoryByCode(req, res, next)); +router.get('/tax-categories/by-sat-code/:satCode', (req, res, next) => fiscalController.getTaxCategoryBySatCode(req, res, next)); +router.get('/tax-categories/:id', (req, res, next) => fiscalController.getTaxCategory(req, res, next)); + +// ========== FISCAL REGIMES ========== +router.get('/fiscal-regimes', (req, res, next) => fiscalController.getFiscalRegimes(req, res, next)); +router.get('/fiscal-regimes/by-code/:code', (req, res, next) => fiscalController.getFiscalRegimeByCode(req, res, next)); +router.get('/fiscal-regimes/person-type/:personType', (req, res, next) => fiscalController.getFiscalRegimesForPersonType(req, res, next)); +router.get('/fiscal-regimes/:id', (req, res, next) => fiscalController.getFiscalRegime(req, res, next)); + +// ========== CFDI USES ========== +router.get('/cfdi-uses', (req, res, next) => fiscalController.getCfdiUses(req, res, next)); +router.get('/cfdi-uses/by-code/:code', (req, res, next) => fiscalController.getCfdiUseByCode(req, res, next)); +router.get('/cfdi-uses/person-type/:personType', (req, res, next) => fiscalController.getCfdiUsesForPersonType(req, res, next)); +router.get('/cfdi-uses/regime/:regimeCode', (req, res, next) => fiscalController.getCfdiUsesForRegime(req, res, next)); +router.get('/cfdi-uses/:id', (req, res, next) => fiscalController.getCfdiUse(req, res, next)); + +// ========== PAYMENT METHODS (SAT Forms of Payment) ========== +router.get('/payment-methods', (req, res, next) => fiscalController.getPaymentMethods(req, res, next)); +router.get('/payment-methods/by-code/:code', (req, res, next) => fiscalController.getPaymentMethodByCode(req, res, next)); +router.get('/payment-methods/:id', (req, res, next) => fiscalController.getPaymentMethod(req, res, next)); + +// ========== PAYMENT TYPES (SAT Payment Methods - PUE/PPD) ========== +router.get('/payment-types', (req, res, next) => fiscalController.getPaymentTypes(req, res, next)); +router.get('/payment-types/by-code/:code', (req, res, next) => fiscalController.getPaymentTypeByCode(req, res, next)); +router.get('/payment-types/:id', (req, res, next) => fiscalController.getPaymentType(req, res, next)); + +// ========== WITHHOLDING TYPES ========== +router.get('/withholding-types', (req, res, next) => fiscalController.getWithholdingTypes(req, res, next)); +router.get('/withholding-types/by-code/:code', (req, res, next) => fiscalController.getWithholdingTypeByCode(req, res, next)); +router.get('/withholding-types/by-category/:categoryId', (req, res, next) => fiscalController.getWithholdingTypesByCategory(req, res, next)); +router.get('/withholding-types/:id', (req, res, next) => fiscalController.getWithholdingType(req, res, next)); + +export default router; diff --git a/src/modules/fiscal/index.ts b/src/modules/fiscal/index.ts new file mode 100644 index 0000000..ef65dc9 --- /dev/null +++ b/src/modules/fiscal/index.ts @@ -0,0 +1,4 @@ +export * from './entities/index.js'; +export * from './fiscal-catalogs.service.js'; +export { fiscalController } from './fiscal.controller.js'; +export { default as fiscalRoutes } from './fiscal.routes.js';