Sdílené principy

DROP = systém kdy marketplace (Alza, Mall, …) přijme objednávku od zákazníka, ale fakturuje a expeduje Vapol. Apivapol funguje jako most mezi marketplace API a Karatem:

  1. Příjem objednávky — buď push (marketplace zavolá náš REST endpoint), nebo pull (apivapol periodicky polluje API).
  2. Lokální záznam — DB tabulky api_{drop}_orders + items + address.
  3. Karat insertuser_imp_w3_obj_h + user_imp_w3_obj_p (Doctrine entity W3ImpObjednavkaHlav).
  4. Karat confirm — rezervace zboží + nastavení user_typ_expedice = '{drop}-drop-0' (literál na který Karat reaguje, viz feedback_preserve_sova_literals).
  5. Marketplace confirm — vrátíme marketplace náš Karat doklad / shipment number.
  6. Tracking — sync status doručení (DPD, Ulozenka, …) zpět do marketplace.
  7. Billing — měsíční CSV exporty pro účtárny.

Karat literál Waitting for Confirm v poznámce (překlep tt záměrný — Karat na něj reaguje) drží objednávku ve stavu „čeká na potvrzení" do Confirm fáze.

Překlad chyb dopravců do CZ (create-shipment)

Skladníci při createShipment dostávali z API dopravců (DPD, PPL, GLS, Packeta, UPS, FedEx, Geis, Messenger, InPost) chyby v angličtině a nerozuměli jim. CarrierErrorTranslator je překládá do věcné české hlášky, aniž by měnil existující API kontrakt.

Fallback řetězec
  1. Static pattern (~0.1 ms, v PHP kódu) — 39 patternů napříč 9 dopravci v Services/ErrorTranslation/Patterns/.
  2. DB cache — tabulka carrier_error_translation (PG), source = 'ai' | 'manual', hash normalizuje dynamické části (UUID, ID, timestampy).
  3. AI (Google Gemini) — jen když allowAi = true (pouze createShipment akce) a rate limit OK; výsledek se uloží do cache.
  4. Raw fallback — když vše selže, vrátí se originál s prefixem dopravce + Tracy log.
Odpověď

Nový field errorCz na BaseResultEntity (setError path), nebo exception context ['errorCz' => …, 'source' => …] u throw-pattern controllerů — BC zachována.

AI backend (Gemini)
  • Klient App\Services\Ai\GeminiClient (implementuje AiTextClient) — veřejné Generative Language API (generativelanguage.googleapis.com) + API klíč, ne Vertex AI / ADC (SovaNG běží on-prem, ne na GCP).
  • Config v local.neon: parameters.ai.geminiApiKey (prázdný = AI vypnutá, běží jen static + cache + raw), model v ai.geminiModel.
  • Rate limit 6 AI volání / hodinu (server-wide, přes COUNT nad carrier_error_translation WHERE source='ai').
  • AnthropicClient (Claude) zůstává v kódu jako autowired: false pro případný návrat.

