Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Moduly / E-shop

E-shop#

Největší modul systému — 84 entit, 40 managerů, 31 Api presenterů (ls app/UI/Api/Eshop/Models/Entities/ | wc -l). Katalog, košík, objednávka, reklamace, feedy pro srovnávače. Kód: app/UI/Api/Eshop/, administrace app/UI/Admin/Eshop/, front app/UI/Front/Eshop/.

Z čeho se modul skládá#

Oblast Hlavní entity Kdo to orchestruje
katalog ProductProductVariantProductSet, ProductCategory, Producer, ProductTag ProductSetManager, ProductManager
ceny ProductSetPrice (časové snímky), Vat resolver v OrderSnapshotBuilder
košík Cart, CartItem CartManager
objednávka Order + OrderItem, OrderShipper, OrderPaymentMethod, OrderVoucher, OrderGift, OrderStatus OrderCheckoutManager, OrderTransitionManager
doprava a platba Shipper, ShipperTariff, ShipperRegion, PaymentMethod ShipperTariffManager
slevy Voucher, Gift VoucherManager, GiftManager
reklamace Complaint, ComplaintStatus, ComplaintSolution ComplaintTransitionManager
feedy Feed, FeedParameterAction FeedManager

Kromě managerů má modul i generátory (OrderInvoiceGenerator, OrderEmailGenerator, VoucherGenerator), helpery bez I/O (OrderCalculator) a procesní služby (OrderSnapshotBuilder).

Prodává se sada, ne produkt#

Product          produkt — marketingová jednotka (název, popis, kategorie)
  └ ProductVariant   varianta (barva, velikost)
      └ ProductSet     prodejní jednotka — TA má cenu, sklad a čárový kód

Košík, sklad i faktura pracují se SADOU

Produkt je jen obal. Kdo hledá cenu nebo skladovou dostupnost na Product, nenajde ji — a protože relace existují, nedostane chybu, jen prázdno.

Ceny jsou časové řady, ne sloupec#

Cena je snímek klíčovaný časem zveřejnění, množstevní hranicí, skupinou uživatelů a měnou. Historie se tím dá dohledat a naplánované ceny fungují bez zásahu.

Ceník se needituje, přidává se nový snímek

Změna staré ceny přepíše historii — a přepočet staré objednávky pak vyjde jinak, než jak byla zaplacená.

Naplánovaná cena potřebuje cron

/cron/eshop/price/recalc běží à 15 minut. Delší interval znamená, že se sleva spustí později, než měla — a nikdo to nehlásí.

Uložená cena je vždycky základ bez DPH#

Kontrakt platí pro všechny cenové světy modulu — ceny sad, poplatky sady, doprava i platba. Cena s DPH se dopočítává, nikdy neukládá. Závazný popis je v docbloku OrderSnapshotBuilder.

Vrstva Kdo dopočítává
snímek objednávky OrderSnapshotBuilder + OrderCalculator::withVat() (bcmath, mezivýpočet na 6 míst, round3 half-up)
zobrazení na frontu PriceDisplayHelper — záměrně shodná aritmetika, aby se cena na detailu trefila do haléře na tu z checkoutu

Jestli se má na frontu dopočítat, říkají dva příznaky prodejce Invoiceru (cms_mod_invoicer_sellers): use_vat (plátcovství) a show_prices_with_vat (zobrazovací přepínač). Dopočítává se jen u plátce se zapnutým přepínačem; PriceDisplayHelper::vatNoteMode() vrací null pro neplátce — pak se o DPH v šabloně nesmí zmínit vůbec.

Nastavení e-shopu 4/priceWithVat zaniklo

Ceník je základ bez DPH bezpodmínečně. Kód, který se ptal na tuhle volbu, nemá co nahradit — otázka „v jakém režimu je ceník“ už nemá smysl.

Denormalizovaný sloupec product_sets.price je ZÁKLAD

Řazení ceny (o-2/o-3) i všechno, co nad tím sloupcem filtruje, pracuje se základem — ne s číslem, které vidí zákazník. Při jediné sazbě je pořadí stejné, při míchaných sazbách se rozejde.

