← Zpět na přehled

Duell (Duell Bike Center)

Finský dodavatel moto/ATV/sněžných/lodních dílů a doplňků. Na rozdíl od všech ostatních dodavatelů v tomto projektu negeneruje feed přes URL — data se stahují přes FTP jako sada ;-oddělených CSV souborů.

Přístup

Parametr Hodnota
Host updateftp.duell.fi
Uživatel duellus
Heslo WebUpdate!

Zdroj: email "Feedy Duell" (17.2.2026, přeposlaný od info@ctyrkolky-vrchlabi.cz), příloha Product information to customers.pdf. Na FTP navíc leží novější a přesnější Duell_FTP_fileshare_technical_description_v1.0.pdf (v.1.0, 11.2.2026, "First set of product information from updated eCom system") — implementace vychází z něj, ne ze starší zákaznické PDF (ta popisuje starší strukturu /ProductInformation/{Duell,SDBC}/..., která na serveru sice pořád existuje, ale nová /Data/ struktura ji nahrazuje).

Rozsah — celý katalog

Import zahrnuje celý katalog Duellu, napříč všemi 8 hlavními kategoriemi (potomci kořenové kategorie ecom25 v categories.csv): motorcycle, snowmobile, moped_scooter, atv_utv, marine, bicycle, garden_forest, campaigns. Produkt zařazený do víc kategorií (a takových je hodně) dostane breadcrumby pro všechny z nich, ne jen pro jednu.

Původně byl import omezen jen na atv_utv (~16 tis. produktů) jako konzervativní start — viz commit historie. Rozšířeno na celý katalog na žádost; k tomu bylo potřeba přejít z DOMDocument na XMLWriter (viz níže), protože jinak by strom DOM objektů pro ~125 tis. položek byl srovnatelně paměťově náročný jako u Motopointu (docs/feeds/motopoint.md), kde to na produkčním serveru (jen ~1,9 GB RAM) opakovaně spadlo na nedostatek paměti.

Živě ověřeno (reálná FTP data, php -d memory_limit=512M):

Feed Počet položek Doba Špička paměti
products 125 454 ~146 s 367,8 MB
availability 118 815 ~13 s 212,2 MB

Obě čísla se pohodlně vejdou do produkčního výchozího memory_limit = 512M (docker/php.ini) — na rozdíl od Motopointu tu nebylo potřeba přesouvat úložiště produktů do Postgresu ani zvyšovat limit paměti. Hlavní rozdíl: Duell už předtím zpracovával CSV řádek po řádku přes fgetcsv() (nikdy nedržel celý soubor v paměti najednou) a lookupy pro sklad/ceny/obrázky jsou úzké záznamy (pár krátkých polí), ne bohatá data s HTML popisy jako u Motopointu — jediný skutečný viník byl DOMDocument strom, který XMLWriter řeší.

Riziko souběhu s Motopointem: oba dodavatelé mají products: 12 hours a žádný globální (napříč dodavateli) zámek na "jen jeden náročný refresh najednou" neexistuje — jen per-supplier zámek z FeedRefresher. Pokud by se Motopointův refresh (až ~768 MB) a Duellův (~370 MB) sešly na stejném cronovém tiku, dohromady by na 1,9GB hostu mohly tlačit na hranu i se zbytkem kontejnerů (Postgres, nginx, ...). Zatím nepozorováno jako reálný problém, ale stojí za sledování.

Zdrojové soubory (aktuální /Data/ struktura)

Soubor Cesta na FTP Účel Update interval Velikost (celý katalog)
Produkty /Data/Products/products.csv Plný katalog, ~190 sloupců 1× denně ~190–200 MB / ~125 tis. řádků (mění se, katalog žije)
Kategorie /Data/categories.csv code;label-*;parent;updated — plochý seznam s přímým ukazatelem na rodiče 1× týdně malý (1 424 řádků)
Obrázky /Data/product_images.csv PRODUCT_CODE;IMAGE_URL;IMAGE_NAME — víc řádků na produkt 1× denně ~15 MB / 313 787 řádků
Sklad /ic_CSV.csv PRODUCT_CODE;VAASA;SWEDEN;TOTAL 30 min ~3.7 MB / 148 997 řádků
Ceny /Retail_pricelist.csv PRODUCT_ID;PRODUCT_CODE;EUR_EXCL_VAT;EUR_INCL_VAT;EUR_EXCL_VAT_EU;EUR_INCL_VAT_EU;SEK_*;NOK_*;GBP_NET;DKK_* 2 hodiny ~17 MB / 143 591 řádků

Existuje i /ic_ean_CSV.csv (stejné sloupce + EAN) a starší /ProductInformation/{Duell,SDBC}/... strom — ani jeden se nepoužívá, protože ic_CSV.csv má klíč PRODUCT_CODE shodný se vším ostatním a nová /Data/ struktura je jednodušší a aktuálnější.

Data podporují stránkování/timestamp inkrementů (products_updates_*.csv, denní delty za posledních 7 dní) — tento import je zatím nevyužívá, každý refresh stahuje celý products.csv znovu (stejně jako u všech ostatních dodavatelů v tomto projektu).

Dvouprůchodový import + streamovaný výstup

fetchProducts() prochází products.csv dvakrát, ale stahuje ho jen jednou:

  1. 1. průchod (collectProductCategories()) — pro každý product_code zjistí, jaké kategorie má přiřazené (žádné filtrování — jde jen o to znát množinu reálných kódů a jejich kategorie).
  2. Podle té množiny se stahují a filtrují ic_CSV.csv, Retail_pricelist.csv a product_images.csv — vyřadí se jen záznamy pro kódy, které v products.csv vůbec neexistují.
  3. 2. průchod — znovu se čte stejný už stažený dočasný soubor a staví se výstupní XML pro každý zachycený product_code.

