refactor(card-inventory): route Create/Destruct through InventoryService
RedEther debit now goes through tx.TrySpendAsync (freeplay-aware); Card grants route through tx.GrantAsync (cosmetic cascade for first-time owners). Validation phase unchanged. DestructCards left on direct-viewer path (structural mismatch: validation on one viewer, mutation on same instance — clean tx port deferred to follow-up). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,18 +2,19 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
using SVSim.Database.Services;
|
using SVSim.Database.Services;
|
||||||
|
using SVSim.Database.Services.Inventory;
|
||||||
|
|
||||||
namespace SVSim.Database.Repositories.Card;
|
namespace SVSim.Database.Repositories.Card;
|
||||||
|
|
||||||
public class CardInventoryRepository : ICardInventoryRepository
|
public class CardInventoryRepository : ICardInventoryRepository
|
||||||
{
|
{
|
||||||
private readonly SVSimDbContext _db;
|
private readonly SVSimDbContext _db;
|
||||||
private readonly RewardGrantService _grants;
|
private readonly IInventoryService _inv;
|
||||||
|
|
||||||
public CardInventoryRepository(SVSimDbContext db, RewardGrantService grants)
|
public CardInventoryRepository(SVSimDbContext db, IInventoryService inv)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_grants = grants;
|
_inv = inv;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DestructOutcome> DestructCards(long viewerId, IReadOnlyDictionary<long, int> destructCounts)
|
public async Task<DestructOutcome> DestructCards(long viewerId, IReadOnlyDictionary<long, int> destructCounts)
|
||||||
@@ -129,30 +130,27 @@ public class CardInventoryRepository : ICardInventoryRepository
|
|||||||
totalCost += (ulong)catalogCard.CollectionInfo.CraftCost * (ulong)num;
|
totalCost += (ulong)catalogCard.CollectionInfo.CraftCost * (ulong)num;
|
||||||
}
|
}
|
||||||
|
|
||||||
// insufficient_vials checked after summing the full batch — all-or-nothing
|
// insufficient_vials pre-check (validation-before-mutation atomicity, keeps same error ordering)
|
||||||
if (viewer.Currency.RedEther < totalCost)
|
if (viewer.Currency.RedEther < totalCost)
|
||||||
return CreateOutcome.Fail(CreateError.InsufficientVials);
|
return CreateOutcome.Fail(CreateError.InsufficientVials);
|
||||||
|
|
||||||
using var tx = await _db.Database.BeginTransactionAsync();
|
// Mutation phase via InventoryService transaction — freeplay-aware RedEther debit,
|
||||||
|
// card grants with cosmetic cascade.
|
||||||
|
await using var tx = await _inv.BeginAsync(viewerId);
|
||||||
|
|
||||||
// Debit RedEther directly. ApplyAsync only credits — debit-pair operations live in this
|
var spendResult = await tx.TrySpendAsync(SpendCurrency.RedEther, (long)totalCost);
|
||||||
// repo, symmetric with destruct.
|
if (!spendResult.Success)
|
||||||
viewer.Currency.RedEther -= totalCost;
|
return CreateOutcome.Fail(CreateError.InsufficientVials);
|
||||||
|
|
||||||
// Per-card grant via RewardGrantService — single source of truth for Card-typed grants,
|
|
||||||
// and fires the CardCosmeticReward cascade for first-time owners. See
|
|
||||||
// feedback_reward_grant_service memory.
|
|
||||||
var allGrants = new List<GrantedReward>();
|
var allGrants = new List<GrantedReward>();
|
||||||
foreach (var (cardId, num) in createCounts)
|
foreach (var (cardId, num) in createCounts)
|
||||||
{
|
{
|
||||||
var granted = await _grants.ApplyAsync(viewer, UserGoodsType.Card, cardId, num);
|
var granted = await tx.GrantAsync(UserGoodsType.Card, cardId, num);
|
||||||
allGrants.AddRange(granted);
|
allGrants.AddRange(granted);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
|
||||||
await tx.CommitAsync();
|
await tx.CommitAsync();
|
||||||
|
return CreateOutcome.Ok(new CreateResult(tx.Viewer.Currency.RedEther, allGrants));
|
||||||
return CreateOutcome.Ok(new CreateResult(viewer.Currency.RedEther, allGrants));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ProtectOutcome> SetProtected(long viewerId, long cardId, bool isProtected)
|
public async Task<ProtectOutcome> SetProtected(long viewerId, long cardId, bool isProtected)
|
||||||
|
|||||||
Reference in New Issue
Block a user