using Dapper; using GmRelay.Shared.Domain; using GmRelay.Shared.Platform; using Microsoft.Extensions.Logging; using Npgsql; namespace GmRelay.Shared.Features.Sessions.ListSessions; internal sealed record DeleteSessionInfoDto( string Title, Guid BatchId, Guid GroupId, bool CanManage, int? ThreadId, bool TopicCreatedByBot); public sealed record DeleteSessionResult( bool Success, string? ReplyText, string? Title, Guid? GroupId, int? ThreadId, bool TopicCreatedByBot, int RemainingInTopic); public sealed class DeleteSessionHandler( NpgsqlDataSource dataSource) { public async Task HandleAsync(DeleteSessionCommand command, CancellationToken ct) { await using var connection = await dataSource.OpenConnectionAsync(ct); await using var transaction = await connection.BeginTransactionAsync(ct); // 1. Use the database mutation order before locking the session or linked portfolio cards. await connection.ExecuteAsync( "SELECT pg_advisory_xact_lock(20260530, 108)", transaction: transaction); // 2. Lock the session before any linked portfolio card and verify group manager. var session = await connection.QuerySingleOrDefaultAsync( """ SELECT s.title AS Title, s.batch_id AS BatchId, s.group_id AS GroupId, s.thread_id AS ThreadId, s.topic_created_by_bot AS TopicCreatedByBot, EXISTS ( SELECT 1 FROM group_managers gm JOIN players p ON p.id = gm.player_id WHERE gm.group_id = s.group_id AND p.platform = @Platform AND p.external_user_id = @ExternalUserId ) AS CanManage FROM sessions s WHERE s.id = @SessionId FOR UPDATE OF s """, new { command.SessionId, Platform = command.User.Platform.ToString(), ExternalUserId = command.User.ExternalUserId }, transaction); if (session == null) { return new DeleteSessionResult(false, "Сессия не найдена.", null, null, null, false, 0); } if (!session.CanManage) { return new DeleteSessionResult(false, "Только owner или co-GM может удалять сессию.", null, null, null, false, 0); } // 3. Unpublish a linked portfolio card before its required session link cascades away. await connection.ExecuteAsync( """ UPDATE portfolio_games pg SET is_public = false, updated_at = now() FROM portfolio_game_sessions pgs WHERE pgs.portfolio_game_id = pg.id AND pgs.session_id = @SessionId AND pg.is_public = true """, new { command.SessionId }, transaction); // 4. Delete session await connection.ExecuteAsync("DELETE FROM sessions WHERE id = @Id", new { Id = command.SessionId }, transaction); var remainingInTopic = session.ThreadId.HasValue ? await connection.ExecuteScalarAsync( """ SELECT COUNT(*) FROM sessions WHERE group_id = @GroupId AND thread_id = @ThreadId """, new { session.GroupId, ThreadId = session.ThreadId.Value }, transaction) : 0; await transaction.CommitAsync(ct); return new DeleteSessionResult( true, "Сессия удалена!", session.Title, session.GroupId, session.ThreadId, session.TopicCreatedByBot, remainingInTopic); } }