diff --git a/SVSim.Bootstrap/Importers/StoryDeckImporter.cs b/SVSim.Bootstrap/Importers/StoryDeckImporter.cs new file mode 100644 index 0000000..42b0025 --- /dev/null +++ b/SVSim.Bootstrap/Importers/StoryDeckImporter.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore; +using SVSim.Bootstrap.Models.Seed; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.Database.Models; + +namespace SVSim.Bootstrap.Importers; + +/// +/// Idempotent upsert of story-deck presentation rows from seeds/story-decks.json. +/// Card lists are NOT imported here — they belong to BuildDeckProductEntry (deck_no == product_id), +/// so this importer should run AFTER BuildDeckImporter.ImportPackageAsync. Rows missing from the +/// seed are left intact. +/// +public class StoryDeckImporter +{ + public async Task ImportAsync(SVSimDbContext context, string seedDir) + { + var seed = SeedLoader.LoadList(Path.Combine(seedDir, "story-decks.json")); + if (seed.Count == 0) + { + Console.WriteLine("[StoryDeckImporter] No seed rows; skipping."); + return 0; + } + + var existing = await context.StoryDecks.ToDictionaryAsync(e => e.Id); + int created = 0, updated = 0; + + foreach (var s in seed) + { + if (s.DeckNo == 0) continue; + var entry = existing.TryGetValue(s.DeckNo, out var ex) ? ex : new StoryDeckEntry { DeckNo = s.DeckNo }; + entry.Kind = string.Equals(s.Kind, "trial", StringComparison.OrdinalIgnoreCase) + ? StoryDeckKind.Trial : StoryDeckKind.Build; + entry.ClassId = s.ClassId; + entry.DeckName = s.DeckName; + entry.SleeveId = s.SleeveId; + entry.LeaderSkinId = s.LeaderSkinId; + entry.IsRecommend = s.IsRecommend; + entry.OrderNum = s.OrderNum; + entry.EntryNo = s.EntryNo; + entry.DeckFormat = s.DeckFormat; + + if (ex is null) { context.StoryDecks.Add(entry); existing[s.DeckNo] = entry; created++; } + else updated++; + } + + await context.SaveChangesAsync(); + Console.WriteLine($"[StoryDeckImporter] +{created}/~{updated}"); + return created + updated; + } +} diff --git a/SVSim.Bootstrap/Models/Seed/StoryDeckSeed.cs b/SVSim.Bootstrap/Models/Seed/StoryDeckSeed.cs new file mode 100644 index 0000000..9afe87a --- /dev/null +++ b/SVSim.Bootstrap/Models/Seed/StoryDeckSeed.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace SVSim.Bootstrap.Models.Seed; + +public sealed class StoryDeckSeed +{ + [JsonPropertyName("deck_no")] public int DeckNo { get; set; } + [JsonPropertyName("kind")] public string Kind { get; set; } = "build"; + [JsonPropertyName("class_id")] public int ClassId { get; set; } + [JsonPropertyName("deck_name")] public string DeckName { get; set; } = ""; + [JsonPropertyName("sleeve_id")] public int SleeveId { get; set; } + [JsonPropertyName("leader_skin_id")] public int LeaderSkinId { get; set; } + [JsonPropertyName("is_recommend")] public int IsRecommend { get; set; } + [JsonPropertyName("order_num")] public int OrderNum { get; set; } + [JsonPropertyName("entry_no")] public int EntryNo { get; set; } + [JsonPropertyName("deck_format")] public int? DeckFormat { get; set; } +} diff --git a/SVSim.Bootstrap/Program.cs b/SVSim.Bootstrap/Program.cs index 92e501a..b10ecb2 100644 --- a/SVSim.Bootstrap/Program.cs +++ b/SVSim.Bootstrap/Program.cs @@ -124,6 +124,7 @@ public static class Program await buildDeck.ImportSeriesAsync(context, opts.ReferenceDataDir); await buildDeck.ImportCatalogAsync(context, opts.SeedDir); await buildDeck.ImportPackageAsync(context, opts.ReferenceDataDir); + await new StoryDeckImporter().ImportAsync(context, opts.SeedDir); } else { diff --git a/SVSim.UnitTests/Importers/StoryDeckImporterTests.cs b/SVSim.UnitTests/Importers/StoryDeckImporterTests.cs new file mode 100644 index 0000000..d996686 --- /dev/null +++ b/SVSim.UnitTests/Importers/StoryDeckImporterTests.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Bootstrap.Importers; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Importers; + +public class StoryDeckImporterTests +{ + private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds"); + + [Test] + public async Task Imports_story_decks_from_seed_file() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + await new StoryDeckImporter().ImportAsync(db, SeedDir); + + var decks = await db.StoryDecks.OrderBy(d => d.Id).ToListAsync(); + Assert.That(decks.Count, Is.EqualTo(112), "53 build + 59 trial"); + Assert.That(decks.Count(d => d.Kind == StoryDeckKind.Build), Is.EqualTo(53)); + Assert.That(decks.Count(d => d.Kind == StoryDeckKind.Trial), Is.EqualTo(59)); + var pureDevotion = decks.Single(d => d.DeckNo == 701); + Assert.That(pureDevotion.Kind, Is.EqualTo(StoryDeckKind.Build)); + Assert.That(pureDevotion.ClassId, Is.EqualTo(1)); + Assert.That(pureDevotion.DeckName, Is.EqualTo("Pure Devotion")); + Assert.That(pureDevotion.DeckFormat, Is.Null); + Assert.That(decks.Where(d => d.Kind == StoryDeckKind.Trial).All(d => d.DeckFormat != null), Is.True); + } + + [Test] + public async Task Is_idempotent_on_rerun() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + await new StoryDeckImporter().ImportAsync(db, SeedDir); + int before = await db.StoryDecks.CountAsync(); + await new StoryDeckImporter().ImportAsync(db, SeedDir); + int after = await db.StoryDecks.CountAsync(); + + Assert.That(after, Is.EqualTo(before)); + } +}