diff --git a/services/nginx/app/classes/invoice_period_flag_service.php b/services/nginx/app/classes/invoice_period_flag_service.php index 8d5b7e58..bb8e0a56 100644 --- a/services/nginx/app/classes/invoice_period_flag_service.php +++ b/services/nginx/app/classes/invoice_period_flag_service.php @@ -320,6 +320,16 @@ class invoice_period_flag_service } public function warmManualFlagsCache(): void + { + $flags = $this->fetchActiveManualFlagsFromDb(); + + try { + (new redis())->cache_invoice_period_manual_flags($flags); + } catch (Throwable) { + } + } + + private function fetchActiveManualFlagsFromDb(): array { global $db; @@ -337,10 +347,7 @@ class invoice_period_flag_service } } - try { - (new redis())->cache_invoice_period_manual_flags($flags); - } catch (Throwable) { - } + return $flags; } private function formatStoredFlag(array $row): array @@ -388,15 +395,11 @@ class invoice_period_flag_service } if (!is_array($flags)) { - // Cache miss — warm on demand and re-fetch - $this->warmManualFlagsCache(); + // Cache miss — read from the database and refresh Redis without hiding active flags. + $flags = $this->fetchActiveManualFlagsFromDb(); try { - $flags = (new redis())->get_invoice_period_manual_flags(); + (new redis())->cache_invoice_period_manual_flags($flags); } catch (Throwable) { - return []; - } - if (!is_array($flags)) { - return []; } } @@ -529,7 +532,7 @@ class invoice_period_flag_service } if (!is_array($flags)) { - $flags = $this->buildAutomaticFlagsForPeriod($dateFrom, $dateTo); + $flags = $this->calculateAutomaticFlagsForPeriod($dateFrom, $dateTo); $this->cacheAutomaticFlagsForPeriod($dateFrom, $dateTo, $flags); } @@ -545,15 +548,17 @@ class invoice_period_flag_service public function warmAutomaticFlagsForPeriod(string $dateFrom, string $dateTo): void { + [$dateFrom, $dateTo] = $this->normalizePeriodDateRange($dateFrom, $dateTo); $this->cacheAutomaticFlagsForPeriod( $dateFrom, $dateTo, - $this->buildAutomaticFlagsForPeriod($dateFrom, $dateTo) + $this->calculateAutomaticFlagsForPeriod($dateFrom, $dateTo) ); } - private function buildAutomaticFlagsForPeriod(string $dateFrom, string $dateTo): array + private function calculateAutomaticFlagsForPeriod(string $dateFrom, string $dateTo): array { + [$dateFrom, $dateTo] = $this->normalizePeriodDateRange($dateFrom, $dateTo); $rows = $this->getPeriodOrderItemRows($dateFrom, $dateTo, null); $attributes = $this->getCustomerAttributes(null); @@ -611,6 +616,7 @@ class invoice_period_flag_service private function getPeriodOrderItemRows(string $dateFrom, string $dateTo, ?array $onlyCustomerNumbers): array { + [$dateFrom, $dateTo] = $this->normalizePeriodDateRange($dateFrom, $dateTo); try { $rows = (new redis())->get_invoice_period_order_item_rows($dateFrom, $dateTo); } catch (Throwable) { @@ -639,6 +645,7 @@ class invoice_period_flag_service public function warmOrderItemRowsForPeriod(string $dateFrom, string $dateTo): void { + [$dateFrom, $dateTo] = $this->normalizePeriodDateRange($dateFrom, $dateTo); $rows = $this->fetchOrderItemRowsFromDb($dateFrom, $dateTo); try { (new redis())->cache_invoice_period_order_item_rows($dateFrom, $dateTo, $rows); @@ -646,6 +653,24 @@ class invoice_period_flag_service } } + private function normalizePeriodDateRange(string $dateFrom, string $dateTo): array + { + return [ + $this->normalizePeriodDate($dateFrom, true), + $this->normalizePeriodDate($dateTo, false), + ]; + } + + private function normalizePeriodDate(string $date, bool $startOfDay): string + { + $timestamp = strtotime($date); + if ($timestamp === false) { + return $date; + } + + return date($startOfDay ? 'Y-m-d 00:00:00' : 'Y-m-d 23:59:59', $timestamp); + } + private function fetchOrderItemRowsFromDb(string $dateFrom, string $dateTo): array { global $db; diff --git a/services/nginx/app/classes/redis.php b/services/nginx/app/classes/redis.php index 76c0c96a..709d88b1 100644 --- a/services/nginx/app/classes/redis.php +++ b/services/nginx/app/classes/redis.php @@ -392,7 +392,19 @@ class redis implements redis_i private function invoicePeriodCacheKey(string $prefix, string $dateFrom, string $dateTo): string { - return $prefix . ':' . $dateFrom . ':' . $dateTo; + return $prefix . ':' + . $this->normalizeInvoicePeriodCacheDate($dateFrom, true) . ':' + . $this->normalizeInvoicePeriodCacheDate($dateTo, false); + } + + private function normalizeInvoicePeriodCacheDate(string $date, bool $startOfDay): string + { + $timestamp = strtotime($date); + if ($timestamp === false) { + return $date; + } + + return date($startOfDay ? 'Y-m-d 00:00:00' : 'Y-m-d 23:59:59', $timestamp); } private function workfeedEmployeeNameCacheKey(string $employeeId): string @@ -512,7 +524,11 @@ class redis implements redis_i */ public function enqueue_invoice_period_warming(string $dateFrom, string $dateTo): self { - $this->get_client()->sadd('invoice_period_warming_queue', [$dateFrom . '|' . $dateTo]); + $this->get_client()->sadd('invoice_period_warming_queue', [ + $this->normalizeInvoicePeriodCacheDate($dateFrom, true) + . '|' + . $this->normalizeInvoicePeriodCacheDate($dateTo, false), + ]); return $this; } diff --git a/services/nginx/app/tests/Unit/Invoicing/InvoicePeriodFlagServiceTest.php b/services/nginx/app/tests/Unit/Invoicing/InvoicePeriodFlagServiceTest.php index 4afefd5c..5c4330c9 100644 --- a/services/nginx/app/tests/Unit/Invoicing/InvoicePeriodFlagServiceTest.php +++ b/services/nginx/app/tests/Unit/Invoicing/InvoicePeriodFlagServiceTest.php @@ -21,6 +21,31 @@ function invoice_period_flag_service_invoke(string $method, array $args = []): m return $target->invokeArgs($service, $args); } +it('normalizes invoice period date ranges to full-day timestamps before cache-backed reads', function (): void { + expect(invoice_period_flag_service_invoke('normalizePeriodDateRange', ['2025-03-01', '2025-03-31'])) + ->toBe(['2025-03-01 00:00:00', '2025-03-31 23:59:59']); + + expect(invoice_period_flag_service_invoke('normalizePeriodDateRange', [ + '2025-03-01 00:00:00', + '2025-03-31 23:59:59', + ]))->toBe(['2025-03-01 00:00:00', '2025-03-31 23:59:59']); +}); + +it('canonicalizes invoice period redis keys for bare dates and normalized API timestamps', function (): void { + $reflection = new ReflectionClass(\classes\redis::class); + $redis = $reflection->newInstanceWithoutConstructor(); + $key = $reflection->getMethod('invoicePeriodCacheKey'); + $key->setAccessible(true); + + expect($key->invoke($redis, 'invoice_period_automatic_flags', '2025-03-01', '2025-03-31')) + ->toBe($key->invoke( + $redis, + 'invoice_period_automatic_flags', + '2025-03-01 00:00:00', + '2025-03-31 23:59:59' + )); +}); + it('builds deterministic automatic flag fingerprints and interactive price message parts', function (): void { $row = [ 'customer_number' => 424242,