Files
Antifraude.Net/Antifraude.Net/GestionaDenunciasAN/Services/GestionaService.cs
2026-04-06 17:12:06 +02:00

1105 lines
44 KiB
C#

using GestionaDenunciasAN.Models;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace GestionaDenunciasAN.Services
{
public class GestionaService : IGestionaService
{
private readonly HttpClient _http;
private readonly GestionaOptions _opts;
public GestionaService(HttpClient http, IOptions<GestionaOptions> optsAccessor)
{
_http = http;
_opts = optsAccessor.Value;
}
// =========================================================
// Helpers
// =========================================================
private static string BuildFilterViewParam(object filterView)
{
var json = JsonSerializer.Serialize(filterView);
var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
return $"filter-view={Uri.EscapeDataString(b64)}";
}
// 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);
req.Headers.Accept.Clear();
if (string.IsNullOrWhiteSpace(version))
{
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
}
else
{
var mt = new MediaTypeWithQualityHeaderValue(mediaType);
mt.Parameters.Add(new NameValueHeaderValue("version", version));
req.Headers.Accept.Add(mt);
}
}
// Helper estilo Postman para /rest/files sin filter-view
private void AddBasicHeaders(HttpRequestMessage req)
{
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
req.Headers.Accept.Clear();
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.files-page+json"));
}
// =========================================================
// Expedientes (file)
// =========================================================
public async Task<string> CreateFileAsync(Guid procedureId, string subject, string documentSeries, string siaCode)
{
var effectiveProcedureId = procedureId == Guid.Empty
? Guid.Parse("82722c9b-cecc-4299-8a7b-ce5abeb8170b")
: procedureId;
var url = $"/rest/catalog-2015/procedures/{effectiveProcedureId}/create-file";
var payload = new
{
procedure = new { href = $"/rest/catalog-2015/procedures/{effectiveProcedureId}" },
subject,
document_series = documentSeries,
sia_code = siaCode,
field_groups = Array.Empty<object>()
};
var json = JsonSerializer.Serialize(payload);
using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.file+json");
content.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("version", "2"));
using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
req.Headers.Accept.Clear();
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.file+json")
{ Parameters = { new NameValueHeaderValue("version", "2") } });
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
using var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"CreateFileAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
using var doc = JsonDocument.Parse(body);
return doc.RootElement
.GetProperty("links")
.EnumerateArray()
.First(x => x.GetProperty("rel").GetString() == "file")
.GetProperty("href")
.GetString()!;
}
public async Task OpenFileAsync(
string fileUrl,
Guid managementUnitGroupId,
Guid assignedGroupId,
bool confidential,
string freeTitle,
string siaCode)
{
var url = $"{fileUrl}/open";
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}" }
},
links = new[]
{
new { rel = "management-unit-group", href = $"{_opts.ApiBase}/rest/groups/{managementUnitGroupId}" }
}
};
var json = JsonSerializer.Serialize(payload);
using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.file-opening+json");
content.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("version", "1"));
var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
req.Headers.Accept.Clear();
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.file+json"));
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
using var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"OpenFileAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
}
public async Task<Guid> CreateFolderAsync(string fileUrl, string folderName)
{
var endpoint = $"{fileUrl}/documents-and-folders";
var payload = new { name = folderName };
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.file-folder+json");
content.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("version", "1"));
using var resp = await _http.PostAsync(endpoint, content);
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"CreateFolderAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
var location = resp.Headers.Location?.ToString()
?? throw new InvalidOperationException("CreateFolderAsync: falta Location en respuesta");
var id = location.Split('/').Last();
return Guid.Parse(id);
}
public async Task<string> CreateUploadAsync(byte[] contentBytes, string fileName)
{
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"));
using var createResp = await _http.SendAsync(createReq);
var createBody = await createResp.Content.ReadAsStringAsync();
if (!createResp.IsSuccessStatusCode)
throw new InvalidOperationException($"CreateUpload (POST): {(int)createResp.StatusCode} {createResp.StatusCode}\n{createBody}");
var uploadUri = createResp.Headers.Location?.ToString()
?? throw new InvalidOperationException("No se devolvió Location en /rest/uploads");
string md5Hex;
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(contentBytes);
md5Hex = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
using var putReq = new HttpRequestMessage(HttpMethod.Put, uploadUri);
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.Content = new ByteArrayContent(contentBytes);
putReq.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
using var putResp = await _http.SendAsync(putReq);
var infoJson = await putResp.Content.ReadAsStringAsync();
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}");
return uploadUri;
}
public async Task UploadDocumentAsync(string fileUrl, byte[] contentBytes, string fileName)
{
var uploadUri = await CreateUploadAsync(contentBytes, fileName);
var metaPayload = new
{
type = "DIGITAL",
name = fileName,
description = "Denuncia combinada",
elaboration_state = "EE01",
metadata_language = "ES",
links = new[] { new { rel = "content", href = uploadUri } }
};
var metaJson = JsonSerializer.Serialize(metaPayload);
using var metaContent = new StringContent(metaJson, Encoding.UTF8);
metaContent.Headers.ContentType =
MediaTypeHeaderValue.Parse("application/vnd.gestiona.file-document+json; version=4");
using var metaReq = new HttpRequestMessage(HttpMethod.Post, $"{fileUrl}/documents-and-folders")
{ Content = metaContent };
metaReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
metaReq.Headers.Accept.Clear();
metaReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.file-document+json")
{ Parameters = { new NameValueHeaderValue("version", "4") } });
using var metaResp = await _http.SendAsync(metaReq);
var body = await metaResp.Content.ReadAsStringAsync();
if (!metaResp.IsSuccessStatusCode)
throw new InvalidOperationException($"UploadDocumentAsync: {(int)metaResp.StatusCode} {metaResp.StatusCode}\n{body}");
}
// =========================
// TERCEROS
// =========================
public async Task<(string Id, string SelfHref)> BuscarTerceroPorNifAsync(string nif)
{
var filtro = new
{
result = new { max_results = 25 },
filter = new { 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);
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
req.Headers.TryAddWithoutValidation("Accept", "application/vnd.gestiona.thirds-page+json");
using var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
if (!doc.RootElement.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array)
return default;
var match = content.EnumerateArray().FirstOrDefault(e =>
e.TryGetProperty("nif", out var nifProp) && nifProp.GetString() == nif);
if (match.ValueKind == JsonValueKind.Undefined) return default;
var id = match.GetProperty("id").GetString()!;
var self = match.GetProperty("links").EnumerateArray()
.First(l => l.GetProperty("rel").GetString() == "self")
.GetProperty("href").GetString()!;
return (id, self);
}
public async Task<(string Id, string SelfHref)> CrearTerceroAsync(ThirdPartyIdentityData thirdParty)
{
if (thirdParty is null)
throw new ArgumentNullException(nameof(thirdParty));
if (string.IsNullOrWhiteSpace(thirdParty.DocumentId))
throw new ArgumentException("Documento identificativo obligatorio.", nameof(thirdParty));
if (thirdParty.IsLegalEntity)
{
if (string.IsNullOrWhiteSpace(thirdParty.BusinessName))
throw new ArgumentException("La razón social es obligatoria para terceros jurídicos.", nameof(thirdParty));
}
else
{
if (string.IsNullOrWhiteSpace(thirdParty.FirstName))
throw new ArgumentException("Nombre obligatorio.", nameof(thirdParty));
if (string.IsNullOrWhiteSpace(thirdParty.LastName))
throw new ArgumentException("Primer apellido obligatorio.", nameof(thirdParty));
}
var payload = new Dictionary<string, object?>
{
["nif_country"] = NormalizeCountryCode(
string.IsNullOrWhiteSpace(thirdParty.CountryCode)
? thirdParty.Address?.CountryCode
: thirdParty.CountryCode),
["nif"] = thirdParty.DocumentId.Trim(),
["type"] = thirdParty.IsLegalEntity ? "JURIDICAL" : "PHISIC",
["notification_channel"] = BuildNotificationChannel(thirdParty)
};
var nifType = GuessNifType(thirdParty.DocumentId, thirdParty.IsLegalEntity);
if (!string.IsNullOrWhiteSpace(nifType))
{
payload["nif_type"] = nifType;
}
if (thirdParty.IsLegalEntity)
{
payload["business_name"] = thirdParty.BusinessName.Trim();
}
else
{
var (firstSurname, secondSurname) = SplitSurnames(thirdParty.LastName);
payload["first_name"] = thirdParty.FirstName.Trim();
payload["first_surname"] = firstSurname;
if (!string.IsNullOrWhiteSpace(secondSurname))
payload["second_surname"] = secondSurname;
}
if (!string.IsNullOrWhiteSpace(thirdParty.Email))
payload["email"] = thirdParty.Email.Trim();
var jsonOpts = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var json = JsonSerializer.Serialize(payload, jsonOpts);
using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.third+json");
content.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("version", "3"));
using var req = new HttpRequestMessage(HttpMethod.Post, "/rest/thirds")
{
Content = content
};
req.Headers.Accept.Clear();
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.third+json"));
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
using var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException(
$"Error CrearTerceroAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
using var doc = JsonDocument.Parse(body);
var id = doc.RootElement.GetProperty("id").GetString()!;
var selfHref = doc.RootElement.GetProperty("links").EnumerateArray()
.First(l => l.GetProperty("rel").GetString() == "self")
.GetProperty("href").GetString()!;
if (thirdParty.Address?.HasAnyValue == true)
{
await TryEnsureThirdAddressAsync(selfHref, thirdParty.Address);
}
return (id, selfHref);
}
public async Task<HashSet<string>> ObtenerTercerosEnlazadosAsync(string fileUrl)
{
using var req = new HttpRequestMessage(HttpMethod.Get, $"{fileUrl}/thirdparties");
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
req.Headers.TryAddWithoutValidation("Accept", "*/*");
using var resp = await _http.SendAsync(req);
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
return new HashSet<string>(StringComparer.Ordinal);
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var set = new HashSet<string>(StringComparer.Ordinal);
if (doc.RootElement.TryGetProperty("content", out var content) && content.ValueKind == JsonValueKind.Array)
{
foreach (var item in content.EnumerateArray())
{
if (item.TryGetProperty("links", out var links))
{
var third = links.EnumerateArray().FirstOrDefault(l => l.GetProperty("rel").GetString() == "third");
if (third.ValueKind != JsonValueKind.Undefined)
set.Add(third.GetProperty("href").GetString()!);
}
}
}
return set;
}
public async Task EnlazarTerceroExistenteAsync(string fileUrl, string thirdSelfHref)
{
var payload = new { href = thirdSelfHref };
var json = JsonSerializer.Serialize(payload);
using var content = new StringContent(json, Encoding.UTF8);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/vnd.gestiona.third-link+json");
using var req = new HttpRequestMessage(HttpMethod.Post, $"{fileUrl}/thirdparties");
req.Content = content;
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
req.Headers.TryAddWithoutValidation("Accept", "*/*");
var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"Error EnlazarTerceroExistenteAsync: {resp.StatusCode}\n{body}");
}
public async Task AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty)
{
if (thirdParty is null)
throw new ArgumentNullException(nameof(thirdParty));
thirdParty = NormalizeThirdParty(thirdParty);
if (string.IsNullOrWhiteSpace(thirdParty.DocumentId)) return;
var encontrado = await BuscarTerceroPorNifAsync(thirdParty.DocumentId);
if (string.IsNullOrEmpty(encontrado.SelfHref))
{
encontrado = await CrearTerceroAsync(thirdParty);
}
else 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);
}
private static ThirdPartyIdentityData NormalizeThirdParty(ThirdPartyIdentityData thirdParty)
{
if (!thirdParty.IsAnonymous)
{
return thirdParty;
}
return new ThirdPartyIdentityData
{
IsAnonymous = true,
IsLegalEntity = false,
DocumentId = "00000000T",
FirstName = "Anonimo",
LastName = "-",
BusinessName = string.Empty,
Email = string.Empty,
CountryCode = string.IsNullOrWhiteSpace(thirdParty.CountryCode) ? "ESP" : thirdParty.CountryCode,
Address = null
};
}
// --- 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);
if (filter is not null)
{
var json = JsonSerializer.Serialize(filter);
req.Content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.filter.files");
}
using var resp = await _http.SendAsync(req);
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
{
return "{\"content\":[]}";
}
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"GET /rest/files: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
if (string.IsNullOrWhiteSpace(body))
{
return "{\"content\":[]}";
}
return body;
}
private static bool TryGetFilesContent(JsonDocument doc, out JsonElement content)
{
if (doc.RootElement.TryGetProperty("content", out var contentElement) &&
contentElement.ValueKind == JsonValueKind.Array)
{
content = contentElement;
return true;
}
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
content = doc.RootElement;
return true;
}
content = default;
return false;
}
/// <summary>
/// Devuelve el JSON crudo de /rest/files acumulando hasta maxPages páginas.
/// </summary>
public async Task<string> ListarExpedientesJsonAsyncBasico(int maxPages = 1)
{
_ = maxPages;
var json = await GetFilesAsync();
using var doc = JsonDocument.Parse(json);
if (!TryGetFilesContent(doc, out var content))
{
return "[]";
}
var sb = new StringBuilder();
sb.Append('[');
var first = true;
foreach (var item in content.EnumerateArray())
{
if (!first)
{
sb.Append(',');
}
sb.Append(item.GetRawText());
first = false;
}
sb.Append(']');
return sb.ToString();
}
/// <summary>
/// Busca un expediente cuyo asunto sea "Denuncia {idDenuncia}-CD".
/// </summary>
public async Task<GestionaExpedienteInfo?> BuscarExpedientePorIdEnAsuntoAsync(int idDenuncia)
{
var needle = $"Denuncia {idDenuncia}-CD";
var json = await GetFilesAsync(new
{
subject = needle
});
using var doc = JsonDocument.Parse(json);
if (!TryGetFilesContent(doc, out var content) || content.GetArrayLength() == 0)
{
return null;
}
foreach (var item in content.EnumerateArray())
{
var expediente = BuildExpedienteInfo(item);
if (expediente is not null)
{
return expediente;
}
}
return null;
}
public async Task<GestionaExpedienteInfo?> ObtenerExpedienteAsync(string fileUrl)
{
if (string.IsNullOrWhiteSpace(fileUrl))
{
return null;
}
using var req = new HttpRequestMessage(HttpMethod.Get, fileUrl);
AddTokenAndAccept(req, "application/json");
using var resp = await _http.SendAsync(req);
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(
$"ObtenerExpedienteAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
}
if (string.IsNullOrWhiteSpace(body))
{
return null;
}
using var doc = JsonDocument.Parse(body);
return BuildExpedienteInfo(doc.RootElement, fileUrl);
}
private static GestionaExpedienteInfo? BuildExpedienteInfo(JsonElement item, string? fallbackFileUrl = null)
{
var code = GetJsonString(item, "code");
var freeTitle = GetJsonString(item, "free_title");
var subject = GetJsonString(item, "subject");
var selectableTitle = GetJsonString(item, "selectable_title");
var procedureName = GetJsonString(item, "procedure_name");
string? fileUrl = fallbackFileUrl;
if (item.TryGetProperty("links", out var links) && links.ValueKind == JsonValueKind.Array)
{
foreach (var link in links.EnumerateArray())
{
if (link.TryGetProperty("rel", out var rel) &&
string.Equals(rel.GetString(), "file", StringComparison.OrdinalIgnoreCase) &&
link.TryGetProperty("href", out var href) &&
href.ValueKind == JsonValueKind.String)
{
fileUrl = href.GetString();
break;
}
}
}
if (string.IsNullOrWhiteSpace(fileUrl) &&
item.TryGetProperty("id", out var idProp) &&
idProp.ValueKind == JsonValueKind.String)
{
fileUrl = $"/rest/files/{idProp.GetString()}";
}
if (string.IsNullOrWhiteSpace(fileUrl))
{
return null;
}
return new GestionaExpedienteInfo
{
FileUrl = fileUrl,
CodigoExpediente = code,
FreeTitle = FirstNonEmpty(freeTitle, subject, selectableTitle, procedureName)
};
}
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
)
{
if (string.IsNullOrWhiteSpace(nif))
throw new ArgumentException("NIF obligatorio.", nameof(nif));
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
}
});
using var doc = JsonDocument.Parse(json);
if (!TryGetFilesContent(doc, out var content) || content.GetArrayLength() == 0)
{
return resultados;
}
foreach (var item in content.EnumerateArray())
{
if (resultados.Count >= maxResults)
{
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);
}
}
if (desde.HasValue && creation.HasValue && creation.Value < desde.Value)
continue;
if (hasta.HasValue && creation.HasValue && creation.Value > hasta.Value)
continue;
string? fileUrl = null;
if (item.TryGetProperty("links", out var links) && links.ValueKind == JsonValueKind.Array)
{
foreach (var l in links.EnumerateArray())
{
if (l.TryGetProperty("rel", out var relProp) &&
string.Equals(relProp.GetString(), "file", StringComparison.OrdinalIgnoreCase) &&
l.TryGetProperty("href", out var hrefProp))
{
fileUrl = hrefProp.GetString();
break;
}
}
}
if (string.IsNullOrWhiteSpace(fileUrl) &&
item.TryGetProperty("id", out var idProp) &&
idProp.ValueKind == JsonValueKind.String)
{
fileUrl = $"/rest/files/{idProp.GetString()}";
}
if (string.IsNullOrWhiteSpace(fileUrl))
continue;
var code = GetJsonString(item, "code");
var subject = GetJsonString(item, "subject");
var freeTitle = GetJsonString(item, "free_title");
var selectableTitle = GetJsonString(item, "selectable_title");
var procedureName = GetJsonString(item, "procedure_name");
var asunto = FirstNonEmpty(freeTitle, subject, selectableTitle, procedureName);
string? state = null;
if (item.TryGetProperty("state", out var pState) && pState.ValueKind == JsonValueKind.String)
state = pState.GetString();
else if (item.TryGetProperty("status", out var pStatus) && pStatus.ValueKind == JsonValueKind.String)
state = pStatus.GetString();
resultados.Add(new ExpedienteTerceroDto
{
FileUrl = fileUrl,
CodigoExpediente = code,
Asunto = asunto,
FechaCreacion = creation,
Estado = state
});
}
return resultados;
}
private async Task TryEnsureThirdAddressAsync(string thirdSelfHref, ThirdPartyAddressData address)
{
if (!address.HasAnyValue)
{
return;
}
if (await ThirdHasAddressesAsync(thirdSelfHref))
{
return;
}
var addressPayload = await BuildAddressPayloadAsync(address);
if (addressPayload is null)
{
return;
}
var payload = new Dictionary<string, object?>
{
["content"] = new[] { addressPayload }
};
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
using var content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.third-addresses+json");
content.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("version", "2"));
using var req = new HttpRequestMessage(HttpMethod.Put, $"{thirdSelfHref.TrimEnd('/')}/addresses")
{
Content = content
};
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
throw new InvalidOperationException($"Error actualizando dirección del tercero: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
}
private async Task<bool> ThirdHasAddressesAsync(string thirdSelfHref)
{
using var req = new HttpRequestMessage(HttpMethod.Get, $"{thirdSelfHref.TrimEnd('/')}/addresses");
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var resp = await _http.SendAsync(req);
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent ||
resp.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return false;
}
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
return doc.RootElement.TryGetProperty("content", out var content) &&
content.ValueKind == JsonValueKind.Array &&
content.GetArrayLength() > 0;
}
private async Task<Dictionary<string, object?>?> BuildAddressPayloadAsync(ThirdPartyAddressData address)
{
var provinceCode = await ResolveProvinceCodeAsync(address.Province, NormalizeCountryCode(address.CountryCode));
var townHref = await ResolveTownHrefAsync(provinceCode, address.Municipality);
if (string.IsNullOrWhiteSpace(address.Street) &&
string.IsNullOrWhiteSpace(address.Municipality) &&
string.IsNullOrWhiteSpace(address.ZipCode))
{
return null;
}
var payload = new Dictionary<string, object?>
{
["address"] = address.Street,
["number"] = address.Number,
["floor"] = address.Floor,
["door"] = address.Door,
["block"] = address.Block,
["stair"] = address.Stair,
["zipcode"] = address.ZipCode,
["country"] = NormalizeCountryCode(address.CountryCode),
["province"] = provinceCode,
["type_of_road"] = string.IsNullOrWhiteSpace(address.RoadTypeCode) ? "CL" : address.RoadTypeCode,
["default_address"] = true,
};
if (!string.IsNullOrWhiteSpace(townHref))
{
payload["links"] = new[]
{
new Dictionary<string, string>
{
["rel"] = "town",
["href"] = townHref
}
};
}
else if (!string.IsNullOrWhiteSpace(address.Municipality))
{
payload["zone"] = address.Municipality.Trim();
}
return payload;
}
private async Task<string?> ResolveProvinceCodeAsync(string? province, string countryCode)
{
if (string.IsNullOrWhiteSpace(province))
{
return null;
}
var normalizedSearch = NormalizeKey(province);
using var req = new HttpRequestMessage(HttpMethod.Get, "/rest/provinces");
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
var body = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var collection = doc.RootElement.TryGetProperty("content", out var content) &&
content.ValueKind == JsonValueKind.Array
? content
: doc.RootElement;
if (collection.ValueKind != JsonValueKind.Array)
{
return province.Trim().ToUpperInvariant();
}
foreach (var item in collection.EnumerateArray())
{
var code = item.TryGetProperty("code", out var codeProp) ? codeProp.GetString() : null;
var name = item.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null;
var itemCountry = item.TryGetProperty("country_code", out var countryProp) ? countryProp.GetString() : null;
if (!string.IsNullOrWhiteSpace(itemCountry) &&
!string.Equals(NormalizeCountryCode(itemCountry), countryCode, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (NormalizeKey(code) == normalizedSearch || NormalizeKey(name) == normalizedSearch)
{
return code ?? province.Trim().ToUpperInvariant();
}
}
return province.Trim().ToUpperInvariant();
}
private async Task<string?> ResolveTownHrefAsync(string? provinceCode, string? municipality)
{
if (string.IsNullOrWhiteSpace(provinceCode) || string.IsNullOrWhiteSpace(municipality))
{
return null;
}
using var req = new HttpRequestMessage(HttpMethod.Get, $"/rest/provinces/{Uri.EscapeDataString(provinceCode)}/towns");
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var resp = await _http.SendAsync(req);
if (!resp.IsSuccessStatusCode)
{
return null;
}
var body = await resp.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
var collection = doc.RootElement.TryGetProperty("content", out var content) &&
content.ValueKind == JsonValueKind.Array
? content
: doc.RootElement;
if (collection.ValueKind != JsonValueKind.Array)
{
return null;
}
var normalizedSearch = NormalizeKey(municipality);
foreach (var item in collection.EnumerateArray())
{
var name = item.TryGetProperty("name", out var nameProp) ? nameProp.GetString() : null;
if (NormalizeKey(name) != normalizedSearch)
{
continue;
}
if (!item.TryGetProperty("links", out var links) || links.ValueKind != JsonValueKind.Array)
{
continue;
}
foreach (var link in links.EnumerateArray())
{
if (link.TryGetProperty("rel", out var relProp) &&
string.Equals(relProp.GetString(), "self", StringComparison.OrdinalIgnoreCase) &&
link.TryGetProperty("href", out var hrefProp))
{
return hrefProp.GetString();
}
}
}
return null;
}
private static string? GetJsonString(JsonElement item, string propertyName)
{
return item.TryGetProperty(propertyName, out var property) &&
property.ValueKind == JsonValueKind.String
? property.GetString()
: null;
}
private static string? FirstNonEmpty(params string?[] values)
{
foreach (var value in values)
{
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
return null;
}
private static (string FirstSurname, string? SecondSurname) SplitSurnames(string apellidos)
{
var parts = (apellidos ?? string.Empty)
.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
{
return ("-", null);
}
if (parts.Length == 1)
{
return (parts[0], null);
}
return (parts[0], string.Join(' ', parts.Skip(1)));
}
private static string BuildNotificationChannel(ThirdPartyIdentityData thirdParty)
{
_ = thirdParty;
return "TELEMATIC";
}
private static string? GuessNifType(string nif, bool isLegalEntity)
{
var value = (nif ?? string.Empty).Trim().ToUpperInvariant();
if (isLegalEntity && Regex.IsMatch(value, @"^[A-Z]\d{7}[A-Z0-9]$"))
{
return "CIF";
}
if (value.StartsWith("X", StringComparison.Ordinal) ||
value.StartsWith("Y", StringComparison.Ordinal) ||
value.StartsWith("Z", StringComparison.Ordinal))
{
return "NIE";
}
if (Regex.IsMatch(value, @"^\d{7,8}[A-Z0-9]$"))
{
return "NIF";
}
return null;
}
private static string NormalizeCountryCode(string? country)
{
var value = NormalizeKey(country);
return value switch
{
"" => "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",
};
}
private static string NormalizeKey(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var normalized = value.Normalize(NormalizationForm.FormD);
var builder = new StringBuilder(normalized.Length);
foreach (var character in normalized)
{
var category = CharUnicodeInfo.GetUnicodeCategory(character);
if (category == UnicodeCategory.NonSpacingMark)
{
continue;
}
builder.Append(char.IsLetterOrDigit(character) ? char.ToUpperInvariant(character) : ' ');
}
return string.Join(' ', builder.ToString().Split(' ', StringSplitOptions.RemoveEmptyEntries));
}
}
}