feat(load): emit daily_login_bonus via ILoginBonusService
Grant runs inside the existing inventory tx so streak-bump and reward credit commit atomically with cosmetic backfill.
This commit is contained in:
@@ -49,12 +49,14 @@ public class LoadController : SVSimController
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly IInventoryService _inv;
|
||||
private readonly ICardMasterPayloadProvider _cardMaster;
|
||||
private readonly ILoginBonusService _loginBonus;
|
||||
|
||||
public LoadController(IViewerRepository viewerRepository, IGlobalsRepository globalsRepository,
|
||||
IGameConfigService config,
|
||||
IBattlePassService battlePass, IViewerMissionStateService missionState,
|
||||
SVSimDbContext db, IInventoryService inv,
|
||||
ICardMasterPayloadProvider cardMaster)
|
||||
ICardMasterPayloadProvider cardMaster,
|
||||
ILoginBonusService loginBonus)
|
||||
{
|
||||
_viewerRepository = viewerRepository;
|
||||
_globalsRepository = globalsRepository;
|
||||
@@ -64,6 +66,7 @@ public class LoadController : SVSimController
|
||||
_db = db;
|
||||
_inv = inv;
|
||||
_cardMaster = cardMaster;
|
||||
_loginBonus = loginBonus;
|
||||
}
|
||||
|
||||
[HttpPost("index")]
|
||||
@@ -88,6 +91,7 @@ public class LoadController : SVSimController
|
||||
// the response payload would be one /load/index behind on newly-granted cosmetics.
|
||||
await using var tx = await _inv.BeginAsync(viewer.Id, ct);
|
||||
await tx.BackfillCardCosmeticsAsync(ct);
|
||||
DailyLoginBonus? loginBonusDto = await _loginBonus.GrantIfDueAsync(tx, ct);
|
||||
await tx.CommitAsync(ct);
|
||||
|
||||
// Lazy-materialize mission/achievement state. Idempotent — safe to call every /load/index.
|
||||
@@ -229,9 +233,7 @@ public class LoadController : SVSimController
|
||||
MaintenanceCards = (await _globalsRepository.GetMaintenanceCards())
|
||||
.Select(e => e.Id).ToList(),
|
||||
RedEtherOverrides = new List<RedEtherOverride>(),
|
||||
// Optional per spec (load-index.md:247). Populated by ILoginBonusService (Task 6)
|
||||
// when the viewer has an active bonus period.
|
||||
DailyLoginBonus = null,
|
||||
DailyLoginBonus = loginBonusDto,
|
||||
UserRankedMatches = new List<UserRankedMatches>(),
|
||||
UserRankInfo = RankFormats.Select(f => new UserRankInfo
|
||||
{
|
||||
|
||||
@@ -307,9 +307,11 @@ public class LoadControllerTests
|
||||
Assert.That(root.GetProperty("rotation_card_set_id_list").GetArrayLength(),
|
||||
Is.GreaterThanOrEqualTo(2));
|
||||
|
||||
// Optional/absent fields stay absent when nothing meaningful to surface
|
||||
Assert.That(root.TryGetProperty("daily_login_bonus", out _), Is.False,
|
||||
"daily_login_bonus optional per spec; emit null when no active campaign");
|
||||
// daily_login_bonus IS emitted for a fresh viewer (LastLoginBonusClaimedAt is null →
|
||||
// IsDue returns true). The seeded globals test uses a fresh viewer, so it gets Day 1.
|
||||
Assert.That(root.TryGetProperty("daily_login_bonus", out var dlbGlobals), Is.True,
|
||||
"daily_login_bonus should be present for a fresh viewer (IsDue=true)");
|
||||
Assert.That(dlbGlobals.ValueKind, Is.EqualTo(JsonValueKind.Object));
|
||||
|
||||
// battle_pass_level_info is present when levels are seeded — 100-entry dict keyed by level string.
|
||||
Assert.That(root.TryGetProperty("battle_pass_level_info", out var bpli), Is.True,
|
||||
@@ -380,6 +382,46 @@ public class LoadControllerTests
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Index_emits_daily_login_bonus_for_fresh_viewer()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
|
||||
var root = await PostIndexAndReadBody(factory, viewerId);
|
||||
|
||||
Assert.That(root.TryGetProperty("daily_login_bonus", out var dlb), Is.True);
|
||||
Assert.That(dlb.ValueKind, Is.EqualTo(JsonValueKind.Object));
|
||||
|
||||
var normal = dlb.GetProperty("normal");
|
||||
Assert.That(normal.GetProperty("now_count").GetInt32(), Is.EqualTo(1));
|
||||
Assert.That(normal.GetProperty("name").GetString(), Is.EqualTo("Daily Bonus"));
|
||||
Assert.That(normal.GetProperty("campaign_id").ValueKind, Is.EqualTo(JsonValueKind.String));
|
||||
Assert.That(normal.GetProperty("img").ValueKind, Is.EqualTo(JsonValueKind.String));
|
||||
Assert.That(normal.GetProperty("reward").GetArrayLength(), Is.EqualTo(15));
|
||||
|
||||
var firstReward = normal.GetProperty("reward")[0];
|
||||
Assert.That(firstReward.GetProperty("reward_type").GetString(), Is.EqualTo("9"));
|
||||
Assert.That(firstReward.GetProperty("reward_number").GetString(), Is.EqualTo("20"));
|
||||
|
||||
Assert.That(dlb.GetProperty("campaign").ValueKind, Is.EqualTo(JsonValueKind.Array));
|
||||
Assert.That(dlb.GetProperty("campaign").GetArrayLength(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Index_omits_daily_login_bonus_on_second_call_same_day()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync();
|
||||
|
||||
await PostIndexAndReadBody(factory, viewerId);
|
||||
var root = await PostIndexAndReadBody(factory, viewerId);
|
||||
|
||||
var present = root.TryGetProperty("daily_login_bonus", out var dlb)
|
||||
&& dlb.ValueKind != JsonValueKind.Null;
|
||||
Assert.That(present, Is.False, "Second-same-day /load/index must not re-emit the bonus");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task LoadIndex_emits_battle_pass_level_info_with_100_entries_when_period_active()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user