createMock(OrderRepository::class); } return new OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo); } public function testConstructorAcceptsOnlyOrderRepository(): void { $orderRepo = $this->createMock(OrderRepository::class); $service = new OrderAdminService($orderRepo); $this->assertInstanceOf(OrderAdminService::class, $service); } public function testConstructorAcceptsAllDependencies(): void { $orderRepo = $this->createMock(OrderRepository::class); $productRepo = $this->createMock(ProductRepository::class); $settingsRepo = $this->createMock(SettingsRepository::class); $transportRepo = $this->createMock(TransportRepository::class); $service = new OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo); $this->assertInstanceOf(OrderAdminService::class, $service); } public function testSearchProductsReturnsEmptyForEmptyQuery(): void { $productRepo = $this->createMock(ProductRepository::class); $productRepo->expects($this->never())->method('searchProductByNameAjax'); $service = $this->createService(null, $productRepo); $result = $service->searchProducts('', 'pl'); $this->assertSame([], $result); } public function testSearchProductsReturnsEmptyWithoutProductRepo(): void { $service = $this->createService(); $result = $service->searchProducts('test', 'pl'); $this->assertSame([], $result); } public function testSearchProductsReturnsFormattedResults(): void { $productRepo = $this->createMock(ProductRepository::class); $productRepo->method('searchProductByNameAjax') ->with('koszulka', 'pl') ->willReturn([ ['product_id' => 10], ['product_id' => 20], ]); $productRepo->method('findCached') ->willReturnCallback(function ($id) { if ($id === 10) { return [ 'language' => ['name' => 'Koszulka biała'], 'sku' => 'KB-001', 'ean' => '', 'price_brutto' => 49.99, 'price_brutto_promo' => 39.99, 'vat' => 23, 'quantity' => 15, 'parent_id' => 0, ]; } return null; // product 20 not found }); $productRepo->method('getProductImg') ->willReturn('/images/products/test.jpg'); $service = $this->createService(null, $productRepo); $results = $service->searchProducts('koszulka', 'pl'); $this->assertCount(1, $results); $this->assertSame(10, $results[0]['product_id']); $this->assertSame('Koszulka biała', $results[0]['name']); $this->assertSame('KB-001', $results[0]['sku']); $this->assertSame(49.99, $results[0]['price_brutto']); $this->assertSame(39.99, $results[0]['price_brutto_promo']); $this->assertSame(15, $results[0]['quantity']); } public function testSaveOrderProductsReturnsFalseForInvalidOrderId(): void { $service = $this->createService(); $this->assertFalse($service->saveOrderProducts(0, [])); } public function testSaveOrderProductsDeletesRemovedProducts(): void { $orderRepo = $this->createMock(OrderRepository::class); $productRepo = $this->createMock(ProductRepository::class); $settingsRepo = $this->createMock(SettingsRepository::class); $transportRepo = $this->createMock(TransportRepository::class); // Existing products $orderRepo->method('orderProducts') ->with(1) ->willReturn([ ['id' => 100, 'product_id' => 5, 'quantity' => 2, 'price_brutto' => 10, 'price_brutto_promo' => 0], ['id' => 101, 'product_id' => 6, 'quantity' => 1, 'price_brutto' => 20, 'price_brutto_promo' => 0], ]); // Product 100 is submitted with delete flag // Product 101 is not submitted at all (also deleted) $orderRepo->expects($this->exactly(2))->method('deleteOrderProduct'); $orderRepo->method('findRawById')->willReturn(['id' => 1, 'transport_id' => 1]); $transportRepo->method('findActiveById')->willReturn(null); // Stock should be returned for both deleted products $productRepo->method('getQuantity')->willReturn(10); $productRepo->expects($this->exactly(2))->method('updateQuantity'); $service = new OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo); $result = $service->saveOrderProducts(1, [ ['order_product_id' => 100, 'delete' => '1', 'quantity' => 2], ]); $this->assertTrue($result); } public function testSaveOrderProductsUpdatesQuantityAndAdjustsStock(): void { $orderRepo = $this->createMock(OrderRepository::class); $productRepo = $this->createMock(ProductRepository::class); $settingsRepo = $this->createMock(SettingsRepository::class); $transportRepo = $this->createMock(TransportRepository::class); // Existing: qty=3 $orderRepo->method('orderProducts') ->willReturn([ ['id' => 100, 'product_id' => 5, 'quantity' => 3, 'price_brutto' => 10, 'price_brutto_promo' => 0], ]); // Submit: qty=5 (increased by 2 → stock decreases by 2) $orderRepo->expects($this->once())->method('updateOrderProduct') ->with(100, $this->callback(function ($data) { return $data['quantity'] === 5; })); $orderRepo->method('findRawById')->willReturn(['id' => 1, 'transport_id' => 1]); $transportRepo->method('findActiveById')->willReturn(null); $productRepo->method('getQuantity')->with(5)->willReturn(20); $productRepo->expects($this->once())->method('updateQuantity') ->with(5, 18); // 20 + (3 - 5) = 18 $service = new OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo); $service->saveOrderProducts(1, [ ['order_product_id' => 100, 'product_id' => 5, 'quantity' => 5, 'price_brutto' => 10, 'price_brutto_promo' => 0], ]); } public function testSaveOrderProductsAddsNewProductAndDecreasesStock(): void { $orderRepo = $this->createMock(OrderRepository::class); $productRepo = $this->createMock(ProductRepository::class); $settingsRepo = $this->createMock(SettingsRepository::class); $transportRepo = $this->createMock(TransportRepository::class); $orderRepo->method('orderProducts')->willReturn([]); $orderRepo->expects($this->once())->method('addOrderProduct') ->with(1, $this->callback(function ($data) { return $data['product_id'] === 10 && $data['name'] === 'New Product' && $data['quantity'] === 2; })); $orderRepo->method('findRawById')->willReturn(['id' => 1, 'transport_id' => 1]); $transportRepo->method('findActiveById')->willReturn(null); $productRepo->method('getQuantity')->with(10)->willReturn(15); $productRepo->expects($this->once())->method('updateQuantity') ->with(10, 13); // 15 - 2 $service = new OrderAdminService($orderRepo, $productRepo, $settingsRepo, $transportRepo); $service->saveOrderProducts(1, [ [ 'order_product_id' => 0, 'product_id' => 10, 'parent_product_id' => 10, 'name' => 'New Product', 'vat' => 23, 'price_brutto' => 50, 'price_brutto_promo' => 0, 'quantity' => 2, ], ]); } public function testGetFreeDeliveryThresholdReturnsZeroWithoutSettingsRepo(): void { $service = $this->createService(); $this->assertSame(0.0, $service->getFreeDeliveryThreshold()); } public function testGetFreeDeliveryThresholdReturnsValue(): void { $settingsRepo = $this->createMock(SettingsRepository::class); $settingsRepo->method('getSingleValue') ->with('free_delivery') ->willReturn('150.00'); $service = $this->createService(null, null, $settingsRepo); $this->assertSame(150.0, $service->getFreeDeliveryThreshold()); } // ========================================================================= // processApiloSyncQueue — awaiting apilo_order_id // ========================================================================= private function getQueuePath(): string { // Musi odpowiadać ścieżce w OrderAdminService::apiloSyncQueuePath() // dirname(autoload/Domain/Order/, 2) = autoload/ return dirname(__DIR__, 4) . '/autoload/temp/apilo-sync-queue.json'; } private function writeQueue(array $queue): void { $path = $this->getQueuePath(); $dir = dirname($path); if (!is_dir($dir)) { mkdir($dir, 0777, true); } file_put_contents($path, json_encode($queue, JSON_PRETTY_PRINT)); } private function readQueue(): array { $path = $this->getQueuePath(); if (!file_exists($path)) return []; $content = file_get_contents($path); return $content ? json_decode($content, true) : []; } protected function tearDown(): void { $path = $this->getQueuePath(); if (file_exists($path)) { unlink($path); } parent::tearDown(); } public function testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull(): void { // Zamówienie bez apilo_order_id — task powinien zostać w kolejce $this->writeQueue([ '42' => [ 'order_id' => 42, 'payment' => 1, 'status' => null, 'attempts' => 0, 'last_error' => 'awaiting_apilo_order', 'updated_at' => '2026-01-01 00:00:00', ], ]); $orderRepo = $this->createMock(OrderRepository::class); $orderRepo->method('findRawById') ->with(42) ->willReturn([ 'id' => 42, 'apilo_order_id' => null, 'paid' => 1, 'summary' => '100.00', ]); $service = new OrderAdminService($orderRepo); $processed = $service->processApiloSyncQueue(10); $this->assertSame(1, $processed); $queue = $this->readQueue(); $this->assertArrayHasKey('42', $queue); $this->assertSame('awaiting_apilo_order', $queue['42']['last_error']); $this->assertSame(1, $queue['42']['attempts']); } public function testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts(): void { // Task z 49 próbami — limit to 50, więc powinien zostać usunięty $this->writeQueue([ '42' => [ 'order_id' => 42, 'payment' => 1, 'status' => null, 'attempts' => 49, 'last_error' => 'awaiting_apilo_order', 'updated_at' => '2026-01-01 00:00:00', ], ]); $orderRepo = $this->createMock(OrderRepository::class); $orderRepo->method('findRawById') ->with(42) ->willReturn([ 'id' => 42, 'apilo_order_id' => null, 'paid' => 1, 'summary' => '100.00', ]); $service = new OrderAdminService($orderRepo); $processed = $service->processApiloSyncQueue(10); $this->assertSame(1, $processed); $queue = $this->readQueue(); $this->assertArrayNotHasKey('42', $queue); } }