PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

B2B – Company, Shared Catalog, Negotiable Quote, Requisition List, własne pluginy

by Henryk Tews / wtorek, 21 maja 2024 / Opublikowano w Magento 2

Magento Commerce (Adobe Commerce) ma dedykowany zestaw modułów B2B – Company, Shared Catalog, Negotiable Quotes, Requisition Lists. Jeśli piszesz moduły dla sklepów B2B, musisz rozumieć jak te mechanizmy działają i jak się z nimi integrować. Pokazuję architekturę B2B od środka, typowe scenariusze i jak rozszerzać bez rozwalania wbudowanej funkcjonalności.

Czym B2B różni się od B2C w kontekście technicznym

Aspekt B2C B2B
Klient Indywidualny Customer Company z wieloma użytkownikami i rolami
Ceny Wspólny katalog, customer group Shared Catalog per firma, wynegocjowane ceny
Proces zakupowy Koszyk → checkout → zamówienie Requisition List → Quote → zatwierdzenie → zamówienie
Limity kredytowe Brak Credit limit per firma, Purchase Order number
Uprawnienia Jeden poziom (zalogowany/niezalogowany) Role w firmie (admin, nabywca, menedżer)

Company – rdzeń B2B

<?php

declare(strict_types=1);

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

class CompanyService
{
    public function __construct(
        private CompanyRepositoryInterface $companyRepository,
        private CompanyManagementInterface $companyManagement
    ) {}

    // Pobierz firmę klienta
    public function getCustomerCompany(int $customerId): ?CompanyInterface
    {
        return $this->companyManagement->getByCustomerId($customerId);
    }

    // Sprawdź czy klient jest administratorem firmy
    public function isCompanyAdmin(int $customerId): bool
    {
        $company = $this->getCustomerCompany($customerId);
        if (!$company) {
            return false;
        }

        return (int) $company->getSuperUserId() === $customerId;
    }

    // Pobierz wszystkich użytkowników firmy
    public function getCompanyUsers(int $companyId): array
    {
        $company = $this->companyRepository->get($companyId);
        return $this->companyManagement->getUsers($company);
    }

    // Zmień status firmy
    public function blockCompany(int $companyId, string $reason): void
    {
        $company = $this->companyRepository->get($companyId);
        $company->setStatus(CompanyInterface::STATUS_BLOCKED);
        $company->setRejectReason($reason);
        $this->companyRepository->save($company);
    }
}

Shared Catalog – katalogi cen per firma

<?php

declare(strict_types=1);

use Magento\SharedCatalog\Api\SharedCatalogRepositoryInterface;
use Magento\SharedCatalog\Api\ProductManagementInterface;
use Magento\SharedCatalog\Api\PriceManagementInterface;
use Magento\SharedCatalog\Api\CompanyManagementInterface as SharedCatalogCompanyManagement;

class SharedCatalogService
{
    public function __construct(
        private SharedCatalogRepositoryInterface $catalogRepository,
        private ProductManagementInterface $productManagement,
        private PriceManagementInterface $priceManagement,
        private SharedCatalogCompanyManagement $companyCatalogManagement
    ) {}

    // Dodaj produkty do shared catalog z cenami
    public function addProductsWithPrices(
        int $catalogId,
        array $skusWithPrices // ['SKU-001' => 19.99, 'SKU-002' => 49.99]
    ): void {
        $catalog = $this->catalogRepository->get($catalogId);

        // Dodaj produkty do katalogu
        $products = array_map(fn($sku) => ['sku' => $sku], array_keys($skusWithPrices));
        $this->productManagement->assignProducts($catalog, $products);

        // Ustaw niestandardowe ceny
        $priceData = array_map(fn($sku, $price) => [
            'sku'       => $sku,
            'price'     => $price,
            'website_id' => 1,
        ], array_keys($skusWithPrices), array_values($skusWithPrices));

        $this->priceManagement->saveCustomPrices($catalog, $priceData);
    }

    // Przypisz firmę do katalogu
    public function assignCompanyToCatalog(int $companyId, int $catalogId): void
    {
        $catalog   = $this->catalogRepository->get($catalogId);
        $companies = $this->companyCatalogManagement->getCompanies($catalog);

        $companyIds = array_column($companies, 'company_id');
        if (!in_array($companyId, $companyIds, true)) {
            $companyIds[] = $companyId;
            $this->companyCatalogManagement->assignCompanies($catalog, [
                ['company_id' => $companyId]
            ]);
        }
    }
}

Negotiable Quote – wynegocjowane zamówienia

<?php

