PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Magento B2B – Company, Shared Catalog, Negotiable Quote, custom plugins

by Henryk Tews / Tuesday, 21 May 2024 / Published in Magento 2

Magento B2B is a separate module suite for business-to-business selling. Company accounts, Shared Catalogs with customer-specific pricing, Negotiable Quotes, and Requisition Lists are the core features. I show the architecture, how each feature connects to the core Magento data model, and how to extend them with custom plugins.

B2B module architecture

Module Feature Key tables
Company Company accounts, user hierarchy, credit company, company_advanced_customer_entity
SharedCatalog Customer group pricing and product visibility shared_catalog, shared_catalog_product_item
NegotiableQuote Quote management with custom pricing negotiable_quote, negotiable_quote_item
QuickOrder Order by SKU list Uses standard quote
RequisitionList Saved shopping lists purchase_order_requisition_list
PurchaseOrder Internal approval workflows purchase_order, purchase_order_item

Company – the foundation of B2B

<?php

declare(strict_types=1);

use Magento\Company\Api\CompanyRepositoryInterface;
use Magento\Company\Api\Data\CompanyInterface;

class CompanyService
{
    public function __construct(
        private CompanyRepositoryInterface $companyRepository,
        private \Magento\Company\Api\CompanyManagementInterface $companyManagement,
        private \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder
    ) {}

    public function getCompanyByCustomer(int $customerId): ?CompanyInterface
    {
        // Each customer can belong to one company
        return $this->companyManagement->getByCustomerId($customerId);
    }

    public function getCreditLimit(int $customerId): float
    {
        $company = $this->getCompanyByCustomer($customerId);
        if ($company === null) return 0.0;

        $credit = $company->getExtensionAttributes()?->getCompanyCredit();
        return (float) ($credit?->getCreditLimit() ?? 0.0);
    }

    public function getCompaniesWithLowCredit(float $threshold): array
    {
        $sc = $this->searchCriteriaBuilder->create();
        $companies = $this->companyRepository->getList($sc)->getItems();

        return array_filter($companies, function($company) use ($threshold) {
            $credit = $company->getExtensionAttributes()?->getCompanyCredit();
            $available = ($credit?->getCreditLimit() ?? 0) - ($credit?->getBalance() ?? 0);
            return $available < $threshold;
        });
    }
}

Shared Catalog - customer-specific pricing and visibility

<?php

declare(strict_types=1);

use Magento\SharedCatalog\Api\SharedCatalogRepositoryInterface;
use Magento\SharedCatalog\Api\ProductManagementInterface;
use Magento\SharedCatalog\Api\PriceManagementInterface;

class SharedCatalogService
{
    public function __construct(
        private SharedCatalogRepositoryInterface $sharedCatalogRepository,
        private ProductManagementInterface $productManagement,
        private PriceManagementInterface $priceManagement,
        private \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder
    ) {}

    // Assign products to a shared catalog
    public function assignProducts(int $catalogId, array $skus): void
    {
        $catalog  = $this->sharedCatalogRepository->get($catalogId);
        $products = array_map(fn($sku) => $this->buildProductPayload($sku), $skus);

        $this->productManagement->assignProducts($catalog, $products);
    }

    // Set custom tier prices for a shared catalog
    public function setCustomPrices(int $catalogId, array $priceData): void
    {
        // priceData: [['sku' => 'SKU-001', 'price' => 29.99], ...]
        $catalog = $this->sharedCatalogRepository->get($catalogId);

        $tierPrices = array_map(fn($item) => [
            'price_type' => 'fixed',
            'website_id' => 0,
            'sku'        => $item['sku'],
            'qty'        => 1,
            'value'      => $item['price'],
            'customer_group_id' => $catalog->getCustomerGroupId(),
        ], $priceData);

        $this->priceManagement->saveStore($catalog, $tierPrices, []);
    }
}

Negotiable Quote - B2B pricing workflow

<?php

declare(strict_types=1);

use Magento\NegotiableQuote\Api\NegotiableQuoteRepositoryInterface;
use Magento\NegotiableQuote\Api\NegotiableQuoteManagementInterface;

class NegotiableQuoteService
{
    public function __construct(
        private NegotiableQuoteRepositoryInterface $quoteRepository,
        private NegotiableQuoteManagementInterface $quoteManagement
    ) {}

    // Apply custom discount to a negotiable quote
    public function applyDiscount(int $quoteId, float $discountPercent, string $note): void
    {
        $quote = $this->quoteRepository->getById($quoteId);
        $negotiableQuote = $quote->getExtensionAttributes()?->getNegotiableQuote();

        if ($negotiableQuote === null) {
            throw new \RuntimeException("Quote {$quoteId} is not negotiable");
        }

        // Set discount
        $negotiableQuote->setNegotiatedPriceType(
            \Magento\NegotiableQuote\Api\Data\NegotiableQuoteInterface::NEGOTIATED_PRICE_TYPE_PERCENTAGE_DISCOUNT
        );
        $negotiableQuote->setNegotiatedPriceValue($discountPercent);
        $negotiableQuote->setComments($note);
        $negotiableQuote->setStatus(
            \Magento\NegotiableQuote\Api\Data\NegotiableQuoteInterface::STATUS_SUBMITTED_BY_ADMIN
        );

        $this->quoteManagement->saveByCustomer($quote, $negotiableQuote);
    }
}

Custom plugin - block order if over credit limit

<?php

declare(strict_types=1);

namespace Vendor\B2bExtension\Plugin;

class CreditLimitOrderPlugin
{
    public function __construct(
        private \Magento\Company\Api\CompanyManagementInterface $companyManagement,
        private \Psr\Log\LoggerInterface $logger
    ) {}

    public function beforePlace(
        \Magento\Sales\Api\OrderManagementInterface $subject,
        \Magento\Sales\Api\Data\OrderInterface $order
    ): array {
        $customerId = (int) $order->getCustomerId();
        if ($customerId === 0) return [$order]; // guest checkout

        $company = $this->companyManagement->getByCustomerId($customerId);
        if ($company === null) return [$order]; // not a B2B customer

        $credit    = $company->getExtensionAttributes()?->getCompanyCredit();
        $limit     = (float) ($credit?->getCreditLimit() ?? PHP_FLOAT_MAX);
        $balance   = (float) ($credit?->getBalance() ?? 0.0);
        $available = $limit - $balance;

        if ($order->getGrandTotal() > $available) {
            throw new \Magento\Framework\Exception\LocalizedException(
                __('Order total exceeds your available credit limit of %1.', number_format($available, 2))
            );
        }

        return [$order];
    }
}

Summary

Magento B2B is a powerful but complex suite. Company accounts create the organisational structure; Shared Catalogs control what each company sees and at what price; Negotiable Quotes enable the sales rep workflow. Custom extensions via plugins follow the same patterns as regular Magento development - the B2B interfaces are well-defined and injectable. The credit limit plugin example shows how to intercept order placement to enforce B2B-specific business rules.

About Henryk Tews

What you can read next

Xdebug – configuration, PHPStorm, debugging Magento plugins
Strategy pattern in PHP – and how Magento 2 uses it in pricing

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}