Skip to content

Vendor Management (Admin)

Flow ID: AD-19 Module(s): eshop, video Complexity: Medium Last Updated: 2026-04-04


Business Overview

Vendors (brands/manufacturers) are a core product taxonomy entity. Every product is assigned to exactly one vendor via shop_product.vendor_id. The admin vendor management module provides CRUD operations for vendors including multi-language names/descriptions, four banner images, logo upload, promotional and exclusive flags, builder block integration, slider assignment, supplier linkage, video association, and drag-and-drop ordering.

Vendors also serve as the primary navigation layer on the storefront: the vendor listing page shows all vendors with active products, and each vendor has a dedicated product listing page with optional category/line sub-navigation and a "details" page for exclusive vendors with rich content (builder blocks, sliders, videos).


API Reference

REST API (Modern Layer)

MethodEndpointActionAuth
GET/rest/product/vendorList vendors (paginated, filterable)JWT
GET/rest/product/vendor/{id}Get single vendor by IDJWT
GET/rest/product/vendor/itemGet single vendor by filterJWT
POST/rest/product/vendorCreate new vendorJWT
POST/rest/product/vendor/{id}Update existing vendorJWT
DELETE/rest/product/vendor/{id}Delete vendorJWT

All endpoints support locale-prefixed variants: /{locale}/rest/product/vendor/...

Query Parameters (GET collection):

  • filter[id] -- exact match (comma-separated for multiple)
  • filter[isPromo] -- boolean (0/1)
  • filter[isExclusive] -- boolean (0/1)
  • filter[order] -- exact match
  • filter[sliderId] -- exact match
  • filter[supplierId] -- exact match
  • filter[name.{locale}] -- partial match (LIKE)
  • filter[slug.{locale}] -- exact match
  • sort -- id, isPromo, isExclusive, order, name.{locale}, slug.{locale}
  • with -- translations, supplier, slider, videos
  • page, limit -- pagination

File Uploads: POST endpoints accept multipart/form-data with file fields: logo, vendor_banner, vendor_medium_banner, vendor_small_banner. All uploaded to files/vendors/.

Admin Routes (Legacy Layer)

RouteController MethodDescription
eshop/vendors_adminindex()Vendor listing with drag-and-drop ordering
eshop/vendors_admin/addadd()Create vendor form
eshop/vendors_admin/edit/{id}edit($id)Edit vendor form
eshop/vendors_admin/delete/{id}delete($id)Delete vendor
eshop/vendors_admin/update_orderupdate_order()AJAX drag-and-drop reorder

Auth: Requires roles ADVISABLE, ADMIN, or PRODUCTS.

Admin Menu: Located under Products > Vendors group in application/config/admin_menu.php. Shares a parent menu node with Lines (AD-17).

Frontend Routes

RouteController MethodDescription
vendorsindex()All vendors listing (only those with active products)
vendors/{slug}vendors($slug)Vendor product listing with category/line sub-nav
vendors/{slug}/detailsvendorsDetails($slug)Exclusive vendor detail page (builder, slider, videos)
vendors/{slug}/{lineSlug}lines_list(...)Vendor's line product listing
xml/vendorsvendors_catalog/indexXML vendor catalog feed
xml/doofinder/vendorsdoofinder/vendorsXml/indexDoofinder vendor XML feed

Code Flow

Admin CRUD Flow

