Search by

liquiddesign / eshop-doryo-api

info@lqd.cz

Čtecí API e-shopu pro Doryo — objednávky, zákazníci, produkty, sklad a faktury v jednotné projekci

Package info

github.com/liquiddesign/eshop-doryo-api

pkg:composer/liquiddesign/eshop-doryo-api

Statistics

Installs: 41

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

v1.7.0 2026-09-12 04:46 UTC

README

Čtecí API e-shopu pro Doryo — objednávky, zákazníci, produkty, sklad a faktury v jednotné projekci, ve stejném tvaru, v jakém je vydávají ERP konektory.

Balík staví na liquiddesign/eshop a nepotřebuje od shopu nic než konfiguraci. Rozdíly mezi verzemi 2.0–2.2 řeší uvnitř: repozitáře si bere přes DIConnection::findRepository(), chybějící tabulka nebo relace je null, ne chyba, a /v1/meta/capabilities řekne, co daný shop reálně vede.

Se sloupci, které mezi verzemi přibyly, se počítá zvlášť — chybějící sloupec v podmínce dotaz neshodí, jen se podmínka vynechá (Codebooks::hasColumn()). Týká se to eshop_product.deletedTs a eshop_price.hidden, které jsou až od eshopu 2.1; shop na 2.0 produkty měkce nemaže a ceny neskrývá, takže tam ty podmínky nedávají smysl. Běží to i na StORM 1.1 — balík z něj používá jen API, které je v 1.1 i 2.0 shodné.

Jen ke čtení. Žádný endpoint balíku nemění data; jiná metoda než GET/HEAD vrací 405. Zápis si může přidat projekt vlastním endpointem, viz Vlastní endpointy z projektu.

Instalace

composer require liquiddesign/eshop-doryo-api
extensions:
    doryoApi: DoryoApi\Bridges\DoryoApiDI

doryoApi:
    shopName: 'Můj shop'
    shopUrl: 'https://muj-shop.cz'
    # null = token se vezme z proměnné prostředí DORYO_API_TOKEN; bez tokenu je API vypnuté
    token: null

Služby, routy i mapování presenteru si balík zaregistruje sám. Zbývá jediné, co musí udělat projekt: pustit požadavek přes svou bránu na front. Projekty postavené na Base\Application mají v config/environments.neon seznam frontAccess.exclude — přidej do něj DoryoApi:Api, jinak brána odmítne požadavek dřív, než se dostane na token.

Dvě věci, na které se naráží na klasickém serveru:

  • Adresa shopu. shopUrl bývá v repu s produkční adresou; testovací server ji přepíše env proměnnou DORYO_API_SHOP_URL (má přednost před configem), třeba SetEnv ve vhostu. Bez toho rozcestník i odkazy na produkty ukazují na produkci, i když data jdou z testu.

  • Apache s PHP přes FastCGI/CGI hlavičku Authorization do PHP nepředává a API pak vrací 401 Chybí hlavička Authorization, i když je token správně. Do vhostu dej CGIPassAuth On (Apache ≥ 2.4.13), nebo do .htaccess:

    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    Balík si hlavičku z HTTP_AUTHORIZATION i REDIRECT_HTTP_AUTHORIZATION přečte sám.

Konfigurace

klíč výchozí k čemu
prefix doryo-api prefix cesty; routy se registrují podle něj
token null Bearer token; null = z env DORYO_API_TOKEN
shopUrl null veřejná adresa shopu v odkazech; env DORYO_API_SHOP_URL ji přepíše
allowIps [] whitelist IP/CIDR; prázdné = stačí platný token
currency CZK měna, ve které se vydávají částky bez vazby na ceník
defaultPricelists [] ceníky pro veřejnou cenu; prázdné = ceníky výchozí skupiny zákazníků
defaultCustomerGroup null skupina, ze které se ceníky vezmou; null = výchozí po registraci
orderStates viz níž mapa normalizovaný stav → stavy shopu
invoicePaymentTracked true eviduje shop úhrady faktur? kde je vede ERP, dej false
customerPrices false vydávat ceny konkrétního zákazníka (vědomá výjimka, viz níž)
defaultLimit 200 kolik záznamů vrátí seznam nebo report bez limit
maxLimit 1000 strop parametru limit
defaultWindowMonths 6 výchozí okno seznamů a reportů bez data; shop se statisíci objednávek si ho zkrátí
maxWindowMonths 24 nejdelší okno, které API přijme
extensions [] služby implementující DoryoApi\Extension\DoryoApiExtension

Výchozí mapa stavů je new: [open], processing: [received], delivered: [finished], cancelled: [canceled]. Stavy shipped a returned eshop nerozlišuje; shop, který si je vede po svém, si mapu přepíše.

Co API vydává

