cambios denuncias

This commit is contained in:
2026-05-06 13:48:23 +02:00
parent 1d1211d372
commit 8553dfe90c
76 changed files with 1355 additions and 484 deletions

View File

@@ -0,0 +1,574 @@
using System.Globalization;
using GestionaDenuncias.Shared.Models;
using MySqlConnector;
namespace ApiDenuncias.Services;
public sealed class InboxTrackingService : IInboxTrackingService
{
private readonly IDenunciaStore _denunciaStore;
private readonly MySqlConnectionStringProvider _connectionStringProvider;
public InboxTrackingService(
IDenunciaStore denunciaStore,
MySqlConnectionStringProvider connectionStringProvider)
{
_denunciaStore = denunciaStore;
_connectionStringProvider = connectionStringProvider;
}
public async Task<InboxUserState> GetUserStateAsync(string username, CancellationToken cancellationToken = default)
{
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(username))
{
return new InboxUserState();
}
await using var connection = await OpenConnectionAsync(cancellationToken);
var userId = await EnsureUserAsync(connection, username, cancellationToken);
const string sql = """
SELECT
username,
last_successful_download_at_utc,
last_downloaded_report_moment_utc
FROM app_users
WHERE id = @userId;
""";
await using var command = new MySqlCommand(sql, connection);
command.Parameters.AddWithValue("@userId", userId);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
{
return new InboxUserState { Username = username };
}
return new InboxUserState
{
Username = reader.GetString(reader.GetOrdinal("username")),
LastSuccessfulDownloadAtUtc = GetDateTimeOffset(reader, "last_successful_download_at_utc"),
LastDownloadedReportMomentUtc = GetDateTimeOffset(reader, "last_downloaded_report_moment_utc"),
};
}
public async Task<IReadOnlyList<ReportDto>> RegisterSnapshotAsync(
string username,
IEnumerable<ReportDto> reports,
CancellationToken cancellationToken = default)
{
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
var reportList = reports.ToList();
if (string.IsNullOrWhiteSpace(username) || reportList.Count == 0)
{
return reportList;
}
await using var connection = await OpenConnectionAsync(cancellationToken);
var userId = await EnsureUserAsync(connection, username, cancellationToken);
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
try
{
foreach (var report in reportList)
{
await UpsertInboxReportAsync(connection, transaction, report, cancellationToken);
await UpsertUserSnapshotAsync(connection, transaction, userId, report.Id, cancellationToken);
}
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
var metadata = await LoadMetadataAsync(connection, userId, reportList.Select(r => r.Id).ToList(), cancellationToken);
return reportList
.Select(report =>
{
metadata.TryGetValue(report.Id, out var meta);
return report with
{
DownloadedByCurrentUser = meta?.DownloadedByCurrentUser ?? false,
DownloadedByAnotherUser = meta?.DownloadedByAnotherUser ?? false,
LastDownloadedByUsername = meta?.LastDownloadedByUsername,
LastDownloadedAt = meta?.LastDownloadedAtUtc?.ToString("O", CultureInfo.InvariantCulture),
AlreadyImported = meta?.AlreadyImported ?? false,
AlreadyInGestiona = meta?.AlreadyInGestiona ?? false,
TrackingNote = BuildTrackingNote(meta)
};
})
.Where(report => !IsLockedByAnotherUser(report))
.ToArray();
}
public async Task EnsureReportCanBeImportedByUserAsync(
string username,
ReportDto report,
CancellationToken cancellationToken = default)
{
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(report.Id))
{
throw new InvalidOperationException("No se ha podido validar la propiedad de la denuncia.");
}
await using var connection = await OpenConnectionAsync(cancellationToken);
var userId = await EnsureUserAsync(connection, username, cancellationToken);
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
try
{
await UpsertInboxReportAsync(connection, (MySqlTransaction)transaction, report, cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
var metadata = await LoadMetadataAsync(connection, userId, [report.Id], cancellationToken);
if (metadata.TryGetValue(report.Id, out var meta) && meta.LockedByAnotherUser)
{
var owner = string.IsNullOrWhiteSpace(meta.LastDownloadedByUsername)
? "otro usuario"
: meta.LastDownloadedByUsername;
throw new InvalidOperationException(
$"La denuncia ya fue importada por {owner}. Solo ese usuario puede ver e importar sus actualizaciones.");
}
}
public async Task MarkReportImportedAsync(
string username,
ReportDto report,
int? complaintId,
CancellationToken cancellationToken = default)
{
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(report.Id))
{
return;
}
var reportMoment = ResolveReportMoment(report);
var nowUtc = DateTime.UtcNow;
await using var connection = await OpenConnectionAsync(cancellationToken);
var userId = await EnsureUserAsync(connection, username, cancellationToken);
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
try
{
await UpsertInboxReportAsync(connection, transaction, report, cancellationToken);
const string updateInboxSql = """
UPDATE inbox_reports
SET
last_downloaded_at_utc = @nowUtc,
last_downloaded_by_user_id = @userId,
imported_complaint_report_id = COALESCE(@complaintId, imported_complaint_report_id),
imported_to_store_at_utc = COALESCE(imported_to_store_at_utc, @nowUtc),
updated_at_utc = CURRENT_TIMESTAMP(6)
WHERE global_report_uuid = @reportId;
""";
await using (var updateInbox = new MySqlCommand(updateInboxSql, connection, (MySqlTransaction)transaction))
{
updateInbox.Parameters.AddWithValue("@nowUtc", nowUtc);
updateInbox.Parameters.AddWithValue("@userId", userId);
updateInbox.Parameters.AddWithValue("@complaintId", complaintId.HasValue ? complaintId.Value : DBNull.Value);
updateInbox.Parameters.AddWithValue("@reportId", report.Id);
await updateInbox.ExecuteNonQueryAsync(cancellationToken);
}
const string updateUserReportSql = """
INSERT INTO user_inbox_reports (
app_user_id,
inbox_report_id,
first_seen_at_utc,
last_seen_at_utc,
first_downloaded_at_utc,
last_downloaded_at_utc,
download_count
)
SELECT
@userId,
ir.id,
CURRENT_TIMESTAMP(6),
CURRENT_TIMESTAMP(6),
@nowUtc,
@nowUtc,
1
FROM inbox_reports ir
WHERE ir.global_report_uuid = @reportId
ON DUPLICATE KEY UPDATE
last_seen_at_utc = CURRENT_TIMESTAMP(6),
first_downloaded_at_utc = COALESCE(first_downloaded_at_utc, VALUES(first_downloaded_at_utc)),
last_downloaded_at_utc = VALUES(last_downloaded_at_utc),
download_count = download_count + 1;
""";
await using (var updateUserReport = new MySqlCommand(updateUserReportSql, connection, (MySqlTransaction)transaction))
{
updateUserReport.Parameters.AddWithValue("@userId", userId);
updateUserReport.Parameters.AddWithValue("@nowUtc", nowUtc);
updateUserReport.Parameters.AddWithValue("@reportId", report.Id);
await updateUserReport.ExecuteNonQueryAsync(cancellationToken);
}
const string updateUserSql = """
UPDATE app_users
SET
last_successful_download_at_utc = @nowUtc,
last_downloaded_report_moment_utc =
CASE
WHEN @reportMoment IS NULL THEN last_downloaded_report_moment_utc
WHEN last_downloaded_report_moment_utc IS NULL THEN @reportMoment
WHEN @reportMoment > last_downloaded_report_moment_utc THEN @reportMoment
ELSE last_downloaded_report_moment_utc
END,
updated_at_utc = CURRENT_TIMESTAMP(6)
WHERE id = @userId;
""";
await using (var updateUser = new MySqlCommand(updateUserSql, connection, (MySqlTransaction)transaction))
{
updateUser.Parameters.AddWithValue("@nowUtc", nowUtc);
updateUser.Parameters.AddWithValue("@reportMoment", ToDbDate(reportMoment));
updateUser.Parameters.AddWithValue("@userId", userId);
await updateUser.ExecuteNonQueryAsync(cancellationToken);
}
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
private async Task<long> EnsureUserAsync(MySqlConnection connection, string username, CancellationToken cancellationToken)
{
const string insertSql = """
INSERT INTO app_users (username)
VALUES (@username)
ON DUPLICATE KEY UPDATE
updated_at_utc = CURRENT_TIMESTAMP(6);
""";
await using (var insert = new MySqlCommand(insertSql, connection))
{
insert.Parameters.AddWithValue("@username", username.Trim());
await insert.ExecuteNonQueryAsync(cancellationToken);
}
const string selectSql = """
SELECT id
FROM app_users
WHERE username = @username
LIMIT 1;
""";
await using var select = new MySqlCommand(selectSql, connection);
select.Parameters.AddWithValue("@username", username.Trim());
var result = await select.ExecuteScalarAsync(cancellationToken);
return Convert.ToInt64(result, CultureInfo.InvariantCulture);
}
private static async Task UpsertInboxReportAsync(
MySqlConnection connection,
MySqlTransaction transaction,
ReportDto report,
CancellationToken cancellationToken)
{
const string sql = """
INSERT INTO inbox_reports (
global_report_uuid,
progressive_id,
context_id,
context_name,
creation_date_utc,
update_date_utc,
access_date_utc,
last_access_utc,
gl_status,
gl_label,
is_updated
) VALUES (
@reportId,
@progressiveId,
@contextId,
@contextName,
@creationDateUtc,
@updateDateUtc,
@accessDateUtc,
@lastAccessUtc,
@glStatus,
@glLabel,
@isUpdated
)
ON DUPLICATE KEY UPDATE
progressive_id = VALUES(progressive_id),
context_id = VALUES(context_id),
context_name = VALUES(context_name),
creation_date_utc = VALUES(creation_date_utc),
update_date_utc = VALUES(update_date_utc),
access_date_utc = VALUES(access_date_utc),
last_access_utc = VALUES(last_access_utc),
gl_status = VALUES(gl_status),
gl_label = VALUES(gl_label),
is_updated = VALUES(is_updated),
last_seen_at_utc = CURRENT_TIMESTAMP(6),
updated_at_utc = CURRENT_TIMESTAMP(6);
""";
await using var command = new MySqlCommand(sql, connection, transaction);
command.Parameters.AddWithValue("@reportId", report.Id);
command.Parameters.AddWithValue("@progressiveId", report.Progressive.HasValue ? report.Progressive.Value : DBNull.Value);
command.Parameters.AddWithValue("@contextId", ToDbString(report.ContextId));
command.Parameters.AddWithValue("@contextName", ToDbString(report.ContextName));
command.Parameters.AddWithValue("@creationDateUtc", ToDbDate(ParseDate(report.CreationDate)));
command.Parameters.AddWithValue("@updateDateUtc", ToDbDate(ParseDate(report.UpdateDate)));
command.Parameters.AddWithValue("@accessDateUtc", ToDbDate(ParseDate(report.AccessDate)));
command.Parameters.AddWithValue("@lastAccessUtc", ToDbDate(ParseDate(report.LastAccess)));
command.Parameters.AddWithValue("@glStatus", ToDbString(report.Status));
command.Parameters.AddWithValue("@glLabel", ToDbString(report.Label));
command.Parameters.AddWithValue("@isUpdated", report.Updated);
await command.ExecuteNonQueryAsync(cancellationToken);
}
private static async Task UpsertUserSnapshotAsync(
MySqlConnection connection,
MySqlTransaction transaction,
long userId,
string reportId,
CancellationToken cancellationToken)
{
const string sql = """
INSERT INTO user_inbox_reports (
app_user_id,
inbox_report_id,
first_seen_at_utc,
last_seen_at_utc
)
SELECT
@userId,
ir.id,
CURRENT_TIMESTAMP(6),
CURRENT_TIMESTAMP(6)
FROM inbox_reports ir
WHERE ir.global_report_uuid = @reportId
ON DUPLICATE KEY UPDATE
last_seen_at_utc = CURRENT_TIMESTAMP(6);
""";
await using var command = new MySqlCommand(sql, connection, transaction);
command.Parameters.AddWithValue("@userId", userId);
command.Parameters.AddWithValue("@reportId", reportId);
await command.ExecuteNonQueryAsync(cancellationToken);
}
private async Task<Dictionary<string, ReportMetadata>> LoadMetadataAsync(
MySqlConnection connection,
long userId,
List<string> reportIds,
CancellationToken cancellationToken)
{
var metadata = new Dictionary<string, ReportMetadata>(StringComparer.OrdinalIgnoreCase);
if (reportIds.Count == 0)
{
return metadata;
}
await using var command = connection.CreateCommand();
var parameterNames = new List<string>(reportIds.Count);
for (var i = 0; i < reportIds.Count; i++)
{
var parameterName = $"@reportId{i}";
parameterNames.Add(parameterName);
command.Parameters.AddWithValue(parameterName, reportIds[i]);
}
command.Parameters.AddWithValue("@userId", userId);
command.CommandText = $"""
SELECT
ir.global_report_uuid,
ir.last_downloaded_at_utc,
downloader.username AS last_downloaded_by_username,
ir.imported_to_store_at_utc,
COALESCE(c.is_in_gestiona, 0) AS already_in_gestiona,
CASE WHEN uir.last_downloaded_at_utc IS NULL THEN 0 ELSE 1 END AS downloaded_by_current_user
FROM inbox_reports ir
LEFT JOIN app_users downloader ON downloader.id = ir.last_downloaded_by_user_id
LEFT JOIN user_inbox_reports uir
ON uir.inbox_report_id = ir.id
AND uir.app_user_id = @userId
LEFT JOIN complaints c
ON c.external_report_id = COALESCE(ir.imported_complaint_report_id, ir.progressive_id)
WHERE ir.global_report_uuid IN ({string.Join(", ", parameterNames)});
""";
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
{
var reportId = GetStringValue(reader, "global_report_uuid");
var lastDownloadedByUsername = reader.IsDBNull(reader.GetOrdinal("last_downloaded_by_username"))
? null
: reader.GetString(reader.GetOrdinal("last_downloaded_by_username"));
var downloadedByCurrentUser = reader.GetInt32(reader.GetOrdinal("downloaded_by_current_user")) == 1;
var lockedByAnotherUser =
!downloadedByCurrentUser &&
!reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")) &&
!string.IsNullOrWhiteSpace(lastDownloadedByUsername);
var downloadedByAnotherUser =
!downloadedByCurrentUser &&
!string.IsNullOrWhiteSpace(lastDownloadedByUsername);
metadata[reportId] = new ReportMetadata
{
DownloadedByCurrentUser = downloadedByCurrentUser,
DownloadedByAnotherUser = downloadedByAnotherUser,
LastDownloadedByUsername = lastDownloadedByUsername,
LastDownloadedAtUtc = GetDateTimeOffset(reader, "last_downloaded_at_utc"),
AlreadyImported = !reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")),
AlreadyInGestiona = reader.GetInt32(reader.GetOrdinal("already_in_gestiona")) == 1,
LockedByAnotherUser = lockedByAnotherUser,
};
}
return metadata;
}
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 object ToDbString(string? value)
{
return string.IsNullOrWhiteSpace(value) ? DBNull.Value : value;
}
private static object ToDbDate(DateTimeOffset? value)
{
return value is null ? DBNull.Value : value.Value.UtcDateTime;
}
private static DateTimeOffset? ParseDate(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return DateTimeOffset.TryParse(value.Replace("Z", "+00:00", StringComparison.Ordinal), out var parsed)
? parsed
: null;
}
private static DateTimeOffset? ResolveReportMoment(ReportDto report)
{
return ParseDate(report.UpdateDate) ?? ParseDate(report.CreationDate);
}
private static DateTimeOffset? GetDateTimeOffset(MySqlDataReader reader, string columnName)
{
var ordinal = reader.GetOrdinal(columnName);
return reader.IsDBNull(ordinal)
? null
: new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(ordinal), DateTimeKind.Utc));
}
private static string GetStringValue(MySqlDataReader reader, string columnName)
{
var ordinal = reader.GetOrdinal(columnName);
if (reader.IsDBNull(ordinal))
{
return string.Empty;
}
var value = reader.GetValue(ordinal);
return value switch
{
Guid guid => guid.ToString("D"),
string text => text,
_ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty
};
}
private static string? BuildTrackingNote(ReportMetadata? metadata)
{
if (metadata is null)
{
return null;
}
if (metadata.LockedByAnotherUser)
{
return string.IsNullOrWhiteSpace(metadata.LastDownloadedByUsername)
? "Importada por otro usuario"
: $"Importada por {metadata.LastDownloadedByUsername}";
}
if (metadata.AlreadyInGestiona)
{
return "Ya existe expediente en Gestiona";
}
if (metadata.DownloadedByAnotherUser)
{
if (!string.IsNullOrWhiteSpace(metadata.LastDownloadedByUsername) && metadata.LastDownloadedAtUtc is not null)
{
return $"La descargó {metadata.LastDownloadedByUsername} el {metadata.LastDownloadedAtUtc.Value.ToLocalTime():dd/MM/yyyy HH:mm}";
}
return "Ya la descargó otro usuario";
}
if (metadata.DownloadedByCurrentUser && metadata.LastDownloadedAtUtc is not null)
{
return $"Ya la descargaste el {metadata.LastDownloadedAtUtc.Value.ToLocalTime():dd/MM/yyyy HH:mm}";
}
if (metadata.AlreadyImported)
{
return "Ya está incorporada a la base de trabajo";
}
return null;
}
private sealed record ReportMetadata
{
public bool DownloadedByCurrentUser { get; init; }
public bool DownloadedByAnotherUser { get; init; }
public bool LockedByAnotherUser { get; init; }
public string? LastDownloadedByUsername { get; init; }
public DateTimeOffset? LastDownloadedAtUtc { get; init; }
public bool AlreadyImported { get; init; }
public bool AlreadyInGestiona { get; init; }
}
private static bool IsLockedByAnotherUser(ReportDto report)
=> report.AlreadyImported &&
report.DownloadedByAnotherUser &&
!report.DownloadedByCurrentUser;
}