Starší snímky objednávek se PD nepřepočítaly

Uložená čísla se jen reinterpretovala. Snímky jsou immutable, takže objednávky z doby před etapou PD nejsou dotčené — nesnaž se je „opravit“.

Postup: zakládám objednávku z kódu#

Objednávka vzniká dvěma cestami a nejsou zaměnitelné:

Cesta Vstup Kdo počítá ceny
checkout (OrderCheckoutManager) z prohlížeče přijde jen setId + počet, tariffId, paymentMethodId, kódy voucherů a adresa server z katalogu přes resolver
admin (OrderPresenter::getBuildData()) ceny a sazby přímo z formuláře administrátor (je to důvěryhodná cesta)
  1. Z frontu vždy přes OrderCheckoutManager. Neexistuje pole, kterým by šlo cenu podvrhnout — a to je celý smysl toho, že je to samostatná služba.
  2. Rezervace je striktní (allowPartial = false): zákazník nesmí zaplatit nekryté zboží. Pre-flight kontrola rozhodne před zápisem, finálním rozhodčím je reserve() po commitu.
  3. Chybějící prodejce je tvrdá chyba. Bez nastavení 4/invoicer.sellerId objednávka nevznikne — zákazník dostane srozumitelnou hlášku, technický detail jde do Tracy kanálu eshop.

Admin cesta bere ceny z formuláře, checkout nikdy

Kdo by chtěl checkout „zjednodušit“ tím, že přijme cenu z klienta, otevře tím možnost koupit cokoli za korunu. Rozdělení na dvě služby je právě proto.

Postup: měním stav objednávky#

Jediný vstupní bod je OrderTransitionManager::apply($orderId, $newStatusId). Admin formulář, platební hook i crony volají vždycky tohle, nikdy vlastní UPDATE.

  1. Stav nese příznaky akcí, které se při přechodu provedou — potvrdit sklad, stornovat rezervaci, vystavit fakturu, odeslat e-mail.
  2. Celý přechod běží pod per-order zámkem cms_eshop_order_{id} (GET_LOCK, 5 s), který serializuje souběžné přechody a editace.
  3. E-maily jdou do fronty až po commitu — aby se neodeslaly k přechodu, který se nakonec neuložil.
  4. Idempotence je ve třech vrstvách: zámek, SourceRef ve skladu (opakovaná rezervace týchž řádků je no-op) a lookup existující faktury podle (orderId, invoiceTypeId).

Přeskočení stavu přeskočí i jeho akce

Skok z potvrzené rovnou na dokončenou znamená, že se sklad nikdy neodepsal. Objednávka vypadá dokončeně, zboží je pořád rezervované a rozdíl se najde až při inventuře.

Pořadí zámků je vždy eshop → store, nikdy obráceně

Store si drží ještě vlastní zámek nad zdrojem. Opačné pořadí by při souběhu vyrobilo uváznutí.

Součtová pole objednávky jsou past#

Jsou to dvě nezávislé dvojice a jejich jména klamou:

Pole Co v něm je
totalPrice / totalPriceWithVat total celé objednávky — doprava a platba už přičtené, vouchery odečtené
rounding zaokrouhlení částky bez DPH
roundingVat zaokrouhlení částky s DPH — ne „DPH ze zaokrouhlení“
roundedTotalPrice / roundedTotalPriceWithVat total po zaokrouhlení (ceil na celé jednotky měny)
totalPrice        = itemsTotal        + shipping.price        + payment.price        − discount
totalPriceWithVat = itemsTotalWithVat + shipping.priceWithVat + payment.priceWithVat − discountWithVat
rounding / roundingVat = rounded − total          (vždy ≥ 0)

Souhrn, který dopravu a slevy vypisuje jako samostatné řádky, je započítá DVAKRÁT

totalPrice je už obsahuje. Souhrn po řádcích musí vyjít z itemsTotalPrice*, ne z totálu. Změřeno živě: řádky 5 302,86 + 301,29 proti uvedenému totálu 5 303,00.

