mar-pod-b2b / module-b2b-pricing
Company price lists and pricing-context integration for Marpod B2B.
Package info
gitlab.com/mar-pod-b2b/module-b2b-pricing
Type:magento2-module
pkg:composer/mar-pod-b2b/module-b2b-pricing
Requires
- php: ^8.5
- magento/framework: ^103.0
- magento/module-catalog: ^104.0
- magento/module-graph-ql: ^100.4
- magento/module-store: ^101.1
- magento/module-webapi: ^100.4
- mar-pod-b2b/module-b2b-company: ^1.0
- mar-pod-b2b/module-b2b-core: ^1.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-08-24 16:10:54 UTC
README
Company price lists and pricing-context integration for Marpod B2B.
Implemented foundation
- declarative schema:
marpod_b2b_price_list,marpod_b2b_price_list_company(one price list per company), andmarpod_b2b_price(one row per SKU per list, with an optional valid_from/valid_to window); PriceListRepositoryInterfacefor price list CRUD;CompanyPriceListAssignmentInterfaceto assign/unassign a price list to a company;CompanyPriceManagementInterfaceto set or remove the price of a SKU within a list;CompanyPriceResolverInterface::resolve()/resolveMultiple(): the currently valid company price for one or many SKUs, scoped to website and currency, cached and precisely invalidated;- a
Marpod\B2bCore\Api\Pricing\PricingContextResolverInterfaceimplementation that resolves guest, customer, or company pricing scope from the authenticated customer's company membership (Marpod\B2bCompany\Model\Storefront\CompanyContext), keeping the acting customer separate from the company whose price applies; marpod_b2b_price_list_changeddomain event, dispatched on price list save/delete, assignment change, and individual price save/delete;- cache invalidation observers for
marpod_b2b_company_saved,marpod_b2b_company_member_changed, andmarpod_b2b_price_list_changed.
A null result from resolve()/resolveMultiple() means "no company price for this SKU" and must
fall back to the native Magento price. It never hides the product.
Price fallback order
active company price list, matching website and currency
-> no result: caller falls back to native Magento price
A price list only applies to the exact website_id/currency_code it was created for. A company
without an assigned list, or one assigned in a different currency, always resolves to no company
price rather than a mismatched one.
Requirements
- PHP 8.5
- Magento 2.4.9-compatible framework packages
mar-pod-b2b/module-b2b-coreandmar-pod-b2b/module-b2b-company
Installation
Run from the Magento project root:
composer require mar-pod-b2b/module-b2b-pricing:@dev
bin/magento module:enable Marpod_B2bPricing
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:clean
Cache
CompanyPriceResolver caches the company's price-list assignment (list ID, website, currency,
active flag) in Magento's default cache pool, tagged with MARPOD_B2B_PRICING_COMPANY_<companyId>
and, when a list is assigned, MARPOD_B2B_PRICE_LIST_<priceListId>. Individual price rows are read
fresh on every lookup rather than cached, since a price list realistically holds far fewer rows than
a full catalog and correctness matters more than shaving one query.
Company- and member-level changes invalidate only the affected company's tag. Price list or price row changes invalidate the list's tag, covering every company assigned to that list.
Development
The planned scope, dependency rules, cache boundaries, security requirements, and test strategy are
documented in packages/B2B_LIGHT_PLAN.md in the development Magento project.
Unit tests:
vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist packages/module-b2b-pricing/Test/Unit
Integration tests (run in the Magento PHP container with the integration database available):
php vendor/bin/phpunit \
--bootstrap packages/module-b2b-pricing/Test/Integration/bootstrap.php \
packages/module-b2b-pricing/Test/Integration/Model/CompanyPriceResolverTest.php
Full Admin smoke test (run from the Magento project root):
npx playwright test \
--config=src/packages/module-b2b-pricing/Test/Playwright/playwright.config.js
The smoke test creates and removes its own company and price-list fixtures. It covers General data, price add/update/delete, CSV download/import, grid filtering and pagination, company assignment/removal and active tab persistence after every POST redirect.
Admin UI
Marpod_B2bPricing::manage (under B2B > Price Lists) provides:
- a grid of price lists (
marpod_b2b_pricing/pricelist/index); - a native two-column Magento edit screen per price list (
marpod_b2b_pricing/pricelist/edit) with separate General, Prices and Assigned Companies tabs; - filterable and paginated Magento-style grids for individual SKU prices and company assignments, with separate forms for price maintenance, company assignment and CSV bulk import.
The company-assignment grid includes the company ID, legal name, email, VAT ID, status and website. The CSV import section provides a downloadable example containing both an open-ended price and a date-limited price row. After a price or company action, the edit screen returns to the tab where that action originated.
REST and GraphQL
Administrative REST routes below /rest/V1/marpod-b2b/pricing require the
Marpod_B2bPricing::manage ACL resource. They expose price-list CRUD, company
assignment/unassignment, and per-SKU price get/save/delete within a list. CSV import stays
REST-unexposed — file-upload semantics are a distinct concern from these routes.
Authenticated customers can query marpodB2bCompanyPrices(skus) through GraphQL for their current
company's resolved prices. The company is resolved from the customer token and current website;
callers cannot submit a company ID. A SKU with no company-specific price returns price: null
rather than an error, matching CompanyPriceResolverInterface's documented fallback contract.
REST/GraphQL API tests (real HTTP against a running instance — routing, authentication, ACL
enforcement, and cross-company isolation; see mar-pod-b2b/module-b2b-core's README for the
shared Test/Api base classes these build on):
vendor/bin/phpunit -c dev/tests/api-functional/phpunit_rest.xml \
packages/module-b2b-pricing/Test/Api/Rest/PriceListRestTest.php
vendor/bin/phpunit -c dev/tests/api-functional/phpunit_graphql.xml \
packages/module-b2b-pricing/Test/Api/GraphQl/CompanyPricesGraphQlTest.php
The edit screen is a plain Block/phtml form rather than a ui_component form, by deliberate
choice — see packages/B2B_LIGHT_PLAN.md history for context.
CSV price import
marpod_b2b_pricing/price/import accepts a .csv file (2 MB limit) with a header row:
sku,price,valid_from,valid_to
24-MB01,19.99,,
24-MB02,24.99,2026-01-01,2026-12-31
sku and price are required columns; valid_from/valid_to are optional and may be left empty
per row. Rows are validated individually — invalid rows (missing SKU, non-numeric or negative
price, valid_from after valid_to) are skipped and reported by row number, while every valid row
in the file is still imported. Valid rows are written as a single batch and trigger one
marpod_b2b_price_list_changed cache-invalidation event, regardless of file size (capped at 5000
rows per import).
Limitations
- no CSV export for price lists yet;
- CSV import is Admin-UI-only, not exposed through REST.
Uninstallation
Do not remove the package while another installed module requires it. The marpod_b2b_price_list,
marpod_b2b_price_list_company, and marpod_b2b_price tables contain business data and are
retained when the module is disabled or its package is removed.