ver. 0.295: Admin order product editing — add/remove/modify products, AJAX search, stock adjustment
- Order product CRUD in admin panel (add, delete, edit quantity/prices) - AJAX product search endpoint for order edit form - Automatic stock adjustment when editing order products - Transport cost recalculation based on free delivery threshold - Fix: promo price = 0 when equal to base price (no real promotion) - Clean up stale temp/ build artifacts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
230
tests/Unit/Domain/Order/OrderAdminServiceTest.php
Normal file
230
tests/Unit/Domain/Order/OrderAdminServiceTest.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
@@ -209,6 +209,138 @@ class OrderRepositoryTest extends TestCase
|
||||
$this->assertSame($expectedPrefix . '006', $number);
|
||||
}
|
||||
|
||||
// --- Order product CRUD tests ---
|
||||
|
||||
public function testGetOrderProductReturnsNullForInvalidId(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->never())->method('get');
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$this->assertNull($repository->getOrderProduct(0));
|
||||
$this->assertNull($repository->getOrderProduct(-1));
|
||||
}
|
||||
|
||||
public function testGetOrderProductReturnsArray(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->method('get')
|
||||
->with('pp_shop_order_products', '*', ['id' => 5])
|
||||
->willReturn(['id' => 5, 'order_id' => 1, 'name' => 'Test']);
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$result = $repository->getOrderProduct(5);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertSame(5, $result['id']);
|
||||
}
|
||||
|
||||
public function testAddOrderProductReturnsNullForInvalidOrderId(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->never())->method('insert');
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$this->assertNull($repository->addOrderProduct(0, ['name' => 'Test']));
|
||||
}
|
||||
|
||||
public function testAddOrderProductInsertsAndReturnsId(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->once())->method('insert')
|
||||
->with('pp_shop_order_products', $this->callback(function ($data) {
|
||||
return $data['order_id'] === 10
|
||||
&& $data['product_id'] === 5
|
||||
&& $data['name'] === 'Test Product'
|
||||
&& $data['quantity'] === 2;
|
||||
}));
|
||||
$mockDb->method('id')->willReturn('99');
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$result = $repository->addOrderProduct(10, [
|
||||
'product_id' => 5,
|
||||
'name' => 'Test Product',
|
||||
'quantity' => 2,
|
||||
'price_brutto' => 19.99,
|
||||
]);
|
||||
|
||||
$this->assertSame(99, $result);
|
||||
}
|
||||
|
||||
public function testUpdateOrderProductReturnsFalseForInvalidId(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->never())->method('update');
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$this->assertFalse($repository->updateOrderProduct(0, ['quantity' => 3]));
|
||||
}
|
||||
|
||||
public function testUpdateOrderProductUpdatesFields(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->once())->method('update')
|
||||
->with('pp_shop_order_products', $this->callback(function ($data) {
|
||||
return $data['quantity'] === 3
|
||||
&& $data['price_brutto'] === 25.50;
|
||||
}), ['id' => 7]);
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$result = $repository->updateOrderProduct(7, [
|
||||
'quantity' => 3,
|
||||
'price_brutto' => 25.50,
|
||||
]);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testUpdateOrderProductReturnsFalseForEmptyData(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->never())->method('update');
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$this->assertFalse($repository->updateOrderProduct(7, []));
|
||||
}
|
||||
|
||||
public function testDeleteOrderProductReturnsFalseForInvalidId(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->never())->method('delete');
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$this->assertFalse($repository->deleteOrderProduct(0));
|
||||
}
|
||||
|
||||
public function testDeleteOrderProductCallsDelete(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->once())->method('delete')
|
||||
->with('pp_shop_order_products', ['id' => 12]);
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$this->assertTrue($repository->deleteOrderProduct(12));
|
||||
}
|
||||
|
||||
public function testUpdateTransportCostDoesNothingForInvalidId(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->never())->method('update');
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$repository->updateTransportCost(0, 15.0);
|
||||
}
|
||||
|
||||
public function testUpdateTransportCostUpdatesOrder(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
$mockDb->expects($this->once())->method('update')
|
||||
->with('pp_shop_orders', ['transport_cost' => 12.50], ['id' => 5]);
|
||||
|
||||
$repository = new OrderRepository($mockDb);
|
||||
$repository->updateTransportCost(5, 12.50);
|
||||
}
|
||||
|
||||
public function testGenerateOrderNumberStartsAt001(): void
|
||||
{
|
||||
$mockDb = $this->createMock(\medoo::class);
|
||||
|
||||
Reference in New Issue
Block a user