feat(repo): GetStoryDecksByClass joins story-deck presentation to product card lists
Adds StoryDeckView projection, IBuildDeckRepository.GetStoryDecksByClass interface method, and BuildDeckRepository implementation that loads StoryDeckEntry rows for a class, fetches matching BuildDeckProductEntry card lists, and expands each card by Number into a flat CardIdArray. TDD: 2 tests in StoryDeckRepositoryTests (expand + empty-class). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,4 +64,38 @@ public class BuildDeckRepository : IBuildDeckRepository
|
||||
await _db.SaveChangesAsync();
|
||||
return row.PurchaseCount;
|
||||
}
|
||||
|
||||
public async Task<List<StoryDeckView>> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,11 @@ public interface IBuildDeckRepository
|
||||
/// Returns the new total.
|
||||
/// </summary>
|
||||
Task<int> IncrementPurchaseCount(long viewerId, int productId);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Task<List<StoryDeckView>> GetStoryDecksByClass(int classId);
|
||||
}
|
||||
|
||||
22
SVSim.Database/Repositories/BuildDeck/StoryDeckView.cs
Normal file
22
SVSim.Database/Repositories/BuildDeck/StoryDeckView.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using SVSim.Database.Enums;
|
||||
|
||||
namespace SVSim.Database.Repositories.BuildDeck;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<long> CardIdArray { get; init; } = new();
|
||||
}
|
||||
66
SVSim.UnitTests/Repositories/StoryDeckRepositoryTests.cs
Normal file
66
SVSim.UnitTests/Repositories/StoryDeckRepositoryTests.cs
Normal file
@@ -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<SVSimDbContext>();
|
||||
|
||||
// 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<SVSimDbContext>();
|
||||
|
||||
var repo = new BuildDeckRepository(db);
|
||||
var result = await repo.GetStoryDecksByClass(8);
|
||||
|
||||
Assert.That(result, Is.Empty);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user