Skip to content

Supplier Management (Admin)

Flow ID: AD-20 Module(s): eshop Complexity: Low-Medium Last Updated: 2026-04-04

Business Context

Suppliers represent the upstream entities (manufacturers, distributors) that provide products to the shop. Each supplier groups one or more vendors (brands/companies) under a single umbrella. The relationship is many-to-one: many vendors can belong to one supplier, but a vendor has at most one supplier.

The feature is toggleable via the registry setting SUPPLIER.ENABLED (managed in Admin Settings > General). When disabled, the supplier menu entry and filtering are hidden from the admin panel.

Database Schema

shop_supplier -- main entity:

ColumnTypeNotes
idint(11) PK AI
logotext, nullableFilename relative to files/suppliers/
orderint(11), default 0Sort priority

shop_supplier_mui -- translations:

ColumnTypeNotes
idint(11) PK AI
supplier_idint(11), FKReferences shop_supplier.id
namevarchar(255)Translated supplier name
langvarchar(2), default 'el'Language code

shop_vendor.supplier_id -- the foreign key on the vendor table linking vendors to their supplier (nullable int).

Architecture

The supplier domain is implemented in both legacy and modern layers.

Legacy Layer (Admin CRUD)

  • Controller: ecommercen/eshop/controllers/Adv_suppliers_admin.php (191 lines)
  • Model: ecommercen/eshop/models/Adv_suppliers_model.php (109 lines)
  • Views: application/views/admin/suppliers/ -- list.php, create.php, update.php
  • Vendor model methods: Adv_vendors_model::updateSupplierIds(), get_vendors_of_supplier(), get_vendors_list_combo()

Modern Domain Layer (REST API)

  • Namespace: Advisable\Domains\Product\Supplier\
  • Entity: Repository\Entity -- properties: id, logo, order
  • MUI Entity: Repository\MuiEntity -- properties: id, supplier_id, name, lang
  • Repository: Repository\Repository (table shop_supplier) with RepositoryConfigurator defining translations relation (ONE_TO_MANY via MuiRepository on supplier_id)
  • Service: Service -- read operations: all(), item(), get() with Specification pattern (Filter, Sort, Pagination, WithRelations)
  • WriteService: WriteService -- transactional create(), update(), delete() with MUI handling (delete+re-insert on update)
  • WriteData / MuiWriteData: Value objects with OpenAPI schema annotations
  • Validator: Validates lang requirement on translations; create/update hooks are extensible (currently no-op)
  • WriteRepository / MuiWriteRepository: Extend BaseWriteRepository; MUI repo provides insertForEntity(), replaceForEntity(), deleteForEntity()
  • ListRequest: Allowed filters: id (Exact), priority (Exact), name.{locale} (Partial). Allowed sorts: id, priority, name.{locale}.

REST API Layer

  • Controller: Advisable\Rest\Product\Controllers\Supplier (extends HandlesRestfulActions, uses HandlesUploadActions)
  • Resource: Rest\Product\Resources\Supplier\Resource -- outputs id, logo (formatted with /files/suppliers/ prefix), priority (mapped from order), translations (MuiCollection)
  • Collection: Rest\Product\Resources\Supplier\Collection
  • MUI Resource/Collection: MuiResource outputs id, supplierId, name, lang

DI Container

  • Domain registration: src/Domains/Product/container.php -- registers Repository, RepositoryConfigurator, MuiRepository (with NullRelationConfigurator), Service, WriteRepository, MuiWriteRepository, Validator, WriteService
  • REST registration: src/Rest/Product/container.php -- wires controller with Service, Resource/Collection classes, ListRequest, WriteService, and UploadService

Admin Controller Flow

Auth: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTS

Routes (in application/config/routes.php):

eshop/suppliers_admin        -> Adv_suppliers_admin::index
eshop/suppliers_admin/(.+)   -> Adv_suppliers_admin::$1

Menu location: Admin sidebar > Shop > Products group (icon: fa-home, label: "Suppliers" / "Promutheutes")

index()

Lists all suppliers ordered by shop_supplier.order ASC, joined with MUI for current language. Listing view shows ID, thumbnail image (resized to 96px width via resizeImageOnTheFly), name, and action buttons (edit/delete). Includes sortable jQuery for drag-and-drop reordering (delegates to vendors_admin/update_order).

add()

  1. Validates MUI name per language (required, trimmed)
  2. Uploads logo image via AdvUploader to files/suppliers/
  3. Inserts shop_supplier record with logo filename
  4. Inserts shop_supplier_mui rows per active language
  5. Updates vendor associations: vendors_model->updateSupplierIds($supplierId, $vendorIds) -- clears all vendors' supplier_id for this supplier, then sets it on selected vendors
  6. Calls afterAdd() hook (empty, overridable in client repos)
  7. Redirects to listing with success flash

