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ć.
