not->toBeFalse(); expect($content)->toMatch( "/\\/superuser\\/invoicing\\/period\\/distribution\\/all'.*?\\\$this->requirePermission\\('superuser_invoicing_period'\\);/s" ); }); it('uses shared date-range normalization across invoicing period endpoints', function (): void { $routeFile = app_path('routes/InvoicingPeriodRoute.php'); $content = file_get_contents($routeFile); expect($content)->not->toBeFalse(); expect(substr_count((string)$content, 'requireAndNormalizeDateRange()'))->toBeGreaterThanOrEqual(5); }); it('keeps the main period response local-only for booked state and customer names', function (): void { $routeFile = app_path('routes/InvoicingPeriodRoute.php'); $content = file_get_contents($routeFile); expect($content)->not->toBeFalse(); $content = (string)$content; expect($content)->not->toContain('isBooked(true)') ->and($content)->toContain('isTransactionBookedFromLocalState($transaction)') ->and($content)->toContain('SELECT booked_invoice_id FROM collected_order_invoices') ->and($content)->toContain('SELECT invoice_id FROM economic_module_orders') ->and($content)->toContain('getCustomerNames(array_keys($customer_numbers), false)') ->and($content)->toContain('getCustomerNames(array_map(\'intval\', $customer_numbers), false)'); }); it('streams the main period response instead of encoding the full payload at once', function (): void { $routeFile = app_path('routes/InvoicingPeriodRoute.php'); $content = file_get_contents($routeFile); expect($content)->not->toBeFalse(); $content = (string)$content; expect($content)->toContain('private static function streamInvoicingPeriodResponse(array $period): void') ->and($content)->toContain("\$includeInvoicePeriodFlags = \$this->hasPermission('list_invoice_period_flags');") ->and($content)->toContain('$period = self::getInvoicingPeriod($dateFrom, $dateTo, $customerNumbers, $includeInvoicePeriodFlags);') ->and($content)->toContain('self::streamInvoicingPeriodResponse($period);') ->and($content)->not->toContain('$response->success([' . PHP_EOL . ' ...self::getInvoicingPeriod($dateFrom, $dateTo, $customerNumbers)') ->and($content)->toContain('echo self::jsonFragment($customer);'); }); it('only includes invoice period flags when the list permission is granted', function (): void { $content = file_get_contents(app_path('routes/InvoicingPeriodRoute.php')); $flagService = (string)file_get_contents(app_path('classes/invoice_period_flag_service.php')); expect($content)->not->toBeFalse(); $content = (string)$content; expect($content) ->toContain("\$includeInvoicePeriodFlags = \$this->hasPermission('list_invoice_period_flags');") ->and($content)->toContain('bool $includeInvoicePeriodFlags = false') ->and($content)->toContain('if ($includeInvoicePeriodFlags) {') ->and($content)->toContain('applyFlagsToPeriodTypes(') ->and($content)->toContain('applyManualFlagCountsToPeriodTypes(') ->and($flagService)->toContain("unset(\$types[\$typeName][\$index]['flags']);"); }); it('advertises a server-gated selected-customer tree and fails the dedicated route closed', function (): void { $route = (string)file_get_contents(app_path('routes/InvoicingPeriodRoute.php')); expect($route) ->toContain('INVOICING_PERIOD_OBJECT_TREE_V2') ->toContain("module = 'InvoicingPeriod'") ->toContain("'object_tree_v2_enabled'") ->toContain("'object_tree_v2_superuser_allowlist'") ->not->toContain("includeTreeSnapshot") ->toContain("\$this->get('/superuser/invoicing/period/tree'") ->toContain("Invoice-period object tree is not enabled.") ->toContain('$response->error(\'Invoice-period object tree is not enabled.\', 403);') ->toContain('buildInvoicePeriodTreeSnapshot(') ->toContain("'complete_order_count'") ->toContain("'period_total_net_amount'") ->toContain("'date_from' => substr(\$dateFrom, 0, 10)") ->toContain("'date_to' => substr(\$dateTo, 0, 10)") ->toContain("\$period['capabilities']['object_tree_v2'] = \$enabled;"); }); it('maps batched period transaction rows to the legacy transaction response shape', function (): void { $reflection = new ReflectionClass(InvoicingPeriodRoute::class); $method = $reflection->getMethod('constructTransactionObjectFromPeriodRow'); $transaction = $method->invokeArgs(null, [[ 'id' => '42', 'created_at' => '2026-04-10 12:34:56', 'net_amount' => '123.50', 'booked' => '1', 'department_id' => '7', 'customer_id' => '27983', 'order_reference' => 'REF-42', 'order_po' => 'PO-42', 'order_notes' => 'Driver note', 'reg_1' => 'AB12345', 'reg_2' => 'CD67890', 'reg_3' => '', 'invoice_collection_id' => '314', 'include_in_invoice_effective' => '0', ]]); expect($transaction)->toMatchArray([ 'id' => 42, 'date' => '2026-04-10 12:34:56', 'amount' => 123.5, 'booked' => true, 'department_id' => 7, 'customer_number' => 27983, 'reference' => 'REF-42', 'po' => 'PO-42', 'notes' => 'Driver note', 'reg_1' => 'AB12345', 'reg_2' => 'CD67890', 'reg_3' => '', 'excluded' => true, 'invoice_collection_id' => 314, 'queue_status' => null, 'queue_job_id' => null, ]); }); it('uses batched period transactions and keyed customer maps in the main period route', function (): void { $content = file_get_contents(app_path('routes/InvoicingPeriodRoute.php')); expect($content)->not->toBeFalse(); $content = (string)$content; expect($content)->toContain('getPeriodTransactionsForCustomersInDateRange(') ->and($content)->toContain('private static function indexCustomersByNumber(array $customers): array') ->and($content)->toContain('$customers_by_number = self::indexCustomersByNumber($customersWithTransactions);') ->and($content)->toContain('invoicing_period_utils::filterPossibleDuplicates($ordersByRegistration, 86400)') ->and($content)->not->toContain('getOrdersWithPossibleDuplicates($dateFrom, $dateTo)'); }); it('keeps period search aware of all separate order registration columns', function (): void { $content = file_get_contents(app_path('routes/InvoicingPeriodRoute.php')); expect($content)->not->toBeFalse(); $content = (string)$content; expect($content)->toContain("foreach (['id', 'reference', 'po', 'notes', 'reg_1', 'reg_2', 'reg_3'] as \$field)") ->and($content)->toContain("\$values[] = \$transaction[\$field] ?? '';"); }); it('falls back to configured e-conomic default department for missing customer default department in distributions', function (): void { $content = file_get_contents(app_path('routes/InvoicingPeriodRoute.php')); expect($content)->not->toBeFalse(); $content = (string)$content; expect($content) ->toContain('private static function getEconomicFallbackDepartmentId(): int') ->and($content)->toContain('(new economic())->getDefaultDistributionDepartmentId()') ->and($content)->toContain(': self::getEconomicFallbackDepartmentId();') ->and($content)->toContain("\$department_totals[\$fallback_department_id] = (float)\$customer['meta']['fixed_pricing']['price'];"); });