diff --git a/SVSim.Database/Services/Inventory/InventoryTransaction.cs b/SVSim.Database/Services/Inventory/InventoryTransaction.cs index a6337db..f812ba3 100644 --- a/SVSim.Database/Services/Inventory/InventoryTransaction.cs +++ b/SVSim.Database/Services/Inventory/InventoryTransaction.cs @@ -200,8 +200,42 @@ internal sealed class InventoryTransaction : IInventoryTransaction } } - public Task BackfillCardCosmeticsAsync(CancellationToken ct = default) - => throw new NotImplementedException(); + public async Task BackfillCardCosmeticsAsync(CancellationToken ct = default) + { + ThrowIfCommitted(); + + var lookupIds = Viewer.Cards + .Select(c => c.Card.IsFoil ? c.Card.Id - 1 : c.Card.Id) + .Distinct() + .ToList(); + + var cascade = await _db.CardCosmeticRewards + .Where(r => lookupIds.Contains(r.CardId)) + .ToListAsync(ct); + + int granted = 0; + foreach (var reward in cascade) + { + if (AlreadyOwnsCosmetic(reward.Type, reward.CosmeticId)) continue; + if (TryAddCascadeCosmetic(reward, reward.CardId)) + { + granted++; + _ops.Add(new GrantOp((UserGoodsType)(int)reward.Type, reward.CosmeticId, 1, 1, true)); + } + } + + return granted; + } + + private bool AlreadyOwnsCosmetic(CosmeticType type, long id) => type switch + { + CosmeticType.Sleeve => Viewer.Sleeves.Any(s => s.Id == id), + CosmeticType.Emblem => Viewer.Emblems.Any(e => e.Id == id), + CosmeticType.Skin => Viewer.LeaderSkins.Any(s => s.Id == id), + CosmeticType.Degree => Viewer.Degrees.Any(d => d.Id == id), + CosmeticType.MyPageBG => Viewer.MyPageBackgrounds.Any(b => b.Id == id), + _ => false, + }; public long EffectiveBalance(SpendCurrency currency) => throw new NotImplementedException(); public bool OwnsCard(long cardId) => throw new NotImplementedException(); diff --git a/SVSim.UnitTests/Services/Inventory/InventoryBackfillTests.cs b/SVSim.UnitTests/Services/Inventory/InventoryBackfillTests.cs new file mode 100644 index 0000000..e9edd00 --- /dev/null +++ b/SVSim.UnitTests/Services/Inventory/InventoryBackfillTests.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.Database.Services.Inventory; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Services.Inventory; + +public class InventoryBackfillTests +{ + [Test] + public async Task Backfill_grants_missing_cosmetic_for_already_owned_card() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + long cardId = await factory.SeedCardAsync(); + using var scope = factory.Services.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + + const int sleeveId = 2_000_020_000; + ctx.Sleeves.Add(new SleeveEntry { Id = sleeveId }); + ctx.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = cardId, CosmeticId = sleeveId, Type = CosmeticType.Sleeve }); + var card = await ctx.Cards.FirstAsync(c => c.Id == cardId); + var v = await ctx.Viewers.Include(x => x.Cards).ThenInclude(c => c.Card).FirstAsync(x => x.Id == viewerId); + v.Cards.Add(new OwnedCardEntry { Card = card, Count = 3, IsProtected = false }); + await ctx.SaveChangesAsync(); + + var inv = scope.ServiceProvider.GetRequiredService(); + await using var tx = await inv.BeginAsync(viewerId); + int granted = await tx.BackfillCardCosmeticsAsync(); + + Assert.That(granted, Is.EqualTo(1)); + Assert.That(tx.Viewer.Sleeves.Any(s => s.Id == sleeveId), Is.True); + } + + [Test] + public async Task Backfill_idempotent_on_already_owned_cosmetic() + { + using var factory = new SVSimTestFactory(); + long viewerId = await factory.SeedViewerAsync(); + long cardId = await factory.SeedCardAsync(); + using var scope = factory.Services.CreateScope(); + var ctx = scope.ServiceProvider.GetRequiredService(); + + const int sleeveId = 2_000_020_001; + var sleeve = new SleeveEntry { Id = sleeveId }; + ctx.Sleeves.Add(sleeve); + ctx.CardCosmeticRewards.Add(new CardCosmeticReward { CardId = cardId, CosmeticId = sleeveId, Type = CosmeticType.Sleeve }); + var card = await ctx.Cards.FirstAsync(c => c.Id == cardId); + var v = await ctx.Viewers + .Include(x => x.Cards).ThenInclude(c => c.Card) + .Include(x => x.Sleeves) + .FirstAsync(x => x.Id == viewerId); + v.Cards.Add(new OwnedCardEntry { Card = card, Count = 3, IsProtected = false }); + v.Sleeves.Add(sleeve); + await ctx.SaveChangesAsync(); + + var inv = scope.ServiceProvider.GetRequiredService(); + await using var tx = await inv.BeginAsync(viewerId); + int granted = await tx.BackfillCardCosmeticsAsync(); + + Assert.That(granted, Is.EqualTo(0)); + } +}