Admin_c (Adv_vendors_admin)
  |
  +--> add()
  |      |-- validation() -- form_validation for MUI name (required), flags, banner URLs
  |      |-- advuploader->uploadFilesSameFolder() -- upload to files/vendors/
  |      |-- vendors_model->add_record($data, $dataMui) -- INSERT shop_vendor + shop_vendor_mui
  |      |-- video_model->assignMultipleVideosToVendor() -- sync shop_vendor_video_lp
  |      +-- afterAdd($id) -- hook (empty, overridable by client repos)
  |
  +--> edit($id)
  |      |-- validation(isUpdate: true) -- same rules
  |      |-- advuploader (conditional) -- only if new files posted
  |      |-- del_image/del_image2/del_image3/del_image4 -- null out image fields
  |      |-- vendors_model->update_record($data, $dataMui, $id) -- UPDATE + updateOrInsertMui
  |      |-- video_model->assignMultipleVideosToVendor() -- delete-and-reinsert
  |      |-- setupAiContentGenerationJsonState() -- AI content generation support
  |      +-- afterEdit($id) -- hook
  |
  +--> delete($id)
  |      |-- vendors_model->delete_record($id)
  |      |    |-- canDelete($id) -- checks if any shop_product has this vendor_id
  |      |    |-- IF products exist: set session error, skip deletion
  |      |    +-- ELSE: DELETE shop_vendor + DELETE shop_vendor_mui
  |      +-- afterDelete($id) -- hook
  |
  +--> index()
  |      +-- vendors_model->get_vendors_list() -- JOIN vendor+mui, subquery for most common discount
  |           +-- jquery_on_load(js_listing_sortable()) -- sortable drag-and-drop UI
  |
  +--> update_order()
         +-- vendors_model->updateOrder() -- batch UPDATE order column

REST API Flow

Vendor Controller (HandlesRestfulActions + HandlesUploadActions)
  |
  +--> index() -> Service->all(ListRequest) -> Repository->match(Filter, Sort, Pagination, WithRelations)
  +--> show($id) -> Service->get($id, relations)
  +--> item() -> Service->item(ListRequest)
  +--> store() -> doStore()
  |      |-- UploadService handles file fields (logo, vendor_banner, vendor_medium_banner, vendor_small_banner)
  |      |-- WriteService->create(data)
  |      |    |-- WriteData::fromArray() -- maps camelCase/snake_case input
  |      |    |-- Validator->validateForCreate()
  |      |    |-- parseMuiData() -> MuiWriteData::fromArray() per translation
  |      |    |-- Validator->validateTranslations() -- lang required
  |      |    +-- transactional: WriteRepository->insert() + MuiWriteRepository->insertForEntity()
  |      +-- Returns created entity with translations
  |
  +--> update($id) -> doUpdate($id)
  |      |-- UploadService handles replacement files
  |      |-- WriteService->update($id, data)
  |      |    +-- transactional: WriteRepository->update() + MuiWriteRepository->replaceForEntity() (delete+reinsert)
  |      +-- Returns updated entity
  |
  +--> destroy($id) -> doDestroy($id)
         |-- UploadService removes physical files from storage
         +-- WriteService->delete($id)
              +-- transactional: MuiWriteRepository->deleteForEntity() + WriteRepository->delete()

Slug Generation

In the admin layer, vendor slugs are auto-generated per language from the name field using createSlug(strip_quotes($name)). Each language gets its own slug stored in shop_vendor_mui.vendor_slug. The REST API accepts slugs directly via the vendorSlug field.


Domain Layer

Namespace: Advisable\Domains\Product\Vendor

FileClassPurpose
Repository/Entity.phpEntityBase entity with FilterTranslation trait for MUI-aware filtering
Repository/MuiEntity.phpMuiEntityTranslation entity (name, slug, fulltext, banner URLs)
Repository/Repository.phpRepositoryRead repository, table shop_vendor
Repository/MuiRepository.phpMuiRepositoryMUI read repository, table shop_vendor_mui
Repository/RepositoryConfigurator.phpRepositoryConfiguratorDefines relations: translations, supplier, slider, videos
Repository/WriteRepository.phpWriteRepositoryWrite repository for shop_vendor
Repository/MuiWriteRepository.phpMuiWriteRepositoryWrite repository for shop_vendor_mui with insertForEntity/replaceForEntity/deleteForEntity
Service.phpServiceRead service with all(), item(), get()
WriteService.phpWriteServiceWrite service with create(), update(), delete() (transactional)
ListRequest.phpListRequestDefines allowed filters/sorts with locale-aware MUI fields
WriteData.phpWriteDataDTO for vendor write operations (OpenAPI-annotated)
MuiWriteData.phpMuiWriteDataDTO for translation write operations (OpenAPI-annotated)
Validator.phpValidatorValidation: validateForCreate, validateForUpdate, validateTranslations (lang required)

