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 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-18 09:31:47 -06:00
parent 6b7ea745d8
commit 5fa451e09f
12 changed files with 1035 additions and 0 deletions

View File

@ -27,6 +27,7 @@ import reportsRoutes from './modules/reports/reports.routes.js';
import invoicesRoutes from './modules/invoices/invoices.routes.js'; import invoicesRoutes from './modules/invoices/invoices.routes.js';
import productsRoutes from './modules/products/products.routes.js'; import productsRoutes from './modules/products/products.routes.js';
import warehousesRoutes from './modules/warehouses/warehouses.routes.js'; import warehousesRoutes from './modules/warehouses/warehouses.routes.js';
import fiscalRoutes from './modules/fiscal/fiscal.routes.js';
const app: Application = express(); const app: Application = express();
@ -79,6 +80,7 @@ app.use(`${apiPrefix}/reports`, reportsRoutes);
app.use(`${apiPrefix}/invoices`, invoicesRoutes); app.use(`${apiPrefix}/invoices`, invoicesRoutes);
app.use(`${apiPrefix}/products`, productsRoutes); app.use(`${apiPrefix}/products`, productsRoutes);
app.use(`${apiPrefix}/warehouses`, warehousesRoutes); app.use(`${apiPrefix}/warehouses`, warehousesRoutes);
app.use(`${apiPrefix}/fiscal`, fiscalRoutes);
// 404 handler // 404 handler
app.use((_req: Request, res: Response) => { app.use((_req: Request, res: Response) => {

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