using Dapper; using GmRelay.Shared.Domain; using GmRelay.Shared.Platform; using GmRelay.Shared.Rendering; using Npgsql; namespace GmRelay.Shared.Features.Sessions.CreateSession; internal sealed record SessionCreationGroupAccessDto(Guid GroupId, bool CanManage); public sealed class CreateSessionHandler( NpgsqlDataSource dataSource) { public async Task HandleAsync(CreateSessionCommand command, CancellationToken ct) { await using var connection = await dataSource.OpenConnectionAsync(ct); await using var transaction = await connection.BeginTransactionAsync(ct); var transactionCommitted = false; try { var platform = command.User.Platform.ToString(); var externalUserId = command.User.ExternalUserId; var displayName = command.User.DisplayName; var externalUsername = command.User.ExternalUsername; await connection.ExecuteAsync( """ INSERT INTO players (display_name, platform, external_user_id, external_username) VALUES (@Name, @Platform, @ExternalId, @Username) ON CONFLICT (platform, external_user_id) WHERE platform IS NOT NULL AND external_user_id IS NOT NULL DO UPDATE SET display_name = EXCLUDED.display_name, external_username = EXCLUDED.external_username; """, new { ExternalId = externalUserId, Name = displayName, Username = externalUsername, Platform = platform }, transaction); var existingGroup = await connection.QuerySingleOrDefaultAsync( """ SELECT g.id AS GroupId, EXISTS ( SELECT 1 FROM group_managers gm JOIN players p ON p.id = gm.player_id WHERE gm.group_id = g.id AND p.platform = @Platform AND p.external_user_id = @ExternalGmId ) AS CanManage FROM game_groups g WHERE g.platform = @Platform AND g.external_group_id = @ExternalGroupId """, new { Platform = platform, ExternalGroupId = command.Group.ExternalGroupId, ExternalGmId = externalUserId }, transaction); Guid groupId; if (existingGroup is null) { groupId = await connection.ExecuteScalarAsync( """ INSERT INTO game_groups (name, platform, external_group_id, external_channel_id) VALUES (@ChatName, @Platform, @ExternalGroupId, @ExternalChannelId) RETURNING id; """, new { Platform = platform, ExternalGroupId = command.Group.ExternalGroupId, ExternalChannelId = command.Group.ExternalChannelId, ChatName = command.Group.DisplayName }, transaction); await connection.ExecuteAsync( """ INSERT INTO group_managers (group_id, player_id, role) SELECT @GroupId, p.id, @OwnerRole FROM players p WHERE p.platform = @Platform AND p.external_user_id = @ExternalGmId ON CONFLICT (group_id, player_id) DO NOTHING """, new { GroupId = groupId, Platform = platform, ExternalGmId = externalUserId, OwnerRole = GroupManagerRoleExtensions.OwnerValue }, transaction); } else { if (!existingGroup.CanManage) { await transaction.RollbackAsync(ct); return new CreateSessionResult( false, "⛔ Только owner или co-GM этой группы может создавать игровые сессии.", null, null, null, Array.Empty()); } groupId = existingGroup.GroupId; await connection.ExecuteAsync( """ UPDATE game_groups SET name = @ChatName WHERE id = @GroupId """, new { ChatName = command.Group.DisplayName, GroupId = groupId }, transaction); } var batchId = Guid.NewGuid(); var sessions = new List(); var orderedTimes = command.ScheduledTimes.OrderBy(v => v).ToList(); foreach (var scheduledAt in orderedTimes) { var sessionId = await connection.ExecuteScalarAsync( """ INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, max_players, system, description, format, duration_minutes, is_one_shot, cover_image_url, location_address) VALUES (@BatchId, @GroupId, @Title, @Link, @ScheduledAt, @Status, @MaxPlayers, @System, @Description, @Format, @DurationMinutes, @IsOneShot, @CoverImageUrl, @LocationAddress) RETURNING id; """, new { BatchId = batchId, GroupId = groupId, command.Title, Link = command.Link, ScheduledAt = scheduledAt, Status = SessionStatus.Planned, MaxPlayers = command.MaxPlayers, System = command.System?.ToString(), command.Description, command.Format, DurationMinutes = command.DurationMinutes, IsOneShot = command.IsOneShot, CoverImageUrl = command.ImageReference, command.LocationAddress }, transaction); sessions.Add(new SessionBatchDto( sessionId, scheduledAt.UtcDateTime, SessionStatus.Planned, command.MaxPlayers, command.Link, command.Format, command.LocationAddress)); } await transaction.CommitAsync(ct); transactionCommitted = true; var view = SessionBatchViewBuilder.Build(command.Title, sessions, Array.Empty()); return new CreateSessionResult( true, null, view, batchId, groupId, Array.Empty()); } catch { if (!transactionCommitted) { await transaction.RollbackAsync(ct); } throw; } } }