Entity Properties

Entity (shop_vendor): id, logo, vendor_banner, vendor_medium_banner, vendor_small_banner, is_promo, is_exclusive, order, old_id, old_img_proceed, old_logo, slider_id, supplier_id

MuiEntity (shop_vendor_mui): id, vendor_id, name, vendor_slug, fulltext, exclusive_fulltext, lang, main_banner_url, middle_banner_url, small_banner_url

Additional MUI columns added by migrations: vendor_info (text, product detail page info), builder_block_id (int, page builder block), exclusive_builder_block_id (int, exclusive vendor builder block).

Configured Relations

NameTypeTarget RepositoryFK / Pivot
translationsONE_TO_MANYMuiRepositoryvendor_id
supplierBELONGS_TOSupplierRepositorysupplier_id
sliderBELONGS_TOSliderRepositoryslider_id
videosMANY_TO_MANYVideo\Repositoryshop_vendor_video_lp (vendor_id, video_id)

REST Resource Layer

Namespace: Advisable\Rest\Product

FileClassPurpose
Controllers/Vendor.phpVendorREST controller with full CRUD + file uploads
Resources/Vendor/Resource.phpResourceJSON resource: maps entity to camelCase, formats file URLs, adds relations
Resources/Vendor/Collection.phpCollectionCollection wrapper
Resources/Vendor/MuiResource.phpMuiResourceTranslation resource: vendorId, name, slug, fulltext, exclusiveFulltext, banner URLs, lang
Resources/Vendor/MuiCollection.phpMuiCollectionTranslation collection wrapper

Resource Output Fields: id, logo (full URL), vendorBanner, vendorMediumBanner, vendorSmallBanner, isPromo, isExclusive, priority (mapped from order), sliderId, supplierId, translations[], supplier{}, slider{}, videos[]


Architecture

Dual-Layer Architecture

The vendor domain operates across both legacy and modern layers:

  • Legacy Admin: Adv_vendors_admin extends Admin_c, uses vendors_model (Adv_base_model), direct CI form validation, advuploader library for file handling, BlockBuilder for builder block integration, aiContentGenerationTrait for AI-assisted content.
  • Modern REST API: Vendor controller extends HandlesRestfulActions with HandlesUploadActions trait, uses DI-injected Service/WriteService, UploadService for file handling, WriteData/MuiWriteData DTOs with OpenAPI annotations.
  • Legacy Frontend: Adv_vendors extends Front_c, complex routing via _remap() with vendor slug resolution, category/line sub-navigation, exclusive vendor details page with builder blocks and slider integration.

Key Integration Points

  • BlockBuilder: Admin edit form renders builder block selector per language. Frontend exclusive vendor page renders block content via blockBuilder->frontRender().
  • Video: Many-to-many via shop_vendor_video_lp. Admin manages via multiselect; frontend displays on vendor details page.
  • Slider: Optional slider assignment (slider_id). Rendered on exclusive vendor details page via sliders_model->getFrontMasterRecord().
  • Supplier: Optional many-to-one (supplier_id). Suppliers can manage their vendor assignments bidirectionally.
  • AI Content: Edit form supports AI content generation via aiContentGenerationTrait + setupAiContentGenerationJsonState().
  • PSCache: Frontend vendor queries are wrapped in pscache->model() / pscache->library() calls for page-level caching.

Data Model

shop_vendor

ColumnTypeNullableDefaultDescription
idint(11)NOAUTO_INCREMENTPrimary key
logotextNO--Logo image filename
vendor_bannervarchar(255)YESNULLLarge banner image
vendor_medium_bannervarchar(254)YESNULLMedium banner image
vendor_small_bannervarchar(254)YESNULLSmall banner image
is_promotinyint(1)NO0Show on promo/homepage vendor carousel
is_exclusiveint(2)NO0Exclusive vendor flag (enables details page)
orderint(11)NO0Display sort order (drag-and-drop)
old_idint(11)NO0Legacy migration ID
old_img_proceedint(1)NO0Legacy image migration flag
old_logotextYESNULLLegacy logo filename
slider_idint(11)YESNULLFK to slider (for exclusive vendor page)
supplier_idint(11)YESNULLFK to shop_supplier

