refactor(shared): extract reschedule voting types to Shared
This commit is contained in:
+1
-6
@@ -1,6 +1,7 @@
|
||||
using Dapper;
|
||||
using GmRelay.Bot.Features.Notifications;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||
using GmRelay.Shared.Platform;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Npgsql;
|
||||
@@ -17,12 +18,6 @@ internal sealed record AwaitingProposalDto(
|
||||
Guid Id, Guid SessionId, string Title, DateTime CurrentScheduledAt,
|
||||
Guid BatchId, int? BatchMessageId, long TelegramChatId, int? ThreadId, string NotificationMode);
|
||||
|
||||
internal sealed record VoteParticipantDto(
|
||||
Guid PlayerId,
|
||||
string DisplayName,
|
||||
string? TelegramUsername,
|
||||
long TelegramId = 0);
|
||||
|
||||
// ── Handler ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Dapper;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||
using GmRelay.Shared.Platform;
|
||||
using Npgsql;
|
||||
using Telegram.Bot;
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
namespace GmRelay.Bot.Features.Sessions.RescheduleSession;
|
||||
|
||||
internal enum RescheduleVoteOutcome
|
||||
{
|
||||
Pending,
|
||||
Rejected,
|
||||
Approved
|
||||
}
|
||||
|
||||
internal sealed record RescheduleVoteDecision(
|
||||
RescheduleVoteOutcome Outcome,
|
||||
string Reason,
|
||||
Guid? SelectedOptionId = null,
|
||||
string CallbackText = "",
|
||||
bool ShouldRescheduleSession = false,
|
||||
bool ShouldResetParticipantRsvps = false);
|
||||
|
||||
internal static class RescheduleVoteRules
|
||||
{
|
||||
public static RescheduleVoteDecision SelectWinner(IReadOnlyList<RescheduleOptionVoteCount> voteCounts)
|
||||
{
|
||||
var maxVotes = voteCounts.Count == 0 ? 0 : voteCounts.Max(x => x.VoteCount);
|
||||
if (maxVotes == 0)
|
||||
{
|
||||
return new RescheduleVoteDecision(
|
||||
RescheduleVoteOutcome.Rejected,
|
||||
"Никто не проголосовал до дедлайна, перенос не применяется.");
|
||||
}
|
||||
|
||||
var winners = voteCounts.Where(x => x.VoteCount == maxVotes).ToList();
|
||||
if (winners.Count > 1)
|
||||
{
|
||||
return new RescheduleVoteDecision(
|
||||
RescheduleVoteOutcome.Rejected,
|
||||
"Голоса разделились поровну, перенос не применяется.");
|
||||
}
|
||||
|
||||
return new RescheduleVoteDecision(
|
||||
RescheduleVoteOutcome.Approved,
|
||||
"Победил вариант с большинством голосов.",
|
||||
winners[0].OptionId,
|
||||
ShouldRescheduleSession: true,
|
||||
ShouldResetParticipantRsvps: true);
|
||||
}
|
||||
|
||||
public static RescheduleVoteDecision Evaluate(string vote, int totalParticipants, int approvedParticipants)
|
||||
{
|
||||
if (string.Equals(vote, "no", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new RescheduleVoteDecision(
|
||||
Outcome: RescheduleVoteOutcome.Rejected,
|
||||
Reason: "\u041e\u0434\u0438\u043d \u0438\u0437 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432 \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u043b \u043f\u0435\u0440\u0435\u043d\u043e\u0441.",
|
||||
CallbackText: "\u0412\u044b \u043f\u0440\u043e\u0433\u043e\u043b\u043e\u0441\u043e\u0432\u0430\u043b\u0438 \u043f\u0440\u043e\u0442\u0438\u0432 \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0430.");
|
||||
}
|
||||
|
||||
var everyoneApproved = approvedParticipants == totalParticipants;
|
||||
|
||||
return new RescheduleVoteDecision(
|
||||
Outcome: everyoneApproved ? RescheduleVoteOutcome.Approved : RescheduleVoteOutcome.Pending,
|
||||
Reason: everyoneApproved
|
||||
? "\u0412\u0441\u0435 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u044b."
|
||||
: "\u0413\u043e\u043b\u043e\u0441\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442\u0441\u044f.",
|
||||
CallbackText: everyoneApproved
|
||||
? "\u0412\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043d\u043e\u0441! \u0412\u0441\u0435 \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u044b \u2014 \u0432\u0440\u0435\u043c\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043e."
|
||||
: "\u0412\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043d\u043e\u0441!",
|
||||
ShouldRescheduleSession: everyoneApproved,
|
||||
ShouldResetParticipantRsvps: everyoneApproved);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Dapper;
|
||||
using GmRelay.Bot.Features.Notifications;
|
||||
using GmRelay.Bot.Infrastructure.Scheduling;
|
||||
using GmRelay.Shared.Domain;
|
||||
using GmRelay.Shared.Features.Sessions.RescheduleSession;
|
||||
using GmRelay.Shared.Platform;
|
||||
using GmRelay.Shared.Rendering;
|
||||
using Npgsql;
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using GmRelay.Shared.Domain;
|
||||
|
||||
namespace GmRelay.Bot.Features.Sessions.RescheduleSession;
|
||||
|
||||
internal sealed record RescheduleVotingInput(
|
||||
IReadOnlyList<DateTimeOffset> Options,
|
||||
DateTimeOffset Deadline)
|
||||
{
|
||||
private static readonly Regex DateTimePattern = new(
|
||||
@"(?<date>\d{1,2}\.\d{2}\.\d{4})\s+(?<time>\d{1,2}:\d{2})",
|
||||
RegexOptions.CultureInvariant);
|
||||
|
||||
public static bool TryParse(
|
||||
string text,
|
||||
DateTimeOffset nowUtc,
|
||||
out RescheduleVotingInput input,
|
||||
out string error)
|
||||
{
|
||||
input = new RescheduleVotingInput([], default);
|
||||
error = string.Empty;
|
||||
|
||||
var options = new List<DateTimeOffset>();
|
||||
DateTimeOffset? deadline = null;
|
||||
|
||||
foreach (var rawLine in text.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
var match = DateTimePattern.Match(line);
|
||||
if (!match.Success)
|
||||
continue;
|
||||
|
||||
var value = $"{match.Groups["date"].Value} {match.Groups["time"].Value}";
|
||||
if (!MoscowTime.TryParseMoscow(value, out var parsed))
|
||||
continue;
|
||||
|
||||
if (IsDeadlineLine(line))
|
||||
{
|
||||
deadline = parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
options.Add(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.Count is < 2 or > 3)
|
||||
{
|
||||
error = "Укажите от 2 до 3 вариантов времени.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.Distinct().Count() != options.Count)
|
||||
{
|
||||
error = "Варианты времени не должны повторяться.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deadline is null)
|
||||
{
|
||||
error = "Укажите дедлайн голосования строкой «Дедлайн: ДД.ММ.ГГГГ ЧЧ:ММ».";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.Any(option => option <= nowUtc))
|
||||
{
|
||||
error = "Все варианты времени должны быть в будущем.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deadline.Value <= nowUtc)
|
||||
{
|
||||
error = "Дедлайн голосования должен быть в будущем.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deadline.Value >= options.Min())
|
||||
{
|
||||
error = "Дедлайн голосования должен быть раньше первого предложенного времени.";
|
||||
return false;
|
||||
}
|
||||
|
||||
input = new RescheduleVotingInput(options, deadline.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsDeadlineLine(string line)
|
||||
{
|
||||
var normalized = line.TrimStart('-', '*', ' ', '\t').ToLowerInvariant();
|
||||
|
||||
return normalized.StartsWith("дедлайн", StringComparison.Ordinal)
|
||||
|| normalized.StartsWith("deadline", StringComparison.Ordinal)
|
||||
|| normalized.StartsWith("до:", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record RescheduleOptionDto(
|
||||
Guid OptionId,
|
||||
int DisplayOrder,
|
||||
DateTimeOffset ProposedAt);
|
||||
|
||||
internal sealed record RescheduleOptionVoteDto(
|
||||
Guid OptionId,
|
||||
Guid PlayerId,
|
||||
string DisplayName,
|
||||
string? TelegramUsername);
|
||||
|
||||
internal sealed record RescheduleOptionVoteCount(
|
||||
Guid OptionId,
|
||||
int VoteCount);
|
||||
Reference in New Issue
Block a user