Model se nastavuje aliasem gemini-flash-lite-latest, ne pinned verzí — Google blokuje gemini-2.5-flash-lite pro nové projekty (404 „no longer available to new users"). Volání má thinkingBudget: 0 (textová transformace, ne uvažování) a hard timeout 8 s, aby AI nezpomalila odpověď skladovému systému. Po změně configu je nutné smazat temp/cache (zkompilovaný DIC).

Push-styled (Alza volá naše REST endpointy). Dva partneři rozlišeni přes customer_id: 1017086 (VAPOL, Karat path) a 74 (BIORUZE, L4U path).

Endpointy (API V1, prefix /api/v1)
  • POST /drop/alza/order/{order} — Insert: lokální DB + Karat doklad + rezervace + poznámka „Waitting for Confirm".
  • POST /drop/alza/order/{order}/confirm — Confirm: dispatch v Karatu + přepis poznámky na „Potvrzeno".
  • DELETE /drop/alza/order/{cancelOrder} — Storno objednávky.
  • POST /drop/alza/order/{order}/extend — Prodloužení platnosti rezervace.
  • GET /drop/alza/order/{order}/delivery-address?customerId=X — Doručovací adresa objednávky (data 1:1 se starou sovou): deliveryAddress klíčovaný orderId (10 polí adresy + 6 z objednávky) + errorCode/errorMessage. Nahrazuje sova /api/vapol/alza/{doklad}/orderDeliveryAddress; data skládají modely (AlzaOrderAddress::toApiArray + AlzaOrder::getDeliveryApiExtraFields).
Zpětná kompatibilita starých sova URL

Po přepojení DNS sova.vapol.cz → apivapol volá Alza dál staré URL, na které je nakonfigurovaná: /api/alza/order/… (stará sova, Nette Restful). Tvar cesty za order je shodný s novým Apitte API, liší se jen prefix. BcUrlRewriteDispatcher přepíše cestu před routingem:

  • /api/alza/order/{…}/api/v1/drop/alza/order/{…} (insert / confirm / cancel / extend)

Rozsah je jen Alza order endpointy (Alza push). Staré /api/vapol/* (shipment / delivery / tracking) jsou interní WMS, ne Alza — mimo BC. Nelze řešit přes Apache rewrite (Apitte matchuje cestu z REQUEST_URI, interní rewrite ji nezmění) ani request decoratorem (běží až po routingu) — proto custom dispatcher. Query string (?token=) zůstává zachovaný.

Pozor (bezpečnost): AlzaController ?token= čte, ale nevaliduje, a RequestAuthenticationDecorator (IP allowlist Alzy) je v services.neon zakomentovaný → order endpointy jsou aktuálně bez ověření. Před ostrým přepojením DNS zvážit aktivaci auth decoratoru.

Klíčové soubory
  • app/Api/Module/V1/Drop/AlzaController.php
  • app/Api/Dispatcher/BcUrlRewriteDispatcher.php (BC staré sova URL → Apitte, registrace api.core.dispatcher v services.neon)
  • app/Model/Service/Drop/AlzaManager.php
  • app/Services/RemoteApi/AlzaApiService.php
  • app/Model/Context/LocalTables/AlzaOrder.php (konstanty ALZA_DROP_*, LOCAL_STATE_*, DELIVERY_ACTION_*)
  • app/Presenters/AlzaDropPresenter.php (admin grid + detail)
Local state číselník (api_alza_orders.local_state)

Jednotný číselník v AlzaOrder::LOCAL_STATE_META — pouze tyto hodnoty se zapisují. Static getLocalStates() vrací mapping klíč → CZ label pro grid filter + badge renderer.

KonstantaHodnotaPopis
LOCAL_STATE_CREATEDcreatedPo Insert — Karat doklad vytvořen
LOCAL_STATE_CONFIRMEDconfirmedPo Confirm — Karat dispatch OK
LOCAL_STATE_SHIPMENT_CREATEDsccreateShipment OK
LOCAL_STATE_SHIPMENT_DEPARTUREsdshipmentDeparture OK — odesláno
LOCAL_STATE_SHIPMENT_DELETEDshipmentDeleteddeleteShipment OK
LOCAL_STATE_DELIVERY_REJECTEDdeliveryRejectedAlza delivery Rejected
LOCAL_STATE_DELIVERY_CANCELEDdeliveryCanceledAlza delivery Canceled
LOCAL_STATE_STORNOstornocancelOrder z Alzy
LOCAL_STATE_UNAVAIL_ITEMSunavailableFatal: nedostupné položky
LOCAL_STATE_SD_ERRORsd_errorFatal: shipmentDeparture selhal
LOCAL_STATE_SHIP_CANCEL_ERRORship_cancel_errorFatal: deleteShipment selhal
LOCAL_STATE_ERRORerrorFatal: generic (dispatch/import fail)

DELIVERY_ACTION_REJECTED / _CANCELED (hodnoty Rejected / Canceled) NEJSOU local_state — jsou to inbound tokens z Alza deliveryResult API. AlzaOrder::setDeliveryResult() je mapuje na interní LOCAL_STATE_DELIVERY_REJECTED/_CANCELED.

Cron / diagnostika
  • /console/alza-avail/avail?type=update&partner=… — availability sync (VAPOL: přes KaratAvailabilitySource z w_SOVA_availability_v2, BIORUZE: přes L4U)
  • /console/alza-test-vapol/insert-and-confirm?nomen=…&skipStorno=1 — E2E test proti ostré Karat DB (Insert + Confirm + volitelně Storno)
Ruční storno (admin detail)

Tlačítko „Stornovat" na AlzaDropPresenter detailu → handleStornoAlzaManager::adminStornoOrder. Routuje podle partnera: VAPOL = storno v Karatu (procedura user_o_obj_storno_op, guard canCancelKaratOrder) + lokální storno stav; BIORUZE jde přes L4U (ne Karat) → jen lokální storno, L4U ručně. Do Alza API neposílá nic. Tlačítko je partner-aware (jiný text pro VAPOL vs BIORUZE).

Idempotence + email notifikace

Opakovaný order/insert pro stejnou existující objednávku vrátí errorCode 0 (success), ne 409. Sověžij chování 1:1. Fatal state → email na monitor@vapol.cz přes DropErrorNotifier (viz Sdílená vrstva níže), error_notified_ts guarduje před opakovaným spamem.

Historický fix (2026-07-10): confirmAlzaOrder při chybě aktualizace lokální objednávky/adresy dělal return $e (Throwable), ale AlzaController::confirmOrder návratovou hodnotu nekontroluje → Alze se vracelo HTTP 200 success, přestože confirm interně selhal a objednávka zůstala ve error stavu. Fix: vyhodit ClientErrorException, kterou controller přemapuje na 400. Zároveň odstraněny leftover bdump() z produkčních Alza/import cest (leak HMAC tokenu při zapnutém debug módu).

Pull-styled. Apivapol pollá Allegro REST API (OAuth2 token refresh). DPD CZ shipment pro objednávky z polské Allegro.

Klíčové soubory
  • app/Model/Service/AllegroManager.php
  • app/Services/RemoteApi/AllegroApiService.php
  • app/ConsoleModule/Presenters/AllegroSyncOrders*Presenter.php
  • app/Presenters/AllegroPresenter.php (admin)
Cron actions
  • console-allegro-sync-orders — import nových objednávek
  • console-allegro-sync-offers — sync stavů offer
  • console-allegro-sync-orders-shipping — shipment + invoice
  • console-allegro-messaging — komunikace s zákazníky

Typ_expedice allegro-drop-0, sove Allegro pattern zachován.

Kaufland Global Marketplace. Specifikum: 1 Kaufland order může obsahovat víc units (per-sku samostatné objednávky), které sloučíme do 1 Karat dokladu.

Klíčové soubory
  • app/Model/Service/KauflandManager.php
  • app/Services/RemoteApi/KauflandApiService.php
  • app/Model/Context/LocalTables/KauflandOrderUnitOrder.php (konstanta KOD_CENIKU)
  • app/ConsoleModule/Presenters/Kaufland*Presenter.php

Typ_expedice kaufland-drop-0, typ_uhrady PK, kod_dopravy D01.

Historický bug (2026-07-02): variable shadowing v KauflandOrdersPresenter::actionDownloadOrders — vnější $ordersUnits (all nedokladované) přepisoval vnitřní $orderUnits (aktuální group), takže všechny units napříč všemi Kaufland orders dostávaly stejný karat_doklad + www_doklad. Fix: použít $orderUnits uvnitř foreach cyklu. Detekce obětí: SELECT www_doklad, COUNT(DISTINCT kaufland_order_id) FROM kaufland_order_unit GROUP BY www_doklad HAVING COUNT(DISTINCT kaufland_order_id) > 1.

Decathlon B2B partner. Pull objednávek + shipment notifications.

Klíčové soubory
  • app/Model/Service/DecathlonManager.php
  • app/Services/RemoteApi/DecathlonApiService.php
  • app/ConsoleModule/Presenters/DecathlonSyncOrders*Presenter.php
  • app/Presenters/DecathlonPresenter.php (admin)

Typ_expedice decathlon-drop-0.

Mall Slovenia. Pull pattern (apivapol je klient, polluje Mall API). MPAPI SDK NEPOUŽÍVÁM — vlastní tenký REST klient.

Klíčové konstanty
  • MallOrder::MALL_SI_PARTNER_ID = '01034818' (Karat fakturační partner)
  • MALL_DROP_RADA_SI = 'ML', MALL_DROP_DENIK = '31'
  • MALL_DROP_EXPEDITION_TYPE = 'mall-drop-0'
  • MALL_WAITING_FOR_CONFIRM_NOTE = 'Waitting for Confirm' (překlep záměrně 1:1)
  • MALL_WWW_DOKLAD_PREFIX = 'MLL'
  • MALL_TRACKING_URL_BASE_BY_DELIVERY_METHOD (base URL pro tracking odkaz; zatím prázdné — doplnit reálné SI URL)
REST endpointy (pro sklad Lora / WMS)
  • GET /api/v1/drop/mall/order/{doklad}/status (MallController) — aktuální stav Mall objednávky živě z Mall API. Vstup = Karat doklad (nebo Mall order_id). Vrací {orderStatus: <celé tělo Mall odpovědi>, errorCode, errorMessage} (data 1:1 se sovou). errorCode -1 = neplatná objednávka → HTTP 400; 1 = Mall neodpověděl (orderStatus=false) → HTTP 200. Nahrazuje sova /api/vapol/mall-order/{doklad}/orderStatus.
  • GET /api/v1/drop/mall/order/{order}/delivery-address?customerId=X (MallController) — doručovací adresa objednávky. Vrací identickou strukturu jako Alza EP (deliveryAddress[ORDER] = 16 polí + errorCode + errorMessage), aby ji Lora četla 1:1 pro obě platformy. Data skládají modely MallOrderAddress::toApiArray() (10 adresních polí, mapováno na Alza klíče: company→company_name, name→address_name, street→street_with_number, note='') + MallOrder::getDeliveryApiExtraFields() (6 order-level polí: shipping_carrier_identification z delivery_method, cod, currency, payment_vs=purchase_id, shipment_value=null). errorCode -1 = objednávka bez Karat dokladu → HTTP 400; -2 = adresa nenalezena → HTTP 400; 0 → HTTP 200. POZOR: na rozdíl od Alzy se customerId neporovnává (Mall customer_id = ID kupujícího, ne partner) — sova to řešila stejně. Replikace sova ApiVapolModule\MallPresenter::actionReadOrderDeliveryAddress.
  • POST /api/v1/drop/general/set-order-tracking-number — shipping-push (viz níže).
Push stavů do Mall

Do Mall se stav objednávky propisuje na těchto místech (jinak apivapol stavy jen čte):

KdyCo se pošleOdkud
Po importu (cron / ruční)confirmed=trueMallManager::importOneOrderFromDetail (krok 5)
Storno (cron)confirmed=truecancelOneOrder
Expedice (shipped)status=shipped + tracking_number (+ tracking_url)MallManager::setOrderTrackingNumber
Doručení / vrácení (cron)status=delivered|returned (+ delivered_at)applyTrackingStatusUpdate (check-delivery)
Ruční adminstatus + confirmed=trueadminConfirmOrderToMall / ruční nástroje

Shipping-push (shipped + tracking) je vstupní bod volaný externím expedičním systémem přes REST: POST /api/v1/drop/general/set-order-tracking-number s {drop:'mall', karat_doklad, tracking_number, carrier} (sdílený GenerallController). Teprve tímto se objednávka dostane do shipped, což je předpoklad, aby ji začal sledovat check-delivery cron (filtr status='shipped'). Port sova MallOrderPresenter::actionReadSetOrderTrackingNumber.

Ruční nástroje

/mall-drop/manual (odkaz „Mall — ruční nástroje" v menu) — admin formulář: (1) ruční import jedné objednávky podle order_id s krokovým logem (status nechat nevyplněný = potvrdit jen confirmed; stejné jako cron), (2) stažení a pretty-print JSON detailu objednávky z Mall (read-only).

Ruční storno: tlačítko na MallDropPresenter detailu → handleStornoMallManager::adminStornoOrder — storno v Karatu (procedura user_o_obj_storno_op, guard canCancelKaratOrder) + lokální cancelled. Na Mall API neposílá nic (na rozdíl od cron cancelOneOrder, který pushuje confirmed=true). Storno lze i z Karat detailu (KaratOrderPresenter, čistě op_zahlavi).

Transakční bezpečnost importu

importOneOrderFromDetail je idempotentní resumable automat řízený flagy (karat_doklad / reserved / confirmed): Karat insert → confirm → teprve pak MALL confirmed=true (pořadí brání „confirmed v Mall bez Karatu"). Anti-duplicita: ERP guard KaratOrderManager::getKaratDokladByWwwDoklad (unikátní www_doklad = MLL+orderId, ne op_zahlavi.objednavka — ta je cross-platform kolizní). Souběh cronů řeší PG advisory lock (MallManager::runOrderSyncLocked).

Cron actions
IntervalAkce
*/5api-get-unconfirmed-orders — import nových
*/10api-get-cancelled-orders — sync zrušení
*/15api-update-availability?type=update — dostupnost
*/30sync-order-status-by-mall — Mall status sync
0 * * * *check-delivery — DPD/Ulozenka tracking
0 2 * * *api-update-availability?type=full — full produkt sync
0 6 1 * *generate-order-list-csv — měsíční billing
Klíčové soubory
  • app/Model/Service/MallManager.php (orchestrace)
  • app/Services/RemoteApi/MallApiService.php (REST klient)
  • app/Services/RemoteApi/UlozenkaApiService.php (SI tracking)
  • app/Model/Context/LocalTables/MallOrder.php (konstanty)
  • app/ConsoleModule/Presenters/ConsoleMallPresenter.php (10 actions)
  • app/Presenters/MallDropPresenter.php (admin grid + detail + ruční nástroje + storno)
  • app/Api/Module/V1/Controllers/Drop/MallController.php (order status endpoint pro Loru)
  • app/Api/Module/V1/Controllers/Drop/GenerallController.php (shipping-push endpoint, větev mall)
BaseApiContext quirk

Mall response {result, paging, data}BaseApiContext::setValues bug, který přemapuje data na prázdné pole. Workaround: MallApiService::extractIds() / extractData() / extractPaging() přes getOrigValues().

Sdílená vrstva drop platforem

Namespace App\Services\Drop\ — reuse infrastruktura pro Allegro / Alza / Mall / Decathlon / Kaufland (viz feedback_drop_shared_interfaces memory). Cíl: nová drop platforma stačí implementovat interface, nemusí duplikovat email pattern, fatal-state handling, grid renderer.

TřídaÚčel
DropOrderInterface Kontrakt pro drop order entity. Implementují AllegroOrder, AlzaOrder, MallOrder, DecathlonOrder, KauflandOrderUnitOrder. Metody: getDropPlatformName(), getOrderExternalId(), getPartnerLabel(), hasFatalError(), getFatalStateLabel/Short(), getRecommendedAction(), getDetailUrl(), getErrorNotifiedTs(), markErrorNotified(), clearErrorNotified().
DropErrorNotifier Jednotný email pipeline. Odesílá na monitor@vapol.cz (from it@vapol.cz) když hasFatalError() a error_notified_ts je NULL. Po odeslání volá markErrorNotified() → idempotency guard proti spamu. Selhání mailer nesmí shodit business flow (try/catch + Tracy log).
DropSummary + DropSummaryProviderInterface Value object + kontrakt pro uniform „Total/OK/Cancel/Error" summary za N dní. Managery vrací DropSummary z getUnifiedSummary($days=30). Používá se v grid pill row nad každou drop stránkou + na Drop Dashboardu.
DropOrderSearchService Cross-platform search po karat_doklad / external ID / www_doklad / tracking_number (Alza pozor: cs_shipmentNumber, ne tracking_number). Vrací DropOrderReference[] — shallow objekty pro Dashboard search.
DropOrderRepository UNION SQL nad 5 tabulkami: findTodayOrders($limit=30), findFatalInbox($days=14, $limit=50). Používá Dashboard. Cross-platform SQL patří sem, ne do drop-specific manageru (feedback_no_sql_in_presenter).

Backward compat: Decathlon + Kaufland manageOrderError zachován v původním tvaru (odlišný email pattern it@vapol.czmonitor@vapol.cz + Kaufland má 2h cache rate limit) — nedotčeno kvůli produkční pipeline. Allegro + Alza managery volají DropErrorNotifier přes tenký wrapper manageOrderError().

Fatal state guard v DB

Sloupec error_notified_ts (timestamp) přidán na allegro_order, api_alza_orders, decathlon_order, kaufland_order_unit. Partial index pro efektivní inbox query:

CREATE INDEX api_alza_orders_fatal_unsent_idx
    ON api_alza_orders (created_ts DESC)
    WHERE local_state IN ('error','sd_error','ship_cancel_error','unavailable')
      AND error_notified_ts IS NULL;

Nomen lookup — detail produktu

KaratProductPresenter — samostatná diagnostika „proč mi obj padla na tomto nomen". 3 casey: (A) je ve w_web3_nomenklatury — Doctrine detail, (B) není ve web3 ale je v Karatu — raw dba.nomenklatura (SELECT *, 3-sloupcový layout), (C) neexistuje nikde — alert-danger.

Endpointy
  • GET /karat-product/ — search form (Nette Form component, redirect na detail)
  • GET /karat-product/detail/{id} — full-page detail (banner + body)
  • GET /karat-product/drawer/{id} — AJAX fragment (bez layoutu, setLayout(false)) pro Bootstrap Offcanvas
Klíčové soubory
  • app/Presenters/KaratProductPresenter.php
  • app/Presenters/templates/KaratProduct/_detail_body.latte (sdílený body — 3 case)
  • app/Presenters/templates/KaratProduct/detail.latte (full-page)
  • app/Presenters/templates/KaratProduct/drawer.latte (fragment)
  • app/Model/Karat/KaratService.php: getNomenklaturaRow($code) + getKartyRows($code)
  • app/Model/Service/KaratProductManager.php: getNomenFromKaratRaw($code) + getKartyRows($code)
Sklad JOIN
  • Case A (Web3): $web3->getSklad() (Doctrine LAZY) — dispKusu, datumDostupnosti, typSkladu, userPotvrzeneDod
  • Case B (Karat): SELECT * FROM dba.karty WHERE KARTA = ? per SKLAD ID — stav_rt real-time zůstatek, rezerva, objednáno
Global nomen drawer (Bootstrap 5 Offcanvas)

@layout.latte obsahuje globální #nomenDrawer Offcanvas + JS handler. Klik na libovolný odkaz s .js-nomen-drawer class → fetch('/karat-product/drawer/{code}') → inject do drawer body. Ctrl+/Cmd+/Shift+/middle-click → standardní <a href> navigace na full-page (fallback pro sdílení URL). Tracy Debug Bar je odstřižen z AJAX response na klientu.

Použití v templates: {include _nomen_link.latte, code => $item->getCode(), enabled => $localOrder->isVapol()} (shared include, backward compat přes $enabled=false → plain <code> bez linku). Aktivní v: AlzaDrop/detail.latte (jen VAPOL obj), SyncDropLogPresenter grid.

Deprecated metody

Web3Nomenklatura::getRezervovavoCount() a getActualCount() volaly neexistující getCartItems() (legacy relace w_web3_cart_items odstraněna). Označeno @deprecated, vrací bezpečné hodnoty (0 / dispKusu). Rezervace se v novém apivapol nedrží ve web3 — pokud potřebuješ real-time rezervace, čti z Karat op_polozky.

Drop Dashboard

/drop-dashboard/ — hlavní přehledová obrazovka pro drop sekci. Snapshot přes všech 5 platforem bez nutnosti procházet 5 gridů samostatně.

Sekce
  1. Agregate stats — 5 karet s brand barvami platforem (Allegro #FF5A00, Alza #00A884, Mall #E5006D, Decathlon #0082C3, Kaufland #E10915). Stat pilly „Celkem 30 dní / OK / Cancel / Chyby" per platforma přes getUnifiedSummary(30).
  2. Dnešní objednávky — UNION přes 5 tabulek (DropOrderRepository::findTodayOrders(30)), řazeno DESC.
  3. Fatal error inbox — obj s hasFatalError() max 14 dní zpět (findFatalInbox(14, 50)). Deep-link na detail obj.
  4. Sync status — poslední úspěšný sync per drop_name z sync_drop_log (SyncDropLogger::getLastRunPerDrop()).
Search bar

Nette Form component (createComponentSearchForm) → onSuccess redirect na ?q=…. Cross-platform lookup přes DropOrderSearchService::search($q) po karat_doklad, external ID, www_doklad, tracking_number. Alza tracking je v cs_shipmentNumber, ne tracking_number.

Sjednocené UI napříč gridy

Sdílený Latte include templates/_drop_pill_row.latte — 4 pill boxy (Total/OK/Cancel/Chyby) nad každým drop gridem. Renderuje se z $unifiedSummary = $manager->getUnifiedSummary(30). Platform-specific rozpad (Allegro error kategorie, Alza per-partner) je pod tím v každém default.latte samostatně.

Nomen
Otevřít celou stránku ↗

Načítání...

Karat objednávka
Otevřít celou stránku ↗

Načítání...