refactor(sleeve): route buy through InventoryService
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ using SVSim.Database.Enums;
|
|||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
using SVSim.Database.Repositories.Collectibles;
|
using SVSim.Database.Repositories.Collectibles;
|
||||||
using SVSim.Database.Services;
|
using SVSim.Database.Services;
|
||||||
|
using SVSim.Database.Services.Inventory;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Sleeve;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Sleeve;
|
||||||
@@ -20,17 +21,13 @@ namespace SVSim.EmulatedEntrypoint.Controllers;
|
|||||||
public class SleeveController : SVSimController
|
public class SleeveController : SVSimController
|
||||||
{
|
{
|
||||||
private readonly SVSimDbContext _db;
|
private readonly SVSimDbContext _db;
|
||||||
private readonly RewardGrantService _rewards;
|
private readonly IInventoryService _inv;
|
||||||
private readonly ICurrencySpendService _spend;
|
|
||||||
private readonly IViewerEntitlements _entitlements;
|
|
||||||
private readonly ICollectionRepository _collection;
|
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;
|
_db = db;
|
||||||
_rewards = rewards;
|
_inv = inv;
|
||||||
_spend = spend;
|
|
||||||
_entitlements = entitlements;
|
|
||||||
_collection = collection;
|
_collection = collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,12 +39,13 @@ public class SleeveController : SVSimController
|
|||||||
// is_purchased_product is "viewer owns at least one sleeve granted by this product".
|
// 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
|
// Loading the viewer's sleeve-id set once and checking each product against it avoids
|
||||||
// an N+1 over products.
|
// an N+1 over products.
|
||||||
var ownedSleeveIds = _entitlements.IsFreeplay
|
var viewerForInfo = await _db.Viewers
|
||||||
? (await _collection.GetAllSleeveIds()).Select(id => (long)id).ToHashSet()
|
.Include(v => v.Sleeves)
|
||||||
: (await _db.Viewers
|
.FirstOrDefaultAsync(v => v.Id == viewerId);
|
||||||
.Where(v => v.Id == viewerId)
|
if (viewerForInfo is null) return Unauthorized();
|
||||||
.SelectMany(v => v.Sleeves.Select(s => (long)s.Id))
|
|
||||||
.ToListAsync()).ToHashSet();
|
var cosmeticsForInfo = await _inv.EffectiveCosmeticsAsync(viewerForInfo);
|
||||||
|
var ownedSleeveIds = cosmeticsForInfo.SleeveIds.Select(id => (long)id).ToHashSet();
|
||||||
|
|
||||||
var series = await _db.SleeveShopSeries
|
var series = await _db.SleeveShopSeries
|
||||||
.Where(s => s.IsEnabled)
|
.Where(s => s.IsEnabled)
|
||||||
@@ -113,18 +111,17 @@ public class SleeveController : SVSimController
|
|||||||
if (product.SeriesId != request.SeriesId)
|
if (product.SeriesId != request.SeriesId)
|
||||||
return BadRequest(new { error = "series_product_mismatch" });
|
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" });
|
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" });
|
return BadRequest(new { error = "already_purchased" });
|
||||||
|
|
||||||
// Pricing: capture-confirmed shape is single-price-per-currency (no intro/regular tiers
|
// 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;
|
// like BuildDeck). At least one of crystal/rupy must match the chosen sales_type;
|
||||||
// sales_type==0 means "free", which requires both prices == 0.
|
// sales_type==0 means "free", which requires both prices == 0.
|
||||||
var rewardList = new List<RewardListEntry>();
|
|
||||||
switch (request.SalesType)
|
switch (request.SalesType)
|
||||||
{
|
{
|
||||||
case 0: // free
|
case 0: // free
|
||||||
@@ -134,39 +131,27 @@ public class SleeveController : SVSimController
|
|||||||
case 1: // crystal
|
case 1: // crystal
|
||||||
if (product.PriceCrystal is null)
|
if (product.PriceCrystal is null)
|
||||||
return BadRequest(new { error = "price_not_available_for_currency" });
|
return BadRequest(new { error = "price_not_available_for_currency" });
|
||||||
var crystalRes = await _spend.TrySpendAsync(viewer, SpendCurrency.Crystal, product.PriceCrystal.Value);
|
{ var r = await tx.TrySpendAsync(SpendCurrency.Crystal, product.PriceCrystal.Value); if (!r.Success) return BadRequest(new { error = "insufficient_crystals" }); }
|
||||||
if (!crystalRes.Success) return BadRequest(new { error = "insufficient_crystals" });
|
|
||||||
rewardList.Add(new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)crystalRes.PostStateTotal });
|
|
||||||
break;
|
break;
|
||||||
case 2: // rupy
|
case 2: // rupy
|
||||||
if (product.PriceRupy is null)
|
if (product.PriceRupy is null)
|
||||||
return BadRequest(new { error = "price_not_available_for_currency" });
|
return BadRequest(new { error = "price_not_available_for_currency" });
|
||||||
var rupyRes = await _spend.TrySpendAsync(viewer, SpendCurrency.Rupee, product.PriceRupy.Value);
|
{ var r = await tx.TrySpendAsync(SpendCurrency.Rupee, product.PriceRupy.Value); if (!r.Success) return BadRequest(new { error = "insufficient_rupees" }); }
|
||||||
if (!rupyRes.Success) return BadRequest(new { error = "insufficient_rupees" });
|
|
||||||
rewardList.Add(new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)rupyRes.PostStateTotal });
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grant each catalog reward through the central dispatcher — covers sleeve (6), emblem
|
// Grant each catalog reward through the central dispatcher.
|
||||||
// (7), and any future bundled grants. ApplyAsync returns post-state-aware reward entries
|
|
||||||
// suitable for emission as-is.
|
|
||||||
foreach (var r in product.Rewards.OrderBy(r => r.OrderIndex))
|
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);
|
RewardList = result.RewardList
|
||||||
foreach (var g in granted)
|
.Select(g => new RewardListEntry { RewardType = g.RewardType, RewardId = g.RewardId, RewardNum = g.RewardNum })
|
||||||
{
|
.ToList(),
|
||||||
rewardList.Add(new RewardListEntry
|
};
|
||||||
{
|
|
||||||
RewardType = g.RewardType,
|
|
||||||
RewardId = g.RewardId,
|
|
||||||
RewardNum = g.RewardNum,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return new SleeveBuyResponse { RewardList = rewardList };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -185,14 +170,4 @@ public class SleeveController : SVSimController
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Viewer> 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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user