diff --git a/DoliMiddlewareApi/Controllers/VeriFactuController.cs b/DoliMiddlewareApi/Controllers/VeriFactuController.cs new file mode 100644 index 0000000..61ed6b0 --- /dev/null +++ b/DoliMiddlewareApi/Controllers/VeriFactuController.cs @@ -0,0 +1,90 @@ +using System.Text.Json; +using DoliMiddlewareApi.Dtos.VeriFactu; +using DoliMiddlewareApi.Services.VeriFactu; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DoliMiddlewareApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class VeriFactuController(VeriFactuApiClient verifactuApiClient) : ControllerBase +{ + [HttpGet("health")] + [AllowAnonymous] + public async Task Health() + { + try + { + var result = await verifactuApiClient.GetHealthAsync(); + return Content(result.Content ?? "{\"status\":\"ok\"}", result.ContentType ?? "application/json"); + } + catch (HttpRequestException) + { + return Ok(new + { + status = "down", + error = $"Connection refused ({verifactuApiClient.BaseUrl})", + upstream = verifactuApiClient.BaseUrl + }); + } + } + + [HttpGet("public-key")] + [AllowAnonymous] + public async Task PublicKey() + { + return await Proxy(verifactuApiClient.GetPublicKeyAsync()); + } + + [HttpGet("formats")] + [AllowAnonymous] + public async Task Formats() + { + return await Proxy(verifactuApiClient.GetFormatsAsync()); + } + + [HttpPost("certificates/register")] + public async Task RegisterCertificate([FromBody] RegisterCertificateDto dto) + { + var logger = HttpContext.RequestServices.GetRequiredService>(); + logger.LogInformation("[VeriFactuController] RegisterCertificate called. CertName={CertName}, CertFile length={CertFileLen}, Password length={PasswordLen}", + dto.CertName, dto.CertFile?.Length ?? 0, dto.Password?.Length ?? 0); + return await Proxy(verifactuApiClient.RegisterCertificateAsync(dto)); + } + + [HttpPost("facturas")] + public async Task SendInvoice([FromBody] JsonElement payload) + { + return await Proxy(verifactuApiClient.SendInvoiceAsync(payload)); + } + + [HttpPost("facturas/anular")] + public async Task CancelInvoice([FromBody] JsonElement payload) + { + return await Proxy(verifactuApiClient.CancelInvoiceAsync(payload)); + } + + private async Task Proxy(Task task) + { + try + { + var result = await task; + return new ContentResult + { + StatusCode = result.StatusCode, + Content = result.Content, + ContentType = result.ContentType + }; + } + catch (HttpRequestException) + { + return StatusCode(StatusCodes.Status503ServiceUnavailable, new + { + title = "VeriFactu unavailable", + status = 503, + detail = $"No se puede conectar con VeriFactu en {verifactuApiClient.BaseUrl}" + }); + } + } +} \ No newline at end of file diff --git a/DoliMiddlewareApi/Dtos/VeriFactu/ProxyResponse.cs b/DoliMiddlewareApi/Dtos/VeriFactu/ProxyResponse.cs new file mode 100644 index 0000000..94fc0ef --- /dev/null +++ b/DoliMiddlewareApi/Dtos/VeriFactu/ProxyResponse.cs @@ -0,0 +1,3 @@ +namespace DoliMiddlewareApi.Dtos.VeriFactu; + +public record ProxyResponse(int StatusCode, string? Content, string? ContentType); \ No newline at end of file diff --git a/DoliMiddlewareApi/Dtos/VeriFactu/RegisterCertificateDto.cs b/DoliMiddlewareApi/Dtos/VeriFactu/RegisterCertificateDto.cs new file mode 100644 index 0000000..942f81a --- /dev/null +++ b/DoliMiddlewareApi/Dtos/VeriFactu/RegisterCertificateDto.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace DoliMiddlewareApi.Dtos.VeriFactu; + +public class RegisterCertificateDto +{ + [Required] + [StringLength(100)] + [JsonPropertyName("cert_name")] + public string CertName { get; set; } = string.Empty; + + [Required] + [JsonPropertyName("cert_file")] + public string CertFile { get; set; } = string.Empty; + + [Required] + [StringLength(1024)] + [JsonPropertyName("password")] + public string Password { get; set; } = string.Empty; +} diff --git a/DoliMiddlewareApi/Dtos/query/InvoiceDetailDto.cs b/DoliMiddlewareApi/Dtos/query/InvoiceDetailDto.cs index 94bc271..4690888 100644 --- a/DoliMiddlewareApi/Dtos/query/InvoiceDetailDto.cs +++ b/DoliMiddlewareApi/Dtos/query/InvoiceDetailDto.cs @@ -7,6 +7,7 @@ public class InvoiceDetailDto public DateTime? Date { get; set; } public DateTime? ExpireDate { get; set; } public int ClientId { get; set; } + public string? ClientName { get; set; } public decimal? TotalHt { get; set; } public decimal? TotalTax { get; set; } public decimal? Total { get; set; } diff --git a/DoliMiddlewareApi/Program.cs b/DoliMiddlewareApi/Program.cs index 4adc6a8..b82b5ea 100644 --- a/DoliMiddlewareApi/Program.cs +++ b/DoliMiddlewareApi/Program.cs @@ -5,6 +5,7 @@ using DoliMiddlewareApi.Services; using DoliMiddlewareApi.Services.Auth; using DoliMiddlewareApi.Services.Clients; using DoliMiddlewareApi.Services.Notifications; +using DoliMiddlewareApi.Services.VeriFactu; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Diagnostics; using Microsoft.Extensions.Caching.Memory; @@ -136,12 +137,21 @@ builder.Services.AddHttpClient("Dolibarr", client => client.BaseAddress = new Uri(baseUrl); }); +builder.Services.AddHttpClient("VeriFactu", client => +{ + var baseUrl = builder.Configuration["VeriFactu:ApiUrl"] ?? "http://localhost:6789/"; + if (!baseUrl.EndsWith('/')) baseUrl += '/'; + client.BaseAddress = new Uri(baseUrl); + client.Timeout = TimeSpan.FromSeconds(30); +}); + builder.Services.AddScoped(sp => { var factory = sp.GetRequiredService(); var client = factory.CreateClient("Dolibarr"); var tokenCacheService = sp.GetRequiredService(); - return new DolibarrApiClient(client, tokenCacheService); + var config = sp.GetRequiredService(); + return new DolibarrApiClient(client, tokenCacheService, config); }); // ========================================= @@ -163,6 +173,13 @@ builder.Services.AddScoped(); // Servicio de negocio (setup/diccionarios) builder.Services.AddScoped(); +// Cliente de VeriFactu MidAPI +builder.Services.AddScoped(sp => +{ + var factory = sp.GetRequiredService(); + return new VeriFactuApiClient(factory.CreateClient("VeriFactu")); +}); + // Servicio de negocio (banco) builder.Services.AddScoped(); diff --git a/DoliMiddlewareApi/Services/Clients/DolibarrApiClient.cs b/DoliMiddlewareApi/Services/Clients/DolibarrApiClient.cs index 2add79a..fef7c60 100644 --- a/DoliMiddlewareApi/Services/Clients/DolibarrApiClient.cs +++ b/DoliMiddlewareApi/Services/Clients/DolibarrApiClient.cs @@ -1,10 +1,11 @@ using System.Net; using DoliMiddlewareApi.Exceptions; using DoliMiddlewareApi.Services.Auth; +using Microsoft.Extensions.Configuration; namespace DoliMiddlewareApi.Services.Clients; -public class DolibarrApiClient(HttpClient httpClient, DolibarrTokenCacheService tokenCacheService) +public class DolibarrApiClient(HttpClient httpClient, DolibarrTokenCacheService tokenCacheService, IConfiguration configuration) : IDolibarrApiClient { public async Task GetResourceAsync(string endpoint) where T : class @@ -76,11 +77,10 @@ public class DolibarrApiClient(HttpClient httpClient, DolibarrTokenCacheService private void AddDolibarrTokenHeader(HttpRequestMessage request) { - var dolibarrToken = tokenCacheService.GetDolibarrToken(); - if (!string.IsNullOrEmpty(dolibarrToken)) - { - request.Headers.Add("DOLAPIKEY", dolibarrToken); - } + var sharedKey = configuration["Dolibarr:ApiKey"]; + var token = !string.IsNullOrEmpty(sharedKey) ? sharedKey : tokenCacheService.GetDolibarrToken(); + if (!string.IsNullOrEmpty(token)) + request.Headers.Add("DOLAPIKEY", token); } diff --git a/DoliMiddlewareApi/Services/InvoiceService.cs b/DoliMiddlewareApi/Services/InvoiceService.cs index 0946035..522db99 100644 --- a/DoliMiddlewareApi/Services/InvoiceService.cs +++ b/DoliMiddlewareApi/Services/InvoiceService.cs @@ -16,7 +16,19 @@ public class InvoiceService(IDolibarrApiClient apiClient, INotificationService n public async Task GetInvoiceAsync(int id) { var data = await apiClient.GetResourceAsync($"invoices/{id}"); - return InvoiceMapper.MapToInvoiceDetailDto(data); + var dto = InvoiceMapper.MapToInvoiceDetailDto(data); + + if (dto.ClientId > 0) + { + try + { + var client = await apiClient.GetResourceAsync($"thirdparties/{dto.ClientId}"); + dto.ClientName = client?.name; + } + catch { /* best-effort */ } + } + + return dto; } public async Task> GetInvoicesAsync( diff --git a/DoliMiddlewareApi/Services/VeriFactu/VeriFactuApiClient.cs b/DoliMiddlewareApi/Services/VeriFactu/VeriFactuApiClient.cs new file mode 100644 index 0000000..8b9a94d --- /dev/null +++ b/DoliMiddlewareApi/Services/VeriFactu/VeriFactuApiClient.cs @@ -0,0 +1,52 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using DoliMiddlewareApi.Dtos.VeriFactu; + +namespace DoliMiddlewareApi.Services.VeriFactu; + +public class VeriFactuApiClient(HttpClient httpClient) +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + + public string BaseUrl => httpClient.BaseAddress?.ToString() ?? "http://host.docker.internal:6789/"; + + public Task GetHealthAsync() => SendAsync(HttpMethod.Get, "api/v1/health"); + + public Task GetPublicKeyAsync() => SendAsync(HttpMethod.Get, "api/v1/auth/public-key"); + + public Task GetFormatsAsync() => SendAsync(HttpMethod.Get, "api/v1/formats"); + + public async Task RegisterCertificateAsync(RegisterCertificateDto dto) + { + var payload = new + { + cert_name = dto.CertName, + cert_file = dto.CertFile, + password_encrypted = dto.Password + }; + + return await SendAsync(HttpMethod.Post, "api/v1/auth/register", payload); + } + + public Task SendInvoiceAsync(JsonElement payload) + => SendAsync(HttpMethod.Post, "api/v1/facturas", payload); + + public Task CancelInvoiceAsync(JsonElement payload) + => SendAsync(HttpMethod.Post, "api/v1/facturas/anular", payload); + + private async Task SendAsync(HttpMethod method, string path, object? payload = null) + { + using var request = new HttpRequestMessage(method, path); + + if (payload is not null) + { + request.Content = JsonContent.Create(payload, options: JsonOptions); + } + + using var response = await httpClient.SendAsync(request); + var content = response.Content is null ? string.Empty : await response.Content.ReadAsStringAsync(); + var contentType = response.Content?.Headers.ContentType?.ToString() ?? "application/json"; + + return new ProxyResponse((int)response.StatusCode, content, contentType); + } +} \ No newline at end of file diff --git a/DoliMiddlewareApi/appsettings.json b/DoliMiddlewareApi/appsettings.json index ca0f103..9d641fc 100644 --- a/DoliMiddlewareApi/appsettings.json +++ b/DoliMiddlewareApi/appsettings.json @@ -3,13 +3,14 @@ "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", - "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None" + "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None", + "System.Net.Http.HttpClient.VeriFactu": "None" } }, "AllowedHosts": "*", "Dolibarr": { "ApiUrl": "http://localhost/api/index.php", - "ApiKey": "" + "ApiKey": "847ad838254caa863c2da3dc949a5028e2317be9" }, "Jwt": { "Secret": "", @@ -18,5 +19,8 @@ }, "Notifications": { "WebhookUrl": "" + }, + "VeriFactu": { + "ApiUrl": "http://localhost:6789/" } } \ No newline at end of file