Kořen /{prefix} vrací rozcestník (odkaz na openapi.json, health a capabilities) — kdo si adresu otevře v prohlížeči, dostane odpověď API, ne stránkovou 404 shopu.

Zbytek je na /{prefix}/v1/…, seznamy v obálce { items, nextCursor, hasMore }, částky jako řetězec s měnou ({"amount": "12500.00", "currency": "CZK"}), chyby v application/problem+json česky. /{prefix}/openapi.json popisuje endpointy pro introspekci.

  • zákazníci — seznam, detail, jejich objednávky, faktury, souhrn, odebírané položky
  • objednávky — seznam a detail s položkami, historie změn, zásilky a balíky
  • faktury — seznam a detail, diagnostika úhrady
  • produkty a sklad — katalog s parametry, sklad s rozpadem po skladech, diagnostika viditelnosti a obrázků
  • ceny — ceníky, ceny z ceníku a (volitelně) ceny konkrétního zákazníka
  • reporty — tržby, top produkty, růst a pokles zákazníků, pohledávky, churn, pokrytí zásob, expedice, hodnocení, importy, zdraví katalogu, doklady bez protějšku
  • orientacemeta/capabilities, meta/codebooks, categories, suppliers, search

Aby odpověď nešla přečíst špatně

Odpověď čte model, ne člověk, takže tři věci nejdou nechat na domýšlení:

Prázdný seznam řekne, jestli za tím není jen výchozí okno. Seznamy a reporty bez zadaného rozsahu berou posledních defaultWindowMonths měsíců. Když se okno vzalo z výchozí hodnoty, odpověď to přizná v window — a je-li výsledek prázdný, přidá i note:

{
  "items": [],
  "nextCursor": null,
  "hasMore": false,
  "window": { "from": "2026-03-04", "to": "2026-09-04", "params": ["createdFrom", "createdTo"], "defaulted": true },
  "note": "Prázdné nemusí znamenat, že záznamy nejsou: bez createdFrom a createdTo se bere posledních 6 měsíců…"
}

Bez toho nejde rozeznat „zákazník nic neodebral" od „data jsou starší, než kam výchozí okno sahá" — obojí je items: []. Když si rozsah zadáš sám, window ani note v odpovědi nejsou; víš, na co ses ptal, a nemá smysl tím ujídat kontext.

Překlep v názvu parametru je 400, ne ticho. ?zakaznik=… nebo ?CreatedFrom=… by se jinak zahodily a vrátila by se nefiltrovaná data, která vypadají jako odfiltrovaná. Chyba vyjmenuje, co daný endpoint zná:

Neznámý parametr zakaznik. Tenhle endpoint zná: createdFrom, createdTo, cursor, customerId,
limit, q, status… Úplný popis je v /openapi.json.

Kód produktu se hledá ve všech podobách, v jakých ho člověk píše. Shop má kód rozdělený na code a subCode a každý je skládá jinak: v databázi je 37214 + 1, na dokladu z K2 37214.01, v jiném shopu 37214.1. Kdyby se filtry ptaly jen na code, kód z faktury by nenašel nic. Hledá se proto přes syrový kód, kód s podkódem doplněným na dvě místa i bez doplnění, kód dodavatele a katalogovou podobu bez dodavatelského prefixu — a zadaná hodnota se sama roztáhne na podobu s vodicí nulou i bez ní. Když si shop vede celý kód ve vlastním sloupci eshop_product.fullCode (Levior), bere se i ten. Platí to pro code/codes i pro fulltext q a /v1/search.

Nejdřív se zeptej, co shop vede

GET /v1/meta/capabilities řekne u každé domény, jestli ji shop používá, kolik má záznamů a kdy do ní naposled něco přibylo. Bez toho nejde poznat rozdíl mezi „dnes nic" a „tohle se tu nepoužívá" — a modelu se pak snadno stane, že si domyslí odpověď.

Kontrola se neptá jen na existenci řádků, ale na použitelné řádky. Ověřeno v praxi: shop měl 65 tisíc řádků recenzí, ve kterých nebylo ani jedno vyplněné hodnocení — byly to odeslané žádosti o hodnocení, ne recenze.

Co API nikdy nevydá

Odpověď skládá mapper pole po poli, nikdy toArray() entity. Ven nejdou hesla ani hashe, tokeny, nákupní ceny a marže, interní poznámky adminů ani platební údaje. Osobní údaje zákazníků ano — Doryo je pseudonymizuje na své straně a cenzurovat je tady by API znehodnotilo.

Jediná vědomá výjimka je customerPrices. Ceny konkrétního zákazníka jsou obchodní tajemství a ve výchozím stavu jsou vypnuté (endpoint vrací 403); bez nich ale nejde sestavit cenová nabídka, tak ať je to rozhodnutí shopu, ne balíku.

Rozšíření o vlastní pole

