From 9436a0d21b075f085a910dc2f78a2f94e339904b Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 31 May 2026 16:25:10 -0400 Subject: [PATCH] refactor(sleeve): route buy through InventoryService Co-Authored-By: Claude Opus 4.7 --- .../Controllers/SleeveController.cs | 77 +++++++------------ 1 file changed, 26 insertions(+), 51 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs b/SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs index e852639..4f70bee 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs @@ -5,6 +5,7 @@ using SVSim.Database.Enums; using SVSim.Database.Models; using SVSim.Database.Repositories.Collectibles; 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.Sleeve; @@ -20,17 +21,13 @@ namespace SVSim.EmulatedEntrypoint.Controllers; public class SleeveController : SVSimController { private readonly SVSimDbContext _db; - private readonly RewardGrantService _rewards; - private readonly ICurrencySpendService _spend; - private readonly IViewerEntitlements _entitlements; + private readonly IInventoryService _inv; private readonly ICollectionRepository _collection; - public SleeveController(SVSimDbContext db, RewardGrantService rewards, ICurrencySpendService spend, IViewerEntitlements entitlements, ICollectionRepository collection) + public SleeveController(SVSimDbContext db, IInventoryService inv, ICollectionRepository collection) { _db = db; - _rewards = rewards; - _spend = spend; - _entitlements = entitlements; + _inv = inv; _collection = collection; } @@ -42,12 +39,13 @@ public class SleeveController : SVSimController // is_purchased_product is "viewer owns at least one sleeve granted by this product". // Loading the viewer's sleeve-id set once and checking each product against it avoids // an N+1 over products. - var ownedSleeveIds = _entitlements.IsFreeplay - ? (await _collection.GetAllSleeveIds()).Select(id => (long)id).ToHashSet() - : (await _db.Viewers - .Where(v => v.Id == viewerId) - .SelectMany(v => v.Sleeves.Select(s => (long)s.Id)) - .ToListAsync()).ToHashSet(); + var viewerForInfo = await _db.Viewers + .Include(v => v.Sleeves) + .FirstOrDefaultAsync(v => v.Id == viewerId); + if (viewerForInfo is null) return Unauthorized(); + + var cosmeticsForInfo = await _inv.EffectiveCosmeticsAsync(viewerForInfo); + var ownedSleeveIds = cosmeticsForInfo.SleeveIds.Select(id => (long)id).ToHashSet(); var series = await _db.SleeveShopSeries .Where(s => s.IsEnabled) @@ -113,18 +111,17 @@ public class SleeveController : SVSimController if (product.SeriesId != request.SeriesId) return BadRequest(new { error = "series_product_mismatch" }); - var viewer = await LoadViewerGraphAsync(viewerId); + await using var tx = await _inv.BeginAsync(viewerId, HttpContext.RequestAborted); - if (_entitlements.IsFreeplay) + if (tx.IsFreeplay) return BadRequest(new { error = "already_purchased" }); - if (IsProductPurchased(product, viewer.Sleeves.Select(s => (long)s.Id).ToHashSet())) + if (IsProductPurchased(product, tx.Viewer.Sleeves.Select(s => (long)s.Id).ToHashSet())) return BadRequest(new { error = "already_purchased" }); // Pricing: capture-confirmed shape is single-price-per-currency (no intro/regular tiers // like BuildDeck). At least one of crystal/rupy must match the chosen sales_type; // sales_type==0 means "free", which requires both prices == 0. - var rewardList = new List(); switch (request.SalesType) { case 0: // free @@ -134,39 +131,27 @@ public class SleeveController : SVSimController case 1: // crystal if (product.PriceCrystal is null) return BadRequest(new { error = "price_not_available_for_currency" }); - var crystalRes = await _spend.TrySpendAsync(viewer, SpendCurrency.Crystal, product.PriceCrystal.Value); - if (!crystalRes.Success) return BadRequest(new { error = "insufficient_crystals" }); - rewardList.Add(new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)crystalRes.PostStateTotal }); + { var r = await tx.TrySpendAsync(SpendCurrency.Crystal, product.PriceCrystal.Value); if (!r.Success) return BadRequest(new { error = "insufficient_crystals" }); } break; case 2: // rupy if (product.PriceRupy is null) return BadRequest(new { error = "price_not_available_for_currency" }); - var rupyRes = await _spend.TrySpendAsync(viewer, SpendCurrency.Rupee, product.PriceRupy.Value); - if (!rupyRes.Success) return BadRequest(new { error = "insufficient_rupees" }); - rewardList.Add(new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)rupyRes.PostStateTotal }); + { var r = await tx.TrySpendAsync(SpendCurrency.Rupee, product.PriceRupy.Value); if (!r.Success) return BadRequest(new { error = "insufficient_rupees" }); } break; } - // Grant each catalog reward through the central dispatcher — covers sleeve (6), emblem - // (7), and any future bundled grants. ApplyAsync returns post-state-aware reward entries - // suitable for emission as-is. + // Grant each catalog reward through the central dispatcher. foreach (var r in product.Rewards.OrderBy(r => r.OrderIndex)) + await tx.GrantAsync((UserGoodsType)r.RewardType, r.RewardDetailId, r.RewardNumber); + + var result = await tx.CommitAsync(HttpContext.RequestAborted); + + return new SleeveBuyResponse { - var granted = await _rewards.ApplyAsync(viewer, (UserGoodsType)r.RewardType, r.RewardDetailId, r.RewardNumber); - foreach (var g in granted) - { - rewardList.Add(new RewardListEntry - { - RewardType = g.RewardType, - RewardId = g.RewardId, - RewardNum = g.RewardNum, - }); - } - } - - await _db.SaveChangesAsync(); - - return new SleeveBuyResponse { RewardList = rewardList }; + RewardList = result.RewardList + .Select(g => new RewardListEntry { RewardType = g.RewardType, RewardId = g.RewardId, RewardNum = g.RewardNum }) + .ToList(), + }; } /// @@ -185,14 +170,4 @@ public class SleeveController : SVSimController return false; } - private Task LoadViewerGraphAsync(long viewerId) => _db.Viewers - .Include(v => v.Sleeves) - .Include(v => v.Emblems) - .Include(v => v.LeaderSkins) - .Include(v => v.Degrees) - .Include(v => v.MyPageBackgrounds) - .Include(v => v.Items).ThenInclude(i => i.Item) - .Include(v => v.Cards).ThenInclude(c => c.Card) - .AsSplitQuery() - .FirstAsync(v => v.Id == viewerId); }