getMethod($method); return $target->invokeArgs(null, $args); } final class InvoicingPeriodInvoiceStateFakeResult { public int $num_rows; public function __construct(private array $rows) { $this->num_rows = count($rows); } public function fetch_assoc(): ?array { return array_shift($this->rows); } } final class InvoicingPeriodInvoiceStateFakeDb { public int $queryCount = 0; public string $sql = ''; public function __construct(private array $rows) { } public function query(string $sql): InvoicingPeriodInvoiceStateFakeResult { $this->queryCount++; $this->sql = $sql; return new InvoicingPeriodInvoiceStateFakeResult($this->rows); } } it('applies canonical invoice collection state precedence', function (): void { expect(invoicing_period_invoice_state_invoke('periodInvoiceCollectionState', [[ 'booked_invoice_id' => 9001, 'processor' => 1, 'external_id' => 'draft-1', 'closed_at' => '2026-06-30 12:00:00', ]]))->toBe('economic_booked'); expect(invoicing_period_invoice_state_invoke('periodInvoiceCollectionState', [[ 'booked_invoice_id' => null, 'processor' => 1, 'external_id' => 'draft-2', 'error_message' => null, 'closed_at' => '2026-06-30 12:00:00', ]]))->toBe('economic_draft'); expect(invoicing_period_invoice_state_invoke('periodInvoiceCollectionState', [[ 'processor' => 2, 'external_id' => 'stripe-id', 'closed_at' => '2026-06-30 12:00:00', ]]))->toBe('closed'); expect(invoicing_period_invoice_state_invoke('periodInvoiceCollectionState', [[]]))->toBe('open'); }); it('adds transaction states and collection summaries without losing logical orders', function (): void { $customer = [ 'customer_number' => 42424242, 'transactions' => [ ['id' => 11, 'amount' => 100.5, 'invoice_collection_id' => 501, 'booked' => false], ['id' => 12, 'amount' => 50.0, 'invoice_collection_id' => 501, 'booked' => false], ['id' => 13, 'amount' => 25.0, 'invoice_collection_id' => 502, 'booked' => false], ['id' => 14, 'amount' => 10.0, 'invoice_collection_id' => null, 'booked' => false, 'completed_at' => '2026-06-30 10:00:00'], ], 'queue' => ['invoice_collection_ids' => []], 'draft' => ['invoice_collection_ids' => [503]], ]; $metadata = [ 501 => ['id' => 501, 'invoice_collection_id' => 501, 'customer_number' => 42424242, 'state' => 'economic_booked'], 502 => ['id' => 502, 'invoice_collection_id' => 502, 'customer_number' => 42424242, 'state' => 'closed'], 503 => ['id' => 503, 'invoice_collection_id' => 503, 'customer_number' => 42424242, 'state' => 'economic_draft'], ]; $result = invoicing_period_invoice_state_invoke('applyPeriodInvoiceStateOverlayToCustomer', [$customer, $metadata]); expect(array_column($result['transactions'], 'invoice_state'))->toBe([ 'economic_booked', 'economic_booked', 'closed', 'closed', ]); expect(array_column($result['transactions'], 'booked'))->toBe([true, true, false, false]); expect($result['invoice_collections'])->toHaveCount(3); expect($result['invoice_collections'][0])->toMatchArray([ 'id' => 501, 'order_ids' => [11, 12], 'order_count' => 2, 'total_net_amount' => 150.5, ]); expect($result['invoice_collections'][2])->toMatchArray([ 'id' => 503, 'order_ids' => [], 'order_count' => 0, 'total_net_amount' => 0.0, ]); }); it('loads all referenced invoice collection metadata in one local query', function (): void { $hadDb = array_key_exists('db', $GLOBALS); $originalDb = $GLOBALS['db'] ?? null; $fakeDb = new InvoicingPeriodInvoiceStateFakeDb([ [ 'id' => '501', 'customer_number' => '42424242', 'processor' => '1', 'external_id' => 'draft-501', 'booked_invoice_id' => null, 'error_message' => null, 'closed_at' => '2026-06-30 12:00:00', ], [ 'id' => '502', 'customer_number' => '42424242', 'processor' => '1', 'external_id' => 'draft-502', 'booked_invoice_id' => '7002', 'error_message' => null, 'closed_at' => '2026-06-30 12:00:00', ], ]); $GLOBALS['db'] = $fakeDb; try { $metadata = invoicing_period_invoice_state_invoke('getPeriodInvoiceCollectionMetadata', [[ 'all' => [[ 'customer_number' => 42424242, 'transactions' => [ ['invoice_collection_id' => 501], ['invoice_collection_id' => 501], ], 'queue' => ['invoice_collection_ids' => [502]], 'draft' => ['invoice_collection_ids' => []], ]], 'fixed_pricing' => [[ 'customer_number' => 42424242, 'transactions' => [['invoice_collection_id' => 502]], ]], ]]); } finally { if ($hadDb) { $GLOBALS['db'] = $originalDb; } else { unset($GLOBALS['db']); } } expect($fakeDb->queryCount)->toBe(1); expect($fakeDb->sql)->toContain('WHERE id IN (501,502)'); expect($metadata[501]['state'])->toBe('economic_draft'); expect($metadata[502]['state'])->toBe('economic_booked'); });