final class MojeRozsireni implements DoryoApi\Extension\DoryoApiExtension
{
    public function extendOrder(Eshop\DB\Order $order, array &$out): void
    {
        $out['eshop']['channel'] = 'web';
    }
    // extendCustomer(), extendProduct()
}
doryoApi:
    extensions: [@mojeRozsireni]

Rozšíření smí přidávat jen do klíče eshop — standardní pole mapper po zavolání vrátí zpátky, aby se nedal rozbít kontrakt s Doryo.

Indexy

Seznamy i reporty jdou oknem podle data objednávky a eshop na eshop_order.createdTs index nemá. Bez něj každé volání projde celou tabulku objednávek; na shopu se statisíci objednávek to jsou vteřiny navíc u každého reportu. Balík do schématu nesahá — index založ v projektu shopu (migrace):

ALTER TABLE eshop_order ADD INDEX eshop_order_createdTs (createdTs);

Že chybí, hlásí GET /v1/meta/health v poli warnings.

Reporty nad položkami (top-products, replenishment, sales?groupBy=category|producer) jdou jedním dotazem vedeným od objednávek (STRAIGHT_JOIN), ne přes seznam id nákupů v PHP — ten na čtyřiceti tisících objednávek za půl roku trval déle, než klient čekal. I tak platí: bez from a to se počítá celé výchozí okno; kdo se ptá na poslední týdny, má je zadat.

Vlastní endpointy z projektu

Balík nese jen to, co má každý shop na liquiddesign/eshop stejné. Co je jen tenhle projekt (zápis do jeho administrace, jeho vlastní doména), si přidá projekt sám: služba implementující DoryoApi\Endpoint\Endpoint se zaregistruje v endpoints: a balík ji přidá do routeru i do openapi.json.

final class AdminEndpoint implements
    DoryoApi\Endpoint\Endpoint,
    DoryoApi\Endpoint\MethodAware,
    DoryoApi\OpenApi\SpecificationPart
{
    /** @return array<string, string> vzor cesty => jméno obsluhy */
    public function getRoutes(): array
    {
        return ['v1/admin/pages' => 'pages', 'v1/admin/forms/{presenter}/{form}' => 'form'];
    }

    /** @return array<string, array<string>> bez téhle metody umí endpoint jen GET/HEAD */
    public function getMethods(): array
    {
        return ['v1/admin/forms/{presenter}/{form}' => ['GET', 'POST']];
    }

    /** @param array<string, string> $params */
    public function form(array $params, DoryoApi\Http\Query $query): DoryoApi\Http\Response
    {
        // $query->getMethod() rozliší čtení od zápisu, $query->getBody() je rozparsované JSON tělo
        return new DoryoApi\Http\Response(['ok' => true]);
    }

    /** @return array<string, mixed> */
    public function getOpenApiPaths(): array
    {
        return ['/v1/admin/pages' => ['get' => ['summary' => 'Mapa administrace', ...]]];
    }

    /** @return array<string, mixed> */
    public function getOpenApiComponents(): array
    {
        return [];
    }
}
services:
    adminEndpoint: App\DoryoApi\AdminEndpoint

doryoApi:
    endpoints: [@adminEndpoint]

Pravidla:

  • Výchozí stav je jen ke čtení. Bez MethodAware vrátí cokoliv jiného než GET/HEAD 405. Metody se hlásí per vzor cesty, takže jedna cesta může být jen ke čtení a druhá i k zápisu.
  • Pořadí kontrol je token → routa → metoda. Neznámá cesta je 404 i u POSTu, známá cesta se špatnou metodou 405 se seznamem toho, co umí.
  • Tělo se u zápisových metod rozparsuje z JSONu (neplatný JSON = 400 problem+json) a předá se v Query::getBody(). Signatura obsluh zůstává method(array $params, Query $query): Response.
  • Popis do openapi.json dodá endpoint přes SpecificationPart. Vestavěné cesty a schémata mají přednost — projekt smí přidávat, ne přepisovat. Schémata těla piš inline: parser v Doryo resolvuje $ref jen u parametrů, u schémat těla ne.
  • Chyby se vracejí přes DoryoApi\Http\ApiException (400, 401, 403, 404, 405, …), aby odpověď byla application/problem+json jako ve zbytku API.
  • Log volání nese metodu a u zápisu jen velikost těla, nikdy jeho obsah.

Testy

php tests/smoke.php https://muj-shop.cz/doryo-api <token>

Devadesát kontrol přes HTTP: autentizace, 405 na zápisové metody, tvar obálek, částky jako řetězce, stránkování kurzorem, stropy a validace parametrů, ceny, diagnostika, reporty, whitelist polí a platnost OpenAPI. Jede to schválně po drátě, ne přes DI kontejner — testuje se i routa a autentizace.