Implementing Product Import from Price Aggregators
Price aggregators (Yandex.Market, Price.ru, E-Katalog, OZON, Wildberries) accumulate product data from many stores and manufacturers. For store they're source for catalog enrichment: descriptions, characteristics, images and market price benchmarks. Each aggregator has own format and access model.
Aggregator Adapter Layer
interface AggregatorAdapterInterface
{
public function fetchProducts(array $options = []): iterable;
public function getSupportedFields(): array;
public function getSourceId(): string;
}
Registration in service container:
$this->app->tag([
YandexMarketAdapter::class,
EKatalogAdapter::class,
OzonSellerAdapter::class,
], 'aggregator.adapters');
E-Katalog: Rich Technical Specifications
E-Katalog is richest source of technical specs. Their XML contains standardized specifications with units:
class EKatalogAdapter implements AggregatorAdapterInterface
{
public function fetchProducts(array $options = []): iterable
{
$response = $this->client->get('/api/v2/products', [
'query' => [
'category_id' => $options['category_id'] ?? null,
'lang' => 'en',
'fields' => 'id,name,description,specs,images,brand,price_min,price_max',
'page' => $options['page'] ?? 1,
],
'headers' => ['Authorization' => 'Bearer ' . $this->apiKey],
]);
foreach ($response->json('products') as $product) {
yield $this->normalize($product);
}
}
private function normalize(array $raw): array
{
return [
'external_id' => 'ekatalog_' . $raw['id'],
'name' => $raw['name'],
'description' => $raw['description'],
'brand' => $raw['brand']['name'] ?? null,
'images' => array_column($raw['images'], 'url'),
'price_market_min' => $raw['price_min'],
'price_market_max' => $raw['price_max'],
];
}
}
OZON Seller API
If store sells on OZON, can pull data from there:
class OzonSellerAdapter implements AggregatorAdapterInterface
{
public function fetchProducts(array $options = []): iterable
{
// OZON API v3: get product list
$response = Http::withToken($this->apiKey)
->post('https://api-seller.ozon.ru/v3/product/list', [
'filter' => ['visibility' => 'ALL'],
'limit' => 1000,
]);
// Get details in batches of 100
foreach (array_chunk(array_column($response->json('result.items'), 'product_id'), 100) as $batch) {
$details = Http::withToken($this->apiKey)
->post('https://api-seller.ozon.ru/v2/product/info/list', [
'product_id' => $batch,
])->json('result.items');
foreach ($details as $detail) {
yield $this->normalizeOzon($detail);
}
}
}
}
Market Price Analytics
Aggregators enable tracking competitive pricing:
CREATE TABLE market_price_data (
product_id int REFERENCES products(id),
source varchar(50), -- 'ekatalog' | 'yandex_market'
price_min numeric(12,2),
price_max numeric(12,2),
price_avg numeric(12,2),
offers_count int,
collected_at timestamptz DEFAULT now()
);
Automatically set price as "market minimum - 5%" or "above average by 2%" — dynamic pricing based on real data.
Enriching Existing Products
Main use case: catalog has product with SKU but no specs/description. Aggregator knows it by GTIN or brand+model:
class ProductEnrichmentService
{
public function enrich(Product $product): bool
{
// Search by GTIN in aggregators
foreach ($this->adapters as $adapter) {
$data = $adapter->findByGtin($product->gtin);
if (!$data) $data = $adapter->findByBrandModel($product->brand, $product->model);
if (!$data) continue;
$this->applyEnrichment($product, $data, $adapter->getSourceId());
return true;
}
return false;
}
private function applyEnrichment(Product $product, array $data, string $source): void
{
// Enrich only empty fields — don't overwrite existing
if (!$product->description && !empty($data['description'])) {
$product->description = $data['description'];
}
$product->save();
}
}
Implementation Timeline
- One adapter (YML from Yandex.Market), enrich empty fields — 2 days
- Multi-aggregator structure + priorities + market prices — +2 days
- OZON/WB API, dynamic pricing based on market — +2–3 days







