diff --git a/SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs b/SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs index 7b1d3b4..900dcd6 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs @@ -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(); + 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); - /// - /// Debit of (, ) - /// from the viewer, returning a post-state-aware the client - /// uses to refresh its cached count. Returns an error string on insufficient balance. - /// - 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 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 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); }