diff --git a/Abstractions/IEmployeeService.cs b/Abstractions/IEmployeeService.cs new file mode 100644 index 0000000..63b5191 --- /dev/null +++ b/Abstractions/IEmployeeService.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Fiscalapi.Common; +using Fiscalapi.Models; + +namespace Fiscalapi.Abstractions +{ + /// + /// Interfaz para el servicio de Empleado — gestiona los datos del empleado anidados bajo una Persona. + /// Patrón de endpoint: api/{version}/people/{personId}/employee + /// + public interface IEmployeeService + { + Task> GetByIdAsync(string id); + Task> CreateAsync(EmployeeData requestModel); + Task> UpdateAsync(EmployeeData requestModel); + Task> DeleteAsync(string id); + } +} diff --git a/Abstractions/IEmployerService.cs b/Abstractions/IEmployerService.cs new file mode 100644 index 0000000..d3f899d --- /dev/null +++ b/Abstractions/IEmployerService.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Fiscalapi.Common; +using Fiscalapi.Models; + +namespace Fiscalapi.Abstractions +{ + /// + /// Interfaz para el servicio de Empleador — gestiona los datos del empleador anidados bajo una Persona. + /// Patrón de endpoint: api/{version}/people/{personId}/employer + /// + public interface IEmployerService + { + Task> GetByIdAsync(string id); + Task> CreateAsync(EmployerData requestModel); + Task> UpdateAsync(EmployerData requestModel); + Task> DeleteAsync(string id); + } +} diff --git a/Abstractions/IPersonService.cs b/Abstractions/IPersonService.cs index 32176bb..deb7b90 100644 --- a/Abstractions/IPersonService.cs +++ b/Abstractions/IPersonService.cs @@ -1,5 +1,4 @@ using Fiscalapi.Models; -using Fiscalapi.Services; namespace Fiscalapi.Abstractions { @@ -8,7 +7,7 @@ namespace Fiscalapi.Abstractions /// public interface IPersonService : IFiscalApiService { - EmployerService Employer { get; } - EmployeeService Employee { get; } + IEmployerService Employer { get; } + IEmployeeService Employee { get; } } } \ No newline at end of file diff --git a/Models/Payroll.cs b/Models/Payroll.cs index 1c722fd..42f92e9 100644 --- a/Models/Payroll.cs +++ b/Models/Payroll.cs @@ -61,6 +61,19 @@ public class PayrollRetirement public decimal NonAccumulableIncome { get; set; } } + public class PayrollStockOptions + { + /// + /// Valor de mercado de las acciones o títulos valor al momento de ejercer la opción. + /// + public decimal MarketPrice { get; set; } + + /// + /// Precio pactado al otorgarse la opción de ingresos en acciones o títulos valor. + /// + public decimal GrantPrice { get; set; } + } + public class PayrollEarning { public string EarningTypeCode { get; set; } @@ -68,6 +81,7 @@ public class PayrollEarning public string Concept { get; set; } public decimal TaxedAmount { get; set; } public decimal ExemptAmount { get; set; } + public PayrollStockOptions StockOptions { get; set; } public List Overtime { get; set; } } diff --git a/README.md b/README.md index b28b45a..80a0963 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ - **Soporte completo para CFDI 4.0** con todas las especificaciones oficiales - **Timbrado de facturas de ingreso** con validación automática - **Timbrado de notas de crédito** (facturas de egreso) -- **Timbrado de complementos de pago** en MXN, USD y EUR. +- **Timbrado de complementos de pago** en MXN, USD y EUR +- **Timbrado de facturas de nómina** - Soporte para los 13 tipos de CFDI de nómina - **Consulta del estatus de facturas** en el SAT en tiempo real - **Cancelación de facturas** - **Generación de archivos PDF** de las facturas con formato profesional @@ -35,6 +36,11 @@ - **Administración de personas** (emisores, receptores, clientes, usuarios, etc.) - **Gestión de certificados CSD y FIEL** (subir archivos .cer y .key a FiscalAPI) - **Configuración de datos fiscales** (RFC, domicilio fiscal, régimen fiscal) +- **Datos de empleado** (agrega/actualiza/elimina datos de empleado a una persona. CFDI Nómina) +- **Datos de empleador** (agrega/actualiza/elimina datos de empleador a una persona. CFDI Nómina) + +## 🎖️ Gestión de Timbres +- **Gestión de folios fiscales** Compra timbres a FiscalAPI y transfiere/retira a las personas de tu organización según tus reglas de negocio. ## 🛍️ Gestión de Productos/Servicios - **Gestión de productos y servicios** con catálogo personalizable @@ -45,7 +51,7 @@ - **Consulta en catálogos oficiales de Descarga masiva del SAT** actualizados - **Búsqueda de información** en catálogos del SAT con filtros avanzados - **Acceso y búsqueda** en catálogos completos - + ## 📖 Recursos Adicionales - **Cientos de ejemplos de código** disponibles en múltiples lenguajes de programación - Documentación completa con guías paso a paso diff --git a/Services/EmployeeService.cs b/Services/EmployeeService.cs index 6ec3457..eee993a 100644 --- a/Services/EmployeeService.cs +++ b/Services/EmployeeService.cs @@ -1,50 +1,63 @@ -using Fiscalapi.Common; +using Fiscalapi.Abstractions; +using Fiscalapi.Common; using Fiscalapi.Http; using Fiscalapi.Models; +using System; using System.Threading.Tasks; namespace Fiscalapi.Services { - public class EmployeeService + public class EmployeeService : IEmployeeService { private IFiscalApiHttpClient HttpClient { get; } - private string baseEndpoint = "api/v4/people"; + private readonly string _baseEndpoint; - public EmployeeService(IFiscalApiHttpClient fiscalApiHttpClient) + public EmployeeService(IFiscalApiHttpClient fiscalApiHttpClient, string apiVersion) { HttpClient = fiscalApiHttpClient; + _baseEndpoint = $"api/{apiVersion}/people"; } - // GET /api/v4/people/{personId}/employee + private string BuildEndpoint(string personId) + => $"{_baseEndpoint}/{personId}/employee"; + + // GET /api/{version}/people/{personId}/employee public async Task> GetByIdAsync(string id) { - string endpoint = $"{baseEndpoint}/{id}/employee"; - - return await HttpClient.GetAsync(endpoint); + if (string.IsNullOrWhiteSpace(id)) + throw new ArgumentException("Employee person ID is required to build endpoint", nameof(id)); + return await HttpClient.GetAsync(BuildEndpoint(id)); } - //POST /api/v4/people/{personId}/employee + // POST /api/{version}/people/{personId}/employee public async Task> CreateAsync(EmployeeData requestModel) { - string endpoint = $"{baseEndpoint}/{requestModel.EmployeePersonId}/employee"; - - return await HttpClient.PostAsync(endpoint, requestModel); + ValidateRequestModel(requestModel); + return await HttpClient.PostAsync(BuildEndpoint(requestModel.EmployeePersonId), requestModel); } - // PUT /api/v4/people/{personId}/employee + // PUT /api/{version}/people/{personId}/employee public async Task> UpdateAsync(EmployeeData requestModel) { - string endpoint = $"{baseEndpoint}/{requestModel.EmployeePersonId}/employee"; - - return await HttpClient.PutAsync(endpoint, requestModel); + ValidateRequestModel(requestModel); + return await HttpClient.PutAsync(BuildEndpoint(requestModel.EmployeePersonId), requestModel); } - // DELETE /api/v4/people/{personId}/employee + // DELETE /api/{version}/people/{personId}/employee public async Task> DeleteAsync(string id) { - string endpoint = $"{baseEndpoint}/{id}/employee"; + if (string.IsNullOrWhiteSpace(id)) + throw new ArgumentException("El ID de la persona requerido", nameof(id)); + return await HttpClient.DeleteAsync(BuildEndpoint(id)); + } + + private void ValidateRequestModel(EmployeeData requestModel) + { + if (requestModel == null) + throw new ArgumentNullException(nameof(requestModel), "El modelo de solicitud no puede ser nulo"); - return await HttpClient.DeleteAsync(endpoint); + if (string.IsNullOrWhiteSpace(requestModel.EmployeePersonId)) + throw new ArgumentException("Se requiere el ID de la persona para crear o actualizar los datos del empleado", nameof(requestModel.EmployeePersonId)); } } } diff --git a/Services/EmployerService.cs b/Services/EmployerService.cs index 2aea4f3..cb46388 100644 --- a/Services/EmployerService.cs +++ b/Services/EmployerService.cs @@ -1,50 +1,63 @@ -using Fiscalapi.Common; +using Fiscalapi.Abstractions; +using Fiscalapi.Common; using Fiscalapi.Http; using Fiscalapi.Models; +using System; using System.Threading.Tasks; namespace Fiscalapi.Services { - public class EmployerService + public class EmployerService : IEmployerService { private IFiscalApiHttpClient HttpClient { get; } - private string baseEndpoint = "api/v4/people"; + private readonly string _baseEndpoint; - public EmployerService(IFiscalApiHttpClient fiscalApiHttpClient) + public EmployerService(IFiscalApiHttpClient fiscalApiHttpClient, string apiVersion) { HttpClient = fiscalApiHttpClient; + _baseEndpoint = $"api/{apiVersion}/people"; } - // GET /api/v4/people/{personId}/employer + private string BuildEndpoint(string personId) + => $"{_baseEndpoint}/{personId}/employer"; + + // GET /api/{version}/people/{personId}/employer public async Task> GetByIdAsync(string id) { - string endpoint = $"{baseEndpoint}/{id}/employer"; - - return await HttpClient.GetAsync(endpoint); + if (string.IsNullOrWhiteSpace(id)) + throw new ArgumentException("Person ID is required to build endpoint", nameof(id)); + return await HttpClient.GetAsync(BuildEndpoint(id)); } - //POST /api/v4/people/{personId}/employer + // POST /api/{version}/people/{personId}/employer public async Task> CreateAsync(EmployerData requestModel) { - string endpoint = $"{baseEndpoint}/{requestModel.PersonId}/employer"; - - return await HttpClient.PostAsync(endpoint, requestModel); + ValidateRequestModel(requestModel); + return await HttpClient.PostAsync(BuildEndpoint(requestModel.PersonId), requestModel); } - // PUT /api/v4/people/{personId}/employer + // PUT /api/{version}/people/{personId}/employer public async Task> UpdateAsync(EmployerData requestModel) { - string endpoint = $"{baseEndpoint}/{requestModel.PersonId}/employer"; - - return await HttpClient.PutAsync(endpoint, requestModel); + ValidateRequestModel(requestModel); + return await HttpClient.PutAsync(BuildEndpoint(requestModel.PersonId), requestModel); } - // DELETE /api/v4/people/{personId}/employer + // DELETE /api/{version}/people/{personId}/employer public async Task> DeleteAsync(string id) { - string endpoint = $"{baseEndpoint}/{id}/employer"; + if (string.IsNullOrWhiteSpace(id)) + throw new ArgumentException("El ID de la persona requerido", nameof(id)); + return await HttpClient.DeleteAsync(BuildEndpoint(id)); + } + + private void ValidateRequestModel(EmployerData requestModel) + { + if (requestModel == null) + throw new ArgumentNullException(nameof(requestModel), "El modelo de solicitud no puede ser nulo"); - return await HttpClient.DeleteAsync(endpoint); + if (string.IsNullOrWhiteSpace(requestModel.PersonId)) + throw new ArgumentException("Se requiere el ID de la persona para crear o actualizar los datos del empleador", nameof(requestModel.PersonId)); } } } diff --git a/Services/PersonService.cs b/Services/PersonService.cs index ba58c1a..8d0a36e 100644 --- a/Services/PersonService.cs +++ b/Services/PersonService.cs @@ -6,14 +6,14 @@ namespace Fiscalapi.Services { public class PersonService : BaseFiscalApiService, IPersonService { - public EmployerService Employer { get; } - public EmployeeService Employee { get; } + public IEmployerService Employer { get; } + public IEmployeeService Employee { get; } public PersonService(IFiscalApiHttpClient httpClient, string apiVersion) : base(httpClient, "people", apiVersion) { - Employer = new EmployerService(httpClient); - Employee = new EmployeeService(httpClient); + Employer = new EmployerService(httpClient, apiVersion); + Employee = new EmployeeService(httpClient, apiVersion); } } } \ No newline at end of file diff --git a/Services/StampService.cs b/Services/StampService.cs index 1c8b6f6..71de1c4 100644 --- a/Services/StampService.cs +++ b/Services/StampService.cs @@ -2,6 +2,7 @@ using Fiscalapi.Common; using Fiscalapi.Http; using Fiscalapi.Models; +using System; using System.Threading.Tasks; namespace Fiscalapi.Services @@ -15,12 +16,29 @@ public StampService(IFiscalApiHttpClient httpClient, string apiVersion) public async Task> TransferStamps(StampTransactionParams requestModel) { + ValidateRequest(requestModel); return await HttpClient.PostAsync(BuildEndpoint(), requestModel); } public async Task> WithdrawStamps(StampTransactionParams requestModel) { + ValidateRequest(requestModel); return await HttpClient.PostAsync(BuildEndpoint(), requestModel); } + + private void ValidateRequest(StampTransactionParams requestModel) + { + if (requestModel == null) + throw new ArgumentNullException(nameof(requestModel), "No se acepta modelo de transaccion nulo"); + + if (string.IsNullOrWhiteSpace(requestModel.FromPersonId)) + throw new ArgumentException("Se requiere el ID de la persona de origen para la transferencia de timbres", nameof(requestModel.FromPersonId)); + + if (string.IsNullOrWhiteSpace(requestModel.ToPersonId)) + throw new ArgumentException("Se requiere el ID de la persona de destino para la transferencia de timbres", nameof(requestModel.ToPersonId)); + + if (requestModel.Amount <= 0) + throw new ArgumentException("La cantidad debe ser mayor que cero", nameof(requestModel.Amount)); + } } } diff --git a/fiscalapi-net.csproj b/fiscalapi-net.csproj index eecd8e1..16e6110 100644 --- a/fiscalapi-net.csproj +++ b/fiscalapi-net.csproj @@ -2,7 +2,7 @@ netstandard2.0;net461;net48;netcoreapp3.1;net5.0;net6.0;net8.0 - 4.0.360 + 4.0.361 $(Version) $(Version) $(Version)