Nombres de circuitos actualizados
controlador de datos de OPE estructurado
This commit is contained in:
@@ -336,7 +336,23 @@ public sealed class DenunciasController : ControllerBase
|
||||
SexoDenunciante: ResolveReportField(denuncia, denuncia.Sexo, "sexo"),
|
||||
AutorizaRemisionDenuncia: ResolveReportField(denuncia, denuncia.AutorizaRemision, "autorizacion para remitir su denuncia", "autoriza remision de la denuncia"),
|
||||
AutorizaNotificacionesViaSms: sms,
|
||||
PreferenciaNotificacionSeguimientoDenuncia: JoinDistinct(preferenciaNotificacion, seguimiento));
|
||||
PreferenciaNotificacionSeguimientoDenuncia: JoinDistinct(preferenciaNotificacion, seguimiento),
|
||||
DetalleDenunciado: ResolveReportField(denuncia, denuncia.DenunciadoDetalle, "especifique a quien denuncia", "detalle de la persona u organismo denunciado"),
|
||||
PreferenciaNotificacion: preferenciaNotificacion,
|
||||
AutorizaNotificacionesElectronicas: ResolveReportField(
|
||||
denuncia,
|
||||
denuncia.Notificacion_Electronica,
|
||||
"notificaciones electronicas",
|
||||
"notificacion electronica",
|
||||
"autorizacion para notificaciones electronicas"),
|
||||
PreferenciaNotificacionPostal: ResolveReportField(
|
||||
denuncia,
|
||||
denuncia.NotificacionPostal,
|
||||
"autorizo recibir notificaciones via correo postal",
|
||||
"notificaciones via correo postal",
|
||||
"correo postal"),
|
||||
PreferenciaSeguimientoOnline: seguimiento,
|
||||
ComentariosComunicaciones: denuncia.Comments?.Trim() ?? string.Empty);
|
||||
}
|
||||
|
||||
private static GestionaExternalFieldsResponse ToExternalGestionaFields(GestionaComplaintFieldsResponse source)
|
||||
@@ -355,7 +371,13 @@ public sealed class DenunciasController : ControllerBase
|
||||
["sexoDenunciante"] = StringField(source.SexoDenunciante),
|
||||
["autorizaRemisionDenuncia"] = StringField(source.AutorizaRemisionDenuncia),
|
||||
["autorizaNotificacionesViaSms"] = StringField(source.AutorizaNotificacionesViaSms),
|
||||
["preferenciaNotificacionSeguimientoDenuncia"] = StringField(source.PreferenciaNotificacionSeguimientoDenuncia)
|
||||
["preferenciaNotificacionSeguimientoDenuncia"] = StringField(source.PreferenciaNotificacionSeguimientoDenuncia),
|
||||
["detalleDenunciado"] = StringField(source.DetalleDenunciado),
|
||||
["preferenciaNotificacion"] = StringField(source.PreferenciaNotificacion),
|
||||
["autorizaNotificacionesElectronicas"] = StringField(source.AutorizaNotificacionesElectronicas),
|
||||
["preferenciaNotificacionPostal"] = StringField(source.PreferenciaNotificacionPostal),
|
||||
["preferenciaSeguimientoOnline"] = StringField(source.PreferenciaSeguimientoOnline),
|
||||
["comentariosComunicaciones"] = StringField(source.ComentariosComunicaciones)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,16 +36,16 @@
|
||||
"Gestiona": {
|
||||
"ApiBase": "https://02.g3stiona.com",
|
||||
"AccessToken": "_yr.xVvPOllsyd1TYZRxUxg__c",
|
||||
"ProcedureName": "Procedimiento test 2",
|
||||
"ExternalProcedureName": "",
|
||||
"ExternalProcedureSiaCode": "3109963",
|
||||
"ProcedureName": "Procedimiento Genérico",
|
||||
"ExternalProcedureName": "Denuncia Pruebas V2",
|
||||
"ExternalProcedureSiaCode": "",
|
||||
"ManagementUnitGroupCode": "700",
|
||||
"CircuitTemplateName": "Firma automatizada",
|
||||
"CircuitNewComplaintTemplateName": "CT-Nueva denuncia",
|
||||
"CircuitUpdateSajTemplateName": "CT-Actualización denuncia SAJ",
|
||||
"CircuitUpdateSdiTemplateName": "CT-Actualización denuncia SDI",
|
||||
"CircuitCommunicationSajTemplateName": "CT-Comunicación SAJ a denunciante",
|
||||
"CircuitCommunicationSdiTemplateName": "CT-Comunicación SDI a denunciante",
|
||||
"CircuitNewComplaintTemplateName": "CT-Nueva denuncia buzon",
|
||||
"CircuitUpdateSajTemplateName": "CT-Actualización denuncia Buzon SAJ",
|
||||
"CircuitUpdateSdiTemplateName": "CT-Actualización denuncia buzon SDI",
|
||||
"CircuitCommunicationSajTemplateName": "CT-Comunicación SAJ buzon a denunciante",
|
||||
"CircuitCommunicationSdiTemplateName": "CT-Comunicación SDI buzon a denunciante",
|
||||
"CircuitSignerStampTitle": "oaaf-complaints-tramit",
|
||||
"CircuitVersion": "2",
|
||||
"DocumentMetadataLanguage": "es",
|
||||
|
||||
@@ -1,46 +1,63 @@
|
||||
using ApiOPE.Configuration;
|
||||
using ApiOPE.Contracts;
|
||||
using ApiOPE.Services;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ApiOPE.Tests;
|
||||
|
||||
public sealed class OpeFieldMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void Map_UsesConfiguredOrderAndFieldNames()
|
||||
public void MapBlock_MapsFirstBlockInAgreedOrder()
|
||||
{
|
||||
var options = Options.Create(new OpeOptions
|
||||
{
|
||||
OutputFields = ["numeroDenunciaCanal", "fechaDenuncia", "resumenDenuncia"]
|
||||
});
|
||||
var mapper = new OpeFieldMapper(options);
|
||||
var source = new InternalGestionaFieldsResponse(
|
||||
new Dictionary<string, OpeFieldValue>
|
||||
{
|
||||
["fechaDenuncia"] = new("STRING", "2026-07-10"),
|
||||
["numeroDenunciaCanal"] = new("STRING", "116"),
|
||||
["resumenDenuncia"] = new("STRING", "Prueba")
|
||||
});
|
||||
var mapper = CreateMapper();
|
||||
var source = CreateBlockSource();
|
||||
|
||||
var result = mapper.Map(source);
|
||||
var result = mapper.MapBlock(source, 1, belongsToComplaintsChannel: true);
|
||||
|
||||
Assert.Equal("116", result.Data["FIELD_0"].Value);
|
||||
Assert.Equal("2026-07-10", result.Data["FIELD_1"].Value);
|
||||
Assert.Equal("Prueba", result.Data["FIELD_2"].Value);
|
||||
Assert.Equal("SI", result.Data["FIELD_0"].Value);
|
||||
Assert.Equal("2026-09-11", result.Data["FIELD_1"].Value);
|
||||
Assert.Equal("123", result.Data["FIELD_2"].Value);
|
||||
Assert.Equal("Ambito", result.Data["FIELD_9"].Value);
|
||||
Assert.Equal(10, result.Data.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Map_RejectsMissingInternalField()
|
||||
public void MapBlock_MapsSecondBlockAndLeavesLastTwoFieldsEmpty()
|
||||
{
|
||||
var mapper = new OpeFieldMapper(Options.Create(new OpeOptions
|
||||
{
|
||||
OutputFields = ["fechaDenuncia"]
|
||||
}));
|
||||
var source = new InternalGestionaFieldsResponse(
|
||||
new Dictionary<string, OpeFieldValue>());
|
||||
var mapper = CreateMapper();
|
||||
var source = CreateBlockSource();
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => mapper.Map(source));
|
||||
var result = mapper.MapBlock(source, 2, belongsToComplaintsChannel: true);
|
||||
|
||||
Assert.Equal("Ayuntamiento", result.Data["FIELD_0"].Value);
|
||||
Assert.Equal("Comentarios", result.Data["FIELD_7"].Value);
|
||||
Assert.Equal(string.Empty, result.Data["FIELD_8"].Value);
|
||||
Assert.Equal(string.Empty, result.Data["FIELD_9"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapBlock_ReturnsNoAndEmptyFieldsForUnknownExpedient()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
|
||||
var firstBlock = mapper.MapBlock(null, 1, belongsToComplaintsChannel: false);
|
||||
var secondBlock = mapper.MapBlock(null, 2, belongsToComplaintsChannel: false);
|
||||
|
||||
Assert.Equal("NO", firstBlock.Data["FIELD_0"].Value);
|
||||
Assert.All(firstBlock.Data.Where(field => field.Key != "FIELD_0"), field => Assert.Empty(field.Value.Value));
|
||||
Assert.All(secondBlock.Data, field => Assert.Empty(field.Value.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MapBlock_ReturnsYesAndEmptyDataForPurgedChannelExpedient()
|
||||
{
|
||||
var mapper = CreateMapper();
|
||||
|
||||
var firstBlock = mapper.MapBlock(null, 1, belongsToComplaintsChannel: true);
|
||||
var secondBlock = mapper.MapBlock(null, 2, belongsToComplaintsChannel: true);
|
||||
|
||||
Assert.Equal("SI", firstBlock.Data["FIELD_0"].Value);
|
||||
Assert.All(firstBlock.Data.Where(field => field.Key != "FIELD_0"), field => Assert.Empty(field.Value.Value));
|
||||
Assert.All(secondBlock.Data, field => Assert.Empty(field.Value.Value));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -54,39 +71,28 @@ public sealed class OpeFieldMapperTests
|
||||
Assert.Equal(expectedPath, lookup!.RelativePath);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(11)]
|
||||
public void OptionsValidator_RejectsInvalidOutputCount(int count)
|
||||
{
|
||||
var options = new OpeOptions
|
||||
private static OpeFieldMapper CreateMapper()
|
||||
=> new();
|
||||
|
||||
private static InternalGestionaFieldsResponse CreateBlockSource()
|
||||
=> new(new Dictionary<string, OpeFieldValue>
|
||||
{
|
||||
ClientToken = "client",
|
||||
SecretKey = "test-secret-key-with-more-than-32-characters",
|
||||
PublicBaseUrl = "https://ope.example.test",
|
||||
OrganizationId = "organization",
|
||||
OrganizationDir3 = "dir3",
|
||||
OrganizationCif = "cif",
|
||||
OutputFields = Enumerable.Repeat("fechaDenuncia", count).ToList()
|
||||
};
|
||||
|
||||
Assert.False(OpeOptionsValidator.IsValid(options, isDevelopment: false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OptionsValidator_AcceptsTenUniqueConfiguredFields()
|
||||
{
|
||||
var options = new OpeOptions
|
||||
{
|
||||
ClientToken = "client",
|
||||
SecretKey = "test-secret-key-with-more-than-32-characters",
|
||||
PublicBaseUrl = "https://ope.example.test",
|
||||
OrganizationId = "organization",
|
||||
OrganizationDir3 = "dir3",
|
||||
OrganizationCif = "cif",
|
||||
OutputFields = OpeFieldMapper.SupportedInternalFields.Take(10).ToList()
|
||||
};
|
||||
|
||||
Assert.True(OpeOptionsValidator.IsValid(options, isDevelopment: false));
|
||||
}
|
||||
["fechaDenuncia"] = new("STRING", "2026-09-11"),
|
||||
["numeroDenunciaCanal"] = new("STRING", "123"),
|
||||
["sexoDenunciante"] = new("STRING", "Mujer"),
|
||||
["preferenciaNotificacion"] = new("STRING", "Electronica"),
|
||||
["autorizaNotificacionesElectronicas"] = new("STRING", "Si"),
|
||||
["autorizaNotificacionesViaSms"] = new("STRING", "No"),
|
||||
["preferenciaNotificacionPostal"] = new("STRING", "No"),
|
||||
["preferenciaSeguimientoOnline"] = new("STRING", "Si"),
|
||||
["ambitoCompetencias"] = new("STRING", "Ambito"),
|
||||
["aQuienDenuncia"] = new("STRING", "Ayuntamiento"),
|
||||
["detalleDenunciado"] = new("STRING", "Detalle"),
|
||||
["resumenDenuncia"] = new("STRING", "Resumen"),
|
||||
["solicitaProteccion"] = new("STRING", "Si"),
|
||||
["lugarHechos"] = new("STRING", "Cadiz"),
|
||||
["fechaHechos"] = new("STRING", "10/09/2026"),
|
||||
["autorizaRemisionDenuncia"] = new("STRING", "Si"),
|
||||
["comentariosComunicaciones"] = new("STRING", "Comentarios")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class OpeProtocolTests
|
||||
var context = CreateHttpContext(
|
||||
"application/vnd.generic-operation-request+json; charset=utf-8",
|
||||
"""
|
||||
{"data":{"FIELD_0":{"type":"STRING","value":"53/2026"}}}
|
||||
{"data":{"FIELD_0":{"type":"STRING","value":"53/2026"},"FIELD_1":{"type":"STRING","value":"1"}}}
|
||||
""");
|
||||
|
||||
var result = await GenericOperationRequestReader.ReadAsync(
|
||||
@@ -53,6 +53,7 @@ public sealed class OpeProtocolTests
|
||||
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal("53/2026", result.Request!.Data["FIELD_0"].Value);
|
||||
Assert.Equal("1", result.Request.Data["FIELD_1"].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -61,7 +62,7 @@ public sealed class OpeProtocolTests
|
||||
var context = CreateHttpContext(
|
||||
OpeMediaTypes.GenericOperationRequest,
|
||||
"""
|
||||
{"data":{"FIELD_0":{"type":"STRING","value":"68/2026"}},"links":null}
|
||||
{"data":{"FIELD_0":{"type":"STRING","value":"68/2026"},"FIELD_1":{"type":"STRING","value":"2"}},"links":null}
|
||||
""");
|
||||
|
||||
var result = await GenericOperationRequestReader.ReadAsync(
|
||||
@@ -78,7 +79,7 @@ public sealed class OpeProtocolTests
|
||||
var context = CreateHttpContext(
|
||||
OpeMediaTypes.GenericOperationRequest,
|
||||
"""
|
||||
{"data":{"FIELD_1":{"type":"STRING","value":"unexpected"}}}
|
||||
{"data":{"FIELD_2":{"type":"STRING","value":"unexpected"}}}
|
||||
""");
|
||||
|
||||
var result = await GenericOperationRequestReader.ReadAsync(
|
||||
@@ -86,8 +87,26 @@ public sealed class OpeProtocolTests
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(OpeFieldErrors.NotExpected, result.Errors["FIELD_1"]);
|
||||
Assert.Equal(OpeFieldErrors.NotExpected, result.Errors["FIELD_2"]);
|
||||
Assert.Equal(OpeFieldErrors.Expected, result.Errors["FIELD_0"]);
|
||||
Assert.Equal(OpeFieldErrors.Expected, result.Errors["FIELD_1"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestReader_RejectsUnknownBlock()
|
||||
{
|
||||
var context = CreateHttpContext(
|
||||
OpeMediaTypes.GenericOperationRequest,
|
||||
"""
|
||||
{"data":{"FIELD_0":{"type":"STRING","value":"68/2026"},"FIELD_1":{"type":"STRING","value":"3"}}}
|
||||
""");
|
||||
|
||||
var result = await GenericOperationRequestReader.ReadAsync(
|
||||
context.Request,
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(OpeFieldErrors.UnexpectedFormat, result.Errors["FIELD_1"]);
|
||||
}
|
||||
|
||||
private static DefaultHttpContext CreateHttpContext(string contentType, string body)
|
||||
|
||||
@@ -34,6 +34,10 @@ X-Organization-CIF: {{OrganizationCif}}
|
||||
"FIELD_0": {
|
||||
"type": "STRING",
|
||||
"value": "53/2026"
|
||||
},
|
||||
"FIELD_1": {
|
||||
"type": "STRING",
|
||||
"value": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using ApiOPE.Services;
|
||||
|
||||
namespace ApiOPE.Configuration;
|
||||
|
||||
public sealed class OpeOptions
|
||||
@@ -25,8 +23,6 @@ public sealed class OpeOptions
|
||||
public string OrganizationDir3 { get; set; } = string.Empty;
|
||||
|
||||
public string OrganizationCif { get; set; } = string.Empty;
|
||||
|
||||
public List<string> OutputFields { get; set; } = [];
|
||||
}
|
||||
|
||||
public static class OpeOptionsValidator
|
||||
@@ -41,11 +37,7 @@ public static class OpeOptionsValidator
|
||||
string.IsNullOrWhiteSpace(options.OrganizationDir3) ||
|
||||
string.IsNullOrWhiteSpace(options.OrganizationCif) ||
|
||||
options.TokenMaxAgeSeconds is < 1 or > 900 ||
|
||||
options.ClockSkewSeconds is < 0 or > 300 ||
|
||||
options.OutputFields is null ||
|
||||
options.OutputFields.Count is < 1 or > 10 ||
|
||||
options.OutputFields.Distinct(StringComparer.Ordinal).Count() != options.OutputFields.Count ||
|
||||
options.OutputFields.Any(field => !OpeFieldMapper.SupportedInternalFields.Contains(field)))
|
||||
options.ClockSkewSeconds is < 0 or > 300)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,13 +15,19 @@ public sealed class OpeController : ControllerBase
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
private readonly OpeOptions _options;
|
||||
private readonly InternalDenunciasClient _internalApi;
|
||||
private readonly OpeFieldMapper _fieldMapper;
|
||||
private readonly ILogger<OpeController> _logger;
|
||||
|
||||
public OpeController(
|
||||
IOptions<OpeOptions> options,
|
||||
InternalDenunciasClient internalApi,
|
||||
OpeFieldMapper fieldMapper,
|
||||
ILogger<OpeController> logger)
|
||||
{
|
||||
_options = options.Value;
|
||||
_internalApi = internalApi;
|
||||
_fieldMapper = fieldMapper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -65,6 +71,7 @@ public sealed class OpeController : ControllerBase
|
||||
}
|
||||
|
||||
var identifier = parsedRequest.Request!.Data["FIELD_0"].Value;
|
||||
var block = int.Parse(parsedRequest.Request.Data["FIELD_1"].Value, System.Globalization.CultureInfo.InvariantCulture);
|
||||
if (!DenunciaLookupParser.TryParse(identifier, out var lookup) || lookup is null)
|
||||
{
|
||||
return Error(
|
||||
@@ -76,25 +83,72 @@ public sealed class OpeController : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
// Respuesta temporal de pruebas: no consulta la API interna ni exige
|
||||
// que el expediente exista. FIELD_0 conserva exactamente el valor
|
||||
// recibido y el resto de campos devuelve un valor ficticio.
|
||||
_logger.LogInformation(
|
||||
"Respuesta OPE de prueba para el expediente solicitado {Expediente}. TransactionId={TransactionId}",
|
||||
identifier,
|
||||
requestContext.TransactionId);
|
||||
|
||||
var testData = new Dictionary<string, OpeFieldValue>(StringComparer.Ordinal)
|
||||
try
|
||||
{
|
||||
["FIELD_0"] = new OpeFieldValue("STRING", identifier)
|
||||
};
|
||||
var result = await _internalApi.GetFieldsAsync(
|
||||
lookup,
|
||||
requestContext.TransactionId,
|
||||
cancellationToken);
|
||||
|
||||
for (var index = 1; index < _options.OutputFields.Count; index++)
|
||||
{
|
||||
testData[$"FIELD_{index}"] = new OpeFieldValue("STRING", "dato de prueba");
|
||||
if (result.Status == InternalLookupStatus.NotFound)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"El expediente {Expediente} no pertenece al canal de denuncias. Bloque={Block}; TransactionId={TransactionId}",
|
||||
identifier,
|
||||
block,
|
||||
requestContext.TransactionId);
|
||||
return Json(
|
||||
_fieldMapper.MapBlock(null, block, belongsToComplaintsChannel: false),
|
||||
OpeMediaTypes.GenericOperationResponse);
|
||||
}
|
||||
|
||||
if (result.Status == InternalLookupStatus.DataUnavailable || result.Fields is null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"El expediente {Expediente} pertenece al canal, pero sus datos ya no estan disponibles. Bloque={Block}; TransactionId={TransactionId}",
|
||||
identifier,
|
||||
block,
|
||||
requestContext.TransactionId);
|
||||
return Json(
|
||||
_fieldMapper.MapBlock(null, block, belongsToComplaintsChannel: true),
|
||||
OpeMediaTypes.GenericOperationResponse);
|
||||
}
|
||||
|
||||
return Json(
|
||||
_fieldMapper.MapBlock(result.Fields, block, belongsToComplaintsChannel: true),
|
||||
OpeMediaTypes.GenericOperationResponse);
|
||||
}
|
||||
catch (OperationCanceledException) when (!HttpContext.RequestAborted.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Timeout consultando ApiDenuncias para una OPE. TransactionId={TransactionId}",
|
||||
requestContext.TransactionId);
|
||||
return Error("Error en el conector", OpeErrorCauses.ConnectorError, null);
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
exception,
|
||||
"No se ha podido conectar con ApiDenuncias. TransactionId={TransactionId}",
|
||||
requestContext.TransactionId);
|
||||
return Error("Error en el conector", OpeErrorCauses.ConnectorError, null);
|
||||
}
|
||||
catch (InternalApiException exception)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
exception,
|
||||
"ApiDenuncias ha devuelto una respuesta no utilizable. TransactionId={TransactionId}",
|
||||
requestContext.TransactionId);
|
||||
return Error("Error en el conector", OpeErrorCauses.ConnectorError, null);
|
||||
}
|
||||
catch (InvalidDataException exception)
|
||||
{
|
||||
_logger.LogError(
|
||||
exception,
|
||||
"El contrato entre ApiOPE y ApiDenuncias no coincide. TransactionId={TransactionId}",
|
||||
requestContext.TransactionId);
|
||||
return Error("Error en el conector", OpeErrorCauses.ConnectorError, null);
|
||||
}
|
||||
|
||||
return Json(new OpeDataEnvelope(testData), OpeMediaTypes.GenericOperationResponse);
|
||||
}
|
||||
|
||||
private static ContentResult Error(
|
||||
|
||||
@@ -43,6 +43,12 @@
|
||||
"value": "REEMPLAZAR_EXPEDIENTE_NO_PURGADO",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "bloque_datos",
|
||||
"value": "1",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_postman_variable_scope": "environment",
|
||||
|
||||
@@ -125,6 +125,10 @@
|
||||
" if (!expediente || expediente.startsWith('REEMPLAZAR_')) {",
|
||||
" throw new Error('Debes indicar un expediente no purgado en expediente_gestiona');",
|
||||
" }",
|
||||
" const bloque = pm.variables.get('bloque_datos');",
|
||||
" if (bloque !== '1' && bloque !== '2') {",
|
||||
" throw new Error('bloque_datos debe valer 1 o 2');",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
"if (current.resetTransaction || !pm.collectionVariables.get('ope_transaction_id')) {",
|
||||
@@ -338,7 +342,7 @@
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"data\": {\n \"FIELD_0\": {\n \"type\": \"STRING\",\n \"value\": \"{{expediente_gestiona}}\"\n }\n }\n}",
|
||||
"raw": "{\n \"data\": {\n \"FIELD_0\": {\n \"type\": \"STRING\",\n \"value\": \"{{expediente_gestiona}}\"\n },\n \"FIELD_1\": {\n \"type\": \"STRING\",\n \"value\": \"{{bloque_datos}}\"\n }\n },\n \"links\": null\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -346,7 +350,7 @@
|
||||
}
|
||||
},
|
||||
"url": "{{base_url}}/genericoperations",
|
||||
"description": "Tercera llamada de Gestiona. FIELD_0 contiene el numero de expediente, por ejemplo 53/2026."
|
||||
"description": "Tercera llamada de Gestiona. FIELD_0 contiene el numero de expediente y FIELD_1 selecciona el bloque de datos 1 o 2."
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ Variables que debe rellenar cada probador
|
||||
- organization_dir3: codigo DIR3 de la organizacion remitente.
|
||||
- organization_cif: CIF de la organizacion remitente.
|
||||
- expediente_gestiona: numero de un expediente de Gestiona que exista y cuyos datos diarios sigan disponibles, por ejemplo 53/2026.
|
||||
- bloque_datos: bloque que se desea recuperar. Debe valer 1 o 2.
|
||||
|
||||
Las credenciales no se incluyen en este paquete. Deben facilitarse por un canal seguro y guardarse solo como valores locales del entorno de Postman.
|
||||
|
||||
@@ -38,6 +39,6 @@ Resultado esperado
|
||||
------------------
|
||||
- Las tres respuestas devuelven HTTP 200.
|
||||
- Los tests de Postman aparecen en verde.
|
||||
- La tercera respuesta contiene los campos disponibles del expediente solicitado.
|
||||
- La tercera respuesta contiene siempre FIELD_0 a FIELD_9 para el bloque solicitado.
|
||||
|
||||
Si la peticion no llega al servidor, comprobar que el equipo resuelve gesbuzpre.antifraudeandalucia.es y tiene acceso al puerto 7094.
|
||||
|
||||
@@ -9,8 +9,7 @@ permanecer en la red interna.
|
||||
1. Gestiona consulta `GET /versions/current`.
|
||||
2. Gestiona consulta `GET /` y obtiene el bookmark de la operacion generica.
|
||||
3. Gestiona llama a `POST /genericoperations` con el numero de expediente
|
||||
(`53/2026`) en `FIELD_0`. Para pruebas tambien se admite el identificador de
|
||||
denuncia (`116`).
|
||||
(`53/2026`) en `FIELD_0` y el bloque solicitado (`1` o `2`) en `FIELD_1`.
|
||||
4. `ApiOPE` consulta el endpoint protegido correspondiente de `ApiDenuncias`.
|
||||
5. `ApiOPE` transforma la respuesta interna al formato `FIELD_0`...`FIELD_9` y
|
||||
la devuelve a Gestiona.
|
||||
@@ -52,7 +51,6 @@ Ope__OrganizationDir3
|
||||
Ope__OrganizationCif
|
||||
Ope__TokenMaxAgeSeconds
|
||||
Ope__ClockSkewSeconds
|
||||
Ope__OutputFields__0 ... Ope__OutputFields__9
|
||||
|
||||
InternalApi__BaseUrl
|
||||
InternalApi__ApiKey
|
||||
@@ -71,8 +69,8 @@ OpeBridge__ApiKeyHeaderName=X-ApiOPE-Key
|
||||
incluido cualquier path base. El bookmark se construye a partir de ese valor.
|
||||
|
||||
La aplicacion valida la configuracion al arrancar. Si faltan el secreto, los
|
||||
datos de organizacion, la URL publica o el mapa de salida, no inicia para evitar
|
||||
publicar un conector incompleto.
|
||||
datos de organizacion o la URL publica, no inicia para evitar publicar un
|
||||
conector incompleto.
|
||||
|
||||
### Prueba temporal de preproduccion
|
||||
|
||||
@@ -96,26 +94,15 @@ volver a usar un entorno de produccion.
|
||||
|
||||
## Campos de salida
|
||||
|
||||
La API interna ofrece estos 12 campos:
|
||||
La operacion siempre devuelve `FIELD_0` a `FIELD_9` como valores `STRING`.
|
||||
`FIELD_1=1` solicita el primer bloque: pertenencia al canal (`SI` o `NO`), fecha,
|
||||
numero de denuncia, sexo, preferencias de notificacion y ambito. `FIELD_1=2`
|
||||
solicita el segundo bloque: denunciado, detalle, resumen, proteccion, lugar,
|
||||
fecha de los hechos, autorizacion de remision y comentarios; sus dos ultimas
|
||||
posiciones quedan vacias.
|
||||
|
||||
```text
|
||||
fechaDenuncia
|
||||
numeroDenunciaCanal
|
||||
aQuienDenuncia
|
||||
resumenDenuncia
|
||||
fechaHechos
|
||||
lugarHechos
|
||||
ambitoCompetencias
|
||||
solicitaProteccion
|
||||
sexoDenunciante
|
||||
autorizaRemisionDenuncia
|
||||
autorizaNotificacionesViaSms
|
||||
preferenciaNotificacionSeguimientoDenuncia
|
||||
```
|
||||
|
||||
OPE 1.0 admite como maximo 10 campos por operacion. Antes de configurar PRE hay
|
||||
que acordar cuales diez se publican o dividir la consulta en dos operaciones.
|
||||
El orden de `Ope__OutputFields__N` determina la correspondencia con `FIELD_N`.
|
||||
Si el expediente no pertenece al canal, el primer bloque devuelve `NO` y el
|
||||
resto vacio. Para el segundo bloque se devuelven los diez campos vacios.
|
||||
|
||||
## Publicacion
|
||||
|
||||
@@ -129,8 +116,8 @@ El orden de `Ope__OutputFields__N` determina la correspondencia con `FIELD_N`.
|
||||
5. Validar consecutivamente version, bookmarks y operacion generica, incluida
|
||||
la cabecera `Signature` de cada respuesta.
|
||||
|
||||
Los logs identifican fallos mediante `transaction_id`, pero nunca registran el
|
||||
JWT, el secreto compartido, la clave interna ni el contenido de la denuncia.
|
||||
El registro diagnostico de PRE incluye cabeceras y cuerpos de peticion y
|
||||
respuesta. El JWT, las cookies y los secretos se ocultan siempre.
|
||||
|
||||
## Simulacion desde Postman
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ public sealed record GenericOperationRequestResult(
|
||||
|
||||
public static class GenericOperationRequestReader
|
||||
{
|
||||
private const string ExpectedField = "FIELD_0";
|
||||
private const string ExpedienteField = "FIELD_0";
|
||||
private const string BlockField = "FIELD_1";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
@@ -35,7 +36,7 @@ public static class GenericOperationRequestReader
|
||||
OpeMediaTypes.GenericOperationRequest,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
errors[ExpectedField] = OpeFieldErrors.UnexpectedFormat;
|
||||
errors[ExpedienteField] = OpeFieldErrors.UnexpectedFormat;
|
||||
return new GenericOperationRequestResult(null, errors);
|
||||
}
|
||||
|
||||
@@ -49,19 +50,21 @@ public static class GenericOperationRequestReader
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
errors[ExpectedField] = OpeFieldErrors.UnexpectedFormat;
|
||||
errors[ExpedienteField] = OpeFieldErrors.UnexpectedFormat;
|
||||
return new GenericOperationRequestResult(null, errors);
|
||||
}
|
||||
|
||||
if (envelope?.Data is null)
|
||||
{
|
||||
errors[ExpectedField] = OpeFieldErrors.Expected;
|
||||
errors[ExpedienteField] = OpeFieldErrors.Expected;
|
||||
errors[BlockField] = OpeFieldErrors.Expected;
|
||||
return new GenericOperationRequestResult(null, errors);
|
||||
}
|
||||
|
||||
foreach (var field in envelope.Data)
|
||||
{
|
||||
if (!string.Equals(field.Key, ExpectedField, StringComparison.Ordinal))
|
||||
if (!string.Equals(field.Key, ExpedienteField, StringComparison.Ordinal) &&
|
||||
!string.Equals(field.Key, BlockField, StringComparison.Ordinal))
|
||||
{
|
||||
errors[field.Key] = OpeFieldErrors.NotExpected;
|
||||
continue;
|
||||
@@ -72,22 +75,26 @@ public static class GenericOperationRequestReader
|
||||
string.IsNullOrWhiteSpace(field.Value.Value))
|
||||
{
|
||||
errors[field.Key] = OpeFieldErrors.UnexpectedFormat;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!envelope.Data.ContainsKey(ExpectedField))
|
||||
{
|
||||
errors[ExpectedField] = OpeFieldErrors.Expected;
|
||||
}
|
||||
|
||||
if (envelope.Data.Count > 10)
|
||||
{
|
||||
foreach (var field in envelope.Data.Keys.Where(key => key != ExpectedField))
|
||||
if (string.Equals(field.Key, BlockField, StringComparison.Ordinal) &&
|
||||
field.Value.Value.Trim() is not ("1" or "2"))
|
||||
{
|
||||
errors[field] = OpeFieldErrors.NotExpected;
|
||||
errors[field.Key] = OpeFieldErrors.UnexpectedFormat;
|
||||
}
|
||||
}
|
||||
|
||||
if (!envelope.Data.ContainsKey(ExpedienteField))
|
||||
{
|
||||
errors[ExpedienteField] = OpeFieldErrors.Expected;
|
||||
}
|
||||
|
||||
if (!envelope.Data.ContainsKey(BlockField))
|
||||
{
|
||||
errors[BlockField] = OpeFieldErrors.Expected;
|
||||
}
|
||||
|
||||
return new GenericOperationRequestResult(envelope, errors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ namespace ApiOPE.Services;
|
||||
public enum InternalLookupStatus
|
||||
{
|
||||
Success,
|
||||
NotFound
|
||||
NotFound,
|
||||
DataUnavailable
|
||||
}
|
||||
|
||||
public sealed record InternalLookupResult(
|
||||
@@ -57,11 +58,16 @@ public sealed class InternalDenunciasClient
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
cancellationToken);
|
||||
|
||||
if (response.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Gone)
|
||||
if (response.StatusCode is HttpStatusCode.NotFound)
|
||||
{
|
||||
return new InternalLookupResult(InternalLookupStatus.NotFound, null);
|
||||
}
|
||||
|
||||
if (response.StatusCode is HttpStatusCode.Gone)
|
||||
{
|
||||
return new InternalLookupResult(InternalLookupStatus.DataUnavailable, null);
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
|
||||
@@ -1,49 +1,71 @@
|
||||
using ApiOPE.Configuration;
|
||||
using ApiOPE.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ApiOPE.Services;
|
||||
|
||||
public sealed class OpeFieldMapper
|
||||
{
|
||||
public static readonly IReadOnlySet<string> SupportedInternalFields = new HashSet<string>(
|
||||
[
|
||||
"fechaDenuncia",
|
||||
"numeroDenunciaCanal",
|
||||
"aQuienDenuncia",
|
||||
"resumenDenuncia",
|
||||
"fechaHechos",
|
||||
"lugarHechos",
|
||||
"ambitoCompetencias",
|
||||
"solicitaProteccion",
|
||||
"sexoDenunciante",
|
||||
"autorizaRemisionDenuncia",
|
||||
"autorizaNotificacionesViaSms",
|
||||
"preferenciaNotificacionSeguimientoDenuncia"
|
||||
],
|
||||
StringComparer.Ordinal);
|
||||
|
||||
private readonly string[] _outputFields;
|
||||
|
||||
public OpeFieldMapper(IOptions<OpeOptions> options)
|
||||
public OpeDataEnvelope MapBlock(
|
||||
InternalGestionaFieldsResponse? source,
|
||||
int block,
|
||||
bool belongsToComplaintsChannel)
|
||||
{
|
||||
_outputFields = options.Value.OutputFields.ToArray();
|
||||
if (!belongsToComplaintsChannel || source is null)
|
||||
{
|
||||
return EmptyBlock(block, belongsToComplaintsChannel);
|
||||
}
|
||||
|
||||
return block switch
|
||||
{
|
||||
1 => Envelope(
|
||||
"SI",
|
||||
Value(source, "fechaDenuncia"),
|
||||
Value(source, "numeroDenunciaCanal"),
|
||||
Value(source, "sexoDenunciante"),
|
||||
Value(source, "preferenciaNotificacion"),
|
||||
Value(source, "autorizaNotificacionesElectronicas"),
|
||||
Value(source, "autorizaNotificacionesViaSms"),
|
||||
Value(source, "preferenciaNotificacionPostal"),
|
||||
Value(source, "preferenciaSeguimientoOnline"),
|
||||
Value(source, "ambitoCompetencias")),
|
||||
2 => Envelope(
|
||||
Value(source, "aQuienDenuncia"),
|
||||
Value(source, "detalleDenunciado"),
|
||||
Value(source, "resumenDenuncia"),
|
||||
Value(source, "solicitaProteccion"),
|
||||
Value(source, "lugarHechos"),
|
||||
Value(source, "fechaHechos"),
|
||||
Value(source, "autorizaRemisionDenuncia"),
|
||||
Value(source, "comentariosComunicaciones"),
|
||||
string.Empty,
|
||||
string.Empty),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(block))
|
||||
};
|
||||
}
|
||||
|
||||
public OpeDataEnvelope Map(InternalGestionaFieldsResponse source)
|
||||
private static OpeDataEnvelope EmptyBlock(int block, bool belongsToComplaintsChannel)
|
||||
{
|
||||
var values = Enumerable.Repeat(string.Empty, 10).ToArray();
|
||||
if (block == 1)
|
||||
{
|
||||
values[0] = belongsToComplaintsChannel ? "SI" : "NO";
|
||||
}
|
||||
|
||||
return Envelope(values);
|
||||
}
|
||||
|
||||
private static string Value(InternalGestionaFieldsResponse source, string name)
|
||||
=> source.Data.TryGetValue(name, out var value)
|
||||
? value.Value?.Trim() ?? string.Empty
|
||||
: string.Empty;
|
||||
|
||||
private static OpeDataEnvelope Envelope(params string[] values)
|
||||
{
|
||||
var output = new Dictionary<string, OpeFieldValue>(StringComparer.Ordinal);
|
||||
|
||||
for (var index = 0; index < _outputFields.Length; index++)
|
||||
for (var index = 0; index < 10; index++)
|
||||
{
|
||||
var internalField = _outputFields[index];
|
||||
if (!source.Data.TryGetValue(internalField, out var value))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"La API interna no ha devuelto el campo configurado '{internalField}'.");
|
||||
}
|
||||
|
||||
output[$"FIELD_{index}"] = new OpeFieldValue("STRING", value.Value?.Trim() ?? string.Empty);
|
||||
output[$"FIELD_{index}"] = new OpeFieldValue(
|
||||
"STRING",
|
||||
index < values.Length ? values[index] : string.Empty);
|
||||
}
|
||||
|
||||
return new OpeDataEnvelope(output);
|
||||
|
||||
@@ -14,19 +14,7 @@
|
||||
"ClockSkewSeconds": 60,
|
||||
"OrganizationId": "00000000-0000-0000-0000-000000000001",
|
||||
"OrganizationDir3": "DEV000001",
|
||||
"OrganizationCif": "DEV000001",
|
||||
"OutputFields": [
|
||||
"fechaDenuncia",
|
||||
"numeroDenunciaCanal",
|
||||
"aQuienDenuncia",
|
||||
"resumenDenuncia",
|
||||
"fechaHechos",
|
||||
"lugarHechos",
|
||||
"ambitoCompetencias",
|
||||
"solicitaProteccion",
|
||||
"sexoDenunciante",
|
||||
"autorizaRemisionDenuncia"
|
||||
]
|
||||
"OrganizationCif": "DEV000001"
|
||||
},
|
||||
"InternalApi": {
|
||||
"BaseUrl": "http://localhost:7093",
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
"ClockSkewSeconds": 60,
|
||||
"OrganizationId": "",
|
||||
"OrganizationDir3": "",
|
||||
"OrganizationCif": "",
|
||||
"OutputFields": []
|
||||
"OrganizationCif": ""
|
||||
},
|
||||
"InternalApi": {
|
||||
"BaseUrl": "http://localhost:7093",
|
||||
|
||||
@@ -145,7 +145,13 @@ public sealed record GestionaComplaintFieldsResponse(
|
||||
string SexoDenunciante,
|
||||
string AutorizaRemisionDenuncia,
|
||||
string AutorizaNotificacionesViaSms,
|
||||
string PreferenciaNotificacionSeguimientoDenuncia);
|
||||
string PreferenciaNotificacionSeguimientoDenuncia,
|
||||
string DetalleDenunciado,
|
||||
string PreferenciaNotificacion,
|
||||
string AutorizaNotificacionesElectronicas,
|
||||
string PreferenciaNotificacionPostal,
|
||||
string PreferenciaSeguimientoOnline,
|
||||
string ComentariosComunicaciones);
|
||||
|
||||
public sealed record GestionaExternalFieldValue(
|
||||
string Type,
|
||||
|
||||
@@ -914,7 +914,7 @@ else
|
||||
var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
|
||||
nuevoAsunto,
|
||||
"RQ2ZLC - Expediente de Denuncias",
|
||||
"3109963"
|
||||
string.Empty
|
||||
);
|
||||
fileUrl = createdFile.FileUrl;
|
||||
Busy.Update(message: "Abriendo expediente y asignando el grupo elegido.", detail: "Paso 2 de 8");
|
||||
|
||||
@@ -1005,7 +1005,7 @@ else
|
||||
var createdFile = await ApiDenuncias.CreateGestionaFileAsync(
|
||||
nuevoAsunto,
|
||||
"RQ2ZLC - Expediente de Denuncias",
|
||||
"3109963"
|
||||
string.Empty
|
||||
);
|
||||
fileUrl = createdFile.FileUrl;
|
||||
Busy.Update(message: "Abriendo expediente y asignando el grupo elegido.", detail: "Paso 2 de 7");
|
||||
|
||||
Reference in New Issue
Block a user