feat(guild): invite state machine

Add surrogate PK (IDENTITY bigint) to GuildInvite for wire invite_id.
Implement InviteAsync, CancelInviteAsync, RejectInviteAsync,
ListOutgoingInvitesAsync, ListInvitedGuildsAsync in GuildService.
Wire 5 endpoints: invite_user_list, invited_guild_list, invite,
cancel_invite, reject_invite in GuildController.
Migration: AddGuildInviteSurrogatePk — drops composite PK, adds Id
IDENTITY column, adds unique index on (GuildId, InviteeViewerId).
6 integration tests in GuildInviteFlowTests all green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-27 13:21:57 -04:00
parent c103ab6a87
commit 1b7fa2d525
12 changed files with 5574 additions and 22 deletions

View File

@@ -175,21 +175,72 @@ public sealed class GuildController : SVSimController
=> Task.FromResult<ActionResult<GuildFriendListResponse>>(new GuildFriendListResponse { Friends = new() });
[HttpPost("invite_user_list")]
public Task<ActionResult<GuildInviteUserListResponse>> InviteUserList([FromBody] BaseRequest _, CancellationToken ct)
=> Task.FromResult<ActionResult<GuildInviteUserListResponse>>(new GuildInviteUserListResponse { Users = new() });
public async Task<ActionResult<GuildInviteUserListResponse>> InviteUserList([FromBody] BaseRequest _, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var entries = await _guild.ListOutgoingInvitesAsync(viewerId, ct);
return new GuildInviteUserListResponse
{
Users = entries.Select(e => new GuildOutgoingInviteDto
{
ViewerId = e.InviteeViewerId,
Name = e.Name,
EmblemId = e.EmblemId,
CountryCode = e.CountryCode,
Rank = e.Rank,
DegreeId = e.DegreeId,
InviteId = e.InviteId,
InviteTime = new DateTimeOffset(e.CreatedAt, TimeSpan.Zero).ToUnixTimeSeconds(),
}).ToList(),
};
}
[HttpPost("invited_guild_list")]
public Task<ActionResult<GuildInvitedGuildListResponse>> InvitedGuildList([FromBody] GuildInvitedGuildListRequest req, CancellationToken ct)
=> Task.FromResult<ActionResult<GuildInvitedGuildListResponse>>(new GuildInvitedGuildListResponse { List = new() });
public async Task<ActionResult<GuildInvitedGuildListResponse>> InvitedGuildList([FromBody] GuildInvitedGuildListRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var entries = await _guild.ListInvitedGuildsAsync(viewerId, ct);
return new GuildInvitedGuildListResponse
{
List = entries.Select(e => new GuildReceivedInviteDto
{
GuildId = e.Guild.GuildId,
GuildName = e.Guild.Name,
Description = e.Guild.Description,
GuildEmblemId = e.Guild.EmblemId,
JoinCondition = (int)e.Guild.JoinCondition,
Activity = (int)e.Guild.Activity,
MemberNum = e.MemberNum,
LeaderName = e.LeaderName,
LeaderViewerId = e.Guild.LeaderViewerId,
InviteId = e.InviteId,
}).ToList(),
};
}
[HttpPost("invite")]
public Task<ActionResult<EmptyResponse>> Invite([FromBody] GuildInviteRequest req, CancellationToken ct) => Stub();
public async Task<ActionResult<EmptyResponse>> Invite([FromBody] GuildInviteRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var r = await _guild.InviteAsync(viewerId, req.InvitedViewerId, ct);
return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
}
[HttpPost("cancel_invite")]
public Task<ActionResult<EmptyResponse>> CancelInvite([FromBody] GuildCancelInviteRequest req, CancellationToken ct) => Stub();
public async Task<ActionResult<EmptyResponse>> CancelInvite([FromBody] GuildCancelInviteRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var r = await _guild.CancelInviteAsync(viewerId, req.InviteId, ct);
return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
}
[HttpPost("reject_invite")]
public Task<ActionResult<EmptyResponse>> RejectInvite([FromBody] GuildRejectInviteRequest req, CancellationToken ct) => Stub();
public async Task<ActionResult<EmptyResponse>> RejectInvite([FromBody] GuildRejectInviteRequest req, CancellationToken ct)
{
if (!TryGetViewerId(out var viewerId)) return Unauthorized();
var r = await _guild.RejectInviteAsync(viewerId, req.InviteId, ct);
return r.IsOk ? new EmptyResponse() : MapErrorToWire(r);
}
[HttpPost("join")]
public Task<ActionResult<GuildJoinResponse>> Join([FromBody] GuildJoinEndpointRequest req, CancellationToken ct)