diff --git a/SVSim.Database/Repositories/BuildDeck/BuildDeckRepository.cs b/SVSim.Database/Repositories/BuildDeck/BuildDeckRepository.cs index 108f12f..e9abb21 100644 --- a/SVSim.Database/Repositories/BuildDeck/BuildDeckRepository.cs +++ b/SVSim.Database/Repositories/BuildDeck/BuildDeckRepository.cs @@ -64,4 +64,38 @@ public class BuildDeckRepository : IBuildDeckRepository await _db.SaveChangesAsync(); return row.PurchaseCount; } + + public async Task> GetStoryDecksByClass(int classId) + { + var decks = await _db.StoryDecks.Where(d => d.ClassId == classId).ToListAsync(); + if (decks.Count == 0) return new(); + + var ids = decks.Select(d => d.DeckNo).ToList(); + var products = await _db.BuildDeckProducts + .Where(p => ids.Contains(p.Id)) + .Include(p => p.Cards) + .AsSplitQuery() + .ToListAsync(); + + // Expand each product's owned card rows by Number into a flat card_id list (spots included — + // validated against the prod capture, 112/112 match). + var cardsById = products.ToDictionary( + p => p.Id, + p => p.Cards.SelectMany(c => Enumerable.Repeat(c.CardId, c.Number)).ToList()); + + return decks.Select(d => new StoryDeckView + { + DeckNo = d.DeckNo, + Kind = d.Kind, + ClassId = d.ClassId, + DeckName = d.DeckName, + SleeveId = d.SleeveId, + LeaderSkinId = d.LeaderSkinId, + IsRecommend = d.IsRecommend, + OrderNum = d.OrderNum, + EntryNo = d.EntryNo, + DeckFormat = d.DeckFormat, + CardIdArray = cardsById.TryGetValue(d.DeckNo, out var cards) ? cards : new(), + }).ToList(); + } } diff --git a/SVSim.Database/Repositories/BuildDeck/IBuildDeckRepository.cs b/SVSim.Database/Repositories/BuildDeck/IBuildDeckRepository.cs index 06828a3..26e3c32 100644 --- a/SVSim.Database/Repositories/BuildDeck/IBuildDeckRepository.cs +++ b/SVSim.Database/Repositories/BuildDeck/IBuildDeckRepository.cs @@ -26,4 +26,11 @@ public interface IBuildDeckRepository /// Returns the new total. /// Task IncrementPurchaseCount(long viewerId, int productId); + + /// + /// Story deck-select decks for a class: StoryDeckEntry presentation rows joined to the matching + /// BuildDeckProductEntry card lists (deck_no == product_id), expanded to a flat card_id array. + /// Returns build and trial decks together; the caller splits by Kind. + /// + Task> GetStoryDecksByClass(int classId); } diff --git a/SVSim.Database/Repositories/BuildDeck/StoryDeckView.cs b/SVSim.Database/Repositories/BuildDeck/StoryDeckView.cs new file mode 100644 index 0000000..9a7c8d4 --- /dev/null +++ b/SVSim.Database/Repositories/BuildDeck/StoryDeckView.cs @@ -0,0 +1,22 @@ +using SVSim.Database.Enums; + +namespace SVSim.Database.Repositories.BuildDeck; + +/// +/// A story-select deck ready for the wire: presentation metadata from StoryDeckEntry plus the +/// 40-card list expanded from the matching BuildDeckProductEntry. Plain projection, not an entity. +/// +public sealed class StoryDeckView +{ + public int DeckNo { get; init; } + public StoryDeckKind Kind { get; init; } + public int ClassId { get; init; } + public string DeckName { get; init; } = string.Empty; + public int SleeveId { get; init; } + public int LeaderSkinId { get; init; } + public int IsRecommend { get; init; } + public int OrderNum { get; init; } + public int EntryNo { get; init; } + public int? DeckFormat { get; init; } + public List CardIdArray { get; init; } = new(); +} diff --git a/SVSim.UnitTests/Repositories/StoryDeckRepositoryTests.cs b/SVSim.UnitTests/Repositories/StoryDeckRepositoryTests.cs new file mode 100644 index 0000000..28f866f --- /dev/null +++ b/SVSim.UnitTests/Repositories/StoryDeckRepositoryTests.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.Repositories.BuildDeck; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Repositories; + +public class StoryDeckRepositoryTests +{ + [Test] + public async Task GetStoryDecksByClass_returns_decks_with_expanded_card_arrays() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // FK: BuildDeckProducts requires a parent BuildDeckSeries row. + db.BuildDeckSeries.Add(new BuildDeckSeriesEntry { Id = 0 }); + await db.SaveChangesAsync(); + + // Product 701 (class 1 build): 2x card 100, 1x card 200 = 3-card "deck". + db.BuildDeckProducts.Add(new BuildDeckProductEntry + { + Id = 701, SeriesId = 0, LeaderId = 1, DeckCode = "", ProductNameKey = "", IsEnabled = false, + Cards = new() + { + new BuildDeckProductCardEntry { CardId = 100, Number = 2, IsSpot = false }, + new BuildDeckProductCardEntry { CardId = 200, Number = 1, IsSpot = false }, + }, + }); + db.StoryDecks.Add(new StoryDeckEntry + { + DeckNo = 701, Kind = StoryDeckKind.Build, ClassId = 1, DeckName = "Pure Devotion", + SleeveId = 3000011, LeaderSkinId = 1, IsRecommend = 0, OrderNum = 0, EntryNo = 0, DeckFormat = null, + }); + // A class-2 deck that must NOT be returned for class 1. + db.StoryDecks.Add(new StoryDeckEntry { DeckNo = 702, Kind = StoryDeckKind.Build, ClassId = 2, DeckName = "Other" }); + await db.SaveChangesAsync(); + + var repo = new BuildDeckRepository(db); + var result = await repo.GetStoryDecksByClass(1); + + Assert.That(result.Count, Is.EqualTo(1)); + var deck = result[0]; + Assert.That(deck.DeckNo, Is.EqualTo(701)); + Assert.That(deck.DeckName, Is.EqualTo("Pure Devotion")); + Assert.That(deck.Kind, Is.EqualTo(StoryDeckKind.Build)); + Assert.That(deck.CardIdArray.OrderBy(x => x), Is.EqualTo(new long[] { 100, 100, 200 })); + } + + [Test] + public async Task GetStoryDecksByClass_returns_empty_for_class_with_no_decks() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var repo = new BuildDeckRepository(db); + var result = await repo.GetStoryDecksByClass(8); + + Assert.That(result, Is.Empty); + } +}