Files
Antifraude.Net/Antifraude.Net/ApiDenuncias/Services/MySqlDenunciaStore.cs
2026-05-06 13:48:23 +02:00

1229 lines
54 KiB
C#

using System.Data;
using System.Globalization;
using System.Security.Cryptography;
using ApiDenuncias.Configuration;
using GestionaDenuncias.Shared.Models;
using Microsoft.Extensions.Options;
using MySqlConnector;
namespace ApiDenuncias.Services;
public sealed class MySqlDenunciaStore : IDenunciaStore
{
private static readonly (string Table, string Column, string Definition)[] SchemaColumnsToEnsure =
[
("complaints", "gestiona_file_code", "`gestiona_file_code` VARCHAR(128) NOT NULL DEFAULT ''"),
("complaints", "reporter_kind", "`reporter_kind` VARCHAR(128) NOT NULL DEFAULT ''"),
("complaints", "is_legal_entity", "`is_legal_entity` TINYINT(1) NOT NULL DEFAULT 0"),
("complaints", "reporter_first_surname", "`reporter_first_surname` VARCHAR(256) NOT NULL DEFAULT ''"),
("complaints", "reporter_second_surname", "`reporter_second_surname` VARCHAR(256) NOT NULL DEFAULT ''"),
("complaints", "reporter_business_name", "`reporter_business_name` VARCHAR(256) NOT NULL DEFAULT ''"),
("complaints", "reporter_document_type", "`reporter_document_type` VARCHAR(128) NOT NULL DEFAULT ''"),
("complaints", "reporter_origin_country", "`reporter_origin_country` VARCHAR(128) NOT NULL DEFAULT ''"),
("complaints", "accused_party_details", "`accused_party_details` VARCHAR(512) NOT NULL DEFAULT ''"),
("complaints", "reported_institution_details", "`reported_institution_details` VARCHAR(512) NOT NULL DEFAULT ''"),
("complaints", "requested_protection", "`requested_protection` VARCHAR(128) NOT NULL DEFAULT ''"),
("complaints", "requested_protection_details", "`requested_protection_details` VARCHAR(512) NOT NULL DEFAULT ''"),
("complaints", "forwarding_authorization", "`forwarding_authorization` VARCHAR(128) NOT NULL DEFAULT ''"),
("complaints", "forwarding_personal_data_preference", "`forwarding_personal_data_preference` VARCHAR(256) NOT NULL DEFAULT ''"),
("complaints", "online_tracking_preference", "`online_tracking_preference` VARCHAR(256) NOT NULL DEFAULT ''"),
("complaints", "postal_notification_preference", "`postal_notification_preference` VARCHAR(256) NOT NULL DEFAULT ''"),
("complaints", "address_road_type", "`address_road_type` VARCHAR(32) NOT NULL DEFAULT ''"),
("complaints", "address_extra", "`address_extra` VARCHAR(256) NOT NULL DEFAULT ''"),
("complaints", "form_fields_json", "`form_fields_json` LONGTEXT NULL"),
("complaints", "raw_report_text", "`raw_report_text` LONGTEXT NULL"),
("complaint_attachments", "content_sha256", "`content_sha256` CHAR(64) NOT NULL DEFAULT ''"),
];
private static readonly (string Table, string IndexName, string Definition)[] SchemaIndexesToEnsure =
[
("complaint_attachments", "ix_attachments_sha256", "INDEX `ix_attachments_sha256` (`content_sha256`)"),
];
private static readonly (string Table, string Definition)[] SchemaEncryptedColumnsToEnsure =
[
("complaints", "`gestiona_file_url` TEXT NOT NULL"),
("complaints", "`gestiona_file_code` TEXT NOT NULL"),
("complaints", "`tag` TEXT NOT NULL"),
("complaints", "`status_name` TEXT NOT NULL"),
("complaints", "`complaint_type` TEXT NOT NULL"),
("complaints", "`reporter_kind` TEXT NOT NULL"),
("complaints", "`reporter_first_name` TEXT NOT NULL"),
("complaints", "`reporter_first_surname` TEXT NOT NULL"),
("complaints", "`reporter_second_surname` TEXT NOT NULL"),
("complaints", "`reporter_last_name` TEXT NOT NULL"),
("complaints", "`reporter_business_name` TEXT NOT NULL"),
("complaints", "`reporter_gender` TEXT NOT NULL"),
("complaints", "`reporter_document_id` TEXT NOT NULL"),
("complaints", "`reporter_document_type` TEXT NOT NULL"),
("complaints", "`reporter_origin_country` TEXT NOT NULL"),
("complaints", "`subject` TEXT NOT NULL"),
("complaints", "`accused_party` TEXT NOT NULL"),
("complaints", "`accused_party_details` TEXT NOT NULL"),
("complaints", "`complaint_description` LONGTEXT NULL"),
("complaints", "`reported_to_institution` TEXT NOT NULL"),
("complaints", "`reported_institution_details` TEXT NOT NULL"),
("complaints", "`requested_protection` TEXT NOT NULL"),
("complaints", "`requested_protection_details` TEXT NOT NULL"),
("complaints", "`information_mode` TEXT NOT NULL"),
("complaints", "`facts_location` TEXT NOT NULL"),
("complaints", "`forwarding_authorization` TEXT NOT NULL"),
("complaints", "`forwarding_personal_data_preference` TEXT NOT NULL"),
("complaints", "`notification_preference` TEXT NOT NULL"),
("complaints", "`electronic_notification` TEXT NOT NULL"),
("complaints", "`online_tracking_preference` TEXT NOT NULL"),
("complaints", "`postal_notification_preference` TEXT NOT NULL"),
("complaints", "`email` TEXT NOT NULL"),
("complaints", "`sms_notification` TEXT NOT NULL"),
("complaints", "`comments` LONGTEXT NULL"),
("complaints", "`phone` TEXT NOT NULL"),
("complaints", "`address_line` TEXT NOT NULL"),
("complaints", "`address_road_type` TEXT NOT NULL"),
("complaints", "`address_number` TEXT NOT NULL"),
("complaints", "`address_floor` TEXT NOT NULL"),
("complaints", "`address_door` TEXT NOT NULL"),
("complaints", "`address_block` TEXT NOT NULL"),
("complaints", "`address_stair` TEXT NOT NULL"),
("complaints", "`address_extra` TEXT NOT NULL"),
("complaints", "`municipality` TEXT NOT NULL"),
("complaints", "`province` TEXT NOT NULL"),
("complaints", "`postal_code` TEXT NOT NULL"),
("complaints", "`country_code` TEXT NOT NULL"),
("complaints", "`form_fields_json` LONGTEXT NULL"),
("complaints", "`raw_report_text` LONGTEXT NULL"),
("complaints", "`display_name` TEXT NOT NULL"),
("complaints", "`workflow_status` TEXT NOT NULL"),
("complaints", "`selected_document_name` TEXT NULL"),
("complaint_attachments", "`description` TEXT NULL"),
("complaint_attachments", "`notes` TEXT NOT NULL"),
];
private readonly ComplaintStorageOptions _options;
private readonly IHostEnvironment _environment;
private readonly ILogger<MySqlDenunciaStore> _logger;
private static readonly SemaphoreSlim SchemaGate = new(1, 1);
private static volatile bool SchemaEnsured;
private readonly MySqlConnectionStringProvider _connectionStringProvider;
public MySqlDenunciaStore(
IOptions<ComplaintStorageOptions> options,
IHostEnvironment environment,
ILogger<MySqlDenunciaStore> logger,
MySqlConnectionStringProvider connectionStringProvider)
{
_options = options.Value;
_environment = environment;
_logger = logger;
_connectionStringProvider = connectionStringProvider;
}
public Task EnsureSchemaAsync(CancellationToken cancellationToken = default)
{
return EnsureSchemaReadyAsync(cancellationToken);
}
public async Task<List<DenunciasGestiona>> GetAllDenunciasAsync(CancellationToken cancellationToken = default)
{
await EnsureSchemaReadyAsync(cancellationToken);
const string sql = """
SELECT
external_registry_id,
external_report_id,
report_date_utc,
gestiona_file_url,
gestiona_file_code,
gestiona_person_id,
tag,
status_name,
complaint_type,
reporter_kind,
is_legal_entity,
reporter_first_name,
reporter_first_surname,
reporter_second_surname,
reporter_last_name,
reporter_business_name,
reporter_gender,
reporter_document_id,
reporter_document_type,
reporter_origin_country,
subject,
accused_party,
accused_party_details,
complaint_description,
reported_to_institution,
reported_institution_details,
requested_protection,
requested_protection_details,
information_mode,
facts_location,
facts_date_utc,
forwarding_authorization,
forwarding_personal_data_preference,
notification_preference,
electronic_notification,
online_tracking_preference,
postal_notification_preference,
email,
sms_notification,
accepted_terms,
comments,
phone,
address_line,
address_road_type,
address_number,
address_floor,
address_door,
address_block,
address_stair,
address_extra,
municipality,
province,
postal_code,
country_code,
form_fields_json,
raw_report_text,
is_confidential,
is_update,
gestiona_procedure_id,
gestiona_group_id,
display_name,
workflow_status,
selected_document_name,
gestiona_uploaded_at_utc,
is_in_gestiona,
is_rejected
FROM complaints
ORDER BY COALESCE(gestiona_uploaded_at_utc, report_date_utc) DESC, external_report_id DESC;
""";
await using var connection = await OpenConnectionAsync(cancellationToken);
await using var command = new MySqlCommand(sql, connection);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
var result = new List<DenunciasGestiona>();
while (await reader.ReadAsync(cancellationToken))
{
result.Add(MapComplaint(reader));
}
return result;
}
public async Task<List<FicherosDenuncias>> GetAllFicherosAsync(CancellationToken cancellationToken = default)
{
await EnsureSchemaReadyAsync(cancellationToken);
const string sql = """
SELECT
a.id,
a.attachment_type_id,
a.description,
a.attachment_date_utc,
a.notes,
c.external_report_id,
a.original_file_name,
a.content,
a.content_sha256,
a.uploaded_to_gestiona,
a.uploaded_at_utc
FROM complaint_attachments a
INNER JOIN complaints c ON c.id = a.complaint_id
ORDER BY c.external_report_id DESC, a.original_file_name ASC;
""";
await using var connection = await OpenConnectionAsync(cancellationToken);
await using var command = new MySqlCommand(sql, connection);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
var result = new List<FicherosDenuncias>();
while (await reader.ReadAsync(cancellationToken))
{
result.Add(MapAttachment(reader));
}
return result;
}
public async Task<List<FicherosDenuncias>> GetFicherosByDenunciaAsync(
int denunciaId,
CancellationToken cancellationToken = default)
{
await EnsureSchemaReadyAsync(cancellationToken);
const string sql = """
SELECT
a.id,
a.attachment_type_id,
a.description,
a.attachment_date_utc,
a.notes,
c.external_report_id,
a.original_file_name,
a.content,
a.content_sha256,
a.uploaded_to_gestiona,
a.uploaded_at_utc
FROM complaint_attachments a
INNER JOIN complaints c ON c.id = a.complaint_id
WHERE c.external_report_id = @denunciaId
ORDER BY a.original_file_name ASC;
""";
await using var connection = await OpenConnectionAsync(cancellationToken);
await using var command = new MySqlCommand(sql, connection);
command.Parameters.AddWithValue("@denunciaId", denunciaId);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
var result = new List<FicherosDenuncias>();
while (await reader.ReadAsync(cancellationToken))
{
result.Add(MapAttachment(reader));
}
return result;
}
public async Task<DenunciasGestiona?> GetDenunciaByIdAsync(
int denunciaId,
CancellationToken cancellationToken = default)
{
await EnsureSchemaReadyAsync(cancellationToken);
const string sql = """
SELECT
external_registry_id,
external_report_id,
report_date_utc,
gestiona_file_url,
gestiona_file_code,
gestiona_person_id,
tag,
status_name,
complaint_type,
reporter_kind,
is_legal_entity,
reporter_first_name,
reporter_first_surname,
reporter_second_surname,
reporter_last_name,
reporter_business_name,
reporter_gender,
reporter_document_id,
reporter_document_type,
reporter_origin_country,
subject,
accused_party,
accused_party_details,
complaint_description,
reported_to_institution,
reported_institution_details,
requested_protection,
requested_protection_details,
information_mode,
facts_location,
facts_date_utc,
forwarding_authorization,
forwarding_personal_data_preference,
notification_preference,
electronic_notification,
online_tracking_preference,
postal_notification_preference,
email,
sms_notification,
accepted_terms,
comments,
phone,
address_line,
address_road_type,
address_number,
address_floor,
address_door,
address_block,
address_stair,
address_extra,
municipality,
province,
postal_code,
country_code,
form_fields_json,
raw_report_text,
is_confidential,
is_update,
gestiona_procedure_id,
gestiona_group_id,
display_name,
workflow_status,
selected_document_name,
gestiona_uploaded_at_utc,
is_in_gestiona,
is_rejected
FROM complaints
WHERE external_report_id = @denunciaId
LIMIT 1;
""";
await using var connection = await OpenConnectionAsync(cancellationToken);
await using var command = new MySqlCommand(sql, connection);
command.Parameters.AddWithValue("@denunciaId", denunciaId);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
return await reader.ReadAsync(cancellationToken)
? MapComplaint(reader)
: null;
}
public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
{
await EnsureSchemaReadyAsync(cancellationToken);
const string sql = """
INSERT INTO complaints (
external_registry_id,
external_report_id,
report_date_utc,
gestiona_file_url,
gestiona_file_code,
gestiona_person_id,
tag,
status_name,
complaint_type,
reporter_kind,
is_legal_entity,
reporter_first_name,
reporter_first_surname,
reporter_second_surname,
reporter_last_name,
reporter_business_name,
reporter_gender,
reporter_document_id,
reporter_document_type,
reporter_origin_country,
subject,
accused_party,
accused_party_details,
complaint_description,
reported_to_institution,
reported_institution_details,
requested_protection,
requested_protection_details,
information_mode,
facts_location,
facts_date_utc,
forwarding_authorization,
forwarding_personal_data_preference,
notification_preference,
electronic_notification,
online_tracking_preference,
postal_notification_preference,
email,
sms_notification,
accepted_terms,
comments,
phone,
address_line,
address_road_type,
address_number,
address_floor,
address_door,
address_block,
address_stair,
address_extra,
municipality,
province,
postal_code,
country_code,
form_fields_json,
raw_report_text,
is_confidential,
is_update,
gestiona_procedure_id,
gestiona_group_id,
display_name,
workflow_status,
selected_document_name,
gestiona_uploaded_at_utc,
is_in_gestiona,
is_rejected
) VALUES (
@externalRegistryId,
@externalReportId,
@reportDateUtc,
@gestionaFileUrl,
@gestionaFileCode,
@gestionaPersonId,
@tag,
@statusName,
@complaintType,
@reporterKind,
@isLegalEntity,
@reporterFirstName,
@reporterFirstSurname,
@reporterSecondSurname,
@reporterLastName,
@reporterBusinessName,
@reporterGender,
@reporterDocumentId,
@reporterDocumentType,
@reporterOriginCountry,
@subject,
@accusedParty,
@accusedPartyDetails,
@complaintDescription,
@reportedToInstitution,
@reportedInstitutionDetails,
@requestedProtection,
@requestedProtectionDetails,
@informationMode,
@factsLocation,
@factsDateUtc,
@forwardingAuthorization,
@forwardingPersonalDataPreference,
@notificationPreference,
@electronicNotification,
@onlineTrackingPreference,
@postalNotificationPreference,
@email,
@smsNotification,
@acceptedTerms,
@comments,
@phone,
@addressLine,
@addressRoadType,
@addressNumber,
@addressFloor,
@addressDoor,
@addressBlock,
@addressStair,
@addressExtra,
@municipality,
@province,
@postalCode,
@countryCode,
@formFieldsJson,
@rawReportText,
@isConfidential,
@isUpdate,
@gestionaProcedureId,
@gestionaGroupId,
@displayName,
@workflowStatus,
@selectedDocumentName,
@gestionaUploadedAtUtc,
@isInGestiona,
@isRejected
)
ON DUPLICATE KEY UPDATE
external_registry_id = VALUES(external_registry_id),
report_date_utc = VALUES(report_date_utc),
gestiona_file_url = VALUES(gestiona_file_url),
gestiona_file_code = VALUES(gestiona_file_code),
gestiona_person_id = VALUES(gestiona_person_id),
tag = VALUES(tag),
status_name = VALUES(status_name),
complaint_type = VALUES(complaint_type),
reporter_kind = VALUES(reporter_kind),
is_legal_entity = VALUES(is_legal_entity),
reporter_first_name = VALUES(reporter_first_name),
reporter_first_surname = VALUES(reporter_first_surname),
reporter_second_surname = VALUES(reporter_second_surname),
reporter_last_name = VALUES(reporter_last_name),
reporter_business_name = VALUES(reporter_business_name),
reporter_gender = VALUES(reporter_gender),
reporter_document_id = VALUES(reporter_document_id),
reporter_document_type = VALUES(reporter_document_type),
reporter_origin_country = VALUES(reporter_origin_country),
subject = VALUES(subject),
accused_party = VALUES(accused_party),
accused_party_details = VALUES(accused_party_details),
complaint_description = VALUES(complaint_description),
reported_to_institution = VALUES(reported_to_institution),
reported_institution_details = VALUES(reported_institution_details),
requested_protection = VALUES(requested_protection),
requested_protection_details = VALUES(requested_protection_details),
information_mode = VALUES(information_mode),
facts_location = VALUES(facts_location),
facts_date_utc = VALUES(facts_date_utc),
forwarding_authorization = VALUES(forwarding_authorization),
forwarding_personal_data_preference = VALUES(forwarding_personal_data_preference),
notification_preference = VALUES(notification_preference),
electronic_notification = VALUES(electronic_notification),
online_tracking_preference = VALUES(online_tracking_preference),
postal_notification_preference = VALUES(postal_notification_preference),
email = VALUES(email),
sms_notification = VALUES(sms_notification),
accepted_terms = VALUES(accepted_terms),
comments = VALUES(comments),
phone = VALUES(phone),
address_line = VALUES(address_line),
address_road_type = VALUES(address_road_type),
address_number = VALUES(address_number),
address_floor = VALUES(address_floor),
address_door = VALUES(address_door),
address_block = VALUES(address_block),
address_stair = VALUES(address_stair),
address_extra = VALUES(address_extra),
municipality = VALUES(municipality),
province = VALUES(province),
postal_code = VALUES(postal_code),
country_code = VALUES(country_code),
form_fields_json = VALUES(form_fields_json),
raw_report_text = VALUES(raw_report_text),
is_confidential = VALUES(is_confidential),
is_update = VALUES(is_update),
gestiona_procedure_id = VALUES(gestiona_procedure_id),
gestiona_group_id = VALUES(gestiona_group_id),
display_name = VALUES(display_name),
workflow_status = VALUES(workflow_status),
selected_document_name = VALUES(selected_document_name),
gestiona_uploaded_at_utc = VALUES(gestiona_uploaded_at_utc),
is_in_gestiona = VALUES(is_in_gestiona),
is_rejected = VALUES(is_rejected),
updated_at_utc = UTC_TIMESTAMP(6);
""";
await using var connection = await OpenConnectionAsync(cancellationToken);
await using var command = new MySqlCommand(sql, connection);
command.Parameters.AddWithValue("@externalRegistryId", denuncia.Id_RegistroDenuncia);
command.Parameters.AddWithValue("@externalReportId", denuncia.Id_Denuncia);
command.Parameters.AddWithValue("@reportDateUtc", ToDbDate(denuncia.Fecha));
command.Parameters.AddWithValue("@gestionaFileUrl", denuncia.Expediente_Gestiona ?? string.Empty);
command.Parameters.AddWithValue("@gestionaFileCode", denuncia.CodigoExpedienteGestiona ?? string.Empty);
command.Parameters.AddWithValue("@gestionaPersonId", denuncia.Id_Persona_Gestiona);
command.Parameters.AddWithValue("@tag", denuncia.Etiqueta ?? string.Empty);
command.Parameters.AddWithValue("@statusName", denuncia.Estado ?? string.Empty);
command.Parameters.AddWithValue("@complaintType", denuncia.Tipo_Denuncia ?? string.Empty);
command.Parameters.AddWithValue("@reporterKind", denuncia.TipoDenunciante ?? string.Empty);
command.Parameters.AddWithValue("@isLegalEntity", denuncia.EsPersonaJuridica);
command.Parameters.AddWithValue("@reporterFirstName", denuncia.Nombre ?? string.Empty);
command.Parameters.AddWithValue("@reporterFirstSurname", denuncia.PrimerApellido ?? string.Empty);
command.Parameters.AddWithValue("@reporterSecondSurname", denuncia.SegundoApellido ?? string.Empty);
command.Parameters.AddWithValue("@reporterLastName", denuncia.Apellidos ?? string.Empty);
command.Parameters.AddWithValue("@reporterBusinessName", denuncia.RazonSocial ?? string.Empty);
command.Parameters.AddWithValue("@reporterGender", denuncia.Sexo ?? string.Empty);
command.Parameters.AddWithValue("@reporterDocumentId", denuncia.Dni ?? string.Empty);
command.Parameters.AddWithValue("@reporterDocumentType", denuncia.TipoDocumentoIdentificativo ?? string.Empty);
command.Parameters.AddWithValue("@reporterOriginCountry", denuncia.PaisOrigen ?? string.Empty);
command.Parameters.AddWithValue("@subject", denuncia.Asunto ?? string.Empty);
command.Parameters.AddWithValue("@accusedParty", denuncia.A_Quien_Denuncia ?? string.Empty);
command.Parameters.AddWithValue("@accusedPartyDetails", denuncia.DenunciadoDetalle ?? string.Empty);
command.Parameters.AddWithValue("@complaintDescription", ToDbStringOrNull(denuncia.Descripcion_Denuncia));
command.Parameters.AddWithValue("@reportedToInstitution", denuncia.Denunciado_Ante_Inst ?? string.Empty);
command.Parameters.AddWithValue("@reportedInstitutionDetails", denuncia.OrganismoDenunciado ?? string.Empty);
command.Parameters.AddWithValue("@requestedProtection", denuncia.SolicitaProteccion ?? string.Empty);
command.Parameters.AddWithValue("@requestedProtectionDetails", denuncia.MedidasProteccionSolicitadas ?? string.Empty);
command.Parameters.AddWithValue("@informationMode", denuncia.Modalidad_Informacion ?? string.Empty);
command.Parameters.AddWithValue("@factsLocation", denuncia.Lugar_Hechos ?? string.Empty);
command.Parameters.AddWithValue("@factsDateUtc", ToDbDate(denuncia.Fecha_Hechos));
command.Parameters.AddWithValue("@forwardingAuthorization", denuncia.AutorizaRemision ?? string.Empty);
command.Parameters.AddWithValue("@forwardingPersonalDataPreference", denuncia.PreferenciaRemision ?? string.Empty);
command.Parameters.AddWithValue("@notificationPreference", denuncia.Notificacion_Preferencia ?? string.Empty);
command.Parameters.AddWithValue("@electronicNotification", denuncia.Notificacion_Electronica ?? string.Empty);
command.Parameters.AddWithValue("@onlineTrackingPreference", denuncia.SeguimientoOnline ?? string.Empty);
command.Parameters.AddWithValue("@postalNotificationPreference", denuncia.NotificacionPostal ?? string.Empty);
command.Parameters.AddWithValue("@email", denuncia.Correo_Electronico ?? string.Empty);
command.Parameters.AddWithValue("@smsNotification", denuncia.Notificacion_Sms ?? string.Empty);
command.Parameters.AddWithValue("@acceptedTerms", denuncia.Condiciones);
command.Parameters.AddWithValue("@comments", ToDbStringOrNull(denuncia.Comments));
command.Parameters.AddWithValue("@phone", denuncia.Telefono ?? string.Empty);
command.Parameters.AddWithValue("@addressLine", denuncia.Direccion ?? string.Empty);
command.Parameters.AddWithValue("@addressRoadType", denuncia.DireccionTipoVia ?? string.Empty);
command.Parameters.AddWithValue("@addressNumber", denuncia.DireccionNumero ?? string.Empty);
command.Parameters.AddWithValue("@addressFloor", denuncia.DireccionPiso ?? string.Empty);
command.Parameters.AddWithValue("@addressDoor", denuncia.DireccionPuerta ?? string.Empty);
command.Parameters.AddWithValue("@addressBlock", denuncia.DireccionBloque ?? string.Empty);
command.Parameters.AddWithValue("@addressStair", denuncia.DireccionEscalera ?? string.Empty);
command.Parameters.AddWithValue("@addressExtra", denuncia.DireccionExtra ?? string.Empty);
command.Parameters.AddWithValue("@municipality", denuncia.Municipio ?? string.Empty);
command.Parameters.AddWithValue("@province", denuncia.Provincia ?? string.Empty);
command.Parameters.AddWithValue("@postalCode", denuncia.CodigoPostal ?? string.Empty);
command.Parameters.AddWithValue("@countryCode", denuncia.Pais ?? string.Empty);
command.Parameters.AddWithValue("@formFieldsJson", ToDbStringOrNull(denuncia.CamposFormularioJson));
command.Parameters.AddWithValue("@rawReportText", ToDbStringOrNull(denuncia.TextoOriginalReport));
command.Parameters.AddWithValue("@isConfidential", denuncia.Confidencial);
command.Parameters.AddWithValue("@isUpdate", denuncia.EsActualizacion);
command.Parameters.AddWithValue("@gestionaProcedureId", ToDbGuid(denuncia.ProcedureId));
command.Parameters.AddWithValue("@gestionaGroupId", ToDbGuid(denuncia.GroupId));
command.Parameters.AddWithValue("@displayName", denuncia.NombreDenuncia ?? string.Empty);
command.Parameters.AddWithValue("@workflowStatus", denuncia.EstadoDenuncia ?? string.Empty);
command.Parameters.AddWithValue("@selectedDocumentName", ToDbStringOrNull(denuncia.ArchivoElegido));
command.Parameters.AddWithValue("@gestionaUploadedAtUtc", ToDbDate(denuncia.FechaSubidaAGestiona));
command.Parameters.AddWithValue("@isInGestiona", denuncia.EnGestiona);
command.Parameters.AddWithValue("@isRejected", denuncia.EnRechazada);
await command.ExecuteNonQueryAsync(cancellationToken);
}
public async Task UpsertFicherosAsync(
IEnumerable<FicherosDenuncias> ficheros,
CancellationToken cancellationToken = default)
{
await EnsureSchemaReadyAsync(cancellationToken);
var attachments = ficheros.ToList();
if (attachments.Count == 0)
{
return;
}
const string sql = """
INSERT INTO complaint_attachments (
complaint_id,
attachment_type_id,
description,
attachment_date_utc,
notes,
original_file_name,
content,
content_mime_type,
content_sha256,
uploaded_to_gestiona,
uploaded_at_utc
) VALUES (
(
SELECT c.id
FROM complaints c
WHERE c.external_report_id = @externalReportId
LIMIT 1
),
@attachmentTypeId,
@description,
@attachmentDateUtc,
@notes,
@originalFileName,
@content,
@contentMimeType,
@contentSha256,
@uploadedToGestiona,
@uploadedAtUtc
)
ON DUPLICATE KEY UPDATE
attachment_type_id = @attachmentTypeId,
description = @description,
attachment_date_utc = @attachmentDateUtc,
notes = @notes,
content = @content,
content_mime_type = @contentMimeType,
content_sha256 = @contentSha256,
uploaded_to_gestiona = CASE
WHEN LOWER(@originalFileName) = 'report.txt' OR LOWER(@originalFileName) = 'report.pdf' THEN @uploadedToGestiona
WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_to_gestiona
ELSE @uploadedToGestiona
END,
uploaded_at_utc = CASE
WHEN LOWER(@originalFileName) = 'report.txt' OR LOWER(@originalFileName) = 'report.pdf' THEN @uploadedAtUtc
WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_at_utc
ELSE @uploadedAtUtc
END,
updated_at_utc = CURRENT_TIMESTAMP(6);
""";
await using var connection = await OpenConnectionAsync(cancellationToken);
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
try
{
foreach (var fichero in attachments)
{
await using var command = new MySqlCommand(sql, connection, (MySqlTransaction)transaction);
var content = fichero.Fichero ?? [];
var sha256 = string.IsNullOrWhiteSpace(fichero.ContentSha256)
? ComputeSha256Hex(content)
: fichero.ContentSha256.Trim().ToLowerInvariant();
command.Parameters.AddWithValue("@attachmentTypeId", fichero.Id_Tipo);
command.Parameters.AddWithValue("@description", ToDbStringOrNull(fichero.Descripcion));
command.Parameters.AddWithValue("@attachmentDateUtc", ToDbDate(fichero.Fecha));
command.Parameters.AddWithValue("@notes", fichero.Observaciones ?? string.Empty);
command.Parameters.AddWithValue("@originalFileName", fichero.NombreFichero ?? string.Empty);
command.Parameters.AddWithValue("@content", content);
command.Parameters.AddWithValue("@contentMimeType", DetectMimeType(fichero.NombreFichero));
command.Parameters.AddWithValue("@contentSha256", sha256);
command.Parameters.AddWithValue("@uploadedToGestiona", fichero.Subido);
command.Parameters.AddWithValue("@uploadedAtUtc", ToDbDate(fichero.FechaSubida));
command.Parameters.AddWithValue("@externalReportId", fichero.Id_Denuncia);
await command.ExecuteNonQueryAsync(cancellationToken);
}
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
public async Task MarkFicherosAsUploadedAsync(
int denunciaId,
IEnumerable<string> fileNames,
DateTime uploadedAtUtc,
CancellationToken cancellationToken = default)
{
await EnsureSchemaReadyAsync(cancellationToken);
var names = fileNames
.Where(name => !string.IsNullOrWhiteSpace(name))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (names.Count == 0)
{
return;
}
var parameterNames = new List<string>(names.Count);
await using var connection = await OpenConnectionAsync(cancellationToken);
await using var command = connection.CreateCommand();
for (var i = 0; i < names.Count; i++)
{
var parameterName = $"@name{i}";
parameterNames.Add(parameterName);
command.Parameters.AddWithValue(parameterName, names[i]);
}
command.Parameters.AddWithValue("@denunciaId", denunciaId);
command.Parameters.AddWithValue("@uploadedAtUtc", uploadedAtUtc);
command.CommandText = $"""
UPDATE complaint_attachments a
INNER JOIN complaints c ON c.id = a.complaint_id
SET
a.uploaded_to_gestiona = 1,
a.uploaded_at_utc = @uploadedAtUtc,
a.updated_at_utc = UTC_TIMESTAMP(6)
WHERE c.external_report_id = @denunciaId
AND a.original_file_name IN ({string.Join(", ", parameterNames)});
""";
await command.ExecuteNonQueryAsync(cancellationToken);
}
private async Task EnsureSchemaReadyAsync(CancellationToken cancellationToken)
{
if (SchemaEnsured)
{
return;
}
await SchemaGate.WaitAsync(cancellationToken);
try
{
if (SchemaEnsured)
{
return;
}
if (_options.AutoCreateSchema)
{
var sqlPath = ResolveSchemaPath();
if (!File.Exists(sqlPath))
{
throw new FileNotFoundException($"No se encuentra el script de esquema: {sqlPath}");
}
var sql = await File.ReadAllTextAsync(sqlPath, cancellationToken);
var commands = sql
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(command => !string.IsNullOrWhiteSpace(command))
.ToList();
await using var connection = await OpenConnectionAsync(cancellationToken);
foreach (var commandText in commands)
{
await using var command = new MySqlCommand(commandText, connection);
await command.ExecuteNonQueryAsync(cancellationToken);
}
await EnsureCompatibleSchemaAsync(connection, cancellationToken);
_logger.LogInformation("Esquema de denuncias verificado/creado en MySQL.");
}
SchemaEnsured = true;
}
finally
{
SchemaGate.Release();
}
}
private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
{
var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
var connection = new MySqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await using var timeZoneCommand = new MySqlCommand("SET time_zone = '+00:00';", connection);
await timeZoneCommand.ExecuteNonQueryAsync(cancellationToken);
return connection;
}
private static async Task EnsureCompatibleSchemaAsync(
MySqlConnection connection,
CancellationToken cancellationToken)
{
foreach (var (table, column, definition) in SchemaColumnsToEnsure)
{
if (!await ColumnExistsAsync(connection, table, column, cancellationToken))
{
await using var alterCommand = connection.CreateCommand();
alterCommand.CommandText = $"ALTER TABLE `{table}` ADD COLUMN {definition};";
await alterCommand.ExecuteNonQueryAsync(cancellationToken);
}
}
foreach (var (table, indexName, definition) in SchemaIndexesToEnsure)
{
if (!await IndexExistsAsync(connection, table, indexName, cancellationToken))
{
await using var alterCommand = connection.CreateCommand();
alterCommand.CommandText = $"ALTER TABLE `{table}` ADD {definition};";
await alterCommand.ExecuteNonQueryAsync(cancellationToken);
}
}
foreach (var (table, definition) in SchemaEncryptedColumnsToEnsure)
{
var column = ExtractColumnName(definition);
if (!await ColumnDataTypeIsCompatibleAsync(connection, table, column, definition, cancellationToken))
{
await using var alterCommand = connection.CreateCommand();
alterCommand.CommandText = $"ALTER TABLE `{table}` MODIFY COLUMN {definition};";
await alterCommand.ExecuteNonQueryAsync(cancellationToken);
}
}
}
private static string ExtractColumnName(string definition)
{
var first = definition.IndexOf('`');
var second = first >= 0 ? definition.IndexOf('`', first + 1) : -1;
return first >= 0 && second > first
? definition[(first + 1)..second]
: definition.Split(' ', StringSplitOptions.RemoveEmptyEntries)[0].Trim('`');
}
private static async Task<bool> ColumnDataTypeIsCompatibleAsync(
MySqlConnection connection,
string table,
string column,
string definition,
CancellationToken cancellationToken)
{
const string sql = """
SELECT DATA_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = @table
AND COLUMN_NAME = @column
LIMIT 1;
""";
await using var command = connection.CreateCommand();
command.CommandText = sql;
command.Parameters.AddWithValue("@table", table);
command.Parameters.AddWithValue("@column", column);
var current = (await command.ExecuteScalarAsync(cancellationToken))?.ToString();
if (string.IsNullOrWhiteSpace(current))
{
return false;
}
var expected = definition.ToUpperInvariant();
if (expected.Contains("LONGTEXT", StringComparison.Ordinal))
{
return string.Equals(current, "longtext", StringComparison.OrdinalIgnoreCase);
}
if (expected.Contains("TEXT", StringComparison.Ordinal))
{
return current.Equals("text", StringComparison.OrdinalIgnoreCase) ||
current.Equals("mediumtext", StringComparison.OrdinalIgnoreCase) ||
current.Equals("longtext", StringComparison.OrdinalIgnoreCase);
}
return true;
}
private static async Task<bool> ColumnExistsAsync(
MySqlConnection connection,
string table,
string column,
CancellationToken cancellationToken)
{
const string sql = """
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = @table
AND COLUMN_NAME = @column;
""";
await using var command = connection.CreateCommand();
command.CommandText = sql;
command.Parameters.AddWithValue("@table", table);
command.Parameters.AddWithValue("@column", column);
var result = await command.ExecuteScalarAsync(cancellationToken);
return Convert.ToInt32(result, CultureInfo.InvariantCulture) > 0;
}
private static async Task<bool> IndexExistsAsync(
MySqlConnection connection,
string table,
string indexName,
CancellationToken cancellationToken)
{
const string sql = """
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = @table
AND INDEX_NAME = @indexName;
""";
await using var command = connection.CreateCommand();
command.CommandText = sql;
command.Parameters.AddWithValue("@table", table);
command.Parameters.AddWithValue("@indexName", indexName);
var result = await command.ExecuteScalarAsync(cancellationToken);
return Convert.ToInt32(result, CultureInfo.InvariantCulture) > 0;
}
private string ResolveSchemaPath()
{
var contentRootPath = Path.Combine(_environment.ContentRootPath, "Scripts", "gestiondenuncias_schema.sql");
if (File.Exists(contentRootPath))
{
return contentRootPath;
}
return Path.Combine(AppContext.BaseDirectory, "Scripts", "gestiondenuncias_schema.sql");
}
private static DenunciasGestiona MapComplaint(IDataRecord record)
{
return new DenunciasGestiona
{
Id_RegistroDenuncia = GetInt32(record, "external_registry_id"),
Id_Denuncia = GetInt32(record, "external_report_id"),
Fecha = GetDateTime(record, "report_date_utc"),
Expediente_Gestiona = GetString(record, "gestiona_file_url"),
CodigoExpedienteGestiona = GetString(record, "gestiona_file_code"),
Id_Persona_Gestiona = GetInt32(record, "gestiona_person_id"),
Etiqueta = GetString(record, "tag"),
Estado = GetString(record, "status_name"),
Tipo_Denuncia = GetString(record, "complaint_type"),
TipoDenunciante = GetString(record, "reporter_kind"),
EsPersonaJuridica = GetBoolean(record, "is_legal_entity"),
Nombre = GetString(record, "reporter_first_name"),
PrimerApellido = GetString(record, "reporter_first_surname"),
SegundoApellido = GetString(record, "reporter_second_surname"),
Apellidos = GetString(record, "reporter_last_name"),
RazonSocial = GetString(record, "reporter_business_name"),
Sexo = GetString(record, "reporter_gender"),
Dni = GetString(record, "reporter_document_id"),
TipoDocumentoIdentificativo = GetString(record, "reporter_document_type"),
PaisOrigen = GetString(record, "reporter_origin_country"),
Asunto = GetString(record, "subject"),
A_Quien_Denuncia = GetString(record, "accused_party"),
DenunciadoDetalle = GetString(record, "accused_party_details"),
Descripcion_Denuncia = GetString(record, "complaint_description"),
Denunciado_Ante_Inst = GetString(record, "reported_to_institution"),
OrganismoDenunciado = GetString(record, "reported_institution_details"),
SolicitaProteccion = GetString(record, "requested_protection"),
MedidasProteccionSolicitadas = GetString(record, "requested_protection_details"),
Modalidad_Informacion = GetString(record, "information_mode"),
Lugar_Hechos = GetString(record, "facts_location"),
Fecha_Hechos = GetDateTime(record, "facts_date_utc"),
AutorizaRemision = GetString(record, "forwarding_authorization"),
PreferenciaRemision = GetString(record, "forwarding_personal_data_preference"),
Notificacion_Preferencia = GetString(record, "notification_preference"),
Notificacion_Electronica = GetString(record, "electronic_notification"),
SeguimientoOnline = GetString(record, "online_tracking_preference"),
NotificacionPostal = GetString(record, "postal_notification_preference"),
Correo_Electronico = GetString(record, "email"),
Notificacion_Sms = GetString(record, "sms_notification"),
Condiciones = GetBoolean(record, "accepted_terms"),
Comments = GetString(record, "comments"),
Telefono = GetString(record, "phone"),
Direccion = GetString(record, "address_line"),
DireccionTipoVia = GetString(record, "address_road_type"),
DireccionNumero = GetString(record, "address_number"),
DireccionPiso = GetString(record, "address_floor"),
DireccionPuerta = GetString(record, "address_door"),
DireccionBloque = GetString(record, "address_block"),
DireccionEscalera = GetString(record, "address_stair"),
DireccionExtra = GetString(record, "address_extra"),
Municipio = GetString(record, "municipality"),
Provincia = GetString(record, "province"),
CodigoPostal = GetString(record, "postal_code"),
Pais = GetString(record, "country_code"),
CamposFormularioJson = GetString(record, "form_fields_json"),
TextoOriginalReport = GetString(record, "raw_report_text"),
Confidencial = GetBoolean(record, "is_confidential"),
EsActualizacion = GetBoolean(record, "is_update"),
ProcedureId = GetGuid(record, "gestiona_procedure_id"),
GroupId = GetGuid(record, "gestiona_group_id"),
NombreDenuncia = GetString(record, "display_name"),
EstadoDenuncia = GetString(record, "workflow_status"),
ArchivoElegido = GetString(record, "selected_document_name"),
FechaSubidaAGestiona = GetDateTime(record, "gestiona_uploaded_at_utc"),
EnGestiona = GetBoolean(record, "is_in_gestiona"),
EnRechazada = GetBoolean(record, "is_rejected"),
};
}
private static FicherosDenuncias MapAttachment(IDataRecord record)
{
return new FicherosDenuncias
{
Id_Fichero = Convert.ToInt32(record["id"]),
Id_Tipo = GetInt32(record, "attachment_type_id"),
Descripcion = GetNullableString(record, "description"),
Fecha = GetDateTime(record, "attachment_date_utc"),
Observaciones = GetString(record, "notes"),
Id_Denuncia = GetInt32(record, "external_report_id"),
NombreFichero = GetString(record, "original_file_name"),
Fichero = GetBytes(record, "content"),
ContentSha256 = GetString(record, "content_sha256"),
Subido = GetBoolean(record, "uploaded_to_gestiona"),
FechaSubida = GetNullableDateTime(record, "uploaded_at_utc"),
};
}
private static object ToDbDate(DateTime value)
{
return value == DateTime.MinValue ? DBNull.Value : value;
}
private static object ToDbDate(DateTime? value)
{
return value is null || value == DateTime.MinValue ? DBNull.Value : value.Value;
}
private static object ToDbGuid(Guid value)
{
return value == Guid.Empty ? DBNull.Value : value.ToString();
}
private static object ToDbStringOrNull(string? value)
{
return string.IsNullOrWhiteSpace(value) ? DBNull.Value : value;
}
private static string GetString(IDataRecord record, string columnName)
{
var ordinal = record.GetOrdinal(columnName);
if (record.IsDBNull(ordinal))
{
return string.Empty;
}
return ConvertRecordValueToString(record.GetValue(ordinal)) ?? string.Empty;
}
private static string? GetNullableString(IDataRecord record, string columnName)
{
var ordinal = record.GetOrdinal(columnName);
return record.IsDBNull(ordinal) ? null : ConvertRecordValueToString(record.GetValue(ordinal));
}
private static int GetInt32(IDataRecord record, string columnName)
{
var ordinal = record.GetOrdinal(columnName);
return record.IsDBNull(ordinal) ? 0 : record.GetInt32(ordinal);
}
private static bool GetBoolean(IDataRecord record, string columnName)
{
var ordinal = record.GetOrdinal(columnName);
return !record.IsDBNull(ordinal) && record.GetBoolean(ordinal);
}
private static DateTime GetDateTime(IDataRecord record, string columnName)
{
var ordinal = record.GetOrdinal(columnName);
return record.IsDBNull(ordinal) ? DateTime.MinValue : record.GetDateTime(ordinal);
}
private static DateTime? GetNullableDateTime(IDataRecord record, string columnName)
{
var ordinal = record.GetOrdinal(columnName);
return record.IsDBNull(ordinal) ? null : record.GetDateTime(ordinal);
}
private static Guid GetGuid(IDataRecord record, string columnName)
{
var ordinal = record.GetOrdinal(columnName);
if (record.IsDBNull(ordinal))
{
return Guid.Empty;
}
var value = record.GetValue(ordinal);
return value switch
{
Guid guid => guid,
byte[] bytes when bytes.Length == 16 => new Guid(bytes),
_ when Guid.TryParse(ConvertRecordValueToString(value), out var result) => result,
_ => Guid.Empty
};
}
private static byte[] GetBytes(IDataRecord record, string columnName)
{
var ordinal = record.GetOrdinal(columnName);
if (record.IsDBNull(ordinal))
{
return [];
}
return (byte[])record.GetValue(ordinal);
}
private static string DetectMimeType(string? fileName)
{
var extension = Path.GetExtension(fileName ?? string.Empty).ToLowerInvariant();
return extension switch
{
".pdf" => "application/pdf",
".txt" => "text/plain",
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".gif" => "image/gif",
".doc" => "application/msword",
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".xls" => "application/vnd.ms-excel",
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
_ => "application/octet-stream",
};
}
private static string ComputeSha256Hex(byte[] content)
{
var hash = SHA256.HashData(content);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static string? ConvertRecordValueToString(object? value)
{
return value switch
{
null or DBNull => null,
string text => text,
Guid guid => guid.ToString(),
byte[] bytes when bytes.Length == 16 => new Guid(bytes).ToString(),
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString()
};
}
}