Complete Domain-Driven Architecture migration: - Phase 1-4: Transport, ProductSet, Coupon, Shop, Search, Basket, ProductCustomField, Category, ProductAttribute, Promotion - Phase 5: Order (~562 lines) + Product (~952 lines) - ~20 Product methods migrated to ProductRepository - Apilo sync migrated to OrderAdminService - Production hotfixes: stale Redis cache (prices 0.00), unqualified Product:: refs in LayoutEngine, object->array template conversion - AttributeRepository::getAttributeValueById() Redis cache added Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
369 lines
13 KiB
PHP
369 lines
13 KiB
PHP
<?php
|
|
namespace Tests\Unit\Domain\Promotion;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Domain\Promotion\PromotionRepository;
|
|
|
|
class PromotionRepositoryTest extends TestCase
|
|
{
|
|
public function testFindReturnsDefaultPromotionForInvalidId(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$repository = new PromotionRepository($mockDb);
|
|
|
|
$result = $repository->find(0);
|
|
|
|
$this->assertIsArray($result);
|
|
$this->assertSame(0, (int)$result['id']);
|
|
$this->assertSame(1, (int)$result['status']);
|
|
$this->assertNull($result['date_from']);
|
|
$this->assertSame([], $result['categories']);
|
|
$this->assertSame([], $result['condition_categories']);
|
|
}
|
|
|
|
public function testSaveInsertsPromotionAndReturnsId(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$insertRow = null;
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('insert')
|
|
->willReturnCallback(function ($table, $row) use (&$insertRow) {
|
|
$this->assertSame('pp_shop_promotion', $table);
|
|
$this->assertArrayHasKey('name', $row);
|
|
$insertRow = $row;
|
|
});
|
|
|
|
$mockDb->expects($this->once())
|
|
->method('id')
|
|
->willReturn(123);
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$id = $repository->save([
|
|
'name' => 'Promocja testowa',
|
|
'status' => 'on',
|
|
'condition_type' => 1,
|
|
'discount_type' => 1,
|
|
'amount' => '10',
|
|
'date_from' => '2026-02-01',
|
|
'categories' => [1, 2],
|
|
]);
|
|
|
|
$this->assertSame(123, $id);
|
|
$this->assertIsArray($insertRow);
|
|
$this->assertSame('2026-02-01', $insertRow['date_from'] ?? null);
|
|
}
|
|
|
|
public function testDeleteReturnsFalseForInvalidId(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->never())->method('delete');
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$this->assertFalse($repository->delete(0));
|
|
}
|
|
|
|
public function testDeleteReturnsTrueWhenDatabaseDeleteSucceeds(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$mockDb->expects($this->once())
|
|
->method('delete')
|
|
->with('pp_shop_promotion', ['id' => 55])
|
|
->willReturn(true);
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$this->assertTrue($repository->delete(55));
|
|
}
|
|
|
|
public function testListForAdminWhitelistsSortAndDirection(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
$queries = [];
|
|
|
|
$mockDb->method('query')
|
|
->willReturnCallback(function ($sql, $params = []) use (&$queries) {
|
|
$queries[] = ['sql' => $sql, 'params' => $params];
|
|
|
|
if (strpos($sql, 'COUNT(0)') !== false) {
|
|
return new class {
|
|
public function fetchAll()
|
|
{
|
|
return [[1]];
|
|
}
|
|
};
|
|
}
|
|
|
|
return new class {
|
|
public function fetchAll()
|
|
{
|
|
return [[
|
|
'id' => 1,
|
|
'name' => 'Promo',
|
|
'status' => 1,
|
|
'condition_type' => 1,
|
|
'date_to' => null,
|
|
]];
|
|
}
|
|
};
|
|
});
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$repository->listForAdmin(
|
|
[],
|
|
'date_to DESC; DROP TABLE pp_shop_promotion; --',
|
|
'DESC; DELETE FROM pp_users; --',
|
|
1,
|
|
500
|
|
);
|
|
|
|
$this->assertCount(2, $queries);
|
|
$dataSql = $queries[1]['sql'];
|
|
|
|
$this->assertMatchesRegularExpression('/ORDER BY\s+sp\.id\s+DESC,\s+sp\.id\s+DESC/i', $dataSql);
|
|
$this->assertStringNotContainsString('DROP TABLE', $dataSql);
|
|
$this->assertStringNotContainsString('DELETE FROM pp_users', $dataSql);
|
|
$this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql);
|
|
}
|
|
|
|
public function testCategoriesTreeReturnsHierarchy(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->method('select')
|
|
->willReturnCallback(function ($table, $columns, $where) {
|
|
if ($table === 'pp_shop_categories' && array_key_exists('parent_id', $where)) {
|
|
if ($where['parent_id'] === null) {
|
|
return [['id' => 10]];
|
|
}
|
|
if ((int)$where['parent_id'] === 10) {
|
|
return [['id' => 11]];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
if ($table === 'pp_shop_categories_langs') {
|
|
if ((int)$where['category_id'] === 10) {
|
|
return [['lang_id' => 'pl', 'title' => 'Kategoria A']];
|
|
}
|
|
if ((int)$where['category_id'] === 11) {
|
|
return [['lang_id' => 'pl', 'title' => 'Podkategoria A1']];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
if ($table === 'pp_langs') {
|
|
return [['id' => 'pl', 'start' => 1, 'o' => 1]];
|
|
}
|
|
|
|
return [];
|
|
});
|
|
|
|
$mockDb->method('get')
|
|
->willReturnCallback(function ($table, $columns, $where) {
|
|
if ($table === 'pp_shop_categories') {
|
|
$id = (int)$where['id'];
|
|
return ['id' => $id, 'status' => 1];
|
|
}
|
|
return null;
|
|
});
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$tree = $repository->categoriesTree(null);
|
|
|
|
$this->assertCount(1, $tree);
|
|
$this->assertSame(10, (int)$tree[0]['id']);
|
|
$this->assertSame('Kategoria A', $tree[0]['title']);
|
|
$this->assertCount(1, $tree[0]['subcategories']);
|
|
$this->assertSame(11, (int)$tree[0]['subcategories'][0]['id']);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Frontend: basket promotion logic (migrated from front\factory\ShopPromotion)
|
|
// =========================================================================
|
|
|
|
private function makeBasket(array $items): array
|
|
{
|
|
$basket = [];
|
|
foreach ($items as $i => $item) {
|
|
$basket[$i] = array_merge(['product-id' => $item['id']], $item);
|
|
}
|
|
return $basket;
|
|
}
|
|
|
|
/**
|
|
* Test applyTypeWholeBasket — rabat na cały koszyk
|
|
*/
|
|
public function testApplyTypeWholeBasketAppliesDiscountToAll(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
// productCategoriesFront zwraca kategorie
|
|
$mockDb->method('get')->willReturn(null); // parent_id = null
|
|
$mockStmt = $this->createMock(\PDOStatement::class);
|
|
$mockStmt->method('fetchAll')->willReturn([['category_id' => 1]]);
|
|
$mockDb->method('query')->willReturn($mockStmt);
|
|
|
|
$promotion = [
|
|
'discount_type' => 1,
|
|
'amount' => 10,
|
|
'include_coupon' => 0,
|
|
'include_product_promo' => 0,
|
|
];
|
|
|
|
$basket = $this->makeBasket([
|
|
['id' => 1],
|
|
['id' => 2],
|
|
]);
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$result = $repository->applyTypeWholeBasket($basket, $promotion);
|
|
|
|
$this->assertSame(1, $result[0]['discount_type']);
|
|
$this->assertSame(10, $result[0]['discount_amount']);
|
|
$this->assertSame(1, $result[1]['discount_type']);
|
|
}
|
|
|
|
/**
|
|
* Test applyTypeCategoriesOr — rabat na produkty z kat. 1 lub 2
|
|
*/
|
|
public function testApplyTypeCategoriesOrAppliesDiscountToMatchingCategories(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->method('get')->willReturn(null);
|
|
$mockStmt = $this->createMock(\PDOStatement::class);
|
|
$mockStmt->method('fetchAll')->willReturn([['category_id' => 5]]);
|
|
$mockDb->method('query')->willReturn($mockStmt);
|
|
|
|
$promotion = [
|
|
'categories' => [5],
|
|
'condition_categories' => [10],
|
|
'discount_type' => 1,
|
|
'amount' => 15,
|
|
'include_coupon' => 1,
|
|
'include_product_promo' => 0,
|
|
];
|
|
|
|
$basket = $this->makeBasket([['id' => 1]]);
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$result = $repository->applyTypeCategoriesOr($basket, $promotion);
|
|
|
|
$this->assertSame(1, $result[0]['discount_type']);
|
|
$this->assertSame(15, $result[0]['discount_amount']);
|
|
$this->assertSame(1, $result[0]['discount_include_coupon']);
|
|
}
|
|
|
|
/**
|
|
* Test applyTypeCategoryCondition — rabat na kat. I jeśli kat. II w koszyku
|
|
*/
|
|
public function testApplyTypeCategoryConditionAppliesWhenConditionMet(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$callCount = 0;
|
|
$mockDb->method('get')->willReturn(null);
|
|
|
|
$mockStmt1 = $this->createMock(\PDOStatement::class);
|
|
$mockStmt1->method('fetchAll')->willReturnOnConsecutiveCalls(
|
|
[['category_id' => 10]], // product 1 — condition category
|
|
[['category_id' => 5]], // product 2 — target category
|
|
[['category_id' => 10]], // product 1 — check for discount (not matching target)
|
|
[['category_id' => 5]] // product 2 — check for discount (matching target)
|
|
);
|
|
$mockDb->method('query')->willReturn($mockStmt1);
|
|
|
|
$promotion = [
|
|
'categories' => [5],
|
|
'condition_categories' => [10],
|
|
'discount_type' => 1,
|
|
'amount' => 20,
|
|
'include_coupon' => 0,
|
|
'include_product_promo' => 0,
|
|
];
|
|
|
|
$basket = $this->makeBasket([
|
|
['id' => 1],
|
|
['id' => 2],
|
|
]);
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$result = $repository->applyTypeCategoryCondition($basket, $promotion);
|
|
|
|
// Produkt 2 (kat. 5) powinien mieć rabat
|
|
$this->assertSame(1, $result[1]['discount_type']);
|
|
$this->assertSame(20, $result[1]['discount_amount']);
|
|
}
|
|
|
|
/**
|
|
* Test applyTypeCategoryCondition — brak rabatu gdy warunek niespełniony
|
|
*/
|
|
public function testApplyTypeCategoryConditionNoDiscountWhenConditionNotMet(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->method('get')->willReturn(null);
|
|
$mockStmt = $this->createMock(\PDOStatement::class);
|
|
$mockStmt->method('fetchAll')->willReturn([['category_id' => 99]]); // nie pasuje do condition_categories
|
|
$mockDb->method('query')->willReturn($mockStmt);
|
|
|
|
$promotion = [
|
|
'categories' => [5],
|
|
'condition_categories' => [10],
|
|
'discount_type' => 1,
|
|
'amount' => 20,
|
|
'include_coupon' => 0,
|
|
'include_product_promo' => 0,
|
|
];
|
|
|
|
$basket = $this->makeBasket([['id' => 1]]);
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$result = $repository->applyTypeCategoryCondition($basket, $promotion);
|
|
|
|
$this->assertArrayNotHasKey('discount_type', $result[0]);
|
|
}
|
|
|
|
/**
|
|
* Test applyTypeCategoriesAnd — rabat gdy oba warunki spełnione
|
|
*/
|
|
public function testApplyTypeCategoriesAndAppliesWhenBothConditionsMet(): void
|
|
{
|
|
$mockDb = $this->createMock(\medoo::class);
|
|
|
|
$mockDb->method('get')->willReturn(null);
|
|
|
|
$mockStmt = $this->createMock(\PDOStatement::class);
|
|
$mockStmt->method('fetchAll')->willReturnOnConsecutiveCalls(
|
|
[['category_id' => 10]], // product 1 — condition_categories ✓
|
|
[['category_id' => 5]], // product 2 — categories ✓ (condition_2)
|
|
[['category_id' => 10]], // product 1 — check categories ✓ (condition check)
|
|
[['category_id' => 5]], // product 2 — check categories ✓ (condition check)
|
|
[['category_id' => 10]], // product 1 — discount assignment
|
|
[['category_id' => 5]] // product 2 — discount assignment
|
|
);
|
|
$mockDb->method('query')->willReturn($mockStmt);
|
|
|
|
$promotion = [
|
|
'categories' => [5],
|
|
'condition_categories' => [10],
|
|
'discount_type' => 1,
|
|
'amount' => 25,
|
|
'include_coupon' => 1,
|
|
'include_product_promo' => 0,
|
|
];
|
|
|
|
$basket = $this->makeBasket([
|
|
['id' => 1],
|
|
['id' => 2],
|
|
]);
|
|
|
|
$repository = new PromotionRepository($mockDb);
|
|
$result = $repository->applyTypeCategoriesAnd($basket, $promotion);
|
|
|
|
$this->assertSame(1, $result[0]['discount_type']);
|
|
$this->assertSame(25, $result[0]['discount_amount']);
|
|
$this->assertSame(1, $result[1]['discount_type']);
|
|
}
|
|
}
|