diff --git a/Packages/Persistence/Sources/Internal/Migrations/CreateCommunity.swift b/Packages/Persistence/Sources/Internal/Migrations/CreateCommunity.swift index 66ddd40..b805f40 100644 --- a/Packages/Persistence/Sources/Internal/Migrations/CreateCommunity.swift +++ b/Packages/Persistence/Sources/Internal/Migrations/CreateCommunity.swift @@ -29,7 +29,7 @@ struct CreateCommunity: AsyncMigration { .unique(on: "user_id", "series_slug", "issue") .create() - for (event, shops) in Seed.events { + for (event, shops) in Seed.events(after: .init()) { let record = EventRecord(event) try await record.save(on: database) diff --git a/Packages/Persistence/Sources/Internal/Migrations/RedateEvents.swift b/Packages/Persistence/Sources/Internal/Migrations/RedateEvents.swift new file mode 100644 index 0000000..9986bf7 --- /dev/null +++ b/Packages/Persistence/Sources/Internal/Migrations/RedateEvents.swift @@ -0,0 +1,21 @@ +import FluentKit +import Foundation + +/// Re-dates the demo cons from the first weekend after it runs, for databases seeded when ``CreateCommunity`` used fixed dates. +struct RedateEvents: AsyncMigration { + + // MARK: Methods + + func prepare(on database: Database) async throws { + for (event, _) in Seed.events(after: .init()) { + try await EventRecord.query(on: database) + .filter(\.$slug == event.slug) + .set(\.$startsOn, to: event.startsOn) + .set(\.$endsOn, to: event.endsOn) + .update() + } + } + + func revert(on database: Database) async throws {} + +} diff --git a/Packages/Persistence/Sources/Internal/Seeds/Seed.swift b/Packages/Persistence/Sources/Internal/Seeds/Seed.swift index 05de58a..15f88b2 100644 --- a/Packages/Persistence/Sources/Internal/Seeds/Seed.swift +++ b/Packages/Persistence/Sources/Internal/Seeds/Seed.swift @@ -158,29 +158,36 @@ enum Seed { ), ] - /// The demo cons, soonest first, each with the slugs of the shops trading there. - static let events: [(event: Event, shops: [String])] = [ - (.init(slug: "dutch-comic-con", name: "Dutch Comic Con", venue: "Jaarbeurs, Utrecht", startsOn: day(2026, 9, 26), endsOn: day(2026, 9, 27)), - ["kestrel-row-comics", "silverfish-comics", "third-panel-supply"]), - (.init(slug: "stripdagen-haarlem", name: "Stripdagen Haarlem", venue: "Grote Markt, Haarlem", startsOn: day(2026, 10, 3), endsOn: day(2026, 10, 4)), - ["silverfish-comics"]), - (.init(slug: "comic-con-amsterdam", name: "Comic Con Amsterdam", venue: "RAI, Amsterdam", startsOn: day(2026, 11, 21), endsOn: day(2026, 11, 22)), - ["kestrel-row-comics", "inkwell-and-staple"]), - (.init(slug: "comic-con-brussels", name: "Comic Con Brussels", venue: "Tour & Taxis, Brussels", startsOn: day(2026, 12, 5), endsOn: day(2026, 12, 6)), - ["third-panel-supply"]), - ] + /// The demo cons, soonest first, each with the slugs of the shops trading there, dated from the first weekend after `date`. + static func events( + after date: Date + ) -> [(event: Event, shops: [String])] { + [ + ("dutch-comic-con", "Dutch Comic Con", "Jaarbeurs, Utrecht", 0, ["kestrel-row-comics", "silverfish-comics", "third-panel-supply"]), + ("stripdagen-haarlem", "Stripdagen Haarlem", "Grote Markt, Haarlem", 1, ["silverfish-comics"]), + ("comic-con-amsterdam", "Comic Con Amsterdam", "RAI, Amsterdam", 8, ["kestrel-row-comics", "inkwell-and-staple"]), + ("comic-con-brussels", "Comic Con Brussels", "Tour & Taxis, Brussels", 10, ["third-panel-supply"]), + ] + .map { slug, name, venue, week, shops in + let (startsOn, endsOn) = weekend(week, after: date) - /// Noon on a day in Amsterdam, where the demo cons are dated. - static func day( - _ year: Int, - _ month: Int, - _ day: Int - ) -> Date { + return (.init(slug: slug, name: name, venue: venue, startsOn: startsOn, endsOn: endsOn), shops) + } + } + + /// Noon on the Saturday and Sunday `week` weeks on from the first weekend after `date`, in Amsterdam, where the demo cons are held. + static func weekend( + _ week: Int, + after date: Date + ) -> (startsOn: Date, endsOn: Date) { var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(identifier: "Europe/Amsterdam") ?? .gmt - return calendar.date(from: .init(year: year, month: month, day: day, hour: 12)) ?? .distantPast + let saturday = calendar.nextDate(after: date, matching: .init(hour: 12, weekday: 7), matchingPolicy: .nextTime) ?? date + let startsOn = calendar.date(byAdding: .weekOfYear, value: week, to: saturday) ?? saturday + + return (startsOn, calendar.date(byAdding: .day, value: 1, to: startsOn) ?? startsOn) } /// The runs the demo listings belong to. diff --git a/Packages/Persistence/Sources/Public/Methods/PrepareDB.swift b/Packages/Persistence/Sources/Public/Methods/PrepareDB.swift index 050c0ec..b54cebb 100644 --- a/Packages/Persistence/Sources/Public/Methods/PrepareDB.swift +++ b/Packages/Persistence/Sources/Public/Methods/PrepareDB.swift @@ -33,6 +33,7 @@ public struct PrepareDB: Sendable { CreatePasskey(), CreateCollecting(), CreateCommunity(), + RedateEvents(), ]) } diff --git a/Packages/Persistence/Tests/Cases/Public/Repositories/CommunityRepositoryTests.swift b/Packages/Persistence/Tests/Cases/Public/Repositories/CommunityRepositoryTests.swift index d5363da..8c772be 100644 --- a/Packages/Persistence/Tests/Cases/Public/Repositories/CommunityRepositoryTests.swift +++ b/Packages/Persistence/Tests/Cases/Public/Repositories/CommunityRepositoryTests.swift @@ -46,13 +46,13 @@ struct CommunityRepositoryTests { let clock = Clock() let collecting = CollectingRepository(fluent: fluent, now: clock.now) - // 22 September 2026: every demo con is still to come. - clock.advance(by: Seed.day(2026, 9, 22).timeIntervalSince(clock.now())) + // The present, which every demo con is seeded after. + clock.advance(by: Date().timeIntervalSince(clock.now())) let all = try await collecting.upcomingEvents() - // Sunday 27 September, the con's last day: still on. Two weeks later it is over, and so is Stripdagen. - clock.advance(by: 5 * 86_400) + // The first con's last day: still on. Two weeks later it is over, and so is Stripdagen. + clock.advance(by: try #require(all.first).endsOn.timeIntervalSince(clock.now())) let lastDay = try await collecting.upcomingEvents() @@ -67,17 +67,35 @@ struct CommunityRepositoryTests { } } + @Test + func `dates the demo cons on weekends, from the first one after seeding`() { + var calendar = Calendar(identifier: .gregorian) + + calendar.timeZone = TimeZone(identifier: "Europe/Amsterdam") ?? .gmt + + // Monday 21 September 2026: the dates the prototype first shipped with. + let days = Seed.events(after: Date(timeIntervalSince1970: 1_790_000_000)).map { entry in + [entry.event.startsOn, entry.event.endsOn] + .map { calendar.dateComponents([.month, .day, .hour], from: $0) } + .map { "\($0.month ?? 0)/\($0.day ?? 0) \($0.hour ?? 0):00" } + } + + #expect(days == [ + ["9/26 12:00", "9/27 12:00"], + ["10/3 12:00", "10/4 12:00"], + ["11/21 12:00", "11/22 12:00"], + ["12/5 12:00", "12/6 12:00"], + ]) + } + @Test func `lets a seller pick the events their shop trades at`() async throws { try await withDatabase { fluent in - let clock = Clock() - let collecting = CollectingRepository(fluent: fluent, now: clock.now) + let collecting = CollectingRepository(fluent: fluent) let accounts = AccountRepository(fluent: fluent) let joost = try await accounts.account(email: "joost@kestrelrow.example") let mara = try await accounts.account(email: "mara.v@example.com") - clock.advance(by: Seed.day(2026, 9, 22).timeIntervalSince(clock.now())) - try await collecting.setEvents(["stripdagen-haarlem", "comic-con-amsterdam", "no-such-event"], forShopOwnedBy: joost) // An account without a shop changes nothing. try await collecting.setEvents(["dutch-comic-con"], forShopOwnedBy: mara) diff --git a/Services/Website/Tests/Library/Cases/Public/Controllers/AccountAndCollectingControllerTests.swift b/Services/Website/Tests/Library/Cases/Public/Controllers/AccountAndCollectingControllerTests.swift index d272251..4ae7245 100644 --- a/Services/Website/Tests/Library/Cases/Public/Controllers/AccountAndCollectingControllerTests.swift +++ b/Services/Website/Tests/Library/Cases/Public/Controllers/AccountAndCollectingControllerTests.swift @@ -246,9 +246,8 @@ private extension AccountAndCollectingControllerTests { } } - /// Runs the body against the session, catalogue, account, and collecting routes, over a migrated, seeded in-memory database, judging events by `now`. + /// Runs the body against the session, catalogue, account, and collecting routes, over a migrated, seeded in-memory database. func withApp( - now: @escaping @Sendable () -> Date = { .init() }, _ body: @escaping @Sendable (any TestClientProtocol) async throws -> Void ) async throws { let fluent = try Service( @@ -266,7 +265,7 @@ private extension AccountAndCollectingControllerTests { let accounts = AccountRepository(fluent: fluent) let catalogue = CatalogueRepository(fluent: fluent) - let collecting = CollectingRepository(fluent: fluent, now: now) + let collecting = CollectingRepository(fluent: fluent) let sessions = SessionStore(trade: .init(fluent: fluent), collecting: collecting) let router = Router(context: WebsiteRequestContext.self) @@ -309,8 +308,7 @@ extension AccountAndCollectingControllerTests { @Test func `serves the events, and narrows a search to the copies to pick up at one`() async throws { - // 22 September 2026: every demo con is still to come. - try await withApp(now: { Date(timeIntervalSince1970: 1_790_071_200) }) { client in + try await withApp { client in try await client.execute(uri: "/events", method: .get) { response in let body = String(buffer: response.body)