Añadir endpoint de campos para Gestiona y adaptar login GlobalLeaks a DPoP
- Añade GET /api/denuncias/{id}/gestiona-fields para exponer los campos diarios requeridos por la integración con Gestiona.
- Devuelve un DTO cerrado con fecha, canal, resumen, datos de hechos, protección, sexo y preferencias de notificación.
- Adapta el login contra GlobalLeaks al nuevo flujo DPoP exigido desde la versión 5.0.94.
- Genera proof DPoP con clave EC P-256 efímera y lo envía en la cabecera DPoP junto a X-Token.
- Mejora el mensaje de error cuando GlobalLeaks rechaza el proof DPoP.
This commit is contained in:
@@ -12,6 +12,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ApiDenuncias.Services
|
||||
@@ -106,17 +107,12 @@ namespace ApiDenuncias.Services
|
||||
// Expedientes (file)
|
||||
// =========================================================
|
||||
|
||||
public async Task<GestionaCreateFileResponse> CreateFileAsync(Guid procedureId, string subject, string documentSeries, string siaCode)
|
||||
public async Task<GestionaCreateFileResponse> CreateFileAsync(string subject, string documentSeries, string siaCode)
|
||||
{
|
||||
_ = subject;
|
||||
_ = documentSeries;
|
||||
_ = siaCode;
|
||||
|
||||
var effectiveProcedureId = procedureId == Guid.Empty
|
||||
? Guid.Parse("82722c9b-cecc-4299-8a7b-ce5abeb8170b")
|
||||
: procedureId;
|
||||
|
||||
var url = await ResolveExternalProcedureCreateFileUrlAsync(effectiveProcedureId);
|
||||
var url = await ResolveExternalProcedureCreateFileUrlAsync(siaCode);
|
||||
using var req = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(
|
||||
@@ -138,24 +134,263 @@ namespace ApiDenuncias.Services
|
||||
return new GestionaCreateFileResponse(fileUrl, fileOpenUrl);
|
||||
}
|
||||
|
||||
private Task<string> ResolveExternalProcedureCreateFileUrlAsync(Guid procedureId)
|
||||
private async Task<string> ResolveExternalProcedureCreateFileUrlAsync(string siaCode)
|
||||
{
|
||||
var externalProcedureId = Guid.TryParse(_opts.ExternalProcedureId, out var configuredExternalProcedureId)
|
||||
? configuredExternalProcedureId
|
||||
: procedureId;
|
||||
var proceduresUrl = await ResolveRootLinkHrefAsync("vnd.gestiona.catalog-2015")
|
||||
?? "/rest/catalog-2015/procedures";
|
||||
var procedures = await GetContentArrayAsync(
|
||||
proceduresUrl,
|
||||
"application/vnd.gestiona.procedures-2015-page+json",
|
||||
"GET catalogo de procedimientos Gestiona");
|
||||
|
||||
return Task.FromResult(
|
||||
$"/rest/catalog-2015/procedures/{procedureId}/external-procedures/{externalProcedureId}/create-file");
|
||||
var procedure = SelectProcedure(procedures, siaCode);
|
||||
var externalProceduresUrl = GetLinkHref(procedure, "external-procedures")
|
||||
?? throw new InvalidOperationException(
|
||||
"El procedimiento seleccionado no contiene link 'external-procedures'.");
|
||||
var externalProcedures = await GetContentArrayAsync(
|
||||
externalProceduresUrl,
|
||||
"application/vnd.gestiona.external-procedures-2015-page+json",
|
||||
"GET procedimientos externos Gestiona");
|
||||
|
||||
var externalProcedure = SelectExternalProcedure(externalProcedures, procedure, siaCode);
|
||||
return GetLinkHref(externalProcedure, "create-file")
|
||||
?? throw new InvalidOperationException(
|
||||
"El procedimiento externo seleccionado no contiene link 'create-file'.");
|
||||
}
|
||||
|
||||
private async Task<string?> ResolveRootLinkHrefAsync(string rel)
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, "/rest");
|
||||
AddBasicHeaders(req);
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, "GET /rest");
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException($"ResolveRootLinkHrefAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
return ToAbsoluteGestionaHref(GetLinkHref(doc.RootElement, rel));
|
||||
}
|
||||
|
||||
private async Task<List<JsonElement>> GetContentArrayAsync(string url, string accept, string operation)
|
||||
{
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(accept));
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, operation);
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException($"{operation}: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var source = doc.RootElement.TryGetProperty("content", out var content) &&
|
||||
content.ValueKind == JsonValueKind.Array
|
||||
? content
|
||||
: doc.RootElement;
|
||||
|
||||
if (source.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return source.EnumerateArray().Select(item => item.Clone()).ToList();
|
||||
}
|
||||
|
||||
private JsonElement SelectProcedure(IReadOnlyList<JsonElement> procedures, string siaCode)
|
||||
{
|
||||
if (procedures.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Gestiona no ha devuelto procedimientos en el catalogo.");
|
||||
}
|
||||
|
||||
var configuredName = NormalizeKey(_opts.ProcedureName);
|
||||
if (!string.IsNullOrWhiteSpace(configuredName))
|
||||
{
|
||||
var match = procedures.FirstOrDefault(item =>
|
||||
NormalizeKey(GetJsonString(item, "name")).Contains(configuredName, StringComparison.Ordinal) ||
|
||||
NormalizeKey(GetJsonString(item, "title")).Contains(configuredName, StringComparison.Ordinal) ||
|
||||
NormalizeKey(GetJsonString(item, "description")).Contains(configuredName, StringComparison.Ordinal));
|
||||
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No se ha encontrado en Gestiona el procedimiento configurado '{_opts.ProcedureName}'. Procedimientos disponibles: {DescribeCatalogItems(procedures)}.");
|
||||
}
|
||||
|
||||
var normalizedSia = NormalizeKey(siaCode);
|
||||
if (!string.IsNullOrWhiteSpace(normalizedSia))
|
||||
{
|
||||
var match = procedures.FirstOrDefault(item =>
|
||||
NormalizeKey(GetJsonString(item, "sia_code")) == normalizedSia ||
|
||||
NormalizeKey(GetJsonString(item, "code")) == normalizedSia);
|
||||
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
if (procedures.Count == 1)
|
||||
{
|
||||
return procedures[0];
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Falta configurar Gestiona:ProcedureName para seleccionar el procedimiento sin usar IDs fijos.");
|
||||
}
|
||||
|
||||
private JsonElement SelectExternalProcedure(
|
||||
IReadOnlyList<JsonElement> externalProcedures,
|
||||
JsonElement selectedProcedure,
|
||||
string siaCode)
|
||||
{
|
||||
var withCreateFile = externalProcedures
|
||||
.Where(item => !string.IsNullOrWhiteSpace(GetLinkHref(item, "create-file")))
|
||||
.ToList();
|
||||
|
||||
if (withCreateFile.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("El procedimiento seleccionado no tiene procedimientos externos con link 'create-file'.");
|
||||
}
|
||||
|
||||
var configuredSia = NormalizeKey(
|
||||
string.IsNullOrWhiteSpace(_opts.ExternalProcedureSiaCode)
|
||||
? siaCode
|
||||
: _opts.ExternalProcedureSiaCode);
|
||||
if (!string.IsNullOrWhiteSpace(configuredSia))
|
||||
{
|
||||
var match = withCreateFile.FirstOrDefault(item =>
|
||||
NormalizeKey(GetJsonString(item, "sia_code")) == configuredSia ||
|
||||
NormalizeKey(GetJsonString(item, "code")) == configuredSia);
|
||||
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
var configuredName = NormalizeKey(_opts.ExternalProcedureName);
|
||||
if (!string.IsNullOrWhiteSpace(configuredName))
|
||||
{
|
||||
var match = withCreateFile.FirstOrDefault(item =>
|
||||
NormalizeKey(GetJsonString(item, "name")).Contains(configuredName, StringComparison.Ordinal) ||
|
||||
NormalizeKey(GetJsonString(item, "title")).Contains(configuredName, StringComparison.Ordinal));
|
||||
|
||||
if (match.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
var procedureId = GetJsonString(selectedProcedure, "id");
|
||||
if (!string.IsNullOrWhiteSpace(procedureId))
|
||||
{
|
||||
var sameId = withCreateFile.FirstOrDefault(item =>
|
||||
string.Equals(GetJsonString(item, "id"), procedureId, StringComparison.OrdinalIgnoreCase));
|
||||
if (sameId.ValueKind != JsonValueKind.Undefined)
|
||||
{
|
||||
return sameId;
|
||||
}
|
||||
}
|
||||
|
||||
if (withCreateFile.Count == 1)
|
||||
{
|
||||
return withCreateFile[0];
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No se puede seleccionar de forma inequivoca el procedimiento externo. Configura Gestiona:ExternalProcedureName o Gestiona:ExternalProcedureSiaCode. Disponibles: {DescribeCatalogItems(withCreateFile)}.");
|
||||
}
|
||||
|
||||
private async Task<string> ResolveAssignableGroupHrefAsync(string groupCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(groupCode))
|
||||
{
|
||||
throw new InvalidOperationException("Debe indicarse el codigo funcional del grupo de Gestiona.");
|
||||
}
|
||||
|
||||
var groupsUrl = await ResolveRootLinkHrefAsync("vnd.gestiona.files.assignees.groups")
|
||||
?? "/rest/files/assignees/groups";
|
||||
var groups = await GetContentArrayAsync(
|
||||
groupsUrl,
|
||||
"application/vnd.gestiona.groups-page+json",
|
||||
"GET grupos asignables Gestiona");
|
||||
|
||||
var normalizedCode = NormalizeKey(groupCode);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var code = NormalizeKey(GetJsonString(group, "code"));
|
||||
var name = NormalizeKey(GetJsonString(group, "name"));
|
||||
var title = NormalizeKey(GetJsonString(group, "title"));
|
||||
|
||||
if (code == normalizedCode ||
|
||||
name == normalizedCode ||
|
||||
title == normalizedCode ||
|
||||
name.StartsWith(normalizedCode + " ", StringComparison.Ordinal) ||
|
||||
title.StartsWith(normalizedCode + " ", StringComparison.Ordinal))
|
||||
{
|
||||
var href = ToAbsoluteGestionaHref(GetLinkHref(group, "self"));
|
||||
if (!string.IsNullOrWhiteSpace(href))
|
||||
{
|
||||
return href!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No se ha encontrado en Gestiona el grupo asignable '{groupCode}'. Grupos disponibles: {DescribeCatalogItems(groups)}.");
|
||||
}
|
||||
|
||||
private string? ToAbsoluteGestionaHref(string? href)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(href))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Uri.TryCreate(href, UriKind.Absolute, out _))
|
||||
{
|
||||
return href;
|
||||
}
|
||||
|
||||
return href.StartsWith("/", StringComparison.Ordinal)
|
||||
? $"{_opts.ApiBase.TrimEnd('/')}{href}"
|
||||
: href;
|
||||
}
|
||||
|
||||
private static string DescribeCatalogItems(IEnumerable<JsonElement> items)
|
||||
{
|
||||
var values = items
|
||||
.Select(item => FirstNonEmpty(
|
||||
GetJsonString(item, "name"),
|
||||
GetJsonString(item, "title"),
|
||||
GetJsonString(item, "code"),
|
||||
GetJsonString(item, "sia_code"),
|
||||
GetJsonString(item, "id")))
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
return values.Count == 0 ? "(sin nombres disponibles)" : string.Join("; ", values);
|
||||
}
|
||||
|
||||
public async Task OpenFileAsync(
|
||||
string fileUrl,
|
||||
string? fileOpenUrl,
|
||||
Guid managementUnitGroupId,
|
||||
Guid assignedGroupId,
|
||||
string assignedGroupCode,
|
||||
bool confidential,
|
||||
string freeTitle,
|
||||
string siaCode)
|
||||
string freeTitle)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileOpenUrl))
|
||||
{
|
||||
@@ -164,20 +399,24 @@ namespace ApiDenuncias.Services
|
||||
}
|
||||
|
||||
var url = fileOpenUrl;
|
||||
var assignedGroupHref = await ResolveAssignableGroupHrefAsync(assignedGroupCode);
|
||||
var managementGroupCode = string.IsNullOrWhiteSpace(_opts.ManagementUnitGroupCode)
|
||||
? "700"
|
||||
: _opts.ManagementUnitGroupCode.Trim();
|
||||
var managementGroupHref = await ResolveAssignableGroupHrefAsync(managementGroupCode);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
free_title = freeTitle,
|
||||
location = siaCode,
|
||||
entry_date = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
|
||||
confidential,
|
||||
initial_assignation = new[]
|
||||
{
|
||||
new { rel = "group", href = $"{_opts.ApiBase}/rest/groups/{assignedGroupId}" }
|
||||
new { rel = "group", href = assignedGroupHref }
|
||||
},
|
||||
links = new[]
|
||||
{
|
||||
new { rel = "management-unit-group", href = $"{_opts.ApiBase}/rest/groups/{managementUnitGroupId}" }
|
||||
new { rel = "management-unit-group", href = managementGroupHref }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -195,6 +434,67 @@ namespace ApiDenuncias.Services
|
||||
throw new InvalidOperationException($"OpenFileAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
public async Task AssignFileAsync(string fileUrl, string assignedGroupCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||
{
|
||||
throw new InvalidOperationException("AssignFileAsync: falta la URL del expediente.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(assignedGroupCode))
|
||||
{
|
||||
throw new InvalidOperationException("AssignFileAsync: falta el grupo asignado.");
|
||||
}
|
||||
|
||||
var assigneesUrl = await ResolveFileAssigneesUrlAsync(fileUrl);
|
||||
var assignedGroupHref = await ResolveAssignableGroupHrefAsync(assignedGroupCode);
|
||||
var payload = new
|
||||
{
|
||||
links = new[]
|
||||
{
|
||||
new { rel = "group", href = assignedGroupHref }
|
||||
}
|
||||
};
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.links");
|
||||
|
||||
using var req = new HttpRequestMessage(HttpMethod.Put, assigneesUrl)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.links"));
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, "PUT file assignees");
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException($"AssignFileAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ResolveFileAssigneesUrlAsync(string fileUrl)
|
||||
{
|
||||
var normalizedFileUrl = fileUrl.TrimEnd('/');
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, normalizedFileUrl);
|
||||
AddTokenAndAccept(req, "application/vnd.gestiona.file+json", "2");
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, $"GET {normalizedFileUrl}");
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"ResolveFileAssigneesUrlAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
return ToAbsoluteGestionaHref(GetLinkHref(doc.RootElement, "assignees"))
|
||||
?? $"{normalizedFileUrl}/assignees";
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateFolderAsync(string fileUrl, string folderName)
|
||||
{
|
||||
var endpoint = $"{fileUrl}/documents-and-folders";
|
||||
@@ -336,16 +636,16 @@ namespace ApiDenuncias.Services
|
||||
{
|
||||
var filtro = new
|
||||
{
|
||||
result = new { max_results = 25 },
|
||||
filter = new { nif }
|
||||
nif
|
||||
};
|
||||
var jsonFiltro = JsonSerializer.Serialize(filtro);
|
||||
var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(jsonFiltro));
|
||||
var url = $"{_opts.ApiBase}/rest/thirds?filter-view={Uri.EscapeDataString(b64)}";
|
||||
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, "/rest/thirds");
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.TryAddWithoutValidation("Accept", "application/vnd.gestiona.thirds-page+json");
|
||||
req.Content = new StringContent(
|
||||
JsonSerializer.Serialize(filtro),
|
||||
Encoding.UTF8,
|
||||
"application/vnd.gestiona.filter.thirds+json");
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
@@ -507,14 +807,18 @@ namespace ApiDenuncias.Services
|
||||
throw new InvalidOperationException($"Error EnlazarTerceroExistenteAsync: {resp.StatusCode}\n{body}");
|
||||
}
|
||||
|
||||
public async Task AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty)
|
||||
public async Task<GestionaEnsureThirdResponse> AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty)
|
||||
{
|
||||
if (thirdParty is null)
|
||||
throw new ArgumentNullException(nameof(thirdParty));
|
||||
|
||||
thirdParty = NormalizeThirdParty(thirdParty);
|
||||
var warnings = new List<string>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(thirdParty.DocumentId)) return;
|
||||
if (string.IsNullOrWhiteSpace(thirdParty.DocumentId))
|
||||
{
|
||||
return new GestionaEnsureThirdResponse(true, warnings);
|
||||
}
|
||||
|
||||
var encontrado = await BuscarTerceroPorNifAsync(thirdParty.DocumentId);
|
||||
|
||||
@@ -525,19 +829,175 @@ namespace ApiDenuncias.Services
|
||||
_logger.LogWarning(
|
||||
"Se omite la creacion/enlace del tercero en Gestiona para el expediente {FileUrl}: datos identificativos incompletos.",
|
||||
fileUrl);
|
||||
return;
|
||||
return new GestionaEnsureThirdResponse(true, warnings);
|
||||
}
|
||||
|
||||
encontrado = await CrearTerceroAsync(thirdParty);
|
||||
}
|
||||
else if (thirdParty.Address?.HasAnyValue == true)
|
||||
else
|
||||
{
|
||||
await TryEnsureThirdAddressAsync(encontrado.SelfHref, thirdParty.Address);
|
||||
warnings.AddRange(await BuildThirdPartyDifferenceWarningsAsync(encontrado.SelfHref, thirdParty));
|
||||
if (thirdParty.Address?.HasAnyValue == true)
|
||||
{
|
||||
await TryEnsureThirdAddressAsync(encontrado.SelfHref, thirdParty.Address);
|
||||
}
|
||||
}
|
||||
|
||||
var yaEnlazados = await ObtenerTercerosEnlazadosAsync(fileUrl);
|
||||
if (!yaEnlazados.Contains(encontrado.SelfHref))
|
||||
await EnlazarTerceroExistenteAsync(fileUrl, encontrado.SelfHref);
|
||||
|
||||
return new GestionaEnsureThirdResponse(true, warnings);
|
||||
}
|
||||
|
||||
private async Task<List<string>> BuildThirdPartyDifferenceWarningsAsync(
|
||||
string thirdSelfHref,
|
||||
ThirdPartyIdentityData thirdParty)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
var details = await GetThirdPartyDetailsAsync(thirdSelfHref);
|
||||
if (details is null)
|
||||
{
|
||||
return warnings;
|
||||
}
|
||||
|
||||
var gestionaType = GetJsonString(details.Value, "type");
|
||||
var expectedType = thirdParty.IsLegalEntity ? "JURIDICAL" : "PHISIC";
|
||||
AddThirdDifferenceWarning(warnings, "tipo de tercero", expectedType, gestionaType, normalizeAsCode: true);
|
||||
|
||||
if (thirdParty.IsLegalEntity)
|
||||
{
|
||||
AddThirdDifferenceWarning(
|
||||
warnings,
|
||||
"razon social",
|
||||
thirdParty.BusinessName,
|
||||
GetJsonString(details.Value, "business_name"));
|
||||
}
|
||||
else
|
||||
{
|
||||
AddThirdDifferenceWarning(
|
||||
warnings,
|
||||
"nombre",
|
||||
thirdParty.FirstName,
|
||||
GetJsonString(details.Value, "first_name"));
|
||||
|
||||
var gestionaLastName = string.Join(
|
||||
' ',
|
||||
new[]
|
||||
{
|
||||
GetJsonString(details.Value, "first_surname"),
|
||||
GetJsonString(details.Value, "second_surname")
|
||||
}.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||
AddThirdDifferenceWarning(warnings, "apellidos", thirdParty.LastName, gestionaLastName);
|
||||
}
|
||||
|
||||
AddThirdDifferenceWarning(warnings, "email", thirdParty.Email, GetJsonString(details.Value, "email"));
|
||||
AddThirdDifferenceWarning(
|
||||
warnings,
|
||||
"pais del documento",
|
||||
NormalizeCountryCode(thirdParty.CountryCode),
|
||||
GetJsonString(details.Value, "nif_country"),
|
||||
normalizeAsCode: true);
|
||||
AddThirdDifferenceWarning(
|
||||
warnings,
|
||||
"canal de notificacion",
|
||||
BuildNotificationChannel(thirdParty),
|
||||
GetJsonString(details.Value, "notification_channel"),
|
||||
normalizeAsCode: true);
|
||||
|
||||
if (warnings.Count > 0)
|
||||
{
|
||||
warnings.Insert(
|
||||
0,
|
||||
"El tercero ya existia en Gestiona y no se han sustituido sus datos. Si alguno debe cambiarse, actualizalo manualmente en Gestiona.");
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
private async Task<JsonElement?> GetThirdPartyDetailsAsync(string thirdSelfHref)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(thirdSelfHref))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, thirdSelfHref);
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.third+json; version=3"));
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, "GET third");
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
|
||||
private static void AddThirdDifferenceWarning(
|
||||
List<string> warnings,
|
||||
string fieldName,
|
||||
string? complaintValue,
|
||||
string? gestionaValue,
|
||||
bool normalizeAsCode = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(complaintValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var left = normalizeAsCode
|
||||
? NormalizeKey(complaintValue)
|
||||
: NormalizeForComparison(complaintValue);
|
||||
var right = normalizeAsCode
|
||||
? NormalizeKey(gestionaValue)
|
||||
: NormalizeForComparison(gestionaValue);
|
||||
|
||||
if (string.Equals(left, right, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
warnings.Add(
|
||||
$"El campo {fieldName} no coincide con Gestiona (denuncia: '{FormatWarningValue(complaintValue)}'; Gestiona: '{FormatWarningValue(gestionaValue)}').");
|
||||
}
|
||||
|
||||
private static string NormalizeForComparison(string? value)
|
||||
{
|
||||
return string.Join(
|
||||
' ',
|
||||
(value ?? string.Empty)
|
||||
.Trim()
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||
.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private static string FormatWarningValue(string? value)
|
||||
{
|
||||
var normalized = string.Join(
|
||||
' ',
|
||||
(value ?? string.Empty)
|
||||
.Trim()
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries));
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return "sin dato";
|
||||
}
|
||||
|
||||
const int maxLength = 80;
|
||||
return normalized.Length <= maxLength
|
||||
? normalized
|
||||
: normalized[..maxLength] + "...";
|
||||
}
|
||||
|
||||
private static ThirdPartyIdentityData NormalizeThirdParty(ThirdPartyIdentityData thirdParty)
|
||||
@@ -563,6 +1023,9 @@ namespace ApiDenuncias.Services
|
||||
BusinessName = businessName,
|
||||
Email = (thirdParty.Email ?? string.Empty).Trim(),
|
||||
CountryCode = string.IsNullOrWhiteSpace(thirdParty.CountryCode) ? "ESP" : thirdParty.CountryCode.Trim(),
|
||||
NotificationPreference = thirdParty.NotificationPreference ?? string.Empty,
|
||||
ElectronicNotification = thirdParty.ElectronicNotification ?? string.Empty,
|
||||
PostalNotificationPreference = thirdParty.PostalNotificationPreference ?? string.Empty,
|
||||
Address = thirdParty.Address
|
||||
};
|
||||
}
|
||||
@@ -577,6 +1040,9 @@ namespace ApiDenuncias.Services
|
||||
BusinessName = string.Empty,
|
||||
Email = string.Empty,
|
||||
CountryCode = string.IsNullOrWhiteSpace(thirdParty.CountryCode) ? "ESP" : thirdParty.CountryCode,
|
||||
NotificationPreference = thirdParty.NotificationPreference ?? string.Empty,
|
||||
ElectronicNotification = thirdParty.ElectronicNotification ?? string.Empty,
|
||||
PostalNotificationPreference = thirdParty.PostalNotificationPreference ?? string.Empty,
|
||||
Address = null
|
||||
};
|
||||
}
|
||||
@@ -718,6 +1184,51 @@ namespace ApiDenuncias.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<GestionaExpedienteInfo?> BuscarExpedientePorCodigoAsync(string codigoExpediente)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(codigoExpediente))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalizedCode = codigoExpediente.Trim();
|
||||
var json = await GetFilesAsync(new
|
||||
{
|
||||
code = normalizedCode
|
||||
});
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
|
||||
if (!TryGetFilesContent(doc, out var content) || content.GetArrayLength() == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
GestionaExpedienteInfo? first = null;
|
||||
foreach (var item in content.EnumerateArray())
|
||||
{
|
||||
var expediente = BuildExpedienteInfo(item);
|
||||
if (expediente is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (first is null)
|
||||
{
|
||||
first = expediente;
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
expediente.CodigoExpediente?.Trim(),
|
||||
normalizedCode,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return expediente;
|
||||
}
|
||||
}
|
||||
|
||||
return first;
|
||||
}
|
||||
|
||||
public async Task<GestionaExpedienteInfo?> ObtenerExpedienteAsync(string fileUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||
@@ -797,13 +1308,78 @@ namespace ApiDenuncias.Services
|
||||
}
|
||||
|
||||
|
||||
public async Task<GestionaAuditInfo?> ObtenerUltimaAuditoriaExpedienteAsync(string fileUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var auditUrl = $"{fileUrl.TrimEnd('/')}/audit";
|
||||
using var req = new HttpRequestMessage(HttpMethod.Get, auditUrl);
|
||||
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
|
||||
req.Headers.Accept.Clear();
|
||||
req.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/vnd.gestiona.audit.entries-page"));
|
||||
|
||||
using var resp = await _http.SendAsync(req);
|
||||
LogDeprecatedHeaders(resp, $"GET {auditUrl}");
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent ||
|
||||
resp.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"ObtenerUltimaAuditoriaExpedienteAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
if (!doc.RootElement.TryGetProperty("content", out var content) ||
|
||||
content.ValueKind != JsonValueKind.Array ||
|
||||
content.GetArrayLength() == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
GestionaAuditInfo? latest = null;
|
||||
foreach (var item in content.EnumerateArray())
|
||||
{
|
||||
var entryDate = GetJsonDateTimeOffset(item, "entry_date");
|
||||
if (entryDate is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (latest?.Fecha is not null && entryDate.Value <= latest.Fecha.Value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
latest = new GestionaAuditInfo
|
||||
{
|
||||
Fecha = entryDate,
|
||||
Mensaje = GetJsonString(item, "message")
|
||||
};
|
||||
}
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
public async Task<List<ExpedienteTerceroDto>> ObtenerExpedientesPorTerceroAsync(
|
||||
string nif,
|
||||
DateTimeOffset? desde = null,
|
||||
DateTimeOffset? hasta = null,
|
||||
int maxPages = 1,
|
||||
int maxResults = 30,
|
||||
int maxParallel = 6 // de momento NO se usa, dejamos la firma por compatibilidad
|
||||
int maxParallel = 6
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(nif))
|
||||
@@ -811,21 +1387,11 @@ namespace ApiDenuncias.Services
|
||||
|
||||
nif = nif.Trim().ToUpperInvariant();
|
||||
_ = maxPages;
|
||||
_ = maxParallel;
|
||||
|
||||
// 1) Localizar el tercero por NIF
|
||||
var tercero = await BuscarTerceroPorNifAsync(nif);
|
||||
if (string.IsNullOrEmpty(tercero.SelfHref))
|
||||
return new List<ExpedienteTerceroDto>(); // no hay tercero => no hay expedientes
|
||||
|
||||
var resultados = new List<ExpedienteTerceroDto>();
|
||||
var json = await GetFilesAsync(new
|
||||
{
|
||||
third_rest_link = new
|
||||
{
|
||||
rel = "third",
|
||||
href = tercero.SelfHref
|
||||
}
|
||||
dni = nif
|
||||
});
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
|
||||
@@ -841,20 +1407,15 @@ namespace ApiDenuncias.Services
|
||||
break;
|
||||
}
|
||||
|
||||
DateTimeOffset? creation = null;
|
||||
if (item.TryGetProperty("creation_date", out var pCreation))
|
||||
{
|
||||
if (pCreation.ValueKind == JsonValueKind.Number &&
|
||||
pCreation.TryGetInt64(out var ts))
|
||||
{
|
||||
creation = DateTimeOffset.FromUnixTimeSeconds(ts);
|
||||
}
|
||||
else if (pCreation.ValueKind == JsonValueKind.String &&
|
||||
long.TryParse(pCreation.GetString(), out var tsString))
|
||||
{
|
||||
creation = DateTimeOffset.FromUnixTimeSeconds(tsString);
|
||||
}
|
||||
}
|
||||
var creation = GetJsonDateTimeOffset(item, "creation_date");
|
||||
var updated = GetFirstJsonDateTimeOffset(
|
||||
item,
|
||||
"update_date",
|
||||
"updated_at",
|
||||
"modification_date",
|
||||
"modified_at",
|
||||
"last_update",
|
||||
"last_modified");
|
||||
|
||||
if (desde.HasValue && creation.HasValue && creation.Value < desde.Value)
|
||||
continue;
|
||||
@@ -905,14 +1466,67 @@ namespace ApiDenuncias.Services
|
||||
FileUrl = fileUrl,
|
||||
CodigoExpediente = code,
|
||||
Asunto = asunto,
|
||||
Procedimiento = procedureName,
|
||||
FechaCreacion = creation,
|
||||
FechaUltimaModificacion = updated ?? creation,
|
||||
Estado = state
|
||||
});
|
||||
}
|
||||
|
||||
await EnrichExpedientesWithAuditAsync(resultados, maxParallel);
|
||||
return resultados;
|
||||
}
|
||||
|
||||
private async Task EnrichExpedientesWithAuditAsync(
|
||||
IReadOnlyList<ExpedienteTerceroDto> expedientes,
|
||||
int maxParallel)
|
||||
{
|
||||
if (expedientes.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var parallelism = Math.Clamp(maxParallel, 1, 6);
|
||||
using var gate = new SemaphoreSlim(parallelism, parallelism);
|
||||
var tasks = expedientes
|
||||
.Where(expediente => !string.IsNullOrWhiteSpace(expediente.FileUrl))
|
||||
.Select(async expediente =>
|
||||
{
|
||||
await gate.WaitAsync();
|
||||
try
|
||||
{
|
||||
var audit = await ObtenerUltimaAuditoriaExpedienteAsync(expediente.FileUrl);
|
||||
if (audit is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (audit.Fecha is not null)
|
||||
{
|
||||
expediente.FechaUltimaModificacion = audit.Fecha;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(audit.Mensaje))
|
||||
{
|
||||
expediente.UltimaAuditoriaMensaje = audit.Mensaje;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"No se ha podido consultar la auditoria del expediente {FileUrl}.",
|
||||
expediente.FileUrl);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task TryEnsureThirdAddressAsync(string thirdSelfHref, ThirdPartyAddressData address)
|
||||
{
|
||||
if (!address.HasAnyValue)
|
||||
@@ -1138,6 +1752,58 @@ namespace ApiDenuncias.Services
|
||||
: null;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? GetFirstJsonDateTimeOffset(JsonElement item, params string[] propertyNames)
|
||||
{
|
||||
foreach (var propertyName in propertyNames)
|
||||
{
|
||||
var value = GetJsonDateTimeOffset(item, propertyName);
|
||||
if (value is not null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DateTimeOffset? GetJsonDateTimeOffset(JsonElement item, string propertyName)
|
||||
{
|
||||
if (!item.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (property.ValueKind == JsonValueKind.Number &&
|
||||
property.TryGetInt64(out var timestamp))
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(timestamp);
|
||||
}
|
||||
|
||||
if (property.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var raw = property.GetString();
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var timestampString))
|
||||
{
|
||||
return DateTimeOffset.FromUnixTimeSeconds(timestampString);
|
||||
}
|
||||
|
||||
return DateTimeOffset.TryParse(
|
||||
raw.Replace("Z", "+00:00", StringComparison.Ordinal),
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string? FirstNonEmpty(params string?[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
@@ -1171,7 +1837,25 @@ namespace ApiDenuncias.Services
|
||||
|
||||
private static string BuildNotificationChannel(ThirdPartyIdentityData thirdParty)
|
||||
{
|
||||
_ = thirdParty;
|
||||
var notificationText = NormalizeKey(string.Join(
|
||||
' ',
|
||||
thirdParty.NotificationPreference,
|
||||
thirdParty.ElectronicNotification,
|
||||
thirdParty.PostalNotificationPreference));
|
||||
|
||||
var requestsPostal =
|
||||
notificationText.Contains("CORREO POSTAL", StringComparison.Ordinal) ||
|
||||
notificationText.Contains("POSTAL", StringComparison.Ordinal);
|
||||
var requestsElectronic =
|
||||
notificationText.Contains("ELECTRONICA", StringComparison.Ordinal) ||
|
||||
notificationText.Contains("TELEMATICA", StringComparison.Ordinal) ||
|
||||
notificationText.Contains("ONLINE", StringComparison.Ordinal);
|
||||
|
||||
if (requestsPostal && thirdParty.Address?.HasAnyValue == true && !requestsElectronic)
|
||||
{
|
||||
return "PAPER";
|
||||
}
|
||||
|
||||
return "TELEMATIC";
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user