refactor(bootstrap): finalize load-index migration; GlobalsImporter is now a stub

Stage 9C of the bootstrap-seed-refactor:

- Add 6 seed DTOs for the card-id-keyed load-index tables (SpotCard,
  ReprintedCard, UnlimitedRestriction, LoadingExclusionCard, MaintenanceCard,
  FeatureMaintenance).
- Add CardListsImporter: idempotent upsert of the 6 tables, sharing one
  Cards FK set for orphan-warning. FeatureMaintenances clear-and-rewrites
  (synthetic ordinal Id; no natural key).
- Add RotationFlagUpdater: reads RotationConfig.RotationCardSetIds from the
  GameConfigs section (populated by RotationConfigImporter) and flips
  CardSet.IsInRotation to match.
- Add RotationConfig.RotationCardSetIds list property + wire it through
  RotationConfigImporter. No migration needed (sections are JSON blobs).
- RotationConfigImporter: use legacy local-kind DateTime parse for schedule
  windows so the JSON round-trip stays byte-equivalent to GlobalsImporter.
- Strip GlobalsImporter down to a no-op stub (Task 10 will delete it).
- Wire all 9 new importers into Program.cs and SVSimTestFactory.SeedGlobalsAsync,
  in the order RotationConfigImporter -> ... -> CardListsImporter -> RotationFlagUpdater.
- Delete prod-captures/load-index-2026-05-23.json.
- Add CardListsImporterTests covering each sub-table, idempotency,
  empty-seed handling, orphan-warning, and the clear-and-rewrite path.

Tests: 391 passing (382 baseline + 9 new).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-26 15:46:36 -04:00
parent 87d0001569
commit d14a0be2c8
15 changed files with 550 additions and 15057 deletions

View File

@@ -30,6 +30,7 @@ public class RotationConfigImporter
c.IsBattlePassPeriod = rot.IsBattlePassPeriod;
c.IsBeginnerMission = rot.IsBeginnerMission;
c.CardSetIdForResourceDlView = rot.CardSetIdForResourceDlView;
c.RotationCardSetIds = rot.RotationCardSetIds ?? new List<int>();
});
touched++;
}
@@ -48,10 +49,15 @@ public class RotationConfigImporter
var schedule = SeedLoader.LoadObject<MyRotationScheduleSeed>(Path.Combine(seedDir, "my-rotation-schedule.json"));
if (schedule?.Gathering is not null && schedule.FreeBattle is not null)
{
var gBegin = ParseWireDateTime(schedule.Gathering.Begin);
var gEnd = ParseWireDateTime(schedule.Gathering.End);
var fBegin = ParseWireDateTime(schedule.FreeBattle.Begin);
var fEnd = ParseWireDateTime(schedule.FreeBattle.End);
// Schedule windows are intentionally parsed WITHOUT AssumeUniversal because the seed
// strings ("2024-05-01 20:00:00") are timezone-less and the rest of the pipeline (the
// [ConfigSection] JSON round-trip + LoadController's wire mapping) treats them as
// local-kind ticks. Mirrors the legacy GlobalsImporter.TryParseScheduleWindow behavior
// — see GlobalsRepositoryTests for the round-trip assertion.
var gBegin = ParseScheduleWireDateTime(schedule.Gathering.Begin);
var gEnd = ParseScheduleWireDateTime(schedule.Gathering.End);
var fBegin = ParseScheduleWireDateTime(schedule.FreeBattle.Begin);
var fEnd = ParseScheduleWireDateTime(schedule.FreeBattle.End);
// Only commit when both windows parsed to real DateTimes — a malformed/0001 value
// would silently lock the MyRotation feature off (the original bug the section fixed).
if (gBegin != DateTime.MinValue && gEnd != DateTime.MinValue
@@ -75,6 +81,15 @@ public class RotationConfigImporter
return touched;
}
// Legacy schedule-window parse: default styles (AssumeLocal), matching the original
// GlobalsImporter.TryParseScheduleWindow. The schedule strings are timezone-less; preserving
// legacy local-kind ticks keeps the wire output byte-equivalent across the migration.
private static DateTime ParseScheduleWireDateTime(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return DateTime.MinValue;
return DateTime.TryParse(s, out var dt) ? dt : DateTime.MinValue;
}
// Verbatim copy of GlobalsImporter.UpsertSection<T>. Kept private-static here so this
// importer can stand alone after Stage 9C strips the GlobalsImporter copy.
private static async Task UpsertSection<T>(SVSimDbContext context, Func<T> shippedDefaults, Action<T> mutate)