denuncias

This commit is contained in:
2026-05-21 12:07:51 +02:00
parent 693d950dfa
commit b62cfd46c1
25 changed files with 1805 additions and 418 deletions

View File

@@ -1,4 +1,5 @@
using GestionaDenuncias.Shared.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
@@ -19,11 +20,16 @@ namespace ApiDenuncias.Services
{
private readonly HttpClient _http;
private readonly GestionaOptions _opts;
private readonly ILogger<GestionaService> _logger;
public GestionaService(HttpClient http, IOptions<GestionaOptions> optsAccessor)
public GestionaService(
HttpClient http,
IOptions<GestionaOptions> optsAccessor,
ILogger<GestionaService> logger)
{
_http = http;
_opts = optsAccessor.Value;
_logger = logger;
}
// =========================================================
@@ -58,7 +64,7 @@ namespace ApiDenuncias.Services
return null;
}
// Reemplaza este helper si quieres controlar la versi<EFBFBD>n en Accept:
// Reemplaza este helper si quieres controlar la versión en Accept:
private void AddTokenAndAccept(HttpRequestMessage req, string mediaType, string? version = null)
{
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
@@ -83,6 +89,17 @@ namespace ApiDenuncias.Services
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
private void LogDeprecatedHeaders(HttpResponseMessage response, string operation)
{
if (response.Headers.TryGetValues("X-Gestiona-Deprecated", out var deprecated))
{
_logger.LogWarning(
"Gestiona devolvio X-Gestiona-Deprecated en {Operation}: {Deprecated}",
operation,
string.Join(" | ", deprecated));
}
}
// =========================================================
@@ -113,65 +130,20 @@ namespace ApiDenuncias.Services
using var doc = JsonDocument.Parse(body);
var fileUrl = GetLinkHref(doc.RootElement, "file")
?? throw new InvalidOperationException("CreateFileAsync: Gestiona no ha devuelto link 'file'.");
var fileOpenUrl = GetLinkHref(doc.RootElement, "file-open");
var fileOpenUrl = GetLinkHref(doc.RootElement, "file-open")
?? throw new InvalidOperationException("CreateFileAsync: Gestiona no ha devuelto link 'file-open'.");
return new GestionaCreateFileResponse(fileUrl, fileOpenUrl);
}
private async Task<string> ResolveExternalProcedureCreateFileUrlAsync(Guid procedureId)
private Task<string> ResolveExternalProcedureCreateFileUrlAsync(Guid procedureId)
{
if (Guid.TryParse(_opts.ExternalProcedureId, out var configuredExternalProcedureId))
{
return $"/rest/catalog-2015/procedures/{procedureId}/external-procedures/{configuredExternalProcedureId}/create-file";
}
var externalProcedureId = Guid.TryParse(_opts.ExternalProcedureId, out var configuredExternalProcedureId)
? configuredExternalProcedureId
: procedureId;
using var req = new HttpRequestMessage(HttpMethod.Get, $"/rest/catalog-2015/procedures/{procedureId}/external-procedures");
AddTokenAndAccept(req, "application/vnd.gestiona.external-procedures-page+json");
using var resp = await _http.SendAsync(req);
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
{
throw new InvalidOperationException(
$"El procedimiento {procedureId} no tiene tramites externos configurados en Gestiona.");
}
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
{
throw new InvalidOperationException(
$"ResolveExternalProcedureCreateFileUrlAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
}
using var doc = JsonDocument.Parse(body);
if (!doc.RootElement.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array)
{
throw new InvalidOperationException(
$"El procedimiento {procedureId} no ha devuelto tramites externos validos en Gestiona.");
}
var createFileCandidates = new List<(string? Id, string Href)>();
foreach (var item in content.EnumerateArray())
{
var createFileHref = GetLinkHref(item, "create-file");
if (!string.IsNullOrWhiteSpace(createFileHref))
{
var externalProcedureId = item.TryGetProperty("id", out var idProp) && idProp.ValueKind == JsonValueKind.String
? idProp.GetString()
: null;
createFileCandidates.Add((externalProcedureId, createFileHref!));
}
}
if (createFileCandidates.Count == 0)
{
throw new InvalidOperationException(
$"El procedimiento {procedureId} no tiene ningun tramite externo con link create-file.");
}
return createFileCandidates
.FirstOrDefault(candidate => string.Equals(candidate.Id, procedureId.ToString(), StringComparison.OrdinalIgnoreCase))
.Href
?? createFileCandidates[0].Href;
return Task.FromResult(
$"/rest/catalog-2015/procedures/{procedureId}/external-procedures/{externalProcedureId}/create-file");
}
public async Task OpenFileAsync(
@@ -183,9 +155,13 @@ namespace ApiDenuncias.Services
string freeTitle,
string siaCode)
{
var url = string.IsNullOrWhiteSpace(fileOpenUrl)
? $"{fileUrl.TrimEnd('/')}/open"
: fileOpenUrl;
if (string.IsNullOrWhiteSpace(fileOpenUrl))
{
throw new InvalidOperationException(
"OpenFileAsync: falta el link 'file-open' devuelto por Gestiona. No se usa el fallback /open para evitar la ruta deprecated.");
}
var url = fileOpenUrl;
var payload = new
{
@@ -228,7 +204,8 @@ namespace ApiDenuncias.Services
{
Content = content
};
AddTokenAndAccept(req, "application/json");
req.Headers.TryAddWithoutValidation("Prefer", "return=minimal");
AddTokenAndAccept(req, "application/vnd.gestiona.file-folder+json", "1");
using var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
@@ -248,7 +225,8 @@ namespace ApiDenuncias.Services
{
using var createReq = new HttpRequestMessage(HttpMethod.Post, "/rest/uploads");
createReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
createReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
createReq.Headers.Accept.Add(
MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.upload+json"));
using var createResp = await _http.SendAsync(createReq);
var createBody = await createResp.Content.ReadAsStringAsync();
@@ -256,7 +234,7 @@ namespace ApiDenuncias.Services
throw new InvalidOperationException($"CreateUpload (POST): {(int)createResp.StatusCode} {createResp.StatusCode}\n{createBody}");
var uploadUri = createResp.Headers.Location?.ToString()
?? throw new InvalidOperationException("No se devolvi<EFBFBD> Location en /rest/uploads");
?? throw new InvalidOperationException("No se devolvió Location en /rest/uploads");
string md5Hex;
using (var md5 = MD5.Create())
@@ -269,8 +247,9 @@ namespace ApiDenuncias.Services
putReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
putReq.Headers.TryAddWithoutValidation("X-Gestiona-Upload-MD5", md5Hex);
putReq.Headers.TryAddWithoutValidation("Slug", fileName);
putReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
putReq.Headers.TryAddWithoutValidation("Prefer", "return=minimal");
putReq.Headers.Accept.Add(
MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.upload+json"));
putReq.Content = new ByteArrayContent(contentBytes);
putReq.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
@@ -279,11 +258,15 @@ namespace ApiDenuncias.Services
if (!putResp.IsSuccessStatusCode)
throw new InvalidOperationException($"CreateUpload (PUT): {(int)putResp.StatusCode} {putResp.StatusCode}\n{infoJson}");
using var infoDoc = JsonDocument.Parse(infoJson);
var status = infoDoc.RootElement.GetProperty("status").GetString();
if (status != "READY")
throw new InvalidOperationException($"Upload no READY: {status}");
if (!string.IsNullOrWhiteSpace(infoJson))
{
using var infoDoc = JsonDocument.Parse(infoJson);
var status = infoDoc.RootElement.TryGetProperty("status", out var statusProp)
? statusProp.GetString()
: "READY";
if (!string.Equals(status, "READY", StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException($"Upload no READY: {status}");
}
return uploadUri;
}
@@ -309,8 +292,10 @@ namespace ApiDenuncias.Services
using var metaReq = new HttpRequestMessage(HttpMethod.Post, $"{fileUrl}/documents-and-folders")
{ Content = metaContent };
metaReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
metaReq.Headers.TryAddWithoutValidation("Prefer", "return=minimal");
metaReq.Headers.Accept.Clear();
metaReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
metaReq.Headers.Accept.Add(
MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.file-document+json; version=4"));
using var metaResp = await _http.SendAsync(metaReq);
var body = await metaResp.Content.ReadAsStringAsync();
@@ -386,7 +371,7 @@ namespace ApiDenuncias.Services
if (thirdParty.IsLegalEntity)
{
if (string.IsNullOrWhiteSpace(thirdParty.BusinessName))
throw new ArgumentException("La raz<EFBFBD>n social es obligatoria para terceros jur<EFBFBD>dicos.", nameof(thirdParty));
throw new ArgumentException("La razón social es obligatoria para terceros jurídicos.", nameof(thirdParty));
}
else
{
@@ -485,7 +470,7 @@ namespace ApiDenuncias.Services
{
foreach (var item in content.EnumerateArray())
{
if (item.TryGetProperty("links", out var links))
if (item.TryGetProperty("links", out var links))
{
var third = links.EnumerateArray().FirstOrDefault(l => l.GetProperty("rel").GetString() == "third");
if (third.ValueKind != JsonValueKind.Undefined)
@@ -561,12 +546,15 @@ namespace ApiDenuncias.Services
};
}
// --- CONSULTAS DE EXPEDIENTES (sin recorrer hist<EFBFBD>rico paginado) ---
// --- CONSULTAS DE EXPEDIENTES (sin recorrer histórico paginado) ---
private async Task<string> GetFilesAsync(object? filter = null)
{
using var req = new HttpRequestMessage(HttpMethod.Get, "/rest/files");
AddBasicHeaders(req);
req.Headers.Accept.Clear();
req.Headers.Accept.Add(
MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.files-page+json"));
if (filter is not null)
{
@@ -575,6 +563,7 @@ namespace ApiDenuncias.Services
}
using var resp = await _http.SendAsync(req);
LogDeprecatedHeaders(resp, "GET /rest/files");
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
{
return "{\"content\":[]}";
@@ -612,7 +601,7 @@ namespace ApiDenuncias.Services
}
/// <summary>
/// Devuelve el JSON crudo de /rest/files acumulando hasta maxPages p<EFBFBD>ginas.
/// Devuelve el JSON crudo de /rest/files acumulando hasta maxPages páginas.
/// </summary>
public async Task<string> ListarExpedientesJsonAsyncBasico(int maxPages = 1)
{
@@ -683,9 +672,10 @@ namespace ApiDenuncias.Services
}
using var req = new HttpRequestMessage(HttpMethod.Get, fileUrl);
AddTokenAndAccept(req, "application/json");
AddTokenAndAccept(req, "application/vnd.gestiona.file+json", "2");
using var resp = await _http.SendAsync(req);
LogDeprecatedHeaders(resp, $"GET {fileUrl}");
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent ||
resp.StatusCode == System.Net.HttpStatusCode.NotFound)
{
@@ -910,7 +900,7 @@ namespace ApiDenuncias.Services
using var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"Error actualizando direcci<EFBFBD>n del tercero: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
throw new InvalidOperationException($"Error actualizando dirección del tercero: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
}
private async Task<bool> ThirdHasAddressesAsync(string thirdSelfHref)
@@ -1160,7 +1150,7 @@ namespace ApiDenuncias.Services
return value switch
{
"" => "ESP",
"es" or "esp" or "espana" or "espa<EFBFBD>a" or "spain" => "ESP",
"es" or "esp" or "espana" or "españa" or "spain" => "ESP",
"prt" or "pt" or "portugal" => "PRT",
_ when country is { Length: >= 3 } => country.Trim().ToUpperInvariant()[..3],
_ => "ESP",