refactor(item-purchase): route through InventoryService
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Services;
|
||||
using SVSim.Database.Services.Inventory;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.ItemPurchase;
|
||||
@@ -21,16 +22,14 @@ namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||
public class ItemPurchaseController : SVSimController
|
||||
{
|
||||
private readonly SVSimDbContext _db;
|
||||
private readonly RewardGrantService _rewards;
|
||||
private readonly IInventoryService _inv;
|
||||
private readonly TimeProvider _time;
|
||||
private readonly ICurrencySpendService _spend;
|
||||
|
||||
public ItemPurchaseController(SVSimDbContext db, RewardGrantService rewards, TimeProvider time, ICurrencySpendService spend)
|
||||
public ItemPurchaseController(SVSimDbContext db, IInventoryService inv, TimeProvider time)
|
||||
{
|
||||
_db = db;
|
||||
_rewards = rewards;
|
||||
_inv = inv;
|
||||
_time = time;
|
||||
_spend = spend;
|
||||
}
|
||||
|
||||
[HttpPost("info")]
|
||||
@@ -115,28 +114,17 @@ public class ItemPurchaseController : SVSimController
|
||||
if (rest <= 0)
|
||||
return BadRequest(new { error = "sold_out" });
|
||||
|
||||
var viewer = await LoadViewerGraphAsync(viewerId);
|
||||
var rewardList = new List<RewardListEntry>();
|
||||
await using var tx = await _inv.BeginAsync(viewerId, HttpContext.RequestAborted);
|
||||
|
||||
// Debit the require side. RewardGrantService is grant-only, so handle this inline.
|
||||
var debit = await TryDebit(viewer, (UserGoodsType)entry.RequireItemType, entry.RequireItemId, entry.RequireItemNum);
|
||||
if (debit.Error is not null) return BadRequest(new { error = debit.Error });
|
||||
if (debit.PostState is not null) rewardList.Add(debit.PostState);
|
||||
// Debit the require side via the tx.
|
||||
var debit = await tx.TryDebitAsync(
|
||||
(UserGoodsType)entry.RequireItemType, entry.RequireItemId, entry.RequireItemNum);
|
||||
if (!debit.Success) return BadRequest(new { error = MapDebitError(entry.RequireItemType) });
|
||||
|
||||
// Grant the purchase side through the central dispatcher.
|
||||
var granted = await _rewards.ApplyAsync(viewer,
|
||||
(UserGoodsType)entry.PurchaseItemType, entry.PurchaseItemId, entry.PurchaseItemNum);
|
||||
foreach (var g in granted)
|
||||
{
|
||||
rewardList.Add(new RewardListEntry
|
||||
{
|
||||
RewardType = g.RewardType,
|
||||
RewardId = g.RewardId,
|
||||
RewardNum = g.RewardNum,
|
||||
});
|
||||
}
|
||||
// Grant the purchase side.
|
||||
await tx.GrantAsync((UserGoodsType)entry.PurchaseItemType, entry.PurchaseItemId, entry.PurchaseItemNum);
|
||||
|
||||
// Increment the per-period counter.
|
||||
// Increment the per-period counter (tracked via _db, outside the inventory tx).
|
||||
if (counter is null)
|
||||
{
|
||||
_db.ViewerEventCounters.Add(new ViewerEventCounter
|
||||
@@ -151,52 +139,27 @@ public class ItemPurchaseController : SVSimController
|
||||
{
|
||||
counter.Count++;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return new ItemPurchasePurchaseResponse { RewardList = rewardList };
|
||||
}
|
||||
var result = await tx.CommitAsync(HttpContext.RequestAborted);
|
||||
|
||||
/// <summary>
|
||||
/// Debit <paramref name="num"/> of (<paramref name="type"/>, <paramref name="detailId"/>)
|
||||
/// from the viewer, returning a post-state-aware <see cref="RewardListEntry"/> the client
|
||||
/// uses to refresh its cached count. Returns an error string on insufficient balance.
|
||||
/// </summary>
|
||||
private async Task<(RewardListEntry? PostState, string? Error)> TryDebit(
|
||||
Viewer viewer, UserGoodsType type, long detailId, int num)
|
||||
{
|
||||
switch (type)
|
||||
return new ItemPurchasePurchaseResponse
|
||||
{
|
||||
case UserGoodsType.RedEther:
|
||||
{
|
||||
var r = await _spend.TrySpendAsync(viewer, SpendCurrency.RedEther, num);
|
||||
if (!r.Success) return (null, "insufficient_red_ether");
|
||||
return (new RewardListEntry { RewardType = 1, RewardId = 0, RewardNum = (int)r.PostStateTotal }, null);
|
||||
}
|
||||
case UserGoodsType.Crystal:
|
||||
{
|
||||
var r = await _spend.TrySpendAsync(viewer, SpendCurrency.Crystal, num);
|
||||
if (!r.Success) return (null, "insufficient_crystals");
|
||||
return (new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)r.PostStateTotal }, null);
|
||||
}
|
||||
case UserGoodsType.Rupy:
|
||||
{
|
||||
var r = await _spend.TrySpendAsync(viewer, SpendCurrency.Rupee, num);
|
||||
if (!r.Success) return (null, "insufficient_rupees");
|
||||
return (new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)r.PostStateTotal }, null);
|
||||
}
|
||||
case UserGoodsType.Item:
|
||||
var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)detailId);
|
||||
if (owned is null || owned.Count < num)
|
||||
return (null, "insufficient_item");
|
||||
owned.Count -= num;
|
||||
return (new RewardListEntry { RewardType = 4, RewardId = detailId, RewardNum = owned.Count }, null);
|
||||
|
||||
default:
|
||||
return (null, $"debit_type_not_supported:{type}");
|
||||
}
|
||||
RewardList = result.RewardList
|
||||
.Select(g => new RewardListEntry { RewardType = g.RewardType, RewardId = g.RewardId, RewardNum = g.RewardNum })
|
||||
.ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
private static string MapDebitError(int requireType) => requireType switch
|
||||
{
|
||||
(int)UserGoodsType.RedEther => "insufficient_red_ether",
|
||||
(int)UserGoodsType.Crystal => "insufficient_crystals",
|
||||
(int)UserGoodsType.Rupy => "insufficient_rupees",
|
||||
(int)UserGoodsType.Item => "insufficient_item",
|
||||
_ => "debit_type_not_supported",
|
||||
};
|
||||
|
||||
private static string CounterKey(int purchaseId) => $"item_purchase:{purchaseId}";
|
||||
|
||||
private static int CounterCount(List<ViewerEventCounter> counters, ItemPurchaseCatalogEntry entry, string monthKey)
|
||||
@@ -204,15 +167,4 @@ public class ItemPurchaseController : SVSimController
|
||||
var period = entry.IsMonthlyReset ? monthKey : JstPeriod.AllTime;
|
||||
return counters.FirstOrDefault(c => c.EventKey == CounterKey(entry.Id) && c.Period == period)?.Count ?? 0;
|
||||
}
|
||||
|
||||
private Task<Viewer> LoadViewerGraphAsync(long viewerId) => _db.Viewers
|
||||
.Include(v => v.Items).ThenInclude(i => i.Item)
|
||||
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||
.Include(v => v.Sleeves)
|
||||
.Include(v => v.Emblems)
|
||||
.Include(v => v.LeaderSkins)
|
||||
.Include(v => v.Degrees)
|
||||
.Include(v => v.MyPageBackgrounds)
|
||||
.AsSplitQuery()
|
||||
.FirstAsync(v => v.Id == viewerId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user