conneqt/module-customer-assortment

N/A

Maintainers

Package info

git.dev.epartment.nl/conneqt/m2/customer-assortment-module

Type:magento2-module

pkg:composer/conneqt/module-customer-assortment

Transparency log

Statistics

Installs: 2 473

Dependents: 0

Suggesters: 0

1.2.7 2026-07-16 08:33 UTC

README

What this module does

conneqt/module-customer-assortment adds customer-specific product visibility to Magento 2.

In short, the module lets you assign products to a customer-specific assortment and then uses that assortment to:

  • filter catalog search results
  • block direct access to product detail pages when a product is not allowed
  • support guest visibility rules for selected products and categories
  • expose REST endpoints for synchronizing assortments from an external system
  • optionally keep product flags and parent-product relations in sync through Magento's message queue

This is useful when not every customer should see the same catalog, or when an ERP/PIM/integration decides which SKUs a customer may buy or search for.

How it works

At a high level, the module works in four layers:

  1. Assortment storage
    Customer-to-product assignments are stored in the conneqt_customer_assortment table declared in etc/db_schema.xml.

  2. Sync and maintenance
    The helpers behind the REST API create, update, remove, and check assortment rows. When enabled, they also publish product update messages so Magento can update derived product state asynchronously.

  3. Storefront enforcement
    Magento search and product-view flows are intercepted so shoppers only see products they are allowed to access.

  4. Admin and configuration
    The module adds configuration fields and a customer admin tab so assortment behavior can be controlled and reviewed from the backend.

Request flow overview

1. Assortment data is stored per customer

The main table is etc/db_schema.xml:

  • customer_id: the external/customer identifier used by the module
  • product_sku: the Magento SKU that belongs to the customer assortment
  • external_id: optional external reference from an upstream system
  • personal_sku: optional customer-specific alias that can be searched

The module also stores customer-specific search terms in conneqt_customer_search_query.

2. API calls or internal code update the assortment

The main write/read helpers are:

REST routes are declared in etc/webapi.xml, while dependency wiring for API implementations lives in etc/di.xml.

3. Product visibility is enforced on the storefront

The most important visibility flow is configured in etc/di.xml:

4. Search is extended with assortment-specific data

The module also extends Magento search behavior:

5. Queue consumers keep product metadata in sync

If automatic updates are enabled, Helper/ProductAdd.php publishes messages to the conneqt.assortment.product.update topic.

Queue configuration is defined in:

The queue handler Model/Queue/Handler/Handler.php can:

  • automatically toggle the customer_specific product attribute
  • automatically add parent products for configurable, bundle, and grouped products

Installation

Install the module with Composer:

composer require conneqt/module-customer-assortment

Then enable and upgrade Magento as usual:

bin/magento module:enable Conneqt_CustomerAssortment
bin/magento setup:upgrade
bin/magento cache:flush

If you use the asynchronous product update flow, make sure the Magento queue consumer for conneqt.assortment.product.update is running in your environment.

Requirements

  • PHP >= 8.1
  • magento/framework
  • magento/module-catalog-search ^102.0
  • cweagans/composer-patches
  • conneqt/m2-base >=1.0

These constraints are declared in composer.json.

Configuration

Configuration is available in the Magento admin at:

Stores → Configuration → Conneqt → Customer Assortment

The fields are declared in etc/adminhtml/system.xml.

Important settings

  • Customer Id Attribute
    Selects which customer attribute the module uses as the assortment identifier. This makes it possible to use an ERP/customer number instead of Magento's internal customer entity ID.

  • Automatically adjust customer specific
    When enabled, product visibility metadata is recalculated after assortment changes so products can be marked as customer-specific automatically.

  • Automatically add parent product to a customers assortment
    When enabled, parent products of assigned simple products can be added automatically through the queue handler.

  • No assortment full access
    Lets logged-in customers without any assortment rows see the full catalog. Guests are still handled by the guest visibility rules.

Product and category attributes added by this module

The data patches in Setup/Patch/Data create the attributes used by the visibility logic:

These attributes are part of the guest and customer-specific visibility checks performed in search and on the product detail page.

Admin UI

The module adds assortment-related admin UI for customer management:

Important files at a glance

FilePurpose
etc/db_schema.xmlDeclares the assortment and customer search query tables.
etc/di.xmlWires API preferences, Magento overrides, and plugins.
etc/webapi.xmlDeclares the REST endpoints for external integrations.
etc/adminhtml/system.xmlAdds the module configuration section in admin.
Helper/ProductAdd.phpMain entry point for inserting/updating assortment rows and publishing queue updates.
Helper/CustomerAssortment.phpReads current-customer assortment state from HTTP context and storage helpers.
Plugin/ElasticsearchPlugin.phpRestricts search results to allowed products and guest-visible items.
Plugin/ProductDetailBlockPlugin.phpBlocks direct PDP access for products outside the active shopper's assortment.
Model/Queue/Handler/Handler.phpProcesses async product updates after assortment changes.
view/adminhtml/ui_component/assortment_listing.xmlDefines the customer assortment grid in admin.

API reference

The REST API documentation has been moved to a separate file:

That file contains the endpoint list, request payloads, and response structure for:

  • add products
  • remove products
  • remove all products
  • get products
  • check whether a customer has a product

Search query integration and Mirasvit (read before touching the search plugins)

