From c303d3040d72bd8963e3153a98da89f57f7dae3b Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 27 May 2026 00:05:48 -0400 Subject: [PATCH] fix(bp): convert seed JST dates to UTC for Postgres timestamp-with-tz MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Npgsql rejects DateTimeOffset writes to timestamp-with-tz unless offset is zero. Caught by manual bootstrap against a real Postgres DB; SQLite test provider didn't enforce this. Converting to UTC post-parse is semantically lossless — DateTimeOffset comparisons are instant-based. Co-Authored-By: Claude Opus 4.7 (1M context) --- SVSim.Bootstrap/Importers/BattlePassSeasonImporter.cs | 7 +++++-- SVSim.UnitTests/Importers/BattlePassSeasonImporterTests.cs | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/SVSim.Bootstrap/Importers/BattlePassSeasonImporter.cs b/SVSim.Bootstrap/Importers/BattlePassSeasonImporter.cs index 0f8b9bc..6383949 100644 --- a/SVSim.Bootstrap/Importers/BattlePassSeasonImporter.cs +++ b/SVSim.Bootstrap/Importers/BattlePassSeasonImporter.cs @@ -29,8 +29,11 @@ public class BattlePassSeasonImporter var entry = existing.TryGetValue(s.Id, out var ex) ? ex : new BattlePassSeasonEntry { Id = s.Id }; entry.Name = s.Name; entry.MaxLevel = s.MaxLevel; - entry.StartDate = DateTimeOffset.Parse(s.StartDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); - entry.EndDate = DateTimeOffset.Parse(s.EndDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); + // Postgres 'timestamp with time zone' only accepts UTC offset; JST-offset values + // from the seed are converted to UTC to preserve the instant. Comparisons via + // DateTimeOffset are instant-based, so the JST→UTC conversion is semantically lossless. + entry.StartDate = DateTimeOffset.Parse(s.StartDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToUniversalTime(); + entry.EndDate = DateTimeOffset.Parse(s.EndDate, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToUniversalTime(); entry.CanPurchase = s.CanPurchase; entry.PriceCrystal = s.PriceCrystal; entry.Description = s.Description; diff --git a/SVSim.UnitTests/Importers/BattlePassSeasonImporterTests.cs b/SVSim.UnitTests/Importers/BattlePassSeasonImporterTests.cs index 0ab0f19..9489134 100644 --- a/SVSim.UnitTests/Importers/BattlePassSeasonImporterTests.cs +++ b/SVSim.UnitTests/Importers/BattlePassSeasonImporterTests.cs @@ -24,8 +24,10 @@ public class BattlePassSeasonImporterTests Assert.That(season.MaxLevel, Is.EqualTo(100)); Assert.That(season.CanPurchase, Is.True); Assert.That(season.PriceCrystal, Is.EqualTo(980)); - Assert.That(season.StartDate.Offset, Is.EqualTo(TimeSpan.FromHours(9)), - "JST offset (+09:00) must round-trip through DateTimeOffset"); + // JST-offset seed is converted to UTC for Postgres 'timestamp with time zone' compatibility. + // Semantically lossless — the instant 2026-04-01T02:00+09:00 == 2026-03-31T17:00 UTC. + Assert.That(season.StartDate.Offset, Is.EqualTo(TimeSpan.Zero)); + Assert.That(season.StartDate.UtcDateTime, Is.EqualTo(new DateTime(2026, 3, 31, 17, 0, 0, DateTimeKind.Utc))); } [Test]