Výstupní XML se staví přes XMLWriter (streamuje do plochého bufferu), ne DOMDocument — pro ~125 tis. položek by strom DOMElement/DOMText objektů byl zbytečná paměťová režie navíc k datům, která už jednou prošla přes fgetcsv(). fetchAvailability() používá stejný přístup, i když je per-položku o dost lehčí (jen 5 plochých polí, žádné vnořené PHOTOS/PARAMETERS).

Mapování polí (products.csv → products.xml)

Výstupní pole Zdroj
CODE / PRODUCT_CODE product_code — sdílený klíč napříč všemi soubory
EAN barcode
PRODUCT product_name-en, fallback short_description-en
DESCRIPTION long_description-en, fallback short_description-en
SHORT_DESCRIPTION short_description-en
PRICE Retail_pricelist.csv → EUR_EXCL_VAT_EU (viz níže, proč ne EUR_EXCL_VAT)
VAT natvrdo 21 (česká sazba) — Duell neposílá sazbu DPH na produkt, jen předpočítané ceny s/bez DPH pro různé měny/trhy
HIDDEN vždy Y
CATEGORY viz níže
IN_STORE ic_CSV.csv → TOTAL, cross-referenced podle product_code (products.csv sklad neobsahuje)
PRODUCER brand (pozor: hodnoty jsou lowercase kódy, ne hezky formátované názvy — např. metzeler, sram)
PHOTOS/PHOTO product_images.csv → IMAGE_URL, může být víc obrázků na produkt
PARAMETERS/PARAMETER generický průchod přes zbylé sloupce — viz níže

Proč EUR_EXCL_VAT_EU, ne EUR_EXCL_VAT

Retail_pricelist.csv má dvě sady EUR cen: EUR_EXCL_VAT/EUR_INCL_VAT jsou pro Nordics/Baltics zákazníky, EUR_EXCL_VAT_EU/EUR_INCL_VAT_EU pro "outside Nordics/Baltics" — tedy i ČR. Použita je proto ta druhá dvojice (bez DPH varianta).

Kategorie

categories.csv je plochý seznam s přímým parent odkazem (ne strom jako u Motopointu). Cesta se staví procházením parent řetězu nahoru od každé kategorie produktu, dokud se nenarazí na syntetický kořen ecom25 (ten se do cesty nezahrnuje). Kořen výstupní cesty je vždy Duell.

Pole categories v products.csv je čárkami oddělený seznam kódů kategorií (produkt může patřit do víc kategorií najednou, včetně různých hlavních větví zároveň — např. atv_utv i motorcycle). Do breadcrumbs se zahrnou všechny.

Generické PARAMETERS

products.csv má ~150 sloupců specifikací (rozměry pneumatik, přileb, baterií, atd. — viz Duell_FTP_fileshare_technical_description_v1.0.pdf pro plný seznam). Ruční mapování všech by bylo neúměrné, takže se použije obecné pravidlo:

  • Sloupce mapované na pojmenovaná pole výše (product_code, categories, barcode, brand, popisy, jména, vendor_name, interní/ERP pole jako created/updated/parent/vak_code apod.) se do PARAMETERS nedávají.
  • Zbytek se stává PARAMETER s name = syrový název sloupce (např. tyre_width, handlebar_clamp_diameter) — bez hezkých českých názvů.
  • Páry X + X-unit (např. product_weight + product_weight-unit) se spojí do jedné hodnoty: "5.4000 KILOGRAM".
  • Varianty -fi/-sv (finština/švédština) se zahazují ve prospěch -en (čeština k dispozici není, angličtina je nejbližší použitelná náhrada); přípona -en se z názvu parametru odstraní.
  • Prázdné hodnoty se přeskakují.

Neřešeno: attribute_options.csv na FTP mapuje kódované hodnoty atributů (např. bag_type=tank_bag) na hezké popisky ("Tank Bag" / "Tankkilaukku" / "Tankväska") — momentálně se nepoužívá, takže některé PARAMETER hodnoty budou syrové kódy místo čitelného textu. Možné budoucí vylepšení.

Ošetření vstupních dat

  • Desetinná čárka: PRICE a skladové sloupce používají evropský formát s čárkou (23,8645) — převádí se na tečku.
  • Neplatné XML znaky: zdrojová CSV data občas obsahují řídicí znaky mimo rozsah povolený XML 1.0 (ověřeno živě — narazilo na to long_description-en u jednoho produktu). DOMDocument::saveXML() je bez varování zapíše, ale zpětné čtení přes XMLReader (používá ho fetchAvailability() přes buildProductLookup()) na nich spadne s parser errorem. Všechen text z CSV se proto před vložením do XML sanitizuje (sanitizeXmlText()).

Zdroj dat pro dostupnost

Živě se stahuje ic_CSV.csv (sklad) a Retail_pricelist.csv (cena); EAN se bere z mezipaměti posledního produktového XML. Emitují se jen položky, které existují v cachi products.xml (stejné pravidlo jako u ostatních dodavatelů).

Harmonogram

Feed Interval Poznámka
Produkty 12 hodin Duell aktualizuje products.csv jen 1× denně, ale 12h drží konzistenci s ostatními dodavateli v projektu
Dostupnost 30 minut Odpovídá skutečnému update intervalu ic_CSV.csv/Retail_pricelist.csv u Duellu (ostatní dodavatelé mají 15 min, ale u nich se skladem aktualizuje častěji)

Závislosti

Vyžaduje PHP rozšíření ext-ftp (docker-php-ext-install ftp — v Dockerfile tohoto projektu už je nainstalované, přidáno i do composer.json).