Company catalogue#
The Comcat module was created as a copy of Classifieds, so it shares
almost the whole model: a company profile is an Advert with dynamic parameters, the
category is a nested set, listing prices live on the category. The note about its
creation is in docs/comcat-copy-from-bazaar.md.
Read Classifieds first
Everything said there about dynamic parameters, denormalised counters, display rules and the irreversible propagation into a subtree applies here too. This chapter describes only the differences.
Differences from Classifieds#
| Classifieds | Company catalogue | |
|---|---|---|
denormalised price |
yes | no |
the sold flag |
yes | no |
followers (AdvertFollower) |
yes | no |
| saved-search alerts | yes | yes |
| settings index | 5 |
6 |
The missing price is deliberate, not an omission
A company is not for sale. The prices are for the listing in a category, and those
live on the category (CategoryPrice), not on the profile.
The difference propagates into the generated active column too — it has one condition
fewer:
-- cms_mod_comcat_adverts.active (VIRTUAL)
active = CASE WHEN approved_at IS NOT NULL
AND paid = 1 AND banned = 0 AND deleted = 0 AND deactivated = 0
THEN 1 ELSE 0 END
An SQL query copied over from classifieds will hit sold here
The column does not exist. Anyone carrying a finished query across gets an error —
which is still the better outcome; the worse one is carried-over PHP that reaches for
sold through a getter and receives null.
A profile has no fixed fields#
The name, company number, address, phone, description and opening hours are all parameters, exactly as on an advert.
Without parameters the form is empty
Before the first company can be created, parameters have to exist and be targeted at the category. The form throws no error — it simply has nothing to render, so it looks like an unfinished template.
Opening hours#
A field type of its own: a hidden field holding a structure that JavaScript fills in.
The opening hours can be lost silently
When the field is not rendered on save, the parameter collection is replaced without it — the same mechanism as with any other collection (Hydrators). Nothing reports it, which is why the template carries an explicit comment about it.
Categories and prices#
A nested set, a price matrix of region × payment gateway, propagation into a subtree.
Ticking "free" only HIDES the matrix, the prices are still saved
The fields are hidden by styling, not disabled. Unticking "free" reveals prices the administrator never knowingly entered.
Membership × a paid listing#
Access to a profile's detail in a paid category can be conditioned on the visitor's membership — which is something entirely different from the company's paid listing.
| Payment | Who pays | What it unlocks |
|---|---|---|
| the listing | the company | that the profile is in the catalogue at all |
| membership | the visitor | that they can open the profile's detail |
Two different payments, two different payers
Confusing them is the most common misunderstanding in this module. "The company paid and it still is not visible" means the visitor's membership is missing — not that the company's payment failed.
Settings#
The module has index 6 in the settings table (classifieds has 5). The form today
writes only advertsPerPage, maxImageSize, expiration and automaticApproval.
Keys the code does not read were removed from the form — do not bring them back
Image dimensions, the resize type and watermarkFilename are not in the catalogue's
settings: the variants are driven by assetVariants.neon (comcat-adverts) and the
watermark by the global watermarkPath. The keys were deleted on 2026-08-13
(docs/sql/2026-08-13-settings-drop-unread-keys.sql). If somebody put them back into
the form, the first save would recreate them — saveSettingForm() writes every
listed key, including the ones nobody reads.
Scheduled tasks#
| Endpoint | What it does | Recommended interval |
|---|---|---|
/cron/comcat/category/recount |
recounts companies per category | every 10–15 min |
/cron/comcat/advert/notice-expiration |
warns before a listing expires | daily |
/cron/comcat/advert/delete-expired |
marks deleted = 1 after expiry |
daily |
/cron/comcat/watchdog-email/send |
sends the saved-search alerts | every 5–10 min |
/cron/comcat/watchdog-email/digest |
the summary digest | daily in the evening |
/cron/comcat/elastic/sync |
incremental index synchronisation | every 10 min |
/cron/comcat/elastic/reindex |
a full reindex | nightly |
/cron/comcat/sitemap/{category,advert} |
sitemaps | nightly |
Without the expiry cron, listings never expire
Companies that stopped paying stay in the catalogue — and because active knows
nothing about expiry, they keep being displayed normally.
Where to look#
| I want | Where |
|---|---|
| the profile logic | app/UI/Api/Comcat/Models/Managers/AdvertManager.php |
| the count recalculation | app/UI/Api/Comcat/Models/Managers/CategoryManager.php |
| the module settings | app/UI/Admin/Comcat/Presenters/SettingPresenter.php |
| the cron tasks | app/UI/Cron/Comcat/Presenters/ |
| the note about the module's creation | docs/comcat-copy-from-bazaar.md |
Follow-up chapters: Classifieds · Elasticsearch · Scheduled tasks