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:
parent
6b7ea745d8
commit
5fa451e09f
@ -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) => {
|
||||
|
||||
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';
|
||||
Loading…
Reference in New Issue
Block a user