diff --git a/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs b/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs index b6dcf22..33f3d96 100644 --- a/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs +++ b/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs @@ -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) }); } diff --git a/Antifraude.Net/ApiDenuncias/appsettings.json b/Antifraude.Net/ApiDenuncias/appsettings.json index 4c2c64b..37c91ab 100644 --- a/Antifraude.Net/ApiDenuncias/appsettings.json +++ b/Antifraude.Net/ApiDenuncias/appsettings.json @@ -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", diff --git a/Antifraude.Net/ApiOPE.Tests/OpeFieldMapperTests.cs b/Antifraude.Net/ApiOPE.Tests/OpeFieldMapperTests.cs index 5b2789a..4df4f61 100644 --- a/Antifraude.Net/ApiOPE.Tests/OpeFieldMapperTests.cs +++ b/Antifraude.Net/ApiOPE.Tests/OpeFieldMapperTests.cs @@ -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 - { - ["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()); + var mapper = CreateMapper(); + var source = CreateBlockSource(); - Assert.Throws(() => 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 { - 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") + }); } diff --git a/Antifraude.Net/ApiOPE.Tests/OpeProtocolTests.cs b/Antifraude.Net/ApiOPE.Tests/OpeProtocolTests.cs index 32a9f69..9752794 100644 --- a/Antifraude.Net/ApiOPE.Tests/OpeProtocolTests.cs +++ b/Antifraude.Net/ApiOPE.Tests/OpeProtocolTests.cs @@ -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) diff --git a/Antifraude.Net/ApiOPE/ApiOPE.http b/Antifraude.Net/ApiOPE/ApiOPE.http index 1182c88..5e32196 100644 --- a/Antifraude.Net/ApiOPE/ApiOPE.http +++ b/Antifraude.Net/ApiOPE/ApiOPE.http @@ -34,6 +34,10 @@ X-Organization-CIF: {{OrganizationCif}} "FIELD_0": { "type": "STRING", "value": "53/2026" + }, + "FIELD_1": { + "type": "STRING", + "value": "1" } } } diff --git a/Antifraude.Net/ApiOPE/Configuration/OpeOptions.cs b/Antifraude.Net/ApiOPE/Configuration/OpeOptions.cs index 79e10fb..4584534 100644 --- a/Antifraude.Net/ApiOPE/Configuration/OpeOptions.cs +++ b/Antifraude.Net/ApiOPE/Configuration/OpeOptions.cs @@ -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 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; } diff --git a/Antifraude.Net/ApiOPE/Controllers/OpeController.cs b/Antifraude.Net/ApiOPE/Controllers/OpeController.cs index 661fa0a..f98592c 100644 --- a/Antifraude.Net/ApiOPE/Controllers/OpeController.cs +++ b/Antifraude.Net/ApiOPE/Controllers/OpeController.cs @@ -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 _logger; public OpeController( IOptions options, + InternalDenunciasClient internalApi, + OpeFieldMapper fieldMapper, ILogger 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(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( diff --git a/Antifraude.Net/ApiOPE/Postman/ApiOPE-PRE.postman_environment.json b/Antifraude.Net/ApiOPE/Postman/ApiOPE-PRE.postman_environment.json index 6e3f423..bd99405 100644 --- a/Antifraude.Net/ApiOPE/Postman/ApiOPE-PRE.postman_environment.json +++ b/Antifraude.Net/ApiOPE/Postman/ApiOPE-PRE.postman_environment.json @@ -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", diff --git a/Antifraude.Net/ApiOPE/Postman/ApiOPE-Simulacion-Gestiona.postman_collection.json b/Antifraude.Net/ApiOPE/Postman/ApiOPE-Simulacion-Gestiona.postman_collection.json index a84b1f1..5b2d0f6 100644 --- a/Antifraude.Net/ApiOPE/Postman/ApiOPE-Simulacion-Gestiona.postman_collection.json +++ b/Antifraude.Net/ApiOPE/Postman/ApiOPE-Simulacion-Gestiona.postman_collection.json @@ -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": [] } diff --git a/Antifraude.Net/ApiOPE/Postman/INSTRUCCIONES-PRUEBA.txt b/Antifraude.Net/ApiOPE/Postman/INSTRUCCIONES-PRUEBA.txt index e2cb2f3..b18739f 100644 --- a/Antifraude.Net/ApiOPE/Postman/INSTRUCCIONES-PRUEBA.txt +++ b/Antifraude.Net/ApiOPE/Postman/INSTRUCCIONES-PRUEBA.txt @@ -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. diff --git a/Antifraude.Net/ApiOPE/README.md b/Antifraude.Net/ApiOPE/README.md index 62a9eb2..20ee4e7 100644 --- a/Antifraude.Net/ApiOPE/README.md +++ b/Antifraude.Net/ApiOPE/README.md @@ -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 diff --git a/Antifraude.Net/ApiOPE/Services/GenericOperationRequestReader.cs b/Antifraude.Net/ApiOPE/Services/GenericOperationRequestReader.cs index 1cfc9ea..1c3596c 100644 --- a/Antifraude.Net/ApiOPE/Services/GenericOperationRequestReader.cs +++ b/Antifraude.Net/ApiOPE/Services/GenericOperationRequestReader.cs @@ -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); } } diff --git a/Antifraude.Net/ApiOPE/Services/InternalDenunciasClient.cs b/Antifraude.Net/ApiOPE/Services/InternalDenunciasClient.cs index 957abec..64b2da5 100644 --- a/Antifraude.Net/ApiOPE/Services/InternalDenunciasClient.cs +++ b/Antifraude.Net/ApiOPE/Services/InternalDenunciasClient.cs @@ -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( diff --git a/Antifraude.Net/ApiOPE/Services/OpeFieldMapper.cs b/Antifraude.Net/ApiOPE/Services/OpeFieldMapper.cs index ced8f62..fcb163b 100644 --- a/Antifraude.Net/ApiOPE/Services/OpeFieldMapper.cs +++ b/Antifraude.Net/ApiOPE/Services/OpeFieldMapper.cs @@ -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 SupportedInternalFields = new HashSet( - [ - "fechaDenuncia", - "numeroDenunciaCanal", - "aQuienDenuncia", - "resumenDenuncia", - "fechaHechos", - "lugarHechos", - "ambitoCompetencias", - "solicitaProteccion", - "sexoDenunciante", - "autorizaRemisionDenuncia", - "autorizaNotificacionesViaSms", - "preferenciaNotificacionSeguimientoDenuncia" - ], - StringComparer.Ordinal); - - private readonly string[] _outputFields; - - public OpeFieldMapper(IOptions 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(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); diff --git a/Antifraude.Net/ApiOPE/appsettings.Development.json b/Antifraude.Net/ApiOPE/appsettings.Development.json index df35d6a..34c3fa8 100644 --- a/Antifraude.Net/ApiOPE/appsettings.Development.json +++ b/Antifraude.Net/ApiOPE/appsettings.Development.json @@ -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", diff --git a/Antifraude.Net/ApiOPE/appsettings.json b/Antifraude.Net/ApiOPE/appsettings.json index 72cdf80..2bfeaec 100644 --- a/Antifraude.Net/ApiOPE/appsettings.json +++ b/Antifraude.Net/ApiOPE/appsettings.json @@ -17,8 +17,7 @@ "ClockSkewSeconds": 60, "OrganizationId": "", "OrganizationDir3": "", - "OrganizationCif": "", - "OutputFields": [] + "OrganizationCif": "" }, "InternalApi": { "BaseUrl": "http://localhost:7093", diff --git a/Antifraude.Net/GestionaDenuncias.Shared/Models/ApiDenunciasDtos.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ApiDenunciasDtos.cs index 507fbc4..00f7b20 100644 --- a/Antifraude.Net/GestionaDenuncias.Shared/Models/ApiDenunciasDtos.cs +++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ApiDenunciasDtos.cs @@ -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, diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Actualizaciones.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Actualizaciones.razor index d802cc4..88e6e42 100644 --- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Actualizaciones.razor +++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Actualizaciones.razor @@ -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"); diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Pendientes.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Pendientes.razor index cdd4156..851719e 100644 --- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Pendientes.razor +++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Pendientes.razor @@ -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");