diff --git a/SVSim.Database/Repositories/BattlePass/IViewerBattlePassRepository.cs b/SVSim.Database/Repositories/BattlePass/IViewerBattlePassRepository.cs
index 633335e..60730f3 100644
--- a/SVSim.Database/Repositories/BattlePass/IViewerBattlePassRepository.cs
+++ b/SVSim.Database/Repositories/BattlePass/IViewerBattlePassRepository.cs
@@ -6,8 +6,10 @@ namespace SVSim.Database.Repositories.BattlePass;
public interface IViewerBattlePassRepository
{
///
- /// Get-or-create progress row for (viewer, season). New rows are added to the change-tracker
- /// but NOT saved — caller batches with other mutations.
+ /// Get-or-create progress row for (viewer, season). New rows are saved IMMEDIATELY (with
+ /// DbUpdateException catch-and-retry to handle the concurrent-first-visit race against
+ /// the (ViewerId, SeasonId) unique index). Existing rows are returned tracked so callers
+ /// can mutate them and batch the save with other changes.
///
Task GetOrCreateProgressAsync(long viewerId, int seasonId, CancellationToken ct);
diff --git a/SVSim.EmulatedEntrypoint/Controllers/BattlePassController.cs b/SVSim.EmulatedEntrypoint/Controllers/BattlePassController.cs
index d136156..a032ec9 100644
--- a/SVSim.EmulatedEntrypoint/Controllers/BattlePassController.cs
+++ b/SVSim.EmulatedEntrypoint/Controllers/BattlePassController.cs
@@ -25,7 +25,12 @@ public class BattlePassController : SVSimController
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
var info = await _battlePass.GetInfoAsync(viewerId, ct);
- if (info is null) return Ok(new { }); // off-season: empty payload
+ // TODO(off-season-crash): Empty {} body crashes BattlePassInfoTask.Parse() on the
+ // client (unconditional jsonData["season_info"] access). Unreachable in practice —
+ // season 23 outlasts the Cygames shutdown. If a season-24+ ever lands, set
+ // data_headers.result_code != 1 here so base.Parse() short-circuits before the
+ // subclass Parse runs.
+ if (info is null) return Ok(new { });
return Ok(info);
}
@@ -35,6 +40,11 @@ public class BattlePassController : SVSimController
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
var list = await _battlePass.GetItemListAsync(viewerId, ct);
+ // TODO(off-season-crash): Empty {} body crashes BattlePassPurchaseInfoTask.Parse() on the
+ // client (unconditional jsonData["premium_pass_description"] access). Unreachable in
+ // practice — season 23 outlasts the Cygames shutdown. If a season-24+ ever lands, set
+ // data_headers.result_code != 1 here so base.Parse() short-circuits before the
+ // subclass Parse runs.
if (list is null) return Ok(new { });
return Ok(list);
}
diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/BattlePass/BattlePassItemListResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/BattlePass/BattlePassItemListResponse.cs
index 4c7a090..2475522 100644
--- a/SVSim.EmulatedEntrypoint/Models/Dtos/BattlePass/BattlePassItemListResponse.cs
+++ b/SVSim.EmulatedEntrypoint/Models/Dtos/BattlePass/BattlePassItemListResponse.cs
@@ -16,7 +16,8 @@ public class BattlePassItemListResponse
[JsonPropertyName("sales_period_info")]
[Key("sales_period_info")]
- public BattlePassSalesPeriodInfoDto? SalesPeriodInfo { get; set; }
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
+ public BattlePassSalesPeriodInfoDto SalesPeriodInfo { get; set; } = new();
[JsonPropertyName("products")]
[Key("products")]
diff --git a/SVSim.EmulatedEntrypoint/Services/BattlePassService.cs b/SVSim.EmulatedEntrypoint/Services/BattlePassService.cs
index 136bdf7..91a8ce1 100644
--- a/SVSim.EmulatedEntrypoint/Services/BattlePassService.cs
+++ b/SVSim.EmulatedEntrypoint/Services/BattlePassService.cs
@@ -203,7 +203,7 @@ public sealed class BattlePassService : IBattlePassService
// append the post-deduction total so the client gets the correct final balance.
postState.RemoveAll(r => r.RewardType == (int)UserGoodsType.Crystal);
postState.Add(new GrantedReward(
- (int)UserGoodsType.Crystal, 0, checked((int)viewer.Currency.Crystals)));
+ (int)UserGoodsType.Crystal, 0, (int)viewer.Currency.Crystals));
return new BattlePassBuyOutcome(1, achieved, postState);
}