542f15f2d6
PR Checks / test-and-build (pull_request) Successful in 13m48s
- Extract CreateSessionHandler, ListSessionsHandler, DeleteSessionHandler, ExportCalendarHandler, HandleRescheduleTimeInputHandler, HandleRescheduleVoteHandler to GmRelay.Shared - Add IPlatformMessenger methods: SendScheduleAsync, UpdateScheduleAsync, SendGroupMessageAsync with actions, CreateThreadAsync, DeleteThreadAsync - Rewrite Telegram Bot wrappers as thin adapters delegating to shared handlers - Rewrite DiscordRescheduleVoteHandler to use shared HandleRescheduleVoteHandler - Update UpdateRouter with explicit type aliases for ambiguous handler names - Add contract and source-inspection tests for extracted handlers - Bump version 3.1.1 → 3.2.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
157 lines
5.8 KiB
C#
157 lines
5.8 KiB
C#
using Dapper;
|
|
using GmRelay.Shared.Domain;
|
|
using GmRelay.Shared.Platform;
|
|
using Npgsql;
|
|
|
|
namespace GmRelay.Shared.Features.Sessions.RescheduleSession;
|
|
|
|
public sealed class HandleRescheduleVoteHandler(
|
|
NpgsqlDataSource dataSource)
|
|
{
|
|
public async Task<HandleRescheduleVoteResult> HandleAsync(HandleRescheduleVoteCommand command, CancellationToken ct)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(ct);
|
|
await using var transaction = await connection.BeginTransactionAsync(ct);
|
|
|
|
var proposal = await connection.QuerySingleOrDefaultAsync<VoteProposalDto>(
|
|
"""
|
|
SELECT rp.id AS Id,
|
|
rp.session_id AS SessionId,
|
|
rp.voting_deadline_at AS VotingDeadlineAt,
|
|
s.title AS Title,
|
|
s.scheduled_at AS CurrentScheduledAt
|
|
FROM reschedule_options ro
|
|
JOIN reschedule_proposals rp ON rp.id = ro.proposal_id
|
|
JOIN sessions s ON s.id = rp.session_id
|
|
WHERE ro.id = @OptionId AND rp.status = 'Voting'
|
|
""",
|
|
new { command.OptionId },
|
|
transaction);
|
|
|
|
if (proposal is null)
|
|
{
|
|
return new HandleRescheduleVoteResult(
|
|
false,
|
|
"Голосование уже завершено или не найдено.",
|
|
null, null, null, default, default,
|
|
Array.Empty<VoteParticipantDto>(),
|
|
Array.Empty<RescheduleOptionDto>(),
|
|
Array.Empty<RescheduleOptionVoteDto>());
|
|
}
|
|
|
|
if (proposal.VotingDeadlineAt <= DateTimeOffset.UtcNow)
|
|
{
|
|
return new HandleRescheduleVoteResult(
|
|
false,
|
|
"Дедлайн уже прошёл. Результаты скоро будут применены.",
|
|
null, null, null, default, default,
|
|
Array.Empty<VoteParticipantDto>(),
|
|
Array.Empty<RescheduleOptionDto>(),
|
|
Array.Empty<RescheduleOptionVoteDto>());
|
|
}
|
|
|
|
var playerId = await connection.ExecuteScalarAsync<Guid?>(
|
|
"""
|
|
SELECT p.id
|
|
FROM session_participants sp
|
|
JOIN players p ON p.id = sp.player_id
|
|
WHERE sp.session_id = @SessionId
|
|
AND p.platform = @Platform
|
|
AND p.external_user_id = @ExternalUserId
|
|
AND sp.is_gm = false
|
|
AND sp.registration_status = @Active
|
|
""",
|
|
new
|
|
{
|
|
proposal.SessionId,
|
|
Platform = command.User.Platform.ToString(),
|
|
ExternalUserId = command.User.ExternalUserId,
|
|
Active = ParticipantRegistrationStatus.Active
|
|
},
|
|
transaction);
|
|
|
|
if (playerId is null)
|
|
{
|
|
return new HandleRescheduleVoteResult(
|
|
false,
|
|
"Вы не являетесь участником этой сессии.",
|
|
null, null, null, default, default,
|
|
Array.Empty<VoteParticipantDto>(),
|
|
Array.Empty<RescheduleOptionDto>(),
|
|
Array.Empty<RescheduleOptionVoteDto>());
|
|
}
|
|
|
|
await connection.ExecuteAsync(
|
|
"""
|
|
INSERT INTO reschedule_option_votes (proposal_id, player_id, option_id)
|
|
VALUES (@ProposalId, @PlayerId, @OptionId)
|
|
ON CONFLICT (proposal_id, player_id) DO UPDATE
|
|
SET option_id = EXCLUDED.option_id,
|
|
voted_at = now()
|
|
""",
|
|
new
|
|
{
|
|
ProposalId = proposal.Id,
|
|
PlayerId = playerId.Value,
|
|
command.OptionId
|
|
},
|
|
transaction);
|
|
|
|
var participants = (await connection.QueryAsync<VoteParticipantDto>(
|
|
"""
|
|
SELECT p.id AS PlayerId,
|
|
p.display_name AS DisplayName,
|
|
p.external_username AS TelegramUsername,
|
|
p.external_user_id AS TelegramId
|
|
FROM session_participants sp
|
|
JOIN players p ON p.id = sp.player_id
|
|
WHERE sp.session_id = @SessionId
|
|
AND sp.is_gm = false
|
|
AND sp.registration_status = @Active
|
|
ORDER BY p.display_name
|
|
""",
|
|
new { proposal.SessionId, Active = ParticipantRegistrationStatus.Active },
|
|
transaction)).ToList();
|
|
|
|
var options = (await connection.QueryAsync<RescheduleOptionDto>(
|
|
"""
|
|
SELECT id AS OptionId,
|
|
display_order AS DisplayOrder,
|
|
proposed_at AS ProposedAt
|
|
FROM reschedule_options
|
|
WHERE proposal_id = @ProposalId
|
|
ORDER BY display_order
|
|
""",
|
|
new { ProposalId = proposal.Id },
|
|
transaction)).ToList();
|
|
|
|
var votes = (await connection.QueryAsync<RescheduleOptionVoteDto>(
|
|
"""
|
|
SELECT rov.option_id AS OptionId,
|
|
p.id AS PlayerId,
|
|
p.display_name AS DisplayName,
|
|
p.external_username AS TelegramUsername
|
|
FROM reschedule_option_votes rov
|
|
JOIN players p ON p.id = rov.player_id
|
|
WHERE rov.proposal_id = @ProposalId
|
|
ORDER BY rov.voted_at, p.display_name
|
|
""",
|
|
new { ProposalId = proposal.Id },
|
|
transaction)).ToList();
|
|
|
|
await transaction.CommitAsync(ct);
|
|
|
|
return new HandleRescheduleVoteResult(
|
|
true,
|
|
"Ваш голос учтён. До дедлайна его можно изменить.",
|
|
proposal.Id,
|
|
proposal.SessionId,
|
|
proposal.Title,
|
|
proposal.CurrentScheduledAt,
|
|
proposal.VotingDeadlineAt,
|
|
participants,
|
|
options,
|
|
votes);
|
|
}
|
|
}
|