Search by

mar-pod-b2b / module-b2b-pricing

Company price lists and pricing-context integration for Marpod B2B.

Maintainers

Package info

gitlab.com/mar-pod-b2b/module-b2b-pricing

Issues

Type:magento2-module

pkg:composer/mar-pod-b2b/module-b2b-pricing

Transparency log

Statistics

Installs: 1

Dependents: 2

Suggesters: 0

Stars: 0

1.2.18 2026-08-24 16:09 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), and marpod_b2b_price (one row per SKU per list, with an optional valid_from/valid_to window);
  • PriceListRepositoryInterface for price list CRUD;
  • CompanyPriceListAssignmentInterface to assign/unassign a price list to a company;
  • CompanyPriceManagementInterface to 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\PricingContextResolverInterface implementation 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_changed domain 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, and marpod_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-core and mar-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.