- Fix: race condition callback płatności przed wysłaniem do Apilo - Fix: processApiloSyncQueue czeka na apilo_order_id zamiast usuwać task - Fix: drugie wywołanie processApiloSyncQueue po wysyłce zamówień w cronie - Fix: ceny w szczegółach zamówienia (effective price zamiast 0 zł) - New: persistence filtrów tabel admin (localStorage) - Testy: 760 tests, 2141 assertions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
337 lines
12 KiB
PHP
337 lines
12 KiB
PHP
<?php
|
|
namespace Tests\Unit\Domain\Order;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Domain\Order\OrderAdminService;
|
|
use Domain\Order\OrderRepository;
|
|
use Domain\Product\ProductRepository;
|
|
use Domain\Settings\SettingsRepository;
|
|
use Domain\Transport\TransportRepository;
|
|
|
|
class OrderAdminServiceTest extends TestCase
|
|
{
|
|
private function createService(
|
|
$orderRepo = null,
|
|
$productRepo = null,
|
|
$settingsRepo = null,
|
|
$transportRepo = null
|
|
): OrderAdminService {
|
|
if (!$orderRepo) {
|
|
$orderRepo = $this->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);
|
|
}
|
|
}
|