From 22e9859fdf2b18538079724815fd6a0359b998fb Mon Sep 17 00:00:00 2001 From: Toutsu Date: Wed, 3 Jun 2026 11:33:28 +0300 Subject: [PATCH] fix(web): allow cancelling pending applications; drop contradictory message guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from PR #119: - LeaveClubMembershipAsync: was rejecting Pending rows because the SQL required status = 'Active', so clicking "Отозвать заявку" on a Pending membership surfaced a misleading "Active membership X not found" InvalidOperationException. Now the method first tries Active -> Left and falls back to Pending -> Rejected so the same UI flow covers both states. - PublicClub.razor TrySubmitApplicationAsync: removed the empty-input guard that contradicted the "(необязательно)" label and the server side (AuthorizedMembershipService already trims and accepts null). No tests broken (493 still passing), no public-API changes. Co-Authored-By: Claude Opus 4.8 --- .../Components/Pages/PublicClub.razor | 6 ------ src/GmRelay.Web/Services/SessionService.cs | 18 ++++++++++++++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/GmRelay.Web/Components/Pages/PublicClub.razor b/src/GmRelay.Web/Components/Pages/PublicClub.razor index 7283a24..0e31dde 100644 --- a/src/GmRelay.Web/Components/Pages/PublicClub.razor +++ b/src/GmRelay.Web/Components/Pages/PublicClub.razor @@ -167,12 +167,6 @@ else if (club is not null) if (club is null) return; - if (string.IsNullOrWhiteSpace(applicationMessage)) - { - applicationError = "Введите сообщение или оставьте поле пустым."; - return; - } - try { isSubmittingApplication = true; diff --git a/src/GmRelay.Web/Services/SessionService.cs b/src/GmRelay.Web/Services/SessionService.cs index caf35dc..79e5f29 100644 --- a/src/GmRelay.Web/Services/SessionService.cs +++ b/src/GmRelay.Web/Services/SessionService.cs @@ -2707,6 +2707,7 @@ public sealed class SessionService( public async Task LeaveClubMembershipAsync(Guid membershipId, Guid playerId) { await using var conn = await dataSource.OpenConnectionAsync(); + // Active membership: withdraw by setting status = 'Left'. var rows = await conn.ExecuteAsync( """ UPDATE club_memberships @@ -2714,9 +2715,22 @@ public sealed class SessionService( WHERE id = @MembershipId AND player_id = @PlayerId AND status = 'Active' """, new { MembershipId = membershipId, PlayerId = playerId }); - if (rows == 0) + if (rows > 0) { - throw new InvalidOperationException($"Active membership {membershipId} not found for player."); + return; + } + + // Pending application: cancel by setting status = 'Rejected' so the user can re-apply later. + var cancelled = await conn.ExecuteAsync( + """ + UPDATE club_memberships + SET status = 'Rejected', decided_at = now() + WHERE id = @MembershipId AND player_id = @PlayerId AND status = 'Pending' + """, + new { MembershipId = membershipId, PlayerId = playerId }); + if (cancelled == 0) + { + throw new InvalidOperationException($"Membership {membershipId} is not Active or Pending for this player."); } }