Create form fields: MUI name (per language tab), logo image upload, vendor multi-select (Chosen.js dropdown)

edit($id)

  1. Same validation as add
  2. Uploads replacement logo to files/vendors/ (note: legacy inconsistency -- edit uploads to files/vendors/, add uploads to files/suppliers/)
  3. Supports image deletion via del_image checkbox (sets logo = null)
  4. Updates shop_supplier and shop_supplier_mui records
  5. Reassigns vendor associations via updateSupplierIds()
  6. Calls afterEdit() hook

Edit form fields: Same as create, plus: current image preview with popup, image delete checkbox, pre-selected vendors

delete($id)

  1. Checks canDelete($id) -- returns false if any vendor has supplier_id = $id
  2. If deletable: removes shop_supplier and shop_supplier_mui rows
  3. If blocked: sets error flash "Cannot delete this supplier as it is used by companies"
  4. Calls afterDelete() hook
  5. Redirects to listing

REST API Endpoints

Policy: Backend auth required, roles AUTH_ROLE_ADMIN or AUTH_ROLE_PRODUCTS (defined in application/config/rest_policies.php).

MethodRouteActionDescription
GET/rest/product/supplierindex()Paginated collection with filtering/sorting
GET/rest/product/supplier/itemitem()Single resource by filter criteria
GET/rest/product/supplier/{id}show($id)Single resource by ID
POST/rest/product/supplierstore()Create (JSON or multipart with logo upload)
POST/rest/product/supplier/{id}update($id)Update (JSON or multipart with logo upload)
DELETE/rest/product/supplier/{id}destroy($id)Delete supplier and all translations

All routes support locale prefix: /{locale}/rest/product/supplier/...

Upload handling: Logo field configured via uploadFieldsConfig() with path files/suppliers/. Accepts multipart/form-data for file uploads or application/json for data-only operations. On delete, physical files are removed automatically by UploadService.

Vendor-Supplier Relationship

The supplier-vendor link lives on shop_vendor.supplier_id (nullable FK). Management flows:

  1. From Supplier admin: Multi-select vendors in create/edit forms. updateSupplierIds() first nullifies all vendor rows matching the supplier, then sets the new selection. This means reassigning vendors between suppliers is handled atomically.

  2. From Vendor admin (Adv_vendors_admin.php): Vendor create/edit forms include a supplier dropdown (suppliers_model->dropdown()), storing supplier_id directly on the vendor record.

  3. REST API: Vendor Resource exposes supplierId field and can include the full supplier relation (BELONGS_TO) via ?with=supplier.

  4. Product filtering: Product admin listing uses suppliers as a filter. Adv_product_model::get_products() joins shop_vendor and filters by supplier_id when productSuppliers condition is present. This enables filtering products by their vendor's supplier.

Extension Points

  • afterAdd(), afterEdit(), afterDelete(): Protected empty methods on the controller, designed for client repo overrides (e.g., clearing caches, syncing with external systems)
  • Client DI override: Register Custom\Domains\Product\Supplier\Repository\RepositoryConfigurator aliased to the upstream class to customize relations
  • Public2P integration: Commented-out job UpdatePublic2PSupplier in application/config/jobs.php (cron 0 23 * * *). When enabled, syncs product data to Public2P API using PUBLIC_2P.SUPPLIER_ID registry setting. The job class is at ecommercen/job/libraries/AdvUpdatePublic2PSupplier.php.

Business Rules

RuleDescription
Feature toggleSUPPLIER.ENABLED registry flag controls visibility
Deletion guardCannot delete supplier if any vendor references it
MUI name requiredName is required for each active admin language
Vendor reassignmentSaving a supplier clears then reassigns all vendor links atomically
Single supplier per vendorA vendor can belong to at most one supplier (supplier_id is scalar, not a join table)
Logo storageUploaded to files/suppliers/ (add) or files/vendors/ (edit -- legacy inconsistency)

Test Coverage

  • Integration tests: tests/Integration/Domains/Product/Supplier/ServiceTest.php -- read operations (all, filter, pagination, item, get with relations), write operations (create, create with translations, update, update translations replacement, delete with cascade, round-trip)
  • Integration tests: tests/Integration/Domains/Product/Supplier/RepositoryTest.php -- get by ID, match with filters, matchOne, count, sorting, pagination, relation loading, MUI repository operations
  • Unit tests: tests/Unit/Rest/Product/Resources/Supplier/ResourceTest.php -- resource array structure, logo path formatting, null logo handling, null resource handling