diff --git a/SVSim.Database/Repositories/Viewer/IViewerRepository.cs b/SVSim.Database/Repositories/Viewer/IViewerRepository.cs
index cea51a27..a45cebbd 100644
--- a/SVSim.Database/Repositories/Viewer/IViewerRepository.cs
+++ b/SVSim.Database/Repositories/Viewer/IViewerRepository.cs
@@ -104,4 +104,12 @@ public interface IViewerRepository
/// Batch-loads owned guild-emblem ids for a viewer. Used by /guild/emblem_list.
///
Task> GetEmblemListAsync(long viewerId, CancellationToken ct = default);
+
+ ///
+ /// Counts the viewer's unclaimed rows. Drives
+ /// /mypage/index.unread_present_count, which the client casts to
+ /// Data.MyPage.data.unread_mail_count to render the home-screen crate badge
+ /// (MyPageItemHome.cs:148 → SetUnreadGiftCount).
+ ///
+ Task CountUnclaimedPresentsAsync(long viewerId, CancellationToken ct = default);
}
diff --git a/SVSim.Database/Repositories/Viewer/ViewerRepository.cs b/SVSim.Database/Repositories/Viewer/ViewerRepository.cs
index 9fa22386..5c5ae367 100644
--- a/SVSim.Database/Repositories/Viewer/ViewerRepository.cs
+++ b/SVSim.Database/Repositories/Viewer/ViewerRepository.cs
@@ -396,6 +396,13 @@ public class ViewerRepository : IViewerRepository
return viewer?.Emblems.Select(e => (long)e.Id).ToList() ?? new List();
}
+ public Task CountUnclaimedPresentsAsync(long viewerId, CancellationToken ct = default)
+ {
+ return _dbContext.Set()
+ .Where(p => p.ViewerId == viewerId && p.Status == PresentStatus.Unclaimed)
+ .CountAsync(ct);
+ }
+
private async Task BuildDefaultViewer(string displayName, int initialTutorialState = 1)
{
Models.Viewer viewer = new Models.Viewer
diff --git a/SVSim.EmulatedEntrypoint/Controllers/MyPageController.cs b/SVSim.EmulatedEntrypoint/Controllers/MyPageController.cs
index 9f284a74..1c0d1629 100644
--- a/SVSim.EmulatedEntrypoint/Controllers/MyPageController.cs
+++ b/SVSim.EmulatedEntrypoint/Controllers/MyPageController.cs
@@ -100,7 +100,7 @@ public class MyPageController : SVSimController
CanGiveDailyLoginBonus = _loginBonus.IsDue(viewer),
UnreceivedMissionRewardCount = 0, // TODO(mypage-stub): viewer mission progress
ReceiveFriendApplyCount = 0, // TODO(mypage-stub): viewer friend-request inbox
- UnreadPresentCount = 0, // TODO(mypage-stub): viewer presents/mail
+ UnreadPresentCount = await _viewerRepository.CountUnclaimedPresentsAsync(viewer.Id, HttpContext.RequestAborted),
FriendBattleInviteCount = 0, // TODO(mypage-stub): viewer room-invite count
GuildNotification = await BuildGuildNotificationAsync(viewer.Id, HttpContext.RequestAborted),
LastAnnounceId = 0, // TODO(mypage-stub): globals announcement metadata
diff --git a/SVSim.UnitTests/Controllers/MyPageControllerTests.cs b/SVSim.UnitTests/Controllers/MyPageControllerTests.cs
index a3fb9733..004f64de 100644
--- a/SVSim.UnitTests/Controllers/MyPageControllerTests.cs
+++ b/SVSim.UnitTests/Controllers/MyPageControllerTests.cs
@@ -54,4 +54,27 @@ public class MyPageControllerTests
var root = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
Assert.That(root.GetProperty("can_give_daily_login_bonus").GetBoolean(), Is.False);
}
+
+ [Test]
+ public async Task MyPage_unread_present_count_reflects_unclaimed_viewer_presents()
+ {
+ // Drives the home-screen crate badge — MyPageTask parses `unread_present_count` into
+ // Data.MyPage.data.unread_mail_count, which MyPageItemHome.SetUnreadGiftCount reads to
+ // show the "N" bubble on the gift button. Stubbed to 0 would hide it even when the
+ // tutorial gift has 5 rewards sitting unclaimed.
+ using var factory = new SVSimTestFactory();
+ await factory.SeedGlobalsAsync();
+ long viewerId = await factory.SeedViewerAsync();
+ await factory.SeedTutorialPresentsAsync(viewerId);
+ using var client = factory.CreateAuthenticatedClient(viewerId);
+
+ var resp = await client.PostAsync("/mypage/index",
+ new StringContent(MyPageRequestJson, Encoding.UTF8, "application/json"));
+ Assert.That(resp.StatusCode, Is.EqualTo(HttpStatusCode.OK), await resp.Content.ReadAsStringAsync());
+
+ var root = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
+ int count = root.GetProperty("unread_present_count").GetInt32();
+ Assert.That(count, Is.GreaterThan(0),
+ "unread_present_count must be the viewer's live Unclaimed ViewerPresent count — a stub 0 hides the crate badge.");
+ }
}