From c9e1c3fb06ea9ba474a7c4bd14769fbffcb5395f Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Sun, 18 Jan 2026 09:31:14 -0600 Subject: [PATCH] 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 --- src/features/catalogs/api/fiscal.api.ts | 255 ++++++++++++++++ src/features/catalogs/api/index.ts | 16 + .../catalogs/components/CfdiUseSelect.tsx | 68 +++++ .../catalogs/components/FiscalDataInput.tsx | 130 +++++++++ .../components/FiscalPaymentMethodSelect.tsx | 59 ++++ .../components/FiscalPaymentTypeSelect.tsx | 62 ++++ .../components/FiscalRegimeSelect.tsx | 66 +++++ .../catalogs/components/TaxCategorySelect.tsx | 65 +++++ .../components/WithholdingTypeSelect.tsx | 67 +++++ src/features/catalogs/components/index.ts | 10 + src/features/catalogs/hooks/index.ts | 11 + .../catalogs/hooks/useFiscalCatalogs.ts | 276 ++++++++++++++++++ src/features/catalogs/types/index.ts | 83 ++++++ 13 files changed, 1168 insertions(+) create mode 100644 src/features/catalogs/api/fiscal.api.ts create mode 100644 src/features/catalogs/components/CfdiUseSelect.tsx create mode 100644 src/features/catalogs/components/FiscalDataInput.tsx create mode 100644 src/features/catalogs/components/FiscalPaymentMethodSelect.tsx create mode 100644 src/features/catalogs/components/FiscalPaymentTypeSelect.tsx create mode 100644 src/features/catalogs/components/FiscalRegimeSelect.tsx create mode 100644 src/features/catalogs/components/TaxCategorySelect.tsx create mode 100644 src/features/catalogs/components/WithholdingTypeSelect.tsx create mode 100644 src/features/catalogs/hooks/useFiscalCatalogs.ts diff --git a/src/features/catalogs/api/fiscal.api.ts b/src/features/catalogs/api/fiscal.api.ts new file mode 100644 index 0000000..00345c5 --- /dev/null +++ b/src/features/catalogs/api/fiscal.api.ts @@ -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 => { + 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 => { + const response = await axios.get(`${API_BASE}/tax-categories/${id}`); + return response.data.data; + }, + + getByCode: async (code: string): Promise => { + 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 => { + 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 => { + 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 => { + const response = await axios.get(`${API_BASE}/fiscal-regimes/${id}`); + return response.data.data; + }, + + getByCode: async (code: string): Promise => { + 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 => { + 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 => { + 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 => { + const response = await axios.get(`${API_BASE}/cfdi-uses/${id}`); + return response.data.data; + }, + + getByCode: async (code: string): Promise => { + 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 => { + const response = await axios.get(`${API_BASE}/cfdi-uses/person-type/${personType}`); + return response.data.data; + }, + + getForRegime: async (regimeCode: string): Promise => { + 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 => { + 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 => { + const response = await axios.get(`${API_BASE}/payment-methods/${id}`); + return response.data.data; + }, + + getByCode: async (code: string): Promise => { + 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 => { + 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 => { + const response = await axios.get(`${API_BASE}/payment-types/${id}`); + return response.data.data; + }, + + getByCode: async (code: string): Promise => { + 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 => { + 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 => { + const response = await axios.get(`${API_BASE}/withholding-types/${id}`); + return response.data.data; + }, + + getByCode: async (code: string): Promise => { + 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 => { + const response = await axios.get(`${API_BASE}/withholding-types/by-category/${categoryId}`); + return response.data.data; + }, +}; diff --git a/src/features/catalogs/api/index.ts b/src/features/catalogs/api/index.ts index 362629b..1a1c2f7 100644 --- a/src/features/catalogs/api/index.ts +++ b/src/features/catalogs/api/index.ts @@ -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'; diff --git a/src/features/catalogs/components/CfdiUseSelect.tsx b/src/features/catalogs/components/CfdiUseSelect.tsx new file mode 100644 index 0000000..d778da6 --- /dev/null +++ b/src/features/catalogs/components/CfdiUseSelect.tsx @@ -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 ( +
+ {label && ( + + )} + + {(error || loadError) && ( + {error || loadError} + )} +
+ ); +} + +export default CfdiUseSelect; diff --git a/src/features/catalogs/components/FiscalDataInput.tsx b/src/features/catalogs/components/FiscalDataInput.tsx new file mode 100644 index 0000000..95fc04c --- /dev/null +++ b/src/features/catalogs/components/FiscalDataInput.tsx @@ -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 ( +
+

Datos Fiscales

+ + {showTaxId && ( +
+ + 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 && ( + {errors.taxId} + )} +

+ {personType === 'natural' + ? '13 caracteres para persona física' + : personType === 'legal' + ? '12 caracteres para persona moral' + : '12-13 caracteres'} +

+
+ )} + + + + handleChange('cfdiUseId', v)} + personType={personType} + disabled={disabled} + label="Uso del CFDI" + required + error={errors.cfdiUseId} + /> + + {showPaymentFields && ( + <> + handleChange('paymentMethodId', v)} + disabled={disabled} + label="Forma de Pago (SAT)" + required + error={errors.paymentMethodId} + /> + + handleChange('paymentTypeId', v)} + disabled={disabled} + label="Método de Pago (SAT)" + required + error={errors.paymentTypeId} + /> + + )} +
+ ); +} + +export default FiscalDataInput; diff --git a/src/features/catalogs/components/FiscalPaymentMethodSelect.tsx b/src/features/catalogs/components/FiscalPaymentMethodSelect.tsx new file mode 100644 index 0000000..d04772a --- /dev/null +++ b/src/features/catalogs/components/FiscalPaymentMethodSelect.tsx @@ -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 ( +
+ {label && ( + + )} + + {(error || loadError) && ( + {error || loadError} + )} +
+ ); +} + +export default FiscalPaymentMethodSelect; diff --git a/src/features/catalogs/components/FiscalPaymentTypeSelect.tsx b/src/features/catalogs/components/FiscalPaymentTypeSelect.tsx new file mode 100644 index 0000000..397c0b9 --- /dev/null +++ b/src/features/catalogs/components/FiscalPaymentTypeSelect.tsx @@ -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 ( +
+ {label && ( + + )} + + {(error || loadError) && ( + {error || loadError} + )} +

+ PUE: Pago en una exhibición | PPD: Pago en parcialidades o diferido +

+
+ ); +} + +export default FiscalPaymentTypeSelect; diff --git a/src/features/catalogs/components/FiscalRegimeSelect.tsx b/src/features/catalogs/components/FiscalRegimeSelect.tsx new file mode 100644 index 0000000..6b26f1a --- /dev/null +++ b/src/features/catalogs/components/FiscalRegimeSelect.tsx @@ -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 ( +
+ {label && ( + + )} + + {(error || loadError) && ( + {error || loadError} + )} +
+ ); +} + +export default FiscalRegimeSelect; diff --git a/src/features/catalogs/components/TaxCategorySelect.tsx b/src/features/catalogs/components/TaxCategorySelect.tsx new file mode 100644 index 0000000..d42137e --- /dev/null +++ b/src/features/catalogs/components/TaxCategorySelect.tsx @@ -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 ( +
+ {label && ( + + )} + + {(error || loadError) && ( + {error || loadError} + )} +
+ ); +} + +export default TaxCategorySelect; diff --git a/src/features/catalogs/components/WithholdingTypeSelect.tsx b/src/features/catalogs/components/WithholdingTypeSelect.tsx new file mode 100644 index 0000000..49d9f87 --- /dev/null +++ b/src/features/catalogs/components/WithholdingTypeSelect.tsx @@ -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 ( +
+ {label && ( + + )} + + {(error || loadError) && ( + {error || loadError} + )} +
+ ); +} + +export default WithholdingTypeSelect; diff --git a/src/features/catalogs/components/index.ts b/src/features/catalogs/components/index.ts index 898d2d0..5504e38 100644 --- a/src/features/catalogs/components/index.ts +++ b/src/features/catalogs/components/index.ts @@ -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'; diff --git a/src/features/catalogs/hooks/index.ts b/src/features/catalogs/hooks/index.ts index 3afcdf6..b369e65 100644 --- a/src/features/catalogs/hooks/index.ts +++ b/src/features/catalogs/hooks/index.ts @@ -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'; diff --git a/src/features/catalogs/hooks/useFiscalCatalogs.ts b/src/features/catalogs/hooks/useFiscalCatalogs.ts new file mode 100644 index 0000000..4842fa4 --- /dev/null +++ b/src/features/catalogs/hooks/useFiscalCatalogs.ts @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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 }; +} diff --git a/src/features/catalogs/types/index.ts b/src/features/catalogs/types/index.ts index 356f27e..7b7142e 100644 --- a/src/features/catalogs/types/index.ts +++ b/src/features/catalogs/types/index.ts @@ -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; +}