assertFalse(WorkflowRunner::isOverdue(null, self::utc('2026-01-15 12:00:00'))); } public function testIsOverdueReturnsFalseForFutureDueDate(): void { $now = self::utc('2026-01-15 12:00:00'); $this->assertFalse(WorkflowRunner::isOverdue($now->modify('+1 hour'), $now)); $this->assertFalse(WorkflowRunner::isOverdue($now->modify('+3 days'), $now)); } public function testIsOverdueReturnsTrueForPastDueDate(): void { $now = self::utc('2026-01-15 12:00:00'); $this->assertTrue(WorkflowRunner::isOverdue($now->modify('-1 hour'), $now)); $this->assertTrue(WorkflowRunner::isOverdue($now->modify('-30 days'), $now)); } public function testIsOverdueReturnsTrueForADueTimeEarlierTheSameDay(): void { // Regression: comparing whole calendar days (as Deck's own // Card::getDaysUntilDue() does) reports a card due earlier today as // "0 days until due", not overdue, until the calendar date rolls // over - holding every card back for up to 24h after it actually // fell due. isOverdue() must compare the real timestamps instead. $now = self::utc('2026-01-15 12:00:00'); $this->assertTrue(WorkflowRunner::isOverdue(self::utc('2026-01-15 06:00:00'), $now)); } public function testIsOverdueReadsTheStoredWallClockAsUtc(): void { // What Entity hydration produces if PHP's default zone is not UTC: // the stored UTC wall clock "12:00", tagged with the local zone. $due = new DateTimeImmutable('2026-07-15 12:00:00', new DateTimeZone('Europe/Berlin')); // Taken at face value that would be 10:00 UTC and already overdue. $this->assertFalse(WorkflowRunner::isOverdue($due, self::utc('2026-07-15 11:30:00'))); $this->assertTrue(WorkflowRunner::isOverdue($due, self::utc('2026-07-15 12:00:01'))); } public function testIsOverdueComparesNowAsAnAbsoluteInstant(): void { $due = self::utc('2026-07-15 12:00:00'); $berlin = new DateTimeZone('Europe/Berlin'); // 13:30 in Berlin (CEST) is 11:30 UTC - not yet due. $this->assertFalse(WorkflowRunner::isOverdue($due, new DateTimeImmutable('2026-07-15 13:30:00', $berlin))); // 14:30 in Berlin is 12:30 UTC - due. $this->assertTrue(WorkflowRunner::isOverdue($due, new DateTimeImmutable('2026-07-15 14:30:00', $berlin))); } public function testIsOverdueIsFalseAtTheExactDueInstant(): void { $now = self::utc('2026-01-15 12:00:00'); $this->assertFalse(WorkflowRunner::isOverdue($now, $now)); } }