declare(strict_types=1);

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

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

    // Pobierz oferty dla firmy
    public function getCompanyQuotes(int $companyId): array
    {
        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter('company_id', $companyId)
            ->addFilter('status', [
                NegotiableQuoteInterface::STATUS_CREATED,
                NegotiableQuoteInterface::STATUS_SUBMITTED_BY_CUSTOMER,
                NegotiableQuoteInterface::STATUS_PROCESSING_BY_ADMIN,
            ], 'in')
            ->create();

        return $this->quoteRepository->getList($searchCriteria)->getItems();
    }

    // Zaakceptuj ofertę ze strony admina (z rabatem)
    public function adminAcceptQuote(int $quoteId, float $discountPercent): void
    {
        $this->quoteManagement->adminSend($quoteId);

        // Zaaplikuj rabat admina
        $quote = $this->quoteRepository->get($quoteId);
        $negotiableQuote = $quote->getExtensionAttributes()->getNegotiableQuote();
        $negotiableQuote->setNegotiatedPriceType(
            NegotiableQuoteInterface::NEGOTIATED_PRICE_TYPE_PERCENTAGE_DISCOUNT
        );
        $negotiableQuote->setNegotiatedPriceValue($discountPercent);

        $this->quoteRepository->save($quote);
    }
}

Własny plugin zintegrowany z Company

<?php

declare(strict_types=1);

namespace Vendor\Module\Plugin;

use Magento\Company\Api\CompanyManagementInterface;
use Magento\Customer\Api\Data\CustomerInterface;

// Plugin który dodaje własną logikę przy tworzeniu firmy
class CompanyCreationPlugin
{
    public function __construct(
        private \Vendor\Module\Model\CreditLimitService $creditLimitService,
        private \Psr\Log\LoggerInterface $logger
    ) {}

    public function afterCreateCompany(
        \Magento\Company\Model\CompanyManagement $subject,
        \Magento\Company\Api\Data\CompanyInterface $result,
        CustomerInterface $customer
    ): \Magento\Company\Api\Data\CompanyInterface {
        // Ustaw domyślny limit kredytowy per typ firmy
        $companyType = $result->getExtensionAttributes()?->getCompanyType() ?? 'standard';

        $defaultLimit = match($companyType) {
            'vip'      => 50000.0,
            'premium'  => 20000.0,
            default    => 5000.0,
        };

        try {
            $this->creditLimitService->setLimit($result->getId(), $defaultLimit);
            $this->logger->info('Credit limit set', [
                'company_id' => $result->getId(),
                'limit'      => $defaultLimit,
                'type'       => $companyType,
            ]);
        } catch (\Exception $e) {
            $this->logger->error('Failed to set credit limit', [
                'company_id' => $result->getId(),
                'error'      => $e->getMessage(),
            ]);
        }

        return $result;
    }
}

Requisition List – listy zakupowe

<?php

declare(strict_types=1);

use Magento\RequisitionList\Api\RequisitionListRepositoryInterface;
use Magento\RequisitionList\Api\Data\RequisitionListInterface;

class RequisitionListService
{
    public function __construct(
        private RequisitionListRepositoryInterface $listRepository
    ) {}

    // Pobierz wszystkie listy zakupowe klienta
    public function getCustomerLists(int $customerId): array
    {
        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter('customer_id', $customerId)
            ->create();

        return $this->listRepository->getList($searchCriteria)->getItems();
    }

    // Dodaj produkty do listy zakupowej (np. po imporcie CSV)
    public function addItemsToList(int $listId, array $items): void
    {
        $list = $this->listRepository->get($listId);

        foreach ($items as $item) {
            // $item = ['sku' => 'SKU-001', 'qty' => 5, 'options' => []]
            $listItem = $this->requisitionListItemFactory->create();
            $listItem->setSku($item['sku']);
            $listItem->setQty($item['qty']);

            $existingItems = $list->getItems() ?? [];
            $existingItems[] = $listItem;
            $list->setItems($existingItems);
        }

        $this->listRepository->save($list);
    }
}

Observer na eventy B2B

<?php

// Magento B2B emituje własne eventy - można na nie reagować
// events.xml

// company_save_after          - po zapisaniu firmy
// company_customer_assign     - po przypisaniu klienta do firmy
// negotiable_quote_send       - po wysłaniu oferty do klienta
// requisition_list_item_save  - po dodaniu produktu do listy zakupowej

class CompanySaveObserver implements \Magento\Framework\Event\ObserverInterface
{
    public function execute(\Magento\Framework\Event\Observer $observer): void
    {
        /** @var \Magento\Company\Api\Data\CompanyInterface $company */
        $company = $observer->getData('company');

        // Synchronizuj zmianę firmy z zewnętrznym CRM
        $this->crmSync->syncCompany([
            'id'     => $company->getId(),
            'name'   => $company->getCompanyName(),
            'status' => $company->getStatus(),
            'email'  => $company->getCompanyEmail(),
        ]);
    }
}

Podsumowanie

Magento 2 B2B to potężny, ale skomplikowany ekosystem modułów. Kluczowe rzeczy przy pisaniu własnych modułów: zawsze sprawdzaj czy moduł B2B jest zainstalowany przed użyciem jego klas (interfejsy mogą nie istnieć w CE), używaj eventów zamiast pluginów tam gdzie B2B je emituje, i pamiętaj że Shared Catalog całkowicie zmienia sposób obliczania cen – własne kalkulatory cenowe muszą to respektować.

About Henryk Tews

Co możesz przeczytać następne

GraphQL – własny resolver, schemat, autoryzacja, testowanie w DDEV
DI w Magento 2 – kontener, Virtual Types, Factory, shared/non-shared
Plugin vs Preference w Magento 2 – kiedy co stosować
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
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 Zawsze aktywne
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.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}