feat(catalogs): Add fiscal catalog components (MGN-005)

- Add types for fiscal entities (TaxCategory, FiscalRegime, CfdiUse, etc.)
- Add API clients for fiscal endpoints
- Add hooks for fiscal data fetching
- Add components:
  - FiscalRegimeSelect - SAT fiscal regime selector
  - CfdiUseSelect - CFDI use code selector
  - FiscalPaymentMethodSelect - SAT payment method selector
  - FiscalPaymentTypeSelect - PUE/PPD selector
  - TaxCategorySelect - Tax category selector
  - WithholdingTypeSelect - Withholding type selector
  - FiscalDataInput - Compound component for fiscal data collection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-18 09:31:14 -06:00
parent 94ba9f6bcd
commit c9e1c3fb06
13 changed files with 1168 additions and 0 deletions

View File

@ -0,0 +1,255 @@
import axios from 'axios';
import type {
TaxCategory,
FiscalRegime,
CfdiUse,
FiscalPaymentMethod,
FiscalPaymentType,
WithholdingType,
TaxNature,
PersonType,
} from '../types';
const API_BASE = '/api/v1/fiscal';
// ========== TAX CATEGORIES API ==========
export interface TaxCategoryFilter {
taxNature?: TaxNature;
active?: boolean;
}
export const taxCategoriesApi = {
getAll: async (filter?: TaxCategoryFilter): Promise<TaxCategory[]> => {
const params = new URLSearchParams();
if (filter?.taxNature) params.append('tax_nature', filter.taxNature);
if (filter?.active !== undefined) params.append('active', String(filter.active));
const response = await axios.get(`${API_BASE}/tax-categories`, { params });
return response.data.data;
},
getById: async (id: string): Promise<TaxCategory> => {
const response = await axios.get(`${API_BASE}/tax-categories/${id}`);
return response.data.data;
},
getByCode: async (code: string): Promise<TaxCategory | null> => {
try {
const response = await axios.get(`${API_BASE}/tax-categories/by-code/${code}`);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
},
getBySatCode: async (satCode: string): Promise<TaxCategory | null> => {
try {
const response = await axios.get(`${API_BASE}/tax-categories/by-sat-code/${satCode}`);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
},
};
// ========== FISCAL REGIMES API ==========
export interface FiscalRegimeFilter {
appliesTo?: PersonType;
active?: boolean;
}
export const fiscalRegimesApi = {
getAll: async (filter?: FiscalRegimeFilter): Promise<FiscalRegime[]> => {
const params = new URLSearchParams();
if (filter?.appliesTo) params.append('applies_to', filter.appliesTo);
if (filter?.active !== undefined) params.append('active', String(filter.active));
const response = await axios.get(`${API_BASE}/fiscal-regimes`, { params });
return response.data.data;
},
getById: async (id: string): Promise<FiscalRegime> => {
const response = await axios.get(`${API_BASE}/fiscal-regimes/${id}`);
return response.data.data;
},
getByCode: async (code: string): Promise<FiscalRegime | null> => {
try {
const response = await axios.get(`${API_BASE}/fiscal-regimes/by-code/${code}`);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
},
getForPersonType: async (personType: PersonType): Promise<FiscalRegime[]> => {
const response = await axios.get(`${API_BASE}/fiscal-regimes/person-type/${personType}`);
return response.data.data;
},
};
// ========== CFDI USES API ==========
export interface CfdiUseFilter {
appliesTo?: PersonType;
active?: boolean;
}
export const cfdiUsesApi = {
getAll: async (filter?: CfdiUseFilter): Promise<CfdiUse[]> => {
const params = new URLSearchParams();
if (filter?.appliesTo) params.append('applies_to', filter.appliesTo);
if (filter?.active !== undefined) params.append('active', String(filter.active));
const response = await axios.get(`${API_BASE}/cfdi-uses`, { params });
return response.data.data;
},
getById: async (id: string): Promise<CfdiUse> => {
const response = await axios.get(`${API_BASE}/cfdi-uses/${id}`);
return response.data.data;
},
getByCode: async (code: string): Promise<CfdiUse | null> => {
try {
const response = await axios.get(`${API_BASE}/cfdi-uses/by-code/${code}`);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
},
getForPersonType: async (personType: PersonType): Promise<CfdiUse[]> => {
const response = await axios.get(`${API_BASE}/cfdi-uses/person-type/${personType}`);
return response.data.data;
},
getForRegime: async (regimeCode: string): Promise<CfdiUse[]> => {
const response = await axios.get(`${API_BASE}/cfdi-uses/regime/${regimeCode}`);
return response.data.data;
},
};
// ========== PAYMENT METHODS API (SAT c_FormaPago) ==========
export interface PaymentMethodFilter {
requiresBankInfo?: boolean;
active?: boolean;
}
export const fiscalPaymentMethodsApi = {
getAll: async (filter?: PaymentMethodFilter): Promise<FiscalPaymentMethod[]> => {
const params = new URLSearchParams();
if (filter?.requiresBankInfo !== undefined) {
params.append('requires_bank_info', String(filter.requiresBankInfo));
}
if (filter?.active !== undefined) params.append('active', String(filter.active));
const response = await axios.get(`${API_BASE}/payment-methods`, { params });
return response.data.data;
},
getById: async (id: string): Promise<FiscalPaymentMethod> => {
const response = await axios.get(`${API_BASE}/payment-methods/${id}`);
return response.data.data;
},
getByCode: async (code: string): Promise<FiscalPaymentMethod | null> => {
try {
const response = await axios.get(`${API_BASE}/payment-methods/by-code/${code}`);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
},
};
// ========== PAYMENT TYPES API (SAT c_MetodoPago) ==========
export interface PaymentTypeFilter {
active?: boolean;
}
export const fiscalPaymentTypesApi = {
getAll: async (filter?: PaymentTypeFilter): Promise<FiscalPaymentType[]> => {
const params = new URLSearchParams();
if (filter?.active !== undefined) params.append('active', String(filter.active));
const response = await axios.get(`${API_BASE}/payment-types`, { params });
return response.data.data;
},
getById: async (id: string): Promise<FiscalPaymentType> => {
const response = await axios.get(`${API_BASE}/payment-types/${id}`);
return response.data.data;
},
getByCode: async (code: string): Promise<FiscalPaymentType | null> => {
try {
const response = await axios.get(`${API_BASE}/payment-types/by-code/${code}`);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
},
};
// ========== WITHHOLDING TYPES API ==========
export interface WithholdingTypeFilter {
taxCategoryId?: string;
active?: boolean;
}
export const withholdingTypesApi = {
getAll: async (filter?: WithholdingTypeFilter): Promise<WithholdingType[]> => {
const params = new URLSearchParams();
if (filter?.taxCategoryId) params.append('tax_category_id', filter.taxCategoryId);
if (filter?.active !== undefined) params.append('active', String(filter.active));
const response = await axios.get(`${API_BASE}/withholding-types`, { params });
return response.data.data;
},
getById: async (id: string): Promise<WithholdingType> => {
const response = await axios.get(`${API_BASE}/withholding-types/${id}`);
return response.data.data;
},
getByCode: async (code: string): Promise<WithholdingType | null> => {
try {
const response = await axios.get(`${API_BASE}/withholding-types/by-code/${code}`);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
},
getByCategory: async (categoryId: string): Promise<WithholdingType[]> => {
const response = await axios.get(`${API_BASE}/withholding-types/by-category/${categoryId}`);
return response.data.data;
},
};

View File

@ -1,3 +1,19 @@
export { countriesApi } from './countries.api';
export { currenciesApi } from './currencies.api';
export { uomApi } from './uom.api';
export {
taxCategoriesApi,
fiscalRegimesApi,
cfdiUsesApi,
fiscalPaymentMethodsApi,
fiscalPaymentTypesApi,
withholdingTypesApi,
} from './fiscal.api';
export type {
TaxCategoryFilter,
FiscalRegimeFilter,
CfdiUseFilter,
PaymentMethodFilter,
PaymentTypeFilter,
WithholdingTypeFilter,
} from './fiscal.api';

View File

@ -0,0 +1,68 @@
import { useCfdiUses, useCfdiUsesForRegime } from '../hooks/useFiscalCatalogs';
import type { PersonType } from '../types';
interface CfdiUseSelectProps {
value: string;
onChange: (value: string) => void;
personType?: PersonType;
regimeCode?: string;
activeOnly?: boolean;
placeholder?: string;
disabled?: boolean;
className?: string;
error?: string;
required?: boolean;
label?: string;
}
export function CfdiUseSelect({
value,
onChange,
personType,
regimeCode,
activeOnly = true,
placeholder = 'Seleccionar uso de CFDI',
disabled = false,
className = '',
error,
required = false,
label,
}: CfdiUseSelectProps) {
// Use regime specific hook if regimeCode is provided, otherwise use general hook
const allUses = useCfdiUses({ appliesTo: personType, active: activeOnly });
const regimeUses = useCfdiUsesForRegime(regimeCode);
const { uses, loading, error: loadError } = regimeCode ? regimeUses : allUses;
return (
<div className={`flex flex-col gap-1 ${className}`}>
{label && (
<label className="text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled || loading}
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
error || loadError ? 'border-red-500' : 'border-gray-300'
}`}
required={required}
>
<option value="">{loading ? 'Cargando...' : placeholder}</option>
{uses.map((use) => (
<option key={use.id} value={use.id}>
{use.code} - {use.name}
</option>
))}
</select>
{(error || loadError) && (
<span className="text-sm text-red-500">{error || loadError}</span>
)}
</div>
);
}
export default CfdiUseSelect;

View File

@ -0,0 +1,130 @@
import { FiscalRegimeSelect } from './FiscalRegimeSelect';
import { CfdiUseSelect } from './CfdiUseSelect';
import { FiscalPaymentMethodSelect } from './FiscalPaymentMethodSelect';
import { FiscalPaymentTypeSelect } from './FiscalPaymentTypeSelect';
import type { PersonType } from '../types';
export interface FiscalData {
taxId?: string;
fiscalRegimeId?: string;
cfdiUseId?: string;
paymentMethodId?: string;
paymentTypeId?: string;
}
interface FiscalDataInputProps {
value: FiscalData;
onChange: (value: FiscalData) => void;
personType?: PersonType;
showTaxId?: boolean;
showPaymentFields?: boolean;
disabled?: boolean;
className?: string;
errors?: {
taxId?: string;
fiscalRegimeId?: string;
cfdiUseId?: string;
paymentMethodId?: string;
paymentTypeId?: string;
};
}
export function FiscalDataInput({
value,
onChange,
personType = 'both',
showTaxId = true,
showPaymentFields = false,
disabled = false,
className = '',
errors = {},
}: FiscalDataInputProps) {
const handleChange = (field: keyof FiscalData, fieldValue: string) => {
onChange({ ...value, [field]: fieldValue });
};
const handleRegimeChange = (regimeId: string) => {
handleChange('fiscalRegimeId', regimeId);
// Reset CFDI use when regime changes
handleChange('cfdiUseId', '');
};
return (
<div className={`space-y-4 ${className}`}>
<h3 className="text-lg font-medium text-gray-900">Datos Fiscales</h3>
{showTaxId && (
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-gray-700">
RFC <span className="text-red-500">*</span>
</label>
<input
type="text"
value={value.taxId || ''}
onChange={(e) => handleChange('taxId', e.target.value.toUpperCase())}
placeholder="XAXX010101000"
disabled={disabled}
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed uppercase ${
errors.taxId ? 'border-red-500' : 'border-gray-300'
}`}
maxLength={13}
/>
{errors.taxId && (
<span className="text-sm text-red-500">{errors.taxId}</span>
)}
<p className="text-xs text-gray-500">
{personType === 'natural'
? '13 caracteres para persona física'
: personType === 'legal'
? '12 caracteres para persona moral'
: '12-13 caracteres'}
</p>
</div>
)}
<FiscalRegimeSelect
value={value.fiscalRegimeId || ''}
onChange={handleRegimeChange}
personType={personType}
disabled={disabled}
label="Régimen Fiscal"
required
error={errors.fiscalRegimeId}
/>
<CfdiUseSelect
value={value.cfdiUseId || ''}
onChange={(v) => handleChange('cfdiUseId', v)}
personType={personType}
disabled={disabled}
label="Uso del CFDI"
required
error={errors.cfdiUseId}
/>
{showPaymentFields && (
<>
<FiscalPaymentMethodSelect
value={value.paymentMethodId || ''}
onChange={(v) => handleChange('paymentMethodId', v)}
disabled={disabled}
label="Forma de Pago (SAT)"
required
error={errors.paymentMethodId}
/>
<FiscalPaymentTypeSelect
value={value.paymentTypeId || ''}
onChange={(v) => handleChange('paymentTypeId', v)}
disabled={disabled}
label="Método de Pago (SAT)"
required
error={errors.paymentTypeId}
/>
</>
)}
</div>
);
}
export default FiscalDataInput;

View File

@ -0,0 +1,59 @@
import { useFiscalPaymentMethods } from '../hooks/useFiscalCatalogs';
interface FiscalPaymentMethodSelectProps {
value: string;
onChange: (value: string) => void;
activeOnly?: boolean;
placeholder?: string;
disabled?: boolean;
className?: string;
error?: string;
required?: boolean;
label?: string;
}
export function FiscalPaymentMethodSelect({
value,
onChange,
activeOnly = true,
placeholder = 'Seleccionar forma de pago',
disabled = false,
className = '',
error,
required = false,
label,
}: FiscalPaymentMethodSelectProps) {
const { methods, loading, error: loadError } = useFiscalPaymentMethods({ active: activeOnly });
return (
<div className={`flex flex-col gap-1 ${className}`}>
{label && (
<label className="text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled || loading}
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
error || loadError ? 'border-red-500' : 'border-gray-300'
}`}
required={required}
>
<option value="">{loading ? 'Cargando...' : placeholder}</option>
{methods.map((method) => (
<option key={method.id} value={method.id}>
{method.code} - {method.name}
</option>
))}
</select>
{(error || loadError) && (
<span className="text-sm text-red-500">{error || loadError}</span>
)}
</div>
);
}
export default FiscalPaymentMethodSelect;

View File

@ -0,0 +1,62 @@
import { useFiscalPaymentTypes } from '../hooks/useFiscalCatalogs';
interface FiscalPaymentTypeSelectProps {
value: string;
onChange: (value: string) => void;
activeOnly?: boolean;
placeholder?: string;
disabled?: boolean;
className?: string;
error?: string;
required?: boolean;
label?: string;
}
export function FiscalPaymentTypeSelect({
value,
onChange,
activeOnly = true,
placeholder = 'Seleccionar método de pago',
disabled = false,
className = '',
error,
required = false,
label,
}: FiscalPaymentTypeSelectProps) {
const { types, loading, error: loadError } = useFiscalPaymentTypes({ active: activeOnly });
return (
<div className={`flex flex-col gap-1 ${className}`}>
{label && (
<label className="text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled || loading}
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
error || loadError ? 'border-red-500' : 'border-gray-300'
}`}
required={required}
>
<option value="">{loading ? 'Cargando...' : placeholder}</option>
{types.map((type) => (
<option key={type.id} value={type.id}>
{type.code} - {type.name}
</option>
))}
</select>
{(error || loadError) && (
<span className="text-sm text-red-500">{error || loadError}</span>
)}
<p className="text-xs text-gray-500 mt-1">
PUE: Pago en una exhibición | PPD: Pago en parcialidades o diferido
</p>
</div>
);
}
export default FiscalPaymentTypeSelect;

View File

@ -0,0 +1,66 @@
import { useFiscalRegimes, useFiscalRegimesByPersonType } from '../hooks/useFiscalCatalogs';
import type { PersonType } from '../types';
interface FiscalRegimeSelectProps {
value: string;
onChange: (value: string) => void;
personType?: PersonType;
activeOnly?: boolean;
placeholder?: string;
disabled?: boolean;
className?: string;
error?: string;
required?: boolean;
label?: string;
}
export function FiscalRegimeSelect({
value,
onChange,
personType,
activeOnly = true,
placeholder = 'Seleccionar régimen fiscal',
disabled = false,
className = '',
error,
required = false,
label,
}: FiscalRegimeSelectProps) {
// Use person type specific hook if personType is provided
const allRegimes = useFiscalRegimes({ active: activeOnly });
const filteredRegimes = useFiscalRegimesByPersonType(personType);
const { regimes, loading, error: loadError } = personType ? filteredRegimes : allRegimes;
return (
<div className={`flex flex-col gap-1 ${className}`}>
{label && (
<label className="text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled || loading}
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
error || loadError ? 'border-red-500' : 'border-gray-300'
}`}
required={required}
>
<option value="">{loading ? 'Cargando...' : placeholder}</option>
{regimes.map((regime) => (
<option key={regime.id} value={regime.id}>
{regime.code} - {regime.name}
</option>
))}
</select>
{(error || loadError) && (
<span className="text-sm text-red-500">{error || loadError}</span>
)}
</div>
);
}
export default FiscalRegimeSelect;

View File

@ -0,0 +1,65 @@
import { useTaxCategories } from '../hooks/useFiscalCatalogs';
import type { TaxNature } from '../types';
interface TaxCategorySelectProps {
value: string;
onChange: (value: string) => void;
taxNature?: TaxNature;
activeOnly?: boolean;
placeholder?: string;
disabled?: boolean;
className?: string;
error?: string;
required?: boolean;
label?: string;
}
export function TaxCategorySelect({
value,
onChange,
taxNature,
activeOnly = true,
placeholder = 'Seleccionar categoría de impuesto',
disabled = false,
className = '',
error,
required = false,
label,
}: TaxCategorySelectProps) {
const { categories, loading, error: loadError } = useTaxCategories({
taxNature,
active: activeOnly,
});
return (
<div className={`flex flex-col gap-1 ${className}`}>
{label && (
<label className="text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled || loading}
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
error || loadError ? 'border-red-500' : 'border-gray-300'
}`}
required={required}
>
<option value="">{loading ? 'Cargando...' : placeholder}</option>
{categories.map((cat) => (
<option key={cat.id} value={cat.id}>
{cat.code} - {cat.name}
</option>
))}
</select>
{(error || loadError) && (
<span className="text-sm text-red-500">{error || loadError}</span>
)}
</div>
);
}
export default TaxCategorySelect;

View File

@ -0,0 +1,67 @@
import { useWithholdingTypes, useWithholdingTypesByCategory } from '../hooks/useFiscalCatalogs';
interface WithholdingTypeSelectProps {
value: string;
onChange: (value: string) => void;
taxCategoryId?: string;
activeOnly?: boolean;
placeholder?: string;
disabled?: boolean;
className?: string;
error?: string;
required?: boolean;
label?: string;
showRate?: boolean;
}
export function WithholdingTypeSelect({
value,
onChange,
taxCategoryId,
activeOnly = true,
placeholder = 'Seleccionar tipo de retención',
disabled = false,
className = '',
error,
required = false,
label,
showRate = true,
}: WithholdingTypeSelectProps) {
// Use category specific hook if taxCategoryId is provided
const allTypes = useWithholdingTypes({ active: activeOnly });
const categoryTypes = useWithholdingTypesByCategory(taxCategoryId);
const { types, loading, error: loadError } = taxCategoryId ? categoryTypes : allTypes;
return (
<div className={`flex flex-col gap-1 ${className}`}>
{label && (
<label className="text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled || loading}
className={`block w-full px-3 py-2 bg-white border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed ${
error || loadError ? 'border-red-500' : 'border-gray-300'
}`}
required={required}
>
<option value="">{loading ? 'Cargando...' : placeholder}</option>
{types.map((type) => (
<option key={type.id} value={type.id}>
{type.code} - {type.name} {showRate && `(${type.defaultRate}%)`}
</option>
))}
</select>
{(error || loadError) && (
<span className="text-sm text-red-500">{error || loadError}</span>
)}
</div>
);
}
export default WithholdingTypeSelect;

View File

@ -14,3 +14,13 @@ export type { AddressData } from './AddressInput';
// Display components
export { ConversionTableDisplay } from './ConversionTableDisplay';
export { CurrencyRatesDisplay } from './CurrencyRatesDisplay';
// Fiscal components
export { FiscalRegimeSelect } from './FiscalRegimeSelect';
export { CfdiUseSelect } from './CfdiUseSelect';
export { FiscalPaymentMethodSelect } from './FiscalPaymentMethodSelect';
export { FiscalPaymentTypeSelect } from './FiscalPaymentTypeSelect';
export { TaxCategorySelect } from './TaxCategorySelect';
export { WithholdingTypeSelect } from './WithholdingTypeSelect';
export { FiscalDataInput } from './FiscalDataInput';
export type { FiscalData } from './FiscalDataInput';

View File

@ -1,3 +1,14 @@
export { useCountries, useCountry, useStates } from './useCountries';
export { useCurrencies, useCurrencyRates, useLatestRates, useCurrencyConversion } from './useCurrencies';
export { useUomCategories, useUom, useUomConversion, useConversionTable } from './useUom';
export {
useTaxCategories,
useFiscalRegimes,
useFiscalRegimesByPersonType,
useCfdiUses,
useCfdiUsesForRegime,
useFiscalPaymentMethods,
useFiscalPaymentTypes,
useWithholdingTypes,
useWithholdingTypesByCategory,
} from './useFiscalCatalogs';

View File

@ -0,0 +1,276 @@
import { useState, useEffect, useCallback } from 'react';
import {
taxCategoriesApi,
fiscalRegimesApi,
cfdiUsesApi,
fiscalPaymentMethodsApi,
fiscalPaymentTypesApi,
withholdingTypesApi,
TaxCategoryFilter,
FiscalRegimeFilter,
CfdiUseFilter,
PaymentMethodFilter,
PaymentTypeFilter,
WithholdingTypeFilter,
} from '../api/fiscal.api';
import type {
TaxCategory,
FiscalRegime,
CfdiUse,
FiscalPaymentMethod,
FiscalPaymentType,
WithholdingType,
PersonType,
} from '../types';
// ========== TAX CATEGORIES HOOK ==========
export function useTaxCategories(filter?: TaxCategoryFilter) {
const [categories, setCategories] = useState<TaxCategory[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchCategories = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await taxCategoriesApi.getAll(filter);
setCategories(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar categorías de impuestos');
} finally {
setLoading(false);
}
}, [filter?.taxNature, filter?.active]);
useEffect(() => {
fetchCategories();
}, [fetchCategories]);
return { categories, loading, error, refetch: fetchCategories };
}
// ========== FISCAL REGIMES HOOK ==========
export function useFiscalRegimes(filter?: FiscalRegimeFilter) {
const [regimes, setRegimes] = useState<FiscalRegime[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchRegimes = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await fiscalRegimesApi.getAll(filter);
setRegimes(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar regímenes fiscales');
} finally {
setLoading(false);
}
}, [filter?.appliesTo, filter?.active]);
useEffect(() => {
fetchRegimes();
}, [fetchRegimes]);
return { regimes, loading, error, refetch: fetchRegimes };
}
export function useFiscalRegimesByPersonType(personType: PersonType | undefined) {
const [regimes, setRegimes] = useState<FiscalRegime[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchRegimes = useCallback(async () => {
if (!personType) {
setRegimes([]);
return;
}
setLoading(true);
setError(null);
try {
const data = await fiscalRegimesApi.getForPersonType(personType);
setRegimes(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar regímenes fiscales');
} finally {
setLoading(false);
}
}, [personType]);
useEffect(() => {
fetchRegimes();
}, [fetchRegimes]);
return { regimes, loading, error, refetch: fetchRegimes };
}
// ========== CFDI USES HOOK ==========
export function useCfdiUses(filter?: CfdiUseFilter) {
const [uses, setUses] = useState<CfdiUse[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchUses = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await cfdiUsesApi.getAll(filter);
setUses(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar usos de CFDI');
} finally {
setLoading(false);
}
}, [filter?.appliesTo, filter?.active]);
useEffect(() => {
fetchUses();
}, [fetchUses]);
return { uses, loading, error, refetch: fetchUses };
}
export function useCfdiUsesForRegime(regimeCode: string | undefined) {
const [uses, setUses] = useState<CfdiUse[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchUses = useCallback(async () => {
if (!regimeCode) {
setUses([]);
return;
}
setLoading(true);
setError(null);
try {
const data = await cfdiUsesApi.getForRegime(regimeCode);
setUses(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar usos de CFDI');
} finally {
setLoading(false);
}
}, [regimeCode]);
useEffect(() => {
fetchUses();
}, [fetchUses]);
return { uses, loading, error, refetch: fetchUses };
}
// ========== PAYMENT METHODS HOOK ==========
export function useFiscalPaymentMethods(filter?: PaymentMethodFilter) {
const [methods, setMethods] = useState<FiscalPaymentMethod[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchMethods = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await fiscalPaymentMethodsApi.getAll(filter);
setMethods(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar formas de pago');
} finally {
setLoading(false);
}
}, [filter?.requiresBankInfo, filter?.active]);
useEffect(() => {
fetchMethods();
}, [fetchMethods]);
return { methods, loading, error, refetch: fetchMethods };
}
// ========== PAYMENT TYPES HOOK ==========
export function useFiscalPaymentTypes(filter?: PaymentTypeFilter) {
const [types, setTypes] = useState<FiscalPaymentType[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchTypes = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await fiscalPaymentTypesApi.getAll(filter);
setTypes(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar métodos de pago');
} finally {
setLoading(false);
}
}, [filter?.active]);
useEffect(() => {
fetchTypes();
}, [fetchTypes]);
return { types, loading, error, refetch: fetchTypes };
}
// ========== WITHHOLDING TYPES HOOK ==========
export function useWithholdingTypes(filter?: WithholdingTypeFilter) {
const [types, setTypes] = useState<WithholdingType[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchTypes = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await withholdingTypesApi.getAll(filter);
setTypes(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar tipos de retención');
} finally {
setLoading(false);
}
}, [filter?.taxCategoryId, filter?.active]);
useEffect(() => {
fetchTypes();
}, [fetchTypes]);
return { types, loading, error, refetch: fetchTypes };
}
export function useWithholdingTypesByCategory(categoryId: string | undefined) {
const [types, setTypes] = useState<WithholdingType[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchTypes = useCallback(async () => {
if (!categoryId) {
setTypes([]);
return;
}
setLoading(true);
setError(null);
try {
const data = await withholdingTypesApi.getByCategory(categoryId);
setTypes(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Error al cargar tipos de retención');
} finally {
setLoading(false);
}
}, [categoryId]);
useEffect(() => {
fetchTypes();
}, [fetchTypes]);
return { types, loading, error, refetch: fetchTypes };
}

View File

@ -273,3 +273,86 @@ export interface DiscountRule {
createdAt: string;
updatedAt: string;
}
// ========== FISCAL TYPES ==========
// Tax Nature
export type TaxNature = 'tax' | 'withholding' | 'both';
// Person Type
export type PersonType = 'natural' | 'legal' | 'both';
// Tax Category (IVA, ISR, IEPS, etc.)
export interface TaxCategory {
id: string;
code: string;
name: string;
description?: string;
taxNature: TaxNature;
satCode?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// Fiscal Regime (SAT c_RegimenFiscal)
export interface FiscalRegime {
id: string;
code: string;
name: string;
description?: string;
appliesTo: PersonType;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// CFDI Use (SAT c_UsoCFDI)
export interface CfdiUse {
id: string;
code: string;
name: string;
description?: string;
appliesTo: PersonType;
allowedRegimes?: string[];
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// Payment Method - SAT Forms of Payment (c_FormaPago)
export interface FiscalPaymentMethod {
id: string;
code: string;
name: string;
description?: string;
requiresBankInfo: boolean;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// Payment Type - SAT Payment Methods (c_MetodoPago: PUE, PPD)
export interface FiscalPaymentType {
id: string;
code: string;
name: string;
description?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// Withholding Type
export interface WithholdingType {
id: string;
code: string;
name: string;
description?: string;
defaultRate: number;
taxCategoryId?: string;
taxCategory?: TaxCategory;
isActive: boolean;
createdAt: string;
updatedAt: string;
}