Customer groups & visibility — the site's access model
Audience: developers & AI agents · Scope: the customer-group model and the group-based visibility gating across products, news, brochures, powerpoints and diagrams · Last reviewed: 2026-07-20
TL;DR — Every site user belongs to exactly one customer group, and that group decides their currency, whether they see prices, what they may do (checkout / save order / request a quote), their group discount, whether they count as a BC agent, and — through per-entity xref tables — which products and files exist for them at all. A product with no matching group row is not "hidden", it is absent from every query. This is the single most common cause of "why can't anyone see this?".
What it is & why it exists
Beta-Calco sells into several markets (US, Canada, UK, EU, export) and to two very different audiences (agents who quote and see pricing, specifiers who browse and request). Rather than model those as roles plus a separate currency setting, ConfigBox's customer group carries all of it at once — so "US Agent" and "UK Specifier" are groups, and moving a user between groups changes their whole experience.
The group lives on the user record (#__configbox_users.group_id); everything else reads it from there via
ConfigboxUserHelper::getGroupId().
How it fits together
┌──────────────────────────────┐
registration default │ #__configbox_users.group_id │
region picker │ (one group per user) │
admin (Joomla client) └──────────────┬───────────────┘
│ getGroupId()
┌───────────────────┼────────────────────┐
▼ ▼ ▼
┌───────────────────┐ ┌─────────────────┐ ┌──────────────────────┐
│ group record │ │ permission.php │ │ xref_*_group tables │
│ #__configbox_ │ │ canSeePricing │ │ product / news / │
│ groups (+ │ │ canCheckout… │ │ brochure / powerpoint│
│ _external_ │ │ canRequest… │ │ / diagram │
│ groups_appends) │ │ canGetB2BMode │ │ │
│ currency, is_agent│ └─────────────────┘ │ → filters every list │
│ discounts 1–5 │ │ query │
└─────────┬─────────┘ └──────────────────────┘
│ is_agent
▼
ObserverBcCustomerGroups → syncs the user's **Joomla** user group
| File | Role |
|---|---|
helpers/user.php:291 | ConfigboxUserHelper::getGroupId() — the entry point everything else uses |
helpers/user.php | getGroupData(), initUserRecord() (applies the default group at registration) |
helpers/permission.php | canSeePricing / canSaveOrder / canCheckoutOrder / canRequestQuotation / canGetB2BMode — thin reads of the group's booleans |
helpers/customergroup.php:18,78 | getDiscount() / getDiscountRecurring() — the group's 5 discount levels |
models/admincustomergroups.php (core) | the group model; data/customization/model_property_customization/admincustomergroups.php adds the BC fields |
data/customization/system_overrides/ObserverBcCustomerGroups.php | on customer save, mirrors is_agent into the Joomla user group |
data/customization/controllers/bcregions.php:44-61 | region picker → UPDATE #__configbox_users SET group_id = … |
data/customization/models/bcconfigurator.php:174-181 | injects xref_adminproducts_admincustomergroups.group_id into loadProductData() — the product gate |
data/customization/system_overrides/BcHelper.php:510 | userIsAgent() — reads is_agent |
Admin settings
| Setting | Where set | Default | Blank / off |
|---|---|---|---|
Default Customer Group (default_customer_group_id) | core Settings (adminconfig) | — | New registrations get no usable group; treat as required |
Joomla group for specifiers (joomla_group_id_specifiers) | Custom Settings → Joomla group assignment | — | Observer logs an error and skips the sync entirely |
Joomla group for agents (joomla_group_id_agents) | Custom Settings → Joomla group assignment | — | Same — sync skipped |
Joomla group for quote makers (joomla_group_id_quote_makers) | Custom Settings → Joomla group assignment | — | Not used by the observer; read where quote-maker rights are checked |
Per-group settings live on the group record itself (Customer Groups screen), not in Custom Settings:
Name, Currency (required), Analytics Name, Group members are BC-Agents (is_agent),
Platform Group (joomla_user_group_id, required), Tax display mode (b2b_mode), the six
Permissions booleans, five Regular + five Recurring discount levels, and the three
Email recipients for requests textareas (blank ⇒ falls back to the site-wide addresses in Custom Settings).
The plain-language operator how-to for all of this is Admin Guide → Customer groups (
admin-guide/quotes-sales/customer-groups.md).
Control / data flow
How a user acquires a group — three real paths:
- Registration —
ConfigboxUserHelper::initUserRecord()applies Default Customer Group. - Region picker —
controllers/bcregions.phpwrites the chosen region'sgroup_idonto the user and callsresetUserCache(). An anonymous visitor gets a user record created first. This means a visitor can move themselves between groups, by design — it is the currency/market switch. - Admin — the customer edit form's group dropdown. See the gotcha below: it does not render in the frontend-routed backend.
How the group gates content — each gated entity has an xref table, and the list query filters on the visitor's group:
| Entity | Xref table | Admin field label | Created by |
|---|---|---|---|
| Products | #__configbox_external_xref_product_group | Group Assignments | updates/0.0.1.php |
| News | #__configbox_external_xref_news_group | Visible for these Groups | updates/0.0.45.php |
| Brochures | #__configbox_external_xref_brochure_group | Allowed Customer Groups | updates/0.0.8.php |
| Powerpoints | #__configbox_external_xref_powerpoint_group | Allowed Customer Groups | updates/0.0.29.php |
| Diagrams | #__configbox_external_xref_diagram_group | Allowed Customer Groups | updates/0.0.9.php |
The product filter is applied in ConfigboxModelBcconfigurator::loadProductData(), so every product
surface inherits it: catalog, family pages, downloads, related products, the discontinued page, and the
quote-maker's product picker.
The Joomla group mirror — ObserverBcCustomerGroups::onAfterStoreRecord fires on admincustomers saves:
if the CB group has is_agent, the user is added to the Joomla agents group and removed from specifiers;
otherwise the reverse. It bails out (logging an error) when either setting is missing or the customer has no
platform_user_id.
Data model
| Table | Owns |
|---|---|
#__configbox_groups (core) | name, currency, joomla_user_group_id, b2b_mode, the permission booleans, discount_*_1..5, discount_recurring_*_1..5 |
#__configbox_external_groups_appends | the BC additions — currency_id, analytics_name, is_agent, emails_registration, emails_rfq, emails_brochures · created by updates/0.0.17.php, extended by 0.0.86, 0.0.172, 0.4.56, 0.4.57 |
#__configbox_users.group_id | the user → group link |
xref_*_group (five tables above) | entity → group visibility |
Deployment runbook (manual steps)
No migration or deploy step is needed for the mechanism itself — it ships with the code. To stand up the model on a fresh environment:
- Run migrations (automatic on next page load) so the appends and xref tables exist.
- Create the groups (Customer Groups screen), each with its Currency and Platform Group.
- Set Default Customer Group in core Settings.
- Set Joomla group for specifiers and Joomla group for agents in Custom Settings → Joomla group assignment, or the Joomla-group mirror silently does nothing.
- Create the regions and point each at its Default Customer Group (see the Admin Guide).
- Tick Group Assignments on products — a product with none is invisible to everyone.
- Smoke test: sign in as an account in one group, confirm currency, price visibility, and that a group-restricted product appears; switch region and confirm the currency changes.
Turning it off: not applicable — the group is mandatory on every user; there is no bypass.
Testing
No dedicated spec today. The e2e agent account's group underpins most agent specs — see testing/guide.md. Worth adding: a spec asserting that a product with a non-matching group assignment is absent from the catalog and from the quote-maker picker.
Gotchas & caveats
- Empty group assignments = invisible to everyone. Not an error, not a warning — the row simply never matches. The first thing to check for any "missing product/brochure" report.
- The customer edit form's group dropdown does not render in the normal backend. It lives in
templates/customerform/admin_only_fields.php, gated onviews/customerform/view.html.php:162—isAdminArea() && isAuthorized('com_configbox.core.manage').isAdminArea()is true only under Joomla's/administratorclient, and Beta-Calco's ConfigBox backend is frontend-routed (index.php?option=com_configbox&controller=…). So in the admin operators actually use, the field is absent, along with Platform User ID, User claims to be an agent and Custom field 4. Reaching the same screen via/administrator/index.php?option=com_configbox&…does render them. - Currency belongs to the group, not the user. There is no per-user currency override.
- A visitor can change their own group via the region picker. Anything that assumes a stable group across a session is wrong.
is_agentis duplicated into Joomla groups asynchronously. If the two settings are unset the mirror is skipped and only an error-log line records it — the CB group and the Joomla group then disagree, which shows up as agent-only pages misbehaving.- Group deletion is dangerous — five xref tables plus regions, users and Pipedrive-facing data reference groups. The group edit screen lists its current members at the bottom; prefer renaming.
Possible follow-ups
- A spec covering group-gated product visibility (see Testing).
- The unused Joomla group for quote makers setting — confirm whether anything still reads it.
Related docs
- Admin Guide → Customer groups (
admin-guide/quotes-sales/customer-groups.md) — the operator how-to - Admin Guide → Find and update a customer (
admin-guide/quotes-sales/manage-customers.md) - Discontinued products — the other reason a product disappears
- ConfigBox architecture — the override model these customizations use