The search plugins (Plugin/ElasticsearchPlugin.php, Plugin/OpenSearchPlugin.php) run beforeQuery on the search client and rewrite the raw engine query array. Two clauses are injected: the assortment visibility conditions (always) and the personal-SKU nested clause (logged-in only). The personal-SKU clause depends on the shape of the query another module built, which makes it fragile. Know these facts:

  • The personal-SKU clause requires a query_string clause in bool.must. Only Mirasvit Search Ultimate's query builder (Mirasvit\SearchUltimate\SearchElastic\SearchAdapter\QueryBuilder) produces query_string. Plain core Magento builds match/match_phrase clauses instead, so without Mirasvit the personal-SKU clause is silently never addedgetSearchQuery() returns '' and addPersonalSku() no-ops. There is no error and no log line. If this module is ever used on a shop without Mirasvit Search Ultimate, getSearchQuery() must be extended to also extract the term from core's match clauses.
  • Mirasvit wraps the shopper's term in noise characters: searching 89.0689.065.0013 produces "query": "((((*89.0689.065.0013*))))". When extracting the raw term, strip all wrapping characters with a single order-independent charlist: trim(trim($searchTerm), "()\"'* \t\n\r\0\x0B"). Do not trim * and ( in separate sequential calls — trimming stars before parens leaves the inner stars in place, and escapeWildcardValue() then escapes them, producing a wildcard (*\*term\**) that requires literal asterisks and never matches. This exact bug shipped in 1.2.5 and made personal-SKU search return nothing — and it stayed unnoticed for a long time because a project-level searchable attribute containing the same alias values masked it (see Troubleshooting below).
  • Mirasvit score rules wrap the whole query in script_score. OpenSearchPlugin handles both shapes (getMustQuery()/getMustQueryByValue()); ElasticsearchPlugin assumes a plain bool root and would corrupt a script_score query. If Mirasvit score rules are activated on a shop using the ES7/ES8 client, port the OpenSearch handling first.
  • Mirasvit injects _misc subfields (copy_to: _search) into indexed fields, including the nested personal_skus entries. Personal-SKU text can therefore also match through generic full-text search (for every visitor, unscoped). This can make the customer-scoped nested clause look like it works when it doesn't. When verifying this module's search behavior, test with a logged-in customer AND a guest, and confirm the guest gets no personal-SKU match.
  • Debugging recipe: capture the real query with the ES index slowlog — PUT <index>/_settings {"index.search.slowlog.threshold.query.trace":"0ms"}, run one storefront search (mind FPC: add a throwaway query param), read the slowlog, then set the threshold back to -1. Code reading is not reliable here because the query shape is config/data-driven.

Indexed personal_skus data has its own trap: nothing invalidates catalogsearch_fulltext when assortment rows change (this module has no mview/indexer subscription), so personal SKUs added after the last full reindex are absent from the index until the affected products are reindexed.

Troubleshooting personal-SKU search

Work through these in order — the cheap data checks come first because in practice the cause is usually data or configuration, not code. This is especially true when the same code behaves differently between two environments.

  1. Rule out other match paths first. Check whether the shop has searchable product attributes that contain the same alias values — e.g. a legacy "customer/partner catalog numbers" text attribute filled by an ERP import (SELECT ea.attribute_code FROM catalog_eav_attribute cea JOIN eav_attribute ea ON ea.attribute_id = cea.attribute_id WHERE cea.is_searchable = 1 and inspect suspicious text attributes' values). Such an attribute matches personal SKUs through plain full-text search for every visitor, unscoped, which (a) masks a broken nested clause, (b) makes search behavior differ between environments whose attribute data differs even though code and config are identical, and (c) is usually a data leak that should be made non-searchable once this module's scoped search works.
  2. Check the right index. Every store view has its own index (<prefix>_product_<storeId>). Confirm which store id the storefront you are testing actually uses before inspecting documents — inspecting a healthy sibling store's index proves nothing.
  3. Check the document. GET <index>/_search with {"query":{"term":{"sku.keyword":"<sku>"}}} and _source: ["personal_skus"]. The entries must contain the expected customer_id value — which is the configured customer_id_attribute value as a string, not necessarily the Magento entity id. Remember the staleness trap above: rows added after the last reindex are not in the document.
  4. Check the mapping. personal_skus must be type: nested (forced by the Improved* client overrides). The mapping is only written at index creation, so the first install needs a full catalogsearch_fulltext reindex; a dynamic (object) mapping makes every nested query fail.
  5. Replay the module's clause manually. Run the nested term customer_id + wildcard personal_skus.sku query with curl against the exact index. If the manual query hits but the storefront doesn't, the module never added the clause (or built it wrong) — capture the live query with the slowlog recipe above and compare.
  6. Test hygiene. Use a product that is Visible Individually (configurable/grouped children never appear in results regardless of assortment state); test logged-in AND guest; bypass full-page cache with a throwaway query parameter (&nc=123) — a cached results page never hits the search engine, so code changes appear to have no effect.

Limitations and notes

This module focuses on catalog search and direct product access. Some storefront areas may still need custom integration if they load products outside the standard search and product-view flows.

In particular, review and test:

  • custom product blocks or widgets
  • related products / upsells / cross-sells
  • layered navigation counts
  • configurable and grouped child-product visibility in custom themes
  • compatibility with third-party search, merchandising, or personalization modules

Also note that the module's customerId can be mapped to a custom customer attribute, so it does not have to match Magento's default internal customer entity ID.