roundingVat NENÍ DPH ze zaokrouhlení

Je to zaokrouhlení částky s DPH. Souhrn v cenách s DPH proto musí brát roundingVat; se špatným polem vyšlo 0,47 tam, kde patřilo 0,43 — a faktura přitom ukazovala u téže objednávky jinou hodnotu, protože generátor to má správně.

Dárky do totálu nevstupují a záporný výsledek se neořezává

Dárek je zdarma. Proti zneužití voucherů stojí validace minimálního košíku, ne ořezání totálu na nulu.

Sklad#

Napojení je přes sadu. Rezervace vzniká při objednávce, odpis při přechodu do stavu s příznakem skladu. Podrobně Sklad.

Fakturace#

Objednávka si vyžádá doklad přes OrderInvoiceGenerator. Vazba na odběratele jde přes kartotéku (Customer), ne přímo na uživatele — týž člověk může fakturovat na firmu i na sebe. Podrobně Fakturace.

Zaokrouhlení počítá e-shop, ne fakturace

Když je rounding > 0, přidá se na fakturu extra řádek „Zaokrouhlení“. Invoicer sám zaokrouhlení nepočítá.

Reklamace#

Vlastní stavy s vlastními příznaky, obdobně jako objednávka — jediný vstupní bod je ComplaintTransitionManager. Lhůty se počítají ve dnech a hlídá je cron /cron/eshop/complaint/expire (denně).

Naplánované úlohy#

Endpoint Co dělá Doporučený interval
/cron/eshop/price/recalc aktivuje naplánované ceny à 15 minut
/cron/eshop/order/expire ruší nezaplacené objednávky a uvolňuje rezervace à hodinu
/cron/eshop/order/invoice dogeneruje faktury, které nevznikly hned à 15 minut
/cron/eshop/complaint/expire posune reklamace po lhůtě denně
/cron/eshop/cart/cleanup maže opuštěné košíky denně v noci
/cron/eshop/feed/generate XML feedy pro srovnávače à hodinu

Nespuštěná úloha nevyhodí chybu ani nic nezaloguje

Jen se tiše nic neděje. Pozná se to podle důsledku — třicet tisíc košíků v databázi, nezaplacené objednávky držící rezervace, chybějící faktura.

Editace sady v administraci#

Průvodce s atomickým odesláním — celý formulář se uloží najednou.

Tovární metody sady berou parametry POZIČNĚ

Prohození dvou parametrů se neprojeví chybou, ale špatnou jednotkou nebo množstvím. Sada se uloží, čísla budou nesmyslná a přijde se na to ze skladu.

Formulář sady se nesmí překreslovat po částech

Rozpracované nahrávání by zmizelo — podrobně Formuláře, oddíl o nahrávání souborů.

Filtry a hledání#

Facety mají vlastní pravidla: počty u voleb se zužují vším kromě vlastní facety, jinak by výběr druhé hodnoty téhož filtru ukazoval nulu. Katalog je zároveň 5. zdrojem Elasticsearch — s SQL fallbackem, když ES neběží.

Rozsahový filtr potřebuje round-trip přes adresu

Bez něj se hodnota ztratí mezi požadavky a filtr tiše nefiltruje — vrátí všechno a tváří se, že je nastavený.

Kam sáhnout#

Chci Kde
vznik objednávky z košíku app/UI/Api/Eshop/Models/Managers/OrderCheckoutManager.php
změnu stavu app/UI/Api/Eshop/Models/Managers/OrderTransitionManager.php
přepočet částek app/UI/Api/Eshop/Models/Helpers/OrderCalculator.php
sestavení řádků ze serveru app/UI/Api/Eshop/Models/Services/OrderSnapshotBuilder.php
fakturu z objednávky app/UI/Api/Eshop/Models/Generators/OrderInvoiceGenerator.php
skladové efekty app/UI/Api/Eshop/Models/Managers/OrderStockManager.php
cron úlohy app/UI/Cron/Eshop/Presenters/

Navazující kapitoly: Sklad · Fakturace · Elasticsearch · Filtry · Naplánované úlohy