- Elimina la generación de documentos artificiales para comentarios. - Añade los estados Nueva denuncia, Actualización ciudadano, Actividad OAAF y Actualizada. - Incorpora circuitos específicos de comunicación SAJ y SDI. - Registra correctamente el tipo de última subida en el histórico. - Añade confirmación al asignar una denuncia a un grupo distinto al del usuario. - Mejora etiquetas, mensajes de ayuda e instrucciones de la aplicación. - Añade la persistencia técnica necesaria para identificar el origen de cada actualización.
783 lines
32 KiB
C#
783 lines
32 KiB
C#
using System.Globalization;
|
|
using System.Data;
|
|
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 SafeRollbackAsync(transaction, cancellationToken);
|
|
throw;
|
|
}
|
|
|
|
await EnsureConnectionOpenAsync(connection, cancellationToken);
|
|
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),
|
|
LastGestionaUploadAt = meta?.LastGestionaUploadAtUtc?.ToString("O", CultureInfo.InvariantCulture),
|
|
AlreadyImported = meta?.AlreadyImported ?? false,
|
|
AlreadyInGestiona = meta?.AlreadyInGestiona ?? false,
|
|
OwnerUsername = meta?.OwnerUsername,
|
|
OwnedByCurrentUser = meta?.OwnedByCurrentUser ?? false,
|
|
AccessibleByWorkGroup = meta?.AccessibleByWorkGroup ?? false,
|
|
RequiresOwnerConfirmation = meta?.RequiresOwnerConfirmation ?? false,
|
|
OwnerWorkGroups = meta?.OwnerWorkGroups ?? [],
|
|
TrackingNote = BuildTrackingNote(meta)
|
|
};
|
|
})
|
|
.Where(report =>
|
|
string.IsNullOrWhiteSpace(report.OwnerUsername) ||
|
|
report.OwnedByCurrentUser ||
|
|
report.AccessibleByWorkGroup)
|
|
.ToArray();
|
|
}
|
|
|
|
public async Task EnsureReportCanBeImportedByUserAsync(
|
|
string username,
|
|
ReportDto report,
|
|
bool confirmDifferentOwner = false,
|
|
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 SafeRollbackAsync(transaction, cancellationToken);
|
|
throw;
|
|
}
|
|
|
|
await EnsureConnectionOpenAsync(connection, cancellationToken);
|
|
var metadata = await LoadMetadataAsync(connection, userId, [report.Id], cancellationToken);
|
|
if (!metadata.TryGetValue(report.Id, out var meta) ||
|
|
string.IsNullOrWhiteSpace(meta.OwnerUsername) ||
|
|
meta.OwnedByCurrentUser)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!meta.AccessibleByWorkGroup)
|
|
{
|
|
throw new ReportOwnershipException(
|
|
$"La denuncia pertenece a {meta.OwnerUsername} y no compartis ningun grupo de trabajo.");
|
|
}
|
|
|
|
if (!confirmDifferentOwner)
|
|
{
|
|
throw new ReportOwnershipException(
|
|
$"La denuncia pertenece a {meta.OwnerUsername}. Confirma expresamente que deseas importarla como miembro de su grupo.");
|
|
}
|
|
}
|
|
|
|
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,
|
|
owner_user_id = COALESCE(owner_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 SafeRollbackAsync(transaction, cancellationToken);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task MarkReportHandledInGestionaAsync(
|
|
string username,
|
|
int denunciaId,
|
|
DateTime uploadedAtUtc,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
|
|
|
if (string.IsNullOrWhiteSpace(username) || denunciaId <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var handledAtUtc = uploadedAtUtc.Kind switch
|
|
{
|
|
DateTimeKind.Utc => uploadedAtUtc,
|
|
DateTimeKind.Local => uploadedAtUtc.ToUniversalTime(),
|
|
_ => DateTime.SpecifyKind(uploadedAtUtc, DateTimeKind.Utc)
|
|
};
|
|
|
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
|
var userId = await EnsureUserAsync(connection, username, cancellationToken);
|
|
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
|
|
|
try
|
|
{
|
|
const string updateInboxSql = """
|
|
UPDATE inbox_reports
|
|
SET
|
|
last_downloaded_at_utc =
|
|
CASE
|
|
WHEN last_downloaded_at_utc IS NULL THEN @handledAtUtc
|
|
WHEN @handledAtUtc > last_downloaded_at_utc THEN @handledAtUtc
|
|
ELSE last_downloaded_at_utc
|
|
END,
|
|
last_downloaded_by_user_id = @userId,
|
|
owner_user_id = COALESCE(owner_user_id, @userId),
|
|
imported_complaint_report_id = COALESCE(imported_complaint_report_id, @denunciaId),
|
|
imported_to_store_at_utc = COALESCE(imported_to_store_at_utc, @handledAtUtc),
|
|
updated_at_utc = CURRENT_TIMESTAMP(6)
|
|
WHERE progressive_id = @denunciaId
|
|
OR imported_complaint_report_id = @denunciaId;
|
|
""";
|
|
|
|
await using (var updateInbox = new MySqlCommand(updateInboxSql, connection, (MySqlTransaction)transaction))
|
|
{
|
|
updateInbox.Parameters.AddWithValue("@handledAtUtc", handledAtUtc);
|
|
updateInbox.Parameters.AddWithValue("@userId", userId);
|
|
updateInbox.Parameters.AddWithValue("@denunciaId", denunciaId);
|
|
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),
|
|
@handledAtUtc,
|
|
@handledAtUtc,
|
|
1
|
|
FROM inbox_reports ir
|
|
WHERE ir.progressive_id = @denunciaId
|
|
OR ir.imported_complaint_report_id = @denunciaId
|
|
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 =
|
|
CASE
|
|
WHEN last_downloaded_at_utc IS NULL THEN VALUES(last_downloaded_at_utc)
|
|
WHEN VALUES(last_downloaded_at_utc) > last_downloaded_at_utc THEN VALUES(last_downloaded_at_utc)
|
|
ELSE last_downloaded_at_utc
|
|
END;
|
|
""";
|
|
|
|
await using (var updateUserReport = new MySqlCommand(updateUserReportSql, connection, (MySqlTransaction)transaction))
|
|
{
|
|
updateUserReport.Parameters.AddWithValue("@userId", userId);
|
|
updateUserReport.Parameters.AddWithValue("@handledAtUtc", handledAtUtc);
|
|
updateUserReport.Parameters.AddWithValue("@denunciaId", denunciaId);
|
|
await updateUserReport.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
|
|
await transaction.CommitAsync(cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
await SafeRollbackAsync(transaction, 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)
|
|
{
|
|
await EnsureConnectionOpenAsync(connection, 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,
|
|
owner.username AS owner_username,
|
|
ir.imported_to_store_at_utc,
|
|
COALESCE(c.is_in_gestiona, 0) AS already_in_gestiona,
|
|
COALESCE(
|
|
(
|
|
SELECT MAX(history.uploaded_at_utc)
|
|
FROM gestiona_upload_history history
|
|
WHERE history.external_report_id =
|
|
COALESCE(ir.imported_complaint_report_id, ir.progressive_id)
|
|
),
|
|
c.gestiona_uploaded_at_utc
|
|
) AS last_gestiona_upload_at_utc,
|
|
CASE WHEN uir.last_downloaded_at_utc IS NULL THEN 0 ELSE 1 END AS downloaded_by_current_user,
|
|
CASE WHEN ir.owner_user_id = @userId THEN 1 ELSE 0 END AS owned_by_current_user,
|
|
CASE
|
|
WHEN ir.owner_user_id IS NULL THEN 0
|
|
WHEN EXISTS (
|
|
SELECT 1
|
|
FROM app_user_groups shared_membership
|
|
INNER JOIN work_groups active_group
|
|
ON active_group.id = shared_membership.work_group_id
|
|
AND active_group.is_active = 1
|
|
WHERE shared_membership.app_user_id IN (@userId, ir.owner_user_id)
|
|
GROUP BY shared_membership.work_group_id
|
|
HAVING COUNT(DISTINCT shared_membership.app_user_id) = 2
|
|
) THEN 1
|
|
ELSE 0
|
|
END AS accessible_by_group,
|
|
(
|
|
SELECT GROUP_CONCAT(DISTINCT wg.code ORDER BY wg.code SEPARATOR ',')
|
|
FROM app_user_groups owner_membership
|
|
INNER JOIN work_groups wg
|
|
ON wg.id = owner_membership.work_group_id
|
|
AND wg.is_active = 1
|
|
WHERE owner_membership.app_user_id = ir.owner_user_id
|
|
) AS owner_group_codes
|
|
FROM inbox_reports ir
|
|
LEFT JOIN app_users downloader ON downloader.id = ir.last_downloaded_by_user_id
|
|
LEFT JOIN app_users owner ON owner.id = ir.owner_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 downloadedByAnotherUser =
|
|
!downloadedByCurrentUser &&
|
|
!string.IsNullOrWhiteSpace(lastDownloadedByUsername);
|
|
var ownerUsername = reader.IsDBNull(reader.GetOrdinal("owner_username"))
|
|
? null
|
|
: reader.GetString(reader.GetOrdinal("owner_username"));
|
|
var ownedByCurrentUser =
|
|
reader.GetInt32(reader.GetOrdinal("owned_by_current_user")) == 1;
|
|
var accessibleByGroup =
|
|
reader.GetInt32(reader.GetOrdinal("accessible_by_group")) == 1;
|
|
var ownerWorkGroups = reader.IsDBNull(reader.GetOrdinal("owner_group_codes"))
|
|
? []
|
|
: reader.GetString(reader.GetOrdinal("owner_group_codes"))
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
metadata[reportId] = new ReportMetadata
|
|
{
|
|
DownloadedByCurrentUser = downloadedByCurrentUser,
|
|
DownloadedByAnotherUser = downloadedByAnotherUser,
|
|
LastDownloadedByUsername = lastDownloadedByUsername,
|
|
LastDownloadedAtUtc = GetDateTimeOffset(reader, "last_downloaded_at_utc"),
|
|
LastGestionaUploadAtUtc = GetDateTimeOffset(reader, "last_gestiona_upload_at_utc"),
|
|
AlreadyImported = !reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")),
|
|
AlreadyInGestiona = reader.GetInt32(reader.GetOrdinal("already_in_gestiona")) == 1,
|
|
OwnerUsername = ownerUsername,
|
|
OwnedByCurrentUser = ownedByCurrentUser,
|
|
AccessibleByWorkGroup = accessibleByGroup,
|
|
OwnerWorkGroups = ownerWorkGroups,
|
|
};
|
|
}
|
|
|
|
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 async Task EnsureConnectionOpenAsync(
|
|
MySqlConnection connection,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (connection.State == ConnectionState.Open)
|
|
{
|
|
return;
|
|
}
|
|
|
|
await connection.OpenAsync(cancellationToken);
|
|
await using var timeZoneCommand = new MySqlCommand("SET time_zone = '+00:00';", connection);
|
|
await timeZoneCommand.ExecuteNonQueryAsync(cancellationToken);
|
|
}
|
|
|
|
private async Task SafeRollbackAsync(
|
|
MySqlTransaction transaction,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
}
|
|
catch
|
|
{
|
|
// No dejamos que un rollback sobre una conexion ya cerrada oculte el error real.
|
|
}
|
|
}
|
|
|
|
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 (!string.IsNullOrWhiteSpace(metadata.OwnerUsername) &&
|
|
!metadata.OwnedByCurrentUser)
|
|
{
|
|
return $"Propiedad de {metadata.OwnerUsername}";
|
|
}
|
|
|
|
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 string? LastDownloadedByUsername { get; init; }
|
|
public DateTimeOffset? LastDownloadedAtUtc { get; init; }
|
|
public DateTimeOffset? LastGestionaUploadAtUtc { get; init; }
|
|
public bool AlreadyImported { get; init; }
|
|
public bool AlreadyInGestiona { get; init; }
|
|
public string? OwnerUsername { get; init; }
|
|
public bool OwnedByCurrentUser { get; init; }
|
|
public bool AccessibleByWorkGroup { get; init; }
|
|
public IReadOnlyList<string> OwnerWorkGroups { get; init; } = [];
|
|
public bool RequiresOwnerConfirmation =>
|
|
!OwnedByCurrentUser &&
|
|
AccessibleByWorkGroup &&
|
|
!string.IsNullOrWhiteSpace(OwnerUsername);
|
|
}
|
|
}
|
|
|
|
public sealed class ReportOwnershipException : InvalidOperationException
|
|
{
|
|
public ReportOwnershipException(string message)
|
|
: base(message)
|
|
{
|
|
}
|
|
}
|