ver. 0.302: REST API product variants, attributes dictionary, attribute filtering

- Add variant CRUD endpoints (variants, create_variant, update_variant, delete_variant)
- Add dictionaries/attributes endpoint with multilingual names and values
- Add attribute_* filter params for product list filtering by attribute values
- Enrich product detail attributes with translated names (attribute_names, value_names)
- Include variants array in product detail response for parent products
- Add price_brutto validation on product create
- Batch-load attribute/value translations (4 queries instead of N+1)
- Add 43 new unit tests (730 total, 2066 assertions)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 14:42:52 +01:00
parent c0cdaaf638
commit 1fc36e4403
18 changed files with 1721 additions and 22 deletions

View File

@@ -359,4 +359,81 @@ class AttributeRepositoryTest extends TestCase
return false;
}
// --- listForApi ---
public function testListForApiReturnsActiveAttributesWithValues(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('select')
->willReturnCallback(function ($table, $columns, $where) {
if ($table === 'pp_shop_attributes') {
return [
['id' => '5', 'type' => '0', 'status' => '1'],
];
}
if ($table === 'pp_shop_attributes_langs') {
return [
['attribute_id' => '5', 'lang_id' => 'pl', 'name' => 'Rozmiar'],
['attribute_id' => '5', 'lang_id' => 'en', 'name' => 'Size'],
];
}
if ($table === 'pp_shop_attributes_values') {
return [
['id' => '12', 'attribute_id' => '5', 'is_default' => '1', 'impact_on_the_price' => null],
['id' => '13', 'attribute_id' => '5', 'is_default' => '0', 'impact_on_the_price' => '10.00'],
];
}
if ($table === 'pp_shop_attributes_values_langs') {
return [
['value_id' => '12', 'lang_id' => 'pl', 'name' => 'M'],
['value_id' => '12', 'lang_id' => 'en', 'name' => 'M'],
['value_id' => '13', 'lang_id' => 'pl', 'name' => 'L'],
['value_id' => '13', 'lang_id' => 'en', 'name' => 'L'],
];
}
if ($table === 'pp_langs') {
return [['id' => 'pl', 'start' => 1, 'o' => 1]];
}
return [];
});
$repository = new AttributeRepository($mockDb);
$result = $repository->listForApi();
$this->assertCount(1, $result);
$this->assertSame(5, $result[0]['id']);
$this->assertSame(0, $result[0]['type']);
$this->assertSame(1, $result[0]['status']);
$this->assertSame('Rozmiar', $result[0]['names']['pl']);
$this->assertSame('Size', $result[0]['names']['en']);
$this->assertCount(2, $result[0]['values']);
$this->assertSame(12, $result[0]['values'][0]['id']);
$this->assertSame(1, $result[0]['values'][0]['is_default']);
$this->assertNull($result[0]['values'][0]['impact_on_the_price']);
$this->assertSame(13, $result[0]['values'][1]['id']);
$this->assertSame(10.0, $result[0]['values'][1]['impact_on_the_price']);
}
public function testListForApiReturnsEmptyWhenNoAttributes(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->method('select')
->willReturnCallback(function ($table) {
if ($table === 'pp_shop_attributes') {
return [];
}
if ($table === 'pp_langs') {
return [['id' => 'pl', 'start' => 1, 'o' => 1]];
}
return [];
});
$repository = new AttributeRepository($mockDb);
$result = $repository->listForApi();
$this->assertSame([], $result);
}
}