feat: expose eBay aspect usage tier (RECOMMENDED vs OPTIONAL)

EbayTaxonomyService.getCategoryAspects() now returns 'usage' alongside
'required'. eBay has three effective tiers for category 177/Notebooks:
  required=true  + usage=RECOMMENDED → hard gate (3 aspects)
  required=false + usage=RECOMMENDED → search ranking signal (17 aspects)
  required=false + usage=OPTIONAL    → truly optional (11 aspects)

Integration test covers all three tiers explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Kuehn 2026-05-18 18:21:31 +00:00
parent 68a9f0094e
commit 7f2ec21c64
2 changed files with 26 additions and 11 deletions

View file

@ -20,9 +20,13 @@ final class EbayTaxonomyService
} }
/** /**
* Returns the required item aspect names for a given eBay category. * Returns item aspects for a given eBay category, including eBay's usage tier.
* *
* @return list<array{name: string, required: bool, values: list<string>}> * usage values:
* 'RECOMMENDED' eBay search-ranking signal; include whenever possible
* 'OPTIONAL' truly optional, low impact
*
* @return list<array{name: string, required: bool, usage: string, values: list<string>}>
*/ */
public function getCategoryAspects(string $categoryId): array public function getCategoryAspects(string $categoryId): array
{ {
@ -45,14 +49,15 @@ final class EbayTaxonomyService
], ],
); );
/** @var array{aspects?: list<array{localizedAspectName: string, aspectConstraint?: array{aspectRequired?: bool}, aspectValues?: list<array{localizedValue: string}>}>} $data */ /** @var array{aspects?: list<array{localizedAspectName: string, aspectConstraint?: array{aspectRequired?: bool, aspectUsage?: string}, aspectValues?: list<array{localizedValue: string}>}>} $data */
$data = $response->toArray(); $data = $response->toArray();
$aspects = []; $aspects = [];
foreach ($data['aspects'] ?? [] as $aspect) { foreach ($data['aspects'] ?? [] as $aspect) {
$aspects[] = [ $aspects[] = [
'name' => $aspect['localizedAspectName'], 'name' => $aspect['localizedAspectName'],
'required' => $aspect['aspectConstraint']['aspectRequired'] ?? false, 'required' => (bool) ($aspect['aspectConstraint']['aspectRequired'] ?? false),
'usage' => $aspect['aspectConstraint']['aspectUsage'] ?? 'OPTIONAL',
'values' => array_column($aspect['aspectValues'] ?? [], 'localizedValue'), 'values' => array_column($aspect['aspectValues'] ?? [], 'localizedValue'),
]; ];
} }

View file

@ -71,35 +71,45 @@ final class EbayTaxonomyIntegrationTest extends TestCase
$this->assertNotEmpty($aspects, 'Category 177 should have aspects'); $this->assertNotEmpty($aspects, 'Category 177 should have aspects');
// Each aspect must have the expected shape
foreach ($aspects as $aspect) { foreach ($aspects as $aspect) {
$this->assertArrayHasKey('name', $aspect); $this->assertArrayHasKey('name', $aspect);
$this->assertArrayHasKey('required', $aspect); $this->assertArrayHasKey('required', $aspect);
$this->assertArrayHasKey('usage', $aspect);
$this->assertArrayHasKey('values', $aspect); $this->assertArrayHasKey('values', $aspect);
$this->assertIsString($aspect['name']); $this->assertIsString($aspect['name']);
$this->assertIsBool($aspect['required']); $this->assertIsBool($aspect['required']);
$this->assertContains($aspect['usage'], ['RECOMMENDED', 'OPTIONAL'], 'usage must be a known eBay tier');
$this->assertIsArray($aspect['values']); $this->assertIsArray($aspect['values']);
} }
// Notebooks should have at least a Prozessor/CPU aspect
$names = array_column($aspects, 'name'); $names = array_column($aspects, 'name');
$this->assertNotEmpty(array_filter($names, static fn (string $n) => str_contains(strtolower($n), 'prozes') || str_contains(strtolower($n), 'cpu') || str_contains(strtolower($n), 'speicher')), 'Expected at least one hardware aspect (CPU/RAM)'); $this->assertNotEmpty(
array_filter($names, static fn (string $n) => str_contains(strtolower($n), 'prozes') || str_contains(strtolower($n), 'cpu') || str_contains(strtolower($n), 'speicher')),
'Expected at least one hardware aspect (CPU/RAM)',
);
} }
public function test_required_aspects_are_flagged(): void public function test_three_tier_aspect_classification(): void
{ {
// eBay has three effective tiers:
// required=true + usage=RECOMMENDED → hard gate, eBay blocks listing without it
// required=false + usage=RECOMMENDED → "you should have this", search ranking signal
// required=false + usage=OPTIONAL → truly optional, low impact
$aspects = $this->taxonomy->getCategoryAspects('177'); $aspects = $this->taxonomy->getCategoryAspects('177');
$required = array_filter($aspects, static fn (array $a) => $a['required']); $hardRequired = array_filter($aspects, static fn (array $a) => $a['required']);
$recommended = array_filter($aspects, static fn (array $a) => !$a['required'] && 'RECOMMENDED' === $a['usage']);
$optional = array_filter($aspects, static fn (array $a) => !$a['required'] && 'OPTIONAL' === $a['usage']);
$this->assertNotEmpty($required, 'Category 177 should have at least one required aspect'); $this->assertNotEmpty($hardRequired, 'Should have hard-required aspects');
$this->assertNotEmpty($recommended, 'Should have recommended (ranking-signal) aspects');
$this->assertNotEmpty($optional, 'Should have truly optional aspects');
} }
public function test_aspects_with_predefined_values_have_options(): void public function test_aspects_with_predefined_values_have_options(): void
{ {
$aspects = $this->taxonomy->getCategoryAspects('177'); $aspects = $this->taxonomy->getCategoryAspects('177');
// At least some aspects should have predefined value lists (e.g. Zustand, Prozessorfamilie)
$withOptions = array_filter($aspects, static fn (array $a) => [] !== $a['values']); $withOptions = array_filter($aspects, static fn (array $a) => [] !== $a['values']);
$this->assertNotEmpty($withOptions, 'Some aspects should have predefined selectable values'); $this->assertNotEmpty($withOptions, 'Some aspects should have predefined selectable values');