Inzerce#
Modul Bazaar — inzeráty s dynamickými poli, placené tarify, hlídací pes a odpovědi.
Kód: app/UI/Api/Bazaar/, front app/UI/Front/Bazaar/, administrace
app/UI/Admin/Bazaar/.
Z čeho se modul skládá#
| Oblast | Hlavní entity | Poznámka |
|---|---|---|
| inzerát | Advert + relace AdvertParameterRel, AdvertCategoryRel, AdvertAssetRel, AdvertFileRel |
téměř všechna data jsou v relacích, ne ve sloupcích |
| kategorie | Category, CategoryText, CategoryPrice, CategoryParameterRel |
nested set |
| parametry | Parameter, ParameterGroup, ParameterRule + ParameterRuleCondition/Source/Target |
pravidla řídí, které pole se kdy zobrazí |
| placení | Tariff, TariffPrice, TariffQuickPrice, PaymentItem |
|
| interakce | Response, AdvertFollower, AdvertReport, AdvertStatistic |
|
| hlídací pes | Watchdog, WatchdogEmail + relace |
uložené hledání |
Vlastní zakázaná slova modul nemá, viz Zakázaná slova.
Inzerát nemá pevná pole#
Název, popis, cena, e-mail i region jsou dynamické parametry (AdvertParameterRel).
Sloupce v tabulce jsou jen denormalizované kopie kvůli řazení a filtrování — ty se
totiž nad textovou hodnotou parametru dělat nedají.
| Klíč parametru | Denormalizovaný sloupec | Kdo ho plní |
|---|---|---|
price |
price (číslo, kvůli řazení a rozsahovému filtru) |
applyPriceColumn() v beforeSave() z kolekce parameterRels |
email |
email |
zápis inzerátu |
regionId |
region_id |
zápis inzerátu |
Přejmenování vyhrazeného klíče parametru tiše rozbije řazení
Nic to neohlásí — beforeSave() prostě nenajde, co má do sloupce zapsat. Řazení
podle ceny přestane fungovat a odpovědi přestanou chodit, protože se nemá kam
poslat e-mail. Vypadá to jako chyba výpisu.
Ceny v různých měnách se nepřepočítávají
Do sloupce jde holé číslo bez přepočtu. Řazení podle ceny tak míchá měny dohromady — 100 EUR se seřadí před 200 CZK.
Sloupec price má self-heal cron
/cron/bazaar/advert/recount-prices (à 10–15 minut) srovná nesrovnalosti po
importech a ručních zásazích do databáze. Je to pojistka, ne primární plnění.
Přeindexuje jen inzeráty, kterým se cena opravdu změnila; od 200 změn najednou spustí
úplný reindex.
Sloupec active počítá databáze#
cms_mod_bazaar_adverts.active je generovaný sloupec (VIRTUAL):
active = CASE WHEN approved_at IS NOT NULL AND confirmed_at IS NOT NULL
AND paid = 1 AND banned = 0 AND deleted = 0
AND deactivated = 0 AND sold = 0
THEN 1 ELSE 0 END
active nejde nastavit ani z administrace, ani z API
Mění se jedině zápisem některého z těch příznaků (včetně approved_at a confirmed_at). Entita ho má proto
označený insertable: false, updatable: false a navíc #[ExternalReadonly] —
bez toho druhého by setActive() přepsal hodnotu v paměti ještě před uložením
a odpověď by vrátila klientem vymyšlený stav místo skutečného.
Expirace v tom vzorci NENÍ
Prošlý inzerát zůstává active = 1; z výpisů ho vyřazuje až aplikační filtr. Kdo
si napíše vlastní dotaz jen na active = 1, dostane i inzeráty po expiraci.
Postup: přidávám dynamický parametr#
- Založ parametr a připoj ho ke kategorii (
CategoryParameterRel). - Volby výběru jsou děti parametru — parametr je kořen stromu, jeho potomci jsou jednotlivé volby.
- Rozhodni, jestli má mít denormalizovaný sloupec. Potřebuje ho jen to, podle čeho se řadí nebo filtruje rozsahem.
- Pravidla zobrazení (
ParameterRule) řeší, kdy se pole ukáže — podmínka nad jiným parametrem. - Prokliknij vložení i editaci inzerátu v té kategorii.
Smazání uzlu smaže celý podstrom
U parametru včetně všech jeho voleb — a s nimi hodnoty ve všech inzerátech, které tu volbu měly vyplněnou. Data se nedají vrátit.
Propsání cen nebo parametrů do podstromu je nevratné
Přepíše všechny podkategorie bez ptaní a bez zálohy. Na velkém stromu to znamená
stovky přepsaných kategorií jedním kliknutím. S cenami (meta['setPricesToChildren'])
se propíše i příznak „Zdarma“ (forFree), takže z placených podkategorií se stanou
bezplatné, nebo naopak.
Zápis inzerátu přes Api#
Externí klient (JWT) posílá inzerát na :Api:Bazaar:Advert:save. Externí meta se zahazuje
celé, takže vše, co administrace posílá v meta, musí klient poslat vnořené v data.
$graphDataKeys manageru to povoluje pro categoryIds, assets a files (viz
Klíče mimo sloupce).
Klíč v data |
Tvar | Kdy platí | Co se stane |
|---|---|---|---|
categoryIds |
int[] |
jen vložení, při úpravě se tiše ignoruje | syncCategoryRels() v afterSave(); paid podle Category::getForFree() |
categoryRels |
[{category:{id:{id:X}}, paid:bool}] |
vložení i úprava | kolekce v objektovém grafu; paid od klienta server přepíše (níž) |
parameterRels |
[{parameter:{id:{id:X}}, value:'…', value2?:'…'}] |
vložení i úprava | kolekce v objektovém grafu, páruje se podle dvojice inzerát + parametr |
assets, files |
[{type:'existing'\|'new', imageId?/fileId?, name?, data?, order?}] |
vložení i úprava | type:'new' nese obsah souboru v base64 v data; [] odebere všechny přílohy |
password |
— | nikdy | neposílá se ani nečte; e-mail po vložení nese odkaz „Správa inzerátu“ (Odkaz s klíčem) |
Ploché tvary {id, value} nebo {categoryId, paid} hydrátor nezná. U obou kolekcí v grafu
platí: chybějící klíč kolekci nechá, jak je, [] ji celou smaže.
paid u categoryRels na externí cestě ($meta['__external']) přepíše
CategoryRelsPaidSanitizationTrait v beforeSave():
| Vazba | Výsledné paid |
|---|---|
| už je v databázi | hodnota z databáze, klient ji nezmění ani nevynuluje |
| nová | Category::getForFree(), stejně jako u categoryIds |
Změnu zapíše do logu auth jako EXTERNAL_CATEGORY_REL_PAID_SANITIZED. Administrace paid
posílá z formuláře a přepis ji nepotká.
Vložení bez jediné kategorie odmítne beforeSave() kódem ADVERT_CATEGORY_REQUIRED (618).
Počítá se categoryIds i categoryRels. Úpravu brána nehlídá: odebrat poslední kategorii
existujícímu inzerátu je platné.
Pravidla parametrů na Api#
Délku, zákaz URL, zakázaná slova, povinnost a povolené volby parametrů dřív hlídal jen front
formulář a přímé Api save je obešlo. Teď je v beforeSave() hlídá i Api:
AdvertParameterPolicyTrait::guardParameterValues(), sdílená s Katalogem firem.
Kontroluje se jen neprivilegovaný volající (podle FieldAudienceContext); administrátor,
cron a CLI ne.
| Pravidlo | errorCode (VALIDATION_FAILED, v datech i parameterId) |
Kdy |
|---|---|---|
hodnota delší než max_length (u editoru viditelný text, EditorTextLength::visible()) |
parameterTooLong |
každé uložení |
URL v hodnotě a parametr má throw_error_on_url |
parameterUrlNotAllowed |
každé uložení |
zakázané slovo jazyka stránky a parametr má throw_error_on_blacklisted_word |
parameterBlacklistedWord |
každé uložení |
| volba, kterou by formulář za odeslaných hodnot nenabídl (pravidla odhalení voleb) | parameterOptionNotAllowed |
každé uložení |
| prázdný povinný parametr | parameterRequired |
jen vložení, nebo když formulář pošle meta['parametersTouched'] |
Povinnost a nabízené volby počítá sdílený ParameterRuleEvaluator (app/Core/Form/Parameters/Rules/)
se sémantikou frontu: cíl pravidla zobrazení je povinný jen při splněných podmínkách, necílený
povinný parametr vždy; podmínky se vyhodnotí validátory Nette nad uloženou hodnotou
(RuleValueControl). Front volá stejné funkce (DependentSelectTrait pro odhalení voleb), takže
formulář a Api se nerozejdou. Uploadery a skrytá pole se jako povinné nehodnotí. Kontext
(parametry kategorií, výchozí nebo z vazeb podle první kategorie, pravidla a aktivní volby) čte
skalárně AdvertParameterRuleContextRepositoryTrait::parameterRuleContext(), protože běží uvnitř
ukládání.
Externí úprava povinnost nehodnotí
Externí meta se zahazuje, takže parametersTouched z externího klienta nikdy nepřijde.
Při úpravě přes Api se proto kontrolují jen hodnoty a volby, ne chybějící povinné parametry
(úprava nemusí nést celou sadu).
Odpověď na save je extract() téže instance v paměti. parameterRels a categoryRels
v ní jsou, obrázky a soubory ne (zpracuje je afterSave() mimo graf), takže id nově
nahraných příloh klient z odpovědi nezjistí.
Postup: píšu externího klienta, který zakládá a upravuje inzeráty
- Při vložení pošli kategorie v
categoryIds(stačí id) nebo vcategoryRels. - Parametry pošli v
parameterRelsv tvaru hydrátoru, včetně názvu a ceny. - Při úpravě posílej jen klíče, které měníš. Kategorie měň přes
categoryRels,categoryIdsse při úpravě nepoužije. - Posíláš-li
assetsnebofiles, načti inzerát po uložení znovu sincludes, jinak id příloh nezjistíš. - Ověř v databázi
paidu kategorií aexpiration_date.
Prázdné categoryRels při úpravě smaže všechny kategorie
Klient, který kategorie měnit nechce, klíč nepošle vůbec. S [] inzerát o kategorie
přijde a zmizí z výpisů.
categoryIds při úpravě nic nezmění
Uložení skončí úspěchem a kategorie zůstanou původní. Chyba se neohlásí.
Formulář inzerátu v administraci#
Editace inzerátu je běžný formulář admin presenteru (AdvertFormTrait), dynamické parametry
staví sdílený ParameterFieldBuilder. Každá část inzerátu jde do Api jinou cestou a prázdné
odeslání u každé znamená něco jiného:
| Část | Jak jde do Api | Prázdné odeslání |
|---|---|---|
parametry (parameterRels) |
vlastněná kolekce v objektovém grafu, applyParameterRels() |
kolekce se nepřepíše, uložené hodnoty zůstanou |
kategorie (categoryRels) |
vlastněná kolekce v objektovém grafu, applyCategoryRels() při každém uložení |
odebere všechny kategorie inzerátu (orphanRemoval) |
| obrázky a soubory | meta['assets'], meta['files'], zpracuje Api afterSave() |
[] odebere všechny; null (uploader ve formuláři není) klíč vynechá a nic se nezmění |
assetRels, fileRels na entitě |
jen ke čtení; Api je z dat vyřízne (defaultSkipFields) |
— |
Které parametry formulář ukáže, určují vybrané kategorie (stejně jako na frontu):
| Výběr | Parametry |
|---|---|
| žádná kategorie | žádné, pole se objeví až po výběru kategorie |
| první vybraná kategorie má „použít výchozí parametry“ | výchozí parametry (default = 1), pořadí podle stromu |
| jinak | sjednocení cílení všech vybraných kategorií: povinný, když ho vyžaduje kterákoli; pořadí nejmenší; skupina první vyplněná |
Při překreslení po změně kategorie se vybrané kategorie čtou ze surového POST. Multiplier kategorií se k formuláři připojí až po parametrech, takže z formuláře by ještě nešly přečíst.
Prázdné parameterRels smaže všechny parametry inzerátu
Kolekci parametrů nahrazuj jen neprázdnými řádky. Prázdná kolekce v grafu znamená pro Api
„tyhle řádky už nemají být“ a orphanRemoval smaže všechny hodnoty včetně názvu a ceny.
U kategorií je prázdná kolekce naopak platné „odebrat vše“.
null a [] u uploaderu nejsou totéž
Kdo je sloučí, buď nepůjde odebrat poslední obrázek (prázdný seznam se nepošle), nebo
formulář bez uploaderu při uložení smaže všechny přílohy (chybějící seznam se pošle jako
prázdný). Api hook je rozlišuje array_key_exists().
Cílení parametrů kategorie#
Kategorie neukládá cílení grafem. Formulář pošle ploché řádky v meta['parameterRels']
(parametr, skupina, povinný, hledání, pořadí; volby mají skupinu NULL) a
CategoryParameterRelSyncTrait je v afterSave() přepíše (smaže a vloží znovu):
| Stav kategorie | Uložení | meta['setParametersToChildren'] |
|---|---|---|
| vlastní cílení | řádky se přepíšou podle meta['parameterRels'] |
podkategoriím se propíše příznak i řádky |
| „použít výchozí parametry“ | uložené řádky zůstanou, po vypnutí příznaku se kategorie vrátí ke svému nastavení | propíše se jen příznak, řádky podkategorií zůstanou |
Chybějící klíč cílení nemění, prázdný seznam ho smaže
Když meta['parameterRels'] v uložení chybí (externí Api, import, vlastní kód), uložené
cílení zůstane beze změny. Poslaný prázdný seznam naopak všechny řádky smaže, takže ho
posílej jen tehdy, když cílení opravdu rušíš. setParametersToChildren bez klíče propíše
podkategoriím uložené cílení kategorie. Chování drží
tests/Unit/Bazaar/Api/CategoryParameterRelsMetaTriStateTest.php.
Denormalizovaná počítadla#
| Hodnota | Kdo ji srovná |
|---|---|
advert.price |
cron /cron/bazaar/advert/recount-prices |
category.advert_count |
cron /cron/bazaar/category/recount |
advert.email, advert.region_id |
zápis inzerátu |
Přepočet počtů je set-based UPDATE nad celým stromem — rodič počítá aktivní inzeráty ze svého celého podstromu, ne jen z přímých dětí.
Počet má stejné podmínky jako veřejný výpis
Počítá se active = 1 a expiration_date >= dnes, tedy totéž co ve výpisu kategorie
(AdvertListTrait::prepareFilters()). Žádné nastavení se do toho nemíchá: potvrzení
autorem je součástí active (confirmed_at IS NOT NULL). „Dnes“ dodá PHP v časové zóně
aplikace, ne SQL funkce, protože databáze běží v UTC. Kdo počty ověřuje ručním SQL,
použije obě podmínky.
Inzerát po expiraci zmizí z výpisu hned, z počtu až po přepočtu
Počet je stav k poslednímu přepočtu. Ten spustí cron /cron/{modul}/category/recount
(à 10–15 minut), potvrzení autorem, tvrdé smazání a anonymizace účtu, běžná úprava
inzerátu ne. Mezi přepočty ukazuje kategorie víc inzerátů, než výpis vypíše.
Schválení a potvrzení v nastavení#
Dvě nezávislé podmínky zveřejnění, obě se vyhodnocují při vložení:
| Co | Klíče v nastavení modulu |
|---|---|
| potvrzení autorem odkazem z e-mailu (kladná sémantika „vyžaduje se") | advert.confirmation.required.logged, advert.confirmation.required.guest |
| schválení správcem | advert.approval.auto (u Firemních profilů v modulu 6 totéž) |
Přihlášený autor je ten, kdo má u záznamu vlastníka. Nevyžaduje-li se potvrzení, záznam
dostane confirmed_at hned; vyžaduje-li, dostane aktivační klíč (v DB jen otisk) a čeká na
odkaz. Přepnutí nastavení už vložené záznamy nemění. Aktivační klíč je v administraci jen
ke čtení.
Rozhoduje applyInitialApproval() a applyInitialConfirmation() v beforeSave() Api
manageru, jen při insertu a na každé cestě (web, externí Api, administrace). Logika je
sdílená s příspěvky diskuze (InitialApprovalTrait, InitialConfirmationTrait):
| Situace při vložení | approved_at |
confirmed_at |
E-mail po commitu |
|---|---|---|---|
advert.approval.auto zapnuté |
teď | — | — |
advert.approval.auto vypnuté nebo chybí |
NULL, čeká na správce |
— | — |
| potvrzení se nevyžaduje | — | teď | oznámení o vložení |
| vyžaduje se | — | NULL, vydá se klíč |
s odkazem na potvrzení |
| vyžaduje se, ale správce zaškrtl Potvrzeno | — | ponechá vyplněné | žádný |
| vyžaduje se, ale záznam nemá e-mail (externí Api, import) | — | teď | žádný |
Volba e-mailu se bere ze stavu při vložení ($meta), ne z nastavení přečteného po commitu.
S $meta['suppressNotifications'] (úprava v administraci, import, cron) neodejde žádný.
Potvrzení klíčem pak řeší confirmByKey(), viz
Odkaz s klíčem.
Přepínače „Schváleno“ a „Deaktivováno“ při zakládání v administraci se přepíšou
applyInitialApproval() nastaví deactivated = 0 a approved_at podle nastavení i u
záznamu, který zakládá správce. Kdo chce nový inzerát rovnou schválit, upraví ho po
založení.
Záznam bez potvrzení musí dostat confirmed_at hned
Viditelnost stojí jen na confirmed_at IS NOT NULL (je i ve vzorci active). Nový zápis,
který tuhle větev obejde, vytvoří inzerát, který se nikdy nezobrazí.
Adresa a souřadnice při uložení#
Město, PSČ a stát rozkládá z vybrané obce server (AdvertRegionAddressTrait, sdílí ho
Bazaar i Comcat). JavaScript našeptávače pole plní jen jako náhled. Souřadnice počítá
GeocodingManager z parametrů až po rozkladu; přesnost řídí nastavení modulu
geocoding.precise_address (v Bazaaru výchozí vypnuto, v Comcatu zapnuto). Když souřadnice
nevzniknou, po commitu se zapíše řádek do log/geocoding-miss.log.
| Adresa | Odkud souřadnice | Když se nenajdou |
|---|---|---|
| nepřesná, nebo přesná bez ulice | střed obce: vybrané, jinak přesná shoda obce nebo PSČ v datasetu regionů; Google nikdy | GeocodingMiss |
| přesná s ulicí | cms_system_geocoding_cache, při chybění Google a zápis do cache |
střed obce, jinak GeocodingMiss |
Tentýž GeocodingManager obsluhuje i filtr „do X km“ na frontu (Api akce
:Api:System:Region:geocode, resolveSearchText()). Čtení regionů jde raw SQL,
takže výpočet uvnitř beforeSave() nesahá na identity mapu rozpracovaného grafu inzerátu.
Mění se jen inzerát s vybranou obcí (list stromu regionů). Starý inzerát zařazený na stát nebo kraj zůstane, jak je, město je u něj volný text.
reattachRelReferences() zůstává poslední
applyRegionAddress() → applyCoordinates() → reattachRelReferences(). Oba první kroky
volají findAll() parametrů a kategorií. Prosté čtení identity mapu nečistí, ale kdyby ji
mezitím vyprázdnilo UnitOfWorkReset::abort() nebo hranice cizí operace, vazby relací by
ukazovaly na odpojené entity a flush by spadl na
A new entity was found through the relationship 'AdvertParameterRel#parameter'.
reattachRelReferences() je proto obranně za všemi načítacími kroky a nové načítání patří
před něj.
Viz Identity mapa.
Smazání inzerátu#
| Způsob | Co se stane |
|---|---|
měkké smazání (deleted = 1) |
jen zápis sloupce; inzerát zůstane i s daty, obrázky i indexem a jde vrátit |
smazání nadobro (delete()) |
AdvertHardDeleteTrait přes hooky DeleteTrait; co a v jakém pořadí, určuje AdvertHardDeletePlan |
Cizí klíče na inzerát jsou CASCADE, ale kaskáda nepokryje frontu hlídacího psa v Bazaaru
(NO ACTION), soukromé konverzace odpovědí a hodnoticí vlákno (vazba vede od inzerátu
k vláknu, vlákno by osiřelo), řádky a soubory obrázků a příloh ani ES index a počty
v kategoriích. Proto má mazání explicitní kroky: beforeDelete/afterDelete v transakci,
soubory a index po commitu v completedAdvertHardDelete().
completedAdvertHardDelete() volá manager ručně
Oba AdvertManager mají vlastní completedDelete() (ES) a ta by metodu traitu tiše
přebila. Manager, který trait přebírá, musí completedAdvertHardDelete() zavolat ze své
completedDelete().
Účetní data se s inzerátem nemažou. Doklady (cms_mod_invoicer_*) a platby
(cms_system_payments, cms_system_payment_items, cms_system_payment_transactions) na
inzerát cizí klíč nemají; jediné pojítko je modulová cms_mod_*_payment_items (aktivace
tarifu), která účetním dokladem není a smaže se kaskádou. Doklad i platba zůstanou celé,
snapshot položek nese název i cenu.
Před prvním zápisem projde AdvertHardDeletePlan::uncoveredForeignKeys() graf cizích klíčů
z information_schema od tabulky inzerátů po kaskádách:
| Pravidlo vazby | Výsledek |
|---|---|
CASCADE |
dítě se smaže, kontroluje se dál do jeho dětí; kaskáda do účetní tabulky je chyba |
SET NULL |
dítě přežije s prázdnou vazbou, v pořádku |
NO ACTION / RESTRICT |
v pořádku jen pro sloupce, které plán maže explicitně dřív (explicitlyClearedColumns()) |
Nepokrytá vazba mazání odmítne s ADVERT_DELETE_BLOCKED (624) a seznamem vazeb v logu
delete.advert, nic se nesmaže.
Postup: přidávám tabulku s cizím klíčem na inzerát (nebo na jeho dítě)
- Dej vazbě
ON DELETE CASCADE, když řádky nemají smysl bez inzerátu, jinakSET NULL. - Potřebuje-li vazba
NO ACTION(řádky se mají mazat vlastní logikou), přidej krok doAdvertHardDeletePlana sloupec doexplicitlyClearedColumns(). - Doplň vazbu do snímku schématu v
tests/Unit/Api/AdvertHardDeletePlanTest.phpa spusť ho. - Smaž v administraci testovací inzerát nadobro v obou modulech (Bazaar i Comcat).
Vazba bez kroku v plánu zablokuje mazání všech inzerátů
Nová tabulka s NO ACTION na inzerát nespadne na databázové výjimce uprostřed
kaskády, ale tvrdé mazání v mřížce začne vracet chybu pro každý inzerát modulu,
dokud vazbu plán nepokryje.
Kaskáda do účetní tabulky se odmítne, ne provede
Kdyby nová vazba vedla kaskádu z inzerátu do tabulky s prefixem z
ACCOUNTING_TABLE_PREFIXES, mazání skončí ADVERT_DELETE_BLOCKED. Účetní řádek patří
k dokladu, ne k inzerátu — vazbu přepiš na SET NULL nebo ji zruš.
Hlídací pes#
Uložené hledání, které při novém inzerátu pošle e-mail.
Spouští se JEN při vložení inzerátu
Ne při editaci ani při přesunu do jiné kategorie. Inzerát, který se do kategorie dostane až později, hlídačům nikdy nepřijde — a nikomu nic nechybí, dokud se nezeptá inzerent.
Při vložení prepareEmails() vždy vyrenderuje a uloží e-mail hlídače (WatchdogEmail
s texty ve všech jazycích). Kdo ho doručí, určuje nastavení watchdog.email.mode (skupina 5):
| Režim | Fronta hlídače | Kdo posílá |
|---|---|---|
| 0 | nic | nikdo |
| 1 | řádky v cms_mod_bazaar_watchdog_email_rel |
cron /cron/bazaar/watchdog-email/send (à 10 min), e-mail na každý inzerát |
| 2 | řádky ve frontě | cron /cron/bazaar/watchdog-email/digest (denně 23:00), jeden souhrn na příjemce |
| 3 | řádky vzniknou, ale nikdo je nečte | dispatchImmediateFanout() po commitu rovnou do systémové fronty e-mailů |
Inzerát hlídači padne, když sedí všechno najednou:
| Podmínka | Shoda |
|---|---|
| kategorie | kategorie hlídače nebo kterákoli její podkategorie |
| region | region hlídače je region inzerátu nebo jeho předek; inzerát bez regionu nepadne nikomu |
typ, stav (parameter_id, parameter_id2) |
NULL = cokoli; jinak má inzerát řádek rodičovského parametru s value = id volby |
| hlídač | active = 1, u režimu 3 i shodný jazyk textu |
Režim 1 zkouší odeslat nejvýš watchdog.email.maxAttempts× (výchozí 5). Řádek bez
doručitelného e-mailu (hlídač ani jeho uživatel ho nemají) se zruší (cancelled). Souhrn
v režimu 2 má jen jeden pokus: zpracovaný řádek je vždy sent nebo cancelled, aby nikdo
nedostal souhrn dvakrát.
Souhrn bere jen čekající řádky k inzerátům vloženým za poslední dva dny
(WatchdogEmailRelRepository::DIGEST_MAX_AGE_DAYS, měří se insert_datetime inzerátu). První
běh po nasazení tak nerozešle celou historii čekajících řádků.
Souhrn, který dva dny neběží, inzeráty z té doby už nepošle
Řádky starších inzerátů zůstanou ve stavu „čeká“ a úklid fronty je nesmaže (čekající řádky nemaže nikdy). Výpadek cronu nebo přepnutí do režimu 2 s plnou frontou znamená, že hlídači ty inzeráty nedostanou a fronta s nimi poroste.
V režimu 3 posílá databázový trigger, dokud existuje
PHP rozesílání se drží zpátky, dokud v databázi je trigger
cms_mod_bazaar_watchdog_email_texts_AFTER_INSERT (fanoutTriggerExists()). Po jeho
smazání převezme rozesílání PHP samo. Smazat trigger dřív, než je na serveru nový kód,
znamená, že hlídači nedostanou nic.
Volba hlídače se páruje přes řádek rodičovského parametru
Formulář inzerátu ukládá vybranou volbu jako hodnotu rodičovského parametru
(parameter_id = parametr, value = id volby). Import nebo vlastní zápis, který uloží
volbu jinak, hlídači s typem nebo stavem tiše nezachytí.
Fronta upozornění (cms_mod_bazaar_watchdog_email_rel) roste s každým inzerátem, který
hlídač zachytí. Úklid dělá /cron/bazaar/watchdog-email/purge-old (denně): smaže odeslané
a zrušené řádky starší než N dní a pak e-maily a jejich texty, na které už žádný řádek
neukazuje. Čekající řádky nemaže nikdy a běží v každém režimu rozesílání, i když je
rozesílání vypnuté.
| Retence | Odkud |
|---|---|
?days=N v adrese |
přednost |
nastavení watchdog.email.retentionDays (skupina 5) |
jinak |
90 dní (WatchdogSendManager::DEFAULT_RETENTION_DAYS) |
výchozí |
Stejně funguje fronta v Katalogu firem (/cron/comcat/watchdog-email/purge-old).
Odpovědi na inzerát: příjemce a přečtení#
Reakce rozlišuje odesílatele (owner) a příjemce (receiver). Příjemce je
advert.insertUserId, ne sloupec reakce, proto Response rozhraní ReceiverAwareInterface
neimplementuje a o příjemci rozhoduje ResponseReceiverTrait dotazem: privilegovaná role ∨
majitel inzerátu ∨ heslo k inzerátu ∨ platný read_key (jen interně). Detail reakce
(Response:get) dřív otevřel jen administrátor, takže majitel svou reakci neviděl.
| Věc | Detail |
|---|---|
| detail reakce | Front ResponsePresenter::actionDetail(): jen majitel inzerátu (jinak 403); read se nastaví jen při požadavku z vlastního webu (isSameSite()) |
| tlačítko v e-mailu | {$markReadLink} v šablonách BZ_ADVERT_RESPONSE, BZ_ADVERT_SUGGEST_PRICE; viz Odkaz s klíčem |
| veřejná stránka | Front:Bazaar:Response:markRead, id stránky MARK_READ_SLUG = 2011; výpis reakcí LIST_SLUG = 158, detail DETAIL_SLUG = 159 |
| soukromá konverzace | openThreadForResponse() při INSERTu založí vlákno odesílatel × majitel; je-li jeden host, je vlákno „jen e-mailem" |
| pád reakce s cenou | měna bez překladů se načítala líně na entitě odpojené vyprázdněnou identity mapou; sendEmail() teď bere find($id, ['translations']) |
Podrobně o rolích v Podmíněných rolích.
V administraci se reakce maže měkce (deleted = 1): mřížka reakcí přes
setDeleteMethod('softDelete') (ResponseManager::softDelete()) i záložka Odpovědi v editaci
inzerátu (signál toggleDeleted!). Front reakce s deleted = 1 nevypisuje. Tvrdé smazání by
reakci odstranilo nevratně i s vazbou na soukromou konverzaci (message_thread_id), kterou
založila.
Štítek kategorie na kartě nevedl na inzeráty
Štítek kategorie na kartě inzerátu (AdvertRenderer Style_1–3) dřív vedl na
:Front:Blog:Article:list … category: (/clanky/o-1?category=109). Vede na výpis
inzerátů kategorie. Zástupný segment - pro kategorii viz
Navigace po úrovních.
Admin Bazaar Advert neredeklaroval kolekci followers
include(['followers']) padal na TypeError; kolekce je v admin entitě jako
#[Transient].
Uložení reakce: brány v beforeSave()#
Reakci zapisuje frontový formulář, administrace i externí :Api:*:Response:save. Externí
cesta formulář nepotká vůbec, payload jde hydrátorem rovnou do entity. Jediné místo, kudy
projde každý zápis, je ResponseManager::beforeSave() — v Inzerci i v
Katalogu firem, oba managery sdílejí tytéž traity. Brány jdou v tomto pořadí:
| # | Brána | Odmítne (VALIDATION_FAILED, errorCode) |
Upraví |
|---|---|---|---|
| 1 | guardResponseColumnLimits() |
cena není číslo (invalidPrice), je záporná nebo mimo rozsah sloupce (priceOutOfRange); telefon, e-mail nebo předmět delší než sloupec (phoneTooLong, emailTooLong, subjectTooLong) |
prázdnou cenu na NULL, vyplněnou na tečkový tvar zaokrouhlený na scale sloupce |
| 2 | guardResponseCurrency() |
cena bez měny (currencyRequired); měna, která není veřejná peněžní, třeba kredity (currencyNotAllowed) |
nulovou cenu na NULL; bez ceny měnu zahodí |
| 3 | při INSERTu | — | dosadí náhodný read_key pro odkaz s klíčem |
Meze sloupců se neopisují, brána je čte reflexí z #[ORM\Column] entity, která se opravdu
ukládá (ColumnLimits). Po změně schématu se posunou samy. Prázdná cena a telefon projdou:
jsou nepovinné a povinnost řeší formulář.
Bez normalizace ceny by se do DB zapsalo něco jiného, než brána změřila
"12,5" projde bránou jako 12.5, ale syrový řetězec by ve striktním sql_mode shodil
uložení obecnou chybou databáze a v nestriktním by se uložil jako 12.000.
Proto brána cenu po kontrole přepíše. Bez brány by přetečení ve striktním sql_mode
shodilo uložení; v nestriktním by se uložilo tiše (nabídka za nulu, uříznutý e-mail,
na který odpověď nedojde) a inzerent by ho viděl v e-mailu.
Měnu kontroluj až po mezích sloupců
Teprve první brána převede prázdnou cenu na NULL. V opačném pořadí by měnová brána
nepoznala, jestli je cena vyplněná.
Na UPDATE měnová brána zasáhne jen když se dvojice cena a měna mění. Klasifikaci měny může
administrátor změnit i po vložení reakce (internal/monetary), a úprava textu, read nebo
deleted staré reakce nesmí padnout na dnešní klasifikaci. Uložený stav čte
findPersistedValues() (PersistedValuesTrait) přímým DBAL dotazem: na cestě uložení je
entita už zmutovaná hydratací a find() by z identity mapy vrátil tutéž instanci s novými
hodnotami, takže by brána změnu nikdy neviděla.
Přečtení reakce a vlákna#
Příznak read na reakci a nepřečtené zprávy v soukromé konverzaci, kterou reakce založila,
se drží v souladu (ResponseThreadReadSyncTrait):
| Směr | Kdy | Co se stane |
|---|---|---|
| reakce → vlákno | úspěšné updateColumns() s read = 1 |
vlákno se označí za přečtené majiteli inzerátu, a jen když je jeho jediná nepřečtená zpráva ta první z reakce |
| vlákno → reakce | majitel inzerátu přečte vlákno | navázaná reakce dostane read = 1 (přímo přes service, aby se vlákno neoznačovalo znovu) |
Označuje se vždy jen pro majitele inzerátu, nikdy pro toho, kdo akci vyvolal, takže držitel klíče z e-mailu nezíská nic navíc. U anonymního inzerátu (majitel bez účtu) se vlákno neřeší.
Přečtení zapsané jinak než přes updateColumns() vlákno neoznačí
Synchronizace visí na updateColumns() manageru. read uložený přes save() nebo raw
SQL nechá vlákno nepřečtené a majitel uvidí v poště „novou“ zprávu, kterou už četl.
Topování, zvýraznění a expirace#
Placené zviditelnění má čtyři nezávislé tarify. Jejich id nejsou v kódu, ale v
config/Shared/bazaar.neon (přepsat jdou v config/Shared/local.neon):
| Tarif | Parametr (výchozí id) | Co udělá ve výpisu | Odkud se to pozná |
|---|---|---|---|
| topování | advert.bazaarToppingTariffId, comcatToppingTariffId (1) |
inzerát jde ve výpisu nahoru (toped_until > NOW() první) |
sloupec toped_until, plní ho platba přes AdvertToppingConfig |
| zvýraznění | advert.bazaarHighlightTariffId, comcatHighlightTariffId, discussionHighlightTariffId (2) |
karta dostane třídu highlighted (podbarvení tokenem --list-item-highlighted-bg) |
aktivní platná položka platby toho tarifu, jeden dotaz na vykreslení výpisu (AdvertHighlightConfig) |
| Topbox | id předává šablona ([3]) |
inzerát se ukáže v bočním widgetu „TOP“ (TopAdvertList, {control frontBazaarTopAdvertList [3], 'Style_2', 10} v aside-left.latte všech šablon) |
aktivní platná položka platby toho tarifu |
| zobrazení odkazů | advert.bazaarLinksTariffId, comcatLinksTariffId, discussionLinksTariffId (4) |
na detailu jsou adresy v popisu klikací odkazy (TextLinkifier, filtr \|linkify, rel="ugc nofollow") |
aktivní platná položka platby toho tarifu (AdvertLinksConfig); presenter detailu volá Advert::setRenderUrlsAllowed(true) |
Odkazy obnoví jen filtr v režimu „při zobrazení“
Bez tarifu se adresa v parametru s filtrem odkazů nahradí [URL]. Tarif vypne jen
nahrazení v režimu OnRender; v režimu OnSave je v databázi už [URL] a vrátit nejde.
Kdo toped_until přepočítá, závisí na cestě, kterou se platební položka změnila
(PaymentItemManager v Api):
| Cesta | Přepočet toped_until |
|---|---|
aktivace platbou z brány (updateStatusForPayment()) |
monotonní: jen prodlouží na nejzazší platnost aktivních položek |
| uložení položky mimo administraci | monotonní, jen u aktivní položky |
uložení v záložce Platby editace inzerátu (meta['recalcTopedUntil']) |
absolutní: nejzazší validity_datetime aktivních položek tarifu topování, bez nich NULL; umí i zkrátit |
updateColumns() s active nebo validityDatetime (přepínač ve výpisu) |
absolutní |
| smazání položky | absolutní, v transakci mazání (afterDelete()) |
| přesun položky k jinému inzerátu | absolutně se přepočítá i původní inzerát |
Diskuze to má stejně, jen nad příspěvky (recalcTopedUntilForMessages()).
Zápis položky mimo tyhle cesty topování nesrovná
Ruční SQL nebo uložení bez meta['recalcTopedUntil'] nechá toped_until beze změny.
Deaktivovaná nebo zkrácená položka pak dál drží inzerát nahoře až do staré platnosti.
Smazání prostřední položky topování zkrátí jen přibližně
Aktivace platbou staví platnost nové položky na nejzazší platnost dosavadních aktivních
položek téhož tarifu (GREATEST(MAX(validity_datetime), NOW()) + trvání tarifu). Absolutní
přepočet pak bere jen maximum uložených validity_datetime a pozdější položky nepřepočítává.
Smazáním prostřední položky tak inzerát zůstane nahoře o trvání smazané položky déle; přesné
je jen smazání poslední. Platí to pro inzerci, katalog firem i diskuzi.
Id tarifu musí sedět s daty
Když na webu tarif „Zvýraznění“ dostane jiné id než výchozí 2 a konfigurace se nepřepíše, zaplacené zvýraznění se ve výpisu neukáže — a žádná chyba nevznikne.
Datum expirace počítá AdvertExpirationTrait:
| Situace | Nová expirace |
|---|---|
| vložení inzerátu | dnes + nejdelší expirationInterval z kategorií inzerátu |
prodloužení (Advert:extendExpiration) |
současná expirace, je-li v budoucnu, jinak dnes, + nejdelší interval kategorií |
| žádná kategorie nemá interval (všechny 0) | místo intervalu nastavení 5/advert.expiration.days |
aktivace platby (updateExpirationDateForPayment()) |
posune na nejzazší validity_datetime zaplacených položek platby, jen dopředu |
Nulový interval všude znamená, že inzerát nejde vložit
Když mají všechny kategorie interval 0 a advert.expiration.days je 0 nebo chybí,
expirace se nespočítá. Vložení i prodloužení pak skončí INVALID_CONFIGURATION
(errorCode: noExpirationInterval u vložení) dřív, než se cokoli zapíše; žádnou
náhradní délku si systém nevymýšlí. Nastav interval aspoň u jedné kategorie, nebo
výchozí advert.expiration.days.
| Cron | Co dělá | Interval |
|---|---|---|
/cron/bazaar/advert/notice-expiration |
upozorní N dní před expirací (5/advert.expiration.noticeDays), nastaví příznak, aby se neopakovalo |
denně |
/cron/bazaar/advert/delete-expired |
tiše označí deleted = 1 po M dnech od expirace (5/advert.expiration.deleteDays) |
denně |
Kroky „Cena do“ ve vyhledávání#
Nabídku „Cena do“ ve vyhledávacím boxu staví App\Core\Config\AdvertPriceStepsConfig
podle měny webu (měna regionu lokalizace). Kroky jsou v config/Shared/bazaar.neon
pod advert.bazaarPriceSteps jako klíč měny => kroky:
bazaarPriceSteps:
CZK: [1000, 5000, 10000, 50000, 100000, 500000, 1000000]
EUR: [50, 200, 500, 1000, 5000, 20000, 50000]
| Měna webu | Jaké kroky dostane |
|---|---|
| je v mapě | své kroky |
| v mapě není | kroky první měny v mapě přepočtené ručním kurzem (ApproximatePrice) a zaokrouhlené na 1 / 2 / 5 × 10ⁿ |
| přepočet nejde (kurz 0, interní nebo neznámá měna) | kroky první měny i s jejím označením — popisek nikdy netvrdí jinou měnu, než v jaké jsou čísla |
Filtr ceny se nepřepočítává
Krok jde do ?priceTo= jako holé číslo a AdvertListTrait ho porovná s uloženou
cenou inzerátu bez ohledu na její měnu. Na webu, kde mají inzeráty jinou měnu než web,
filtr tiše vybírá podle špatných částek.
Inzerát bez ceny při zvolené hranici zmizí
preparePriceFilter() přidá price <= priceTo nad denormalizovaným sloupcem price.
Inzeráty s price IS NULL (cena dohodou) tím z výpisu vypadnou. Je to záměr: nabídka
bez ceny strop nesplní. Bez priceTo v adrese se nepřidá nic.
Komponenty frontu#
Výpisy inzerátů na hlavní straně#
Tři výpisy vybírají inzeráty každý jinak, kreslí je ale stejný AdvertListRenderer, takže
styly karet jsou sdílené.
| Komponenta | Co vybírá | Volání v šabloně |
|---|---|---|
LastAdvertList |
nejnovější podle data vložení | {control frontBazaarLastAdvertList:box, 6}, :row, :table; třetí argument = id volby parametru type |
TopAdvertList |
topované podle zaplacených tarifů | {control frontBazaarTopAdvertList [3], 'Style_2', 10} (id tarifů, styl, počet) |
AdvertTabList |
taby last, visited (viewsDetail), hot přepínané signálem tab! |
{control frontBazaarAdvertTabList:box, 8} |
Viditelnost řídí presenter: má-li prepareFilters(), výpis použije jeho filtry (kategorie,
region, potvrzení). Jinak nastoupí pevná sada: nesmazaný, zaplacený, neprodaný,
nezabanovaný, neaktivovaný, active = 1 a neprošlá expirace.
Id typu inzerátu je datové
Filtr LastAdvertList podle typu bere id volby (dítě parametru s klíčem type), ne id
parametru. Volby jsou řádky v tabulce parametrů a liší se instalaci od instalace. Id
opsané z jiné instalace vybere jiný typ nebo nic. Zjisti ho v administraci
(Parametry → type). Když parametr type chybí, filtr se tiše vynechá.
Tab hot počítá odpovědi za posledních 30 dní (HOT_WINDOW_DAYS) v PHP, z inzerátů s nejvíc
odpověďmi vezme trojnásobek limitu a teprve na ně pustí viditelnost. Kandidátů je víc, protože
smazaný nebo prodaný inzerát s mnoha odpověďmi by jinak zabral místo.
Tab hot může vyjít kratší než limit
Když z trojnásobku kandidátů projde viditelností méně inzerátů, než je limit, tab jich ukáže méně. Doplňování dalšími kandidáty neexistuje.
Vyhledávací box#
SearchBox má tři styly: Mini (fulltext a tlačítko, hlavičky modulů), Medium (fulltext,
kategorie, region) a Large (přepínač typu, kategorie, region, cena; fulltext: true přidá
textové pole). Style a StyleInline jsou aliasy Mini; StyleInline volají hlavičky šablon, proto ho neruš bez jejich úpravy.
Předvybraná hodnota selectu musí být v nabídce
Nette SelectBox::setDefaultValue() na hodnotu mimo nabídku vyhodí
InvalidArgumentException. Box je v hlavičce výpisů, takže hodnota z adresy, kterou
nabídka nezná, by shodila na 500 /inzerce i všechny výpisy. Kategorie a „Cena do“ proto
jdou přes safeDefault(): neznámou hodnotu zahodí a select zůstane na promptu. Nový select
plněný z adresy veď stejnou cestou.
Názvy kategorií a regionů v nabídce jsou lokalizovaná data, proto je box obaluje
NotTranslate (viz Překlady).
Formulář inzerátu na frontu#
AdvertForm stojí na sdíleném ParameterForm.
Hodnoty pro předvyplnění dává getEntityValues() z LoadingTrait:
| Situace | Odkud hodnoty |
|---|---|
| editace | řádky parameterRels inzerátu (vícehodnotové jako pole, rozsah jako value/value2) |
| nový inzerát, přihlášený uživatel | údaje z účtu přes klíč parametru: email, phone, street, city, zip a postcode, country, regionId, firstname, lastname |
| nový inzerát, host | nic |
Klíč se mapuje jen tehdy, když formulář parametr s tím klíčem opravdu má.
Vlastní getEntityValues() v AdvertForm vypne předvyplnění
Metoda třídy má přednost před metodou traitu. Kdo do AdvertForm dopíše vlastní
getEntityValues(), přebije LoadingTrait a nový inzerát se přestane plnit z účtu,
bez chyby.
Dynamické parametry jdou do Api jako kolekce parameterRels (applyParameterRels()), ale
jen když formulář nějaké řádky odeslal. Prázdné pole by na Api straně orphanRemoval
smazalo všechny parametry inzerátu, proto se při prázdném odeslání kolekce nepřepisuje.
Karta prodejce na detailu#
Další inzeráty majitele a jejich počet hledá prepareOtherAdverts() podle insertUserId,
jedním dotazem (active, neprošlá expirace). Počet jsou všechny nalezené, výpis totéž bez
zobrazeného inzerátu. Inzerát bez účtu majitele nemá podle čeho hledat: výpis je prázdný
a karta s počtem se nezobrazí.
Hledání podle e-mailu nenajde nic
Api e-mail na výstupu maskuje (PersonalDataPolicy, viz
RBAC). Filtr podle maskované hodnoty
nesedí na žádný řádek a výpis by byl vždy prázdný.
Zobrazení telefonu#
Signál showPhone! (#[Requires(sameOrigin: true, ajax: true)]) má dvě větve:
| Inzerát | Odkud číslo | Limit |
|---|---|---|
| s účtem | UserManager::revealPhone() → guard :Api:System:User:showPhone |
ratelimit.showPhone (20 za hodinu na IP) na Api vrstvě |
| bez účtu | $advert->get('phone') |
FrontFormLimiter::isAdvertPhoneRevealAllowed(), stejné hodnoty pod vlastním klíčem |
Prázdná odpověď (zamítnuto, limit, bez čísla) šablonu nepřepne a zůstane maskovaná hodnota.
Telefon nevypisuj jen přepnutím proměnné šablony
Číslo je v datech inzerátu už při vykreslení. Signál, který jen nastaví showPhone, by
obešel guard i limit a čísla by šla posbírat projetím inzerátů.
Notifikace#
Sledující dostanou upozornění POUZE při změně cenovky
Porovnává se trojice cena, typ ceny a měna. Změna názvu, popisu ani fotek neupozorní nikoho — a je to schválně, jinak by úprava překlepu rozeslala stovky e-mailů.
Editace v administraci neposílá žádné e-maily
Ani aktivační, ani hlídačům, ani sledujícím. Je to záměr: administrátor opravuje cizí inzerát a nemá tím spouštět komunikaci jeho jménem.
Zakázaná slova#
Inzerce, Katalog firem i Diskuze čtou jeden seznam pro celý web:
systémovou tabulku cms_system_blacklisted_words (Systém → Zakázaná slova) přes Api
System\BlacklistedWordManager (findActiveValues(?languageId) = aktivní slova jazyka, malými
písmeny; null = všechny jazyky). Modulové kopie cms_mod_*_blacklisted_words i jejich entity
a managery byly odstraněny.
| Kde | Co se stane |
|---|---|
| formulář a detail inzerátu | parametr s throw_error_on_blacklisted_word slovo odmítne, při zobrazení se slova maskují (ContentFilterMode::OnRender) |
| Api uložení inzerátu | AdvertParameterPolicyTrait, viz Pravidla parametrů na Api |
vlákno odpovědi (ResponseManager) |
text se zakázaným slovem vlákno v poště nezaloží; reakce se uloží a e-mail odejde |
index ES (AdvertDocumentSource) |
texty se čtou stejně jako při zobrazení (setRenderBlacklistedWords() se slovy cílového jazyka), takže maskovaná slova do indexu neprosáknou |
Nahlášení inzerátu#
AdvertReport funguje stejně jako nahlášení příspěvku v Diskuzi:
příznak reported inzerátu dorovnává sdílená ReportedFlagSyncTrait v AdvertReportManager
(i po hromadném deleteAll()) a administrace AdvertReportPresenter používá sdílenou
ReportModerationPresenterTrait (výchozí nevyřešená, Vyřešit, Znovu otevřít). Uložení inzerátu
příznak přepočítá z tabulky hlášení, takže ho formulář ani zastaralá entita nepřepíše.
Elasticsearch#
Inzerce je jeden ze zdrojů Elasticsearch — reindex
/cron/bazaar/elastic/reindex, průběžná synchronizace /cron/bazaar/elastic/sync.
Hromadné operace mimo hooky index nevidí
Set-based UPDATE (přepočty, importy, ruční SQL) hook nespustí, takže se do
indexu nepropíše. Konzistenci drží až noční reindex — do té doby hledání vrací
stará data a nikde to není vidět.
Kam sáhnout#
| Chci | Kde |
|---|---|
| logiku inzerátu a denormalizace | app/UI/Api/Bazaar/Models/Managers/AdvertManager.php |
| schválení, potvrzení, adresu a mazání sdílené s Comcatem | app/Core/Traits/Api/Models/Managers/Advert*Trait.php, Initial{Approval,Confirmation}Trait.php |
| přepočet počtů v kategoriích | app/UI/Api/Bazaar/Models/Managers/CategoryManager.php → recountAdverts() |
| plán tvrdého smazání a kontrolu cizích klíčů | app/Core/Utils/ValueObjects/AdvertHardDeletePlan.php |
| brány uložení reakce a synchronizaci přečtení | app/Core/Traits/Api/Models/Managers/Response{ColumnGuard,CurrencyGuard,ThreadReadSync}Trait.php, app/UI/Api/Bazaar/Models/Managers/ResponseManager.php |
| hlídací pes | app/UI/Api/Bazaar/Models/Managers/Watchdog*Manager.php, dotazy shody v Repositories/Db/WatchdogEmailRelRepository.php |
| zápis inzerátu přes Api a výpočet expirace | AdvertManager.php ($graphDataKeys), Traits/AdvertManager/{CategoryRelsPaidSanitization,AdvertExpiration}Trait.php |
| definici entity a jejích příznaků | app/Core/Base/Shared/Bazaar/Models/Entities/Advert.php |
| cron úlohy | app/UI/Cron/Bazaar/Presenters/ |
| tarify topování a zvýraznění, kroky „Cena do“ | config/Shared/bazaar.neon, app/Core/Config/Advert{Topping,Highlight,PriceSteps}Config.php |
| formulář inzerátu na frontu | app/UI/Front/Bazaar/Components/AdvertFormComponent/ |
| výpisy na hlavní straně, vyhledávací box | app/UI/Front/Bazaar/Components/{LastAdvertList,TopAdvertList,AdvertTabList,SearchBox}Component/ |
| kartu prodejce a zobrazení telefonu | app/UI/Front/Bazaar/Presenters/Traits/Details/AdvertDetailTrait.php, Presenters/AdvertPresenter.php (handleShowPhone()) |
| formulář inzerátu a kategorie v administraci | app/UI/Admin/Bazaar/Presenters/Traits/Forms/Advert*Trait.php, CategoryFormTrait.php |
| cílení parametrů kategorie | app/UI/Api/Bazaar/Models/Managers/Traits/CategoryManager/CategoryParameterRelSyncTrait.php |
| přepočet topování | app/UI/Api/Bazaar/Models/Managers/PaymentItemManager.php |
Navazující kapitoly: Katalog firem · Elasticsearch · Filtry · Naplánované úlohy