Commit Graph

13 Commits

Author SHA1 Message Date
gamer147
a248c167e0 refactor(guild_chat): extract chat-user lookup to viewer repo; fix NEW-direction exclusivity
Add ChatUserProfile record and IViewerRepository.LoadChatProfilesAsync (AsNoTracking,
projects Name/EmblemId/CountryCode/Rank/DegreeId from Viewer+Info nav refs).
GuildChatController.Messages now injects IViewerRepository instead of SVSimDbContext;
calls LoadChatProfilesAsync to populate users[] (removes direct db access from controller).
GuildChatMessageRepository direction=2 (NEW): change >= start to > start (exclusive).
GuildChatPollTests updated to assert the start message is NOT returned by direction=NEW.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:51:44 -04:00
gamer147
358f43aa7a feat(guild_chat): messages window + system events
- GuildChatService.EmitSystemEventAsync: replaces no-op stub with real
  chat-row insertion via _msgs.AppendAsync (per-guild monotonic id).
- GuildChatService.GetWindowAsync: real implementation — verifies caller
  membership, delegates to IGuildChatMessageRepository.GetWindowAsync
  (direction 1=OLD/2=NEW/3=BOTH, limit 50), returns adaptive wait interval
  (ChatPollActiveSeconds when messages returned, ChatPollIdleSeconds otherwise).
- Drop unused repo injections from GuildChatService (guilds, invites, joinRequests).
- GuildChatController.Messages: wires the full response — chat_message[],
  users[] (deduplicated author profiles with Emblem+Degree nav includes),
  maintenance_card_list (empty until maintenance service lands), wait_interval.
- GuildChatPollTests (7 integration tests): fresh-guild CreateGuild event,
  sequenced Create+Join ordering, direction=NEW filter, direction=BOTH window,
  users[] deduplication, empty result for viewer-not-in-guild, active vs idle
  interval comparison. All 7 pass; full suite 1515/1515.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:44:09 -04:00
gamer147
484b51086a fix(guild): friend_list returns bare array-at-root (matches GuildFriendListTask.Parse)
GuildFriendListTask.Parse() reads base.ResponseData[data][i] directly —
data must be a bare JSON array, not {friends:[...]}.  Delete the
GuildFriendListResponse wrapper DTO; promote GuildInviteCandidateDto to its
own file.  Controller now returns List<GuildInviteCandidateDto> so the
middleware wraps it as data:[…].  Update wire-shape and integration tests to
assert array-at-root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:35:20 -04:00
gamer147
f2b996a593 feat(guild): change_role atomic transfer + friend_list
- ChangeRoleAsync: Leader-only role management with SubLeader cap check.
  Atomic leader transfer loads both member rows + Guild.LeaderViewerId in
  one _db.SaveChangesAsync call (no split saves). Emits ChangeLeader (6)
  or ChangeSubLeader (7) chat events. Same-role is no-op / returns Ok.
- /guild/change_role controller: calls ChangeRoleAsync, returns full updated
  member list (GuildChangeRoleResponse.Members[]).
- /guild/friend_list controller: calls IFriendService.GetFriendsAsync, then
  annotates each friend with is_join_guild from Viewer.GuildId column.
- IGuildRepository.UpdateLeaderViewerIdAsync: added for completeness even
  though the atomic transfer path bypasses it via direct EF mutation.
- Tests: 8 GuildServiceChangeRoleTests (role changes, cap, atomic transfer,
  no-op, permission); 3 GuildChangeRoleFriendListFlowTests (HTTP roundtrip,
  is_join_guild); 2 GuildWireShape tests (literal JSON shape); 1508 pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:27:13 -04:00
gamer147
901c3d7568 fix(guild): subleaders can accept/reject join requests (matches GuildManager.HasAuthorityInviteAndApproval)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:06:30 -04:00
gamer147
dd955fe4be feat(guild): join_request_accept + reject_join_request 2026-06-27 13:58:54 -04:00
gamer147
771d884314 fix(guild): /guild/join returns guild_status per branch (APPLYING for approval path, JOINING otherwise)
GuildJoinTask.Parse() reads data[guild_status].ToInt() directly; hardcoding 2
for the APPROVAL path was wrong. GuildOpResult gains nullable GuildStatus; JoinAsync
sets 1 (APPLYING) for the pending-request path and 2 (JOINING) for instant joins.
Controller reads r.GuildStatus. Two new tests assert the wire value per path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 13:51:04 -04:00
gamer147
d7f9ba8f2a feat(guild): join / cancel_join_request / join_request_list
Implements three join-path state machine endpoints:
- /guild/join: FREE (instant), APPROVAL (pending row + idempotent re-apply),
  ONLY_INVITE (pending invite required); CommitJoin consumes invites + cancels
  join requests; MaxMemberNum cap enforced.
- /guild/cancel_join_request: cancels all pending requests for caller (no
  request_id on wire per GuildJoinRequestCancelTask decompile, composite PK kept).
- /guild/join_request_list: leader/subleader sees pending applicants with
  viewer profile (name/emblem/degree/request_time unix secs).

IGuildService.CancelJoinRequestAsync signature changed from (viewerId, guildId)
to (viewerId) since the client sends no guildId. GuildJoinRequestEntry enriched
record added to GuildServiceTypes. Re-apply after cancel resets existing row
in-place to avoid composite-PK conflict. 11 integration tests, all green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 13:44:04 -04:00
gamer147
11a129ea90 fix(guild): make GuildInvite unique index partial on Status=Pending to allow re-invite after reject/cancel
Partial index (filter: "Status" = 0) replaces the unconditional unique
index on (GuildId, InviteeViewerId). Old Rejected/Canceled rows no
longer conflict when a new Pending invite is inserted for the same pair.

Adds regression tests: Reinvite_after_reject_succeeds and
Reinvite_after_cancel_succeeds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 13:29:06 -04:00
gamer147
1b7fa2d525 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>
2026-06-27 13:21:57 -04:00
gamer147
c103ab6a87 fix(guild): populate leader_name in search results (matches prod capture)
Add IViewerRepository.LoadDisplayNamesAsync (batch AsNoTracking select),
extend GuildSearchEntry with LeaderName, enrich GuildService.SearchAsync
with a leader-id batch lookup, and thread the name through the controller's
ToDetailDto call. Adds a leader_name assertion to GuildSearchFlowTests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 13:08:11 -04:00
gamer147
3f8f04eb1a feat(guild): /guild/search_guild
Implement SearchAsync in GuildService (wraps IGuildRepository.SearchAsync +
CountBatchByGuildIdsAsync), wire the controller action, add 7 integration
tests (GuildSearchFlowTests) covering no-filter / activity / join_condition /
bucket-1 / bucket-3 / name-prefix / flat-entry shape, and a wire-shape test
in GuildWireShape confirming list entries are flat (no detail wrapper).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 13:01:58 -04:00
gamer147
f7f68d03a7 feat(guild): /guild/create + populated /guild/info
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 12:17:22 -04:00