Nombres de circuitos actualizados

controlador de datos de OPE estructurado
This commit is contained in:
2026-09-14 14:26:43 +02:00
parent 14fffb1501
commit 7f3780497f
19 changed files with 321 additions and 198 deletions

View File

@@ -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)
});
}

View File

@@ -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",

View File

@@ -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")
});
}

View File

@@ -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)

View File

@@ -34,6 +34,10 @@ X-Organization-CIF: {{OrganizationCif}}
"FIELD_0": {
"type": "STRING",
"value": "53/2026"
},
"FIELD_1": {
"type": "STRING",
"value": "1"
}
}
}

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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",

View File

@@ -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": []
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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(

View File

@@ -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);

View File

@@ -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",

View File

@@ -17,8 +17,7 @@
"ClockSkewSeconds": 60,
"OrganizationId": "",
"OrganizationDir3": "",
"OrganizationCif": "",
"OutputFields": []
"OrganizationCif": ""
},
"InternalApi": {
"BaseUrl": "http://localhost:7093",

View File

@@ -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,

View File

@@ -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");

View File

@@ -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");