Indexes: PRIMARY(id), is_exclusive, is_promo, old_id

shop_vendor_mui

ColumnTypeNullableDefaultDescription
idint(11)NOAUTO_INCREMENTPrimary key
vendor_idint(11)NO--FK to shop_vendor.id
namevarchar(255)NO--Vendor display name
vendor_slugvarchar(255)NO--URL-safe slug (auto-generated from name)
fulltexttextYESNULLVendor description (HTML)
exclusive_fulltexttextYESNULLExclusive vendor description
vendor_infotextYESNULLVendor info text (shown on product detail page)
langvarchar(2)NO'el'Language code
main_banner_urlvarchar(255)YESNULLClick-through URL for main banner
middle_banner_urlvarchar(255)YESNULLClick-through URL for medium banner
small_banner_urlvarchar(255)YESNULLClick-through URL for small banner
builder_block_idint(11)YESNULLFK to builder block (vendor page content)
exclusive_builder_block_idint(11)YESNULLFK to builder block (exclusive vendor page)

Indexes: PRIMARY(id), lang, vendor_id, vendor_id_lang(vendor_id, lang), slug(vendor_slug), slug_lang(vendor_slug, lang), builder_block_id, exclusive_builder_block_id

shop_vendor_video_lp

ColumnTypeNullableDefaultDescription
idint(11)NOAUTO_INCREMENTPrimary key
video_idint(11)NO--FK to video table
vendor_idint(11)NO--FK to shop_vendor.id

Indexes: PRIMARY(id), video_id, vendor_id, video_vendor(video_id, vendor_id)

Foreign Key References

Tables referencing shop_vendor.id:

  • shop_product.vendor_id -- every product belongs to one vendor
  • shop_line.vendor_id -- product lines (collections) belong to a vendor
  • shop_vendor_mui.vendor_id -- MUI translations
  • shop_vendor_video_lp.vendor_id -- vendor-video associations
  • coupon_vendors.vendor_id -- coupon vendor restrictions

Configuration

Application Config

FileKeyDescription
application/config/app.phpuseCanonicalEnables canonical URL handling for vendor routes (TODO: rename to eshopHasVendors)
application/config/languages.phpvendors_base_url_{lang}Base URL segment per language (default: vendors for both el/en)

Admin Menu Config

Vendor admin is registered in application/config/admin_menu.php under the PRODUCTS group:

  • Parent menu: Vendors (icon + label from translation keys)
  • Child: eshop/vendors_admin -- Vendor listing
  • Sibling child: lines_admin -- Lines listing (AD-17)

DI Container Registration

Domain (src/Domains/Product/container.php):

Vendor\Repository\Repository
Vendor\Repository\RepositoryConfigurator
Vendor\Repository\MuiRepository (with NullRelationConfigurator)
Vendor\Service
Vendor\Repository\WriteRepository
Vendor\Repository\MuiWriteRepository
Vendor\Validator
Vendor\WriteService

REST (src/Rest/Product/container.php):

Rest\Product\Controllers\Vendor
  -> $service: Vendor\Service
  -> $resourceClass: Vendor\Resource
  -> $collectionClass: Vendor\Collection
  -> $listRequestClass: Vendor\ListRequest
  -> $writeService: Vendor\WriteService
  -> $uploadService: UploadService

File Upload Configuration

All vendor images upload to files/vendors/ via the advuploader library (admin) or UploadService (REST). Upload field config in the REST controller:

Field KeyUpload Path
logofiles/vendors/
vendor_bannerfiles/vendors/
vendor_medium_bannerfiles/vendors/
vendor_small_bannerfiles/vendors/

Reporting

Vendor-related reports accessible from admin menu under REPORTING > Vendors:

  • eshop/reporting/vendors -- Vendor sales report
  • eshop/reporting/vendor_products -- Vendor product performance
  • eshop/reporting/vendors_timespans -- Vendor sales over time
  • eshop/reporting/vendor_products_timespans -- Vendor product sales over time

Cache

  • Admin: Cache cleared via advisable/clearcache/product_categories_vendors (shared with categories)
  • Frontend: PSCache wraps all vendor model queries with configurable TTL

Client Extension Points

Overridable Admin Hooks

The Adv_vendors_admin controller provides three protected hook methods that client repos can override:

php
protected function afterAdd($id) {}
protected function afterEdit($id) {}
protected function afterDelete($id) {}

Client repos extend Adv_vendors_admin in application/controllers/ and override these hooks for custom logic (e.g., cache invalidation, ERP sync, custom logging).

Overridable Frontend Hooks

The Adv_vendors frontend controller provides:

php
protected function indexExtras() {}
protected function vendorsDetailsExtra() {}

Custom Domain Layer (Client Repos)

Client repos can override the vendor domain via custom/:

php
// custom/Domains/container.php
$services->set(\Custom\Domains\Product\Vendor\Repository\RepositoryConfigurator::class);
$services->alias(
    \Advisable\Domains\Product\Vendor\Repository\RepositoryConfigurator::class,
    \Custom\Domains\Product\Vendor\Repository\RepositoryConfigurator::class
);

This allows adding custom relations, modifying filters, or extending the vendor entity behavior.


Business Rules

RuleDescriptionCode Location
Deletion blocked if products existcanDelete($id) checks shop_product.vendor_id; sets session error if foundAdv_vendors_model::canDelete()
Name required per languageForm validation enforces `trimrequiredonname_{lang}`
Slug auto-generatedcreateSlug(strip_quotes($name)) per language on add/editAdv_vendors_admin::add(), edit()
Image deletion explicitMust check del_image/del_image2/del_image3/del_image4 checkbox to removeAdv_vendors_admin::edit()
Exclusive vendor enables details pageis_exclusive == 1 required for /vendors/{slug}/details route to respond (404 otherwise)Adv_vendors::vendorsDetails()
Promo vendors appear on homepageis_promo == 1 vendors returned by getPromoVendors()Adv_vendors_model::getPromoVendors()
Frontend listing filters inactiveOnly vendors with active, non-deleted products with price > 0 and stock > 0 (or negative_stock)Adv_vendors_model::getVendorsWithActiveProductsAndValidPrices()
Video assignment is replace-allassignMultipleVideosToVendor() deletes existing associations and reinsertsAdv_video_model::assignMultipleVideosToVendor()
REST translation update is replace-allWriteService::update() uses replaceForEntity() (delete + reinsert all translations)WriteService::update()
REST translation requires langValidator::validateTranslations() throws ValidationException if lang is emptyValidator::validateTranslations()
Builder blocks per languageEach language can have its own builder_block_id and exclusive_builder_block_idshop_vendor_mui columns
Vendor info on PDPvendor_info text from MUI is joined and displayed on product detail pagesAdv_products, Adv_product_parser_model
Order is drag-and-dropupdate_order() accepts AJAX sortable list, batch updates order columnAdv_vendors_admin::update_order()

Migrations

MigrationDescription
20241031154616_add_vendor_block_ids.phpAdded builder_block_id and exclusive_builder_block_id to shop_vendor_mui
20250509145411_add_vendor_info_field.phpAdded vendor_info TEXT column to shop_vendor_mui

Test Coverage

Integration tests exist at:

  • tests/Integration/Domains/Product/Vendor/RepositoryTest.php -- Repository CRUD, filtering, sorting, pagination, relation loading, MUI queries
  • tests/Integration/Domains/Product/Vendor/ServiceTest.php -- Service read operations, WriteService create/update/delete, translation management, round-trip test, validation exception on missing lang

Both use CiDatabaseTestCase with direct DB seeding of test vendors (IDs 99011+) to avoid conflicts with TestSeed data.