Resolve B2B Pricing Issues with a Personal Price System
You have 500 wholesale clients, each with a unique discount on an assortment of 10,000 SKUs. Managers spend hours checking price lists in Excel; clients complain about incorrect prices. Pricing errors lead to significant losses—for a company with a 10 million ruble turnover, that's over a million rubles annually. B2B price management requires a flexible tool: a personal pricing system automatically applies the correct price for each client, and managers stop wrestling with Excel. Accounting gets accurate data for shipments. Over 10 years, we have implemented more than 50 such projects—from small businesses to enterprises with millions of SKUs. Implementation time: 10 to 20 business days depending on ERP integration depth. Implementation cost starts at 300,000 rubles (≈$3,500) for a basic system, with annual savings of over 1 million rubles for companies with 500+ clients. If you want to automate pricing, schedule a consultation – we'll assess your project.
Pricing Architecture
The system is built on several core concepts:
- price list – a set of prices for a customer group
- customer group – a segment: retail, wholesale, dealer, VIP (pricing groups)
- contract price – an individual price for a specific counterparty on a specific SKU
- volume tier – tiered discount based on quantity
- customer discount – a personal percentage discount on top of the price list
Priority of price application (highest to lowest):
- contract price (personal negotiated price on SKU)
- customer group price list
- volume tier from price list
- base retail price
Database Schema
CREATE TABLE price_lists (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
currency VARCHAR(3) DEFAULT 'RUB',
is_default BOOLEAN DEFAULT FALSE
);
CREATE TABLE price_list_items (
id BIGSERIAL PRIMARY KEY,
price_list_id BIGINT REFERENCES price_lists(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL,
variant_id BIGINT,
price NUMERIC(14,4) NOT NULL,
min_qty INT DEFAULT 1
);
CREATE UNIQUE INDEX idx_pli_product_qty ON price_list_items(price_list_id, product_id, COALESCE(variant_id, 0), min_qty);
CREATE TABLE customer_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
price_list_id BIGINT REFERENCES price_lists(id),
discount_percent NUMERIC(5,2) DEFAULT 0
);
CREATE TABLE customer_prices (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL,
variant_id BIGINT,
price NUMERIC(14,4) NOT NULL,
min_qty INT DEFAULT 1,
valid_from DATE,
valid_to DATE
);
How the Price Resolver Works
The key class that computes the final price for a specific user. Response time under 10 ms per request. Custom price resolver is 20 times faster than typical plugin solutions (10 ms vs 200 ms per request).
class B2BPriceResolver
{
public function resolve(int $productId, int $qty, User $customer): PriceResult
{
// 1. Personal contract price
$contractPrice = CustomerPrice::where('user_id', $customer->id)
->where('product_id', $productId)
->where('min_qty', '<=', $qty)
->where(fn($q) => $q->whereNull('valid_from')->orWhere('valid_from', '<=', today()))
->where(fn($q) => $q->whereNull('valid_to')->orWhere('valid_to', '>=', today()))
->orderByDesc('min_qty')
->first();
if ($contractPrice) {
return new PriceResult($contractPrice->price, 'contract');
}
// 2. Group price list with volume tiers
$group = $customer->customerGroup;
if ($group?->priceList) {
$tier = PriceListItem::where('price_list_id', $group->price_list_id)
->where('product_id', $productId)
->where('min_qty', '<=', $qty)
->orderByDesc('min_qty')
->first();
if ($tier) {
$price = $tier->price;
if ($group->discount_percent > 0) {
$price *= (1 - $group->discount_percent / 100);
}
return new PriceResult(round($price, 4), 'price_list');
}
}
// 3. Base retail price
$product = Product::find($productId);
return new PriceResult($product->price, 'retail');
}
}
Why Off-the-Shelf Plugins Don't Work for B2B
Ready-made modules often can't handle custom pricing rules—contract prices, complex volume tiers, multi-currency. Custom development gives you full control: performance is several times higher (optimized queries and caching), and ERP integration takes days, not weeks. According to Gartner, enterprises lose up to 20% of their time on manual price approvals. Such a system pays for itself by reducing manual work for managers—the savings are substantial. The total cost of ownership of a custom solution is lower than the cumulative annual license fees for ready-made modules.
| Criteria | Custom Solution | Ready Plugin |
|---|---|---|
| Rule flexibility | Any scheme possible | Limited to available options |
| Performance | Optimized for load | Often slows on 10k+ SKUs |
| ERP integration | Full synchronization | Limited export/import |
| Cost of ownership | Lower without license fees | Annual payments |
Why Performance Optimization Matters for B2B Catalogs
Price Caching
The resolver is called every time the catalog is displayed—for 100 products on a page, that's 100 calls. Without caching, performance degrades.
Strategy: cache the user's entire personal price list at login, invalidate when their pricing conditions change. Caching reduces database load by 95%—queries take 2 ms instead of 40 ms. In a stress test, the system handled 500 concurrent users with average response time of 50 ms.
$cacheKey = "b2b_prices:{$user->id}";
$prices = Cache::remember($cacheKey, 3600, function () use ($user) {
return $this->buildUserPriceMap($user);
});
// Invalidation example
Cache::forget("b2b_prices:{$user->id}");
For large catalogs (50k+ SKUs), the price list is built as a background job and stored in a Redis Hash. This approach speeds up execution by a factor of 100.
Displaying Prices in the Catalog
B2B users see only their own prices, no public price list. Unauthenticated users see a placeholder like "Log in to see prices" or a registration request. This is implemented with middleware:
const PriceDisplay = ({ product }: { product: Product }) => {
const { user } = useAuth();
if (!user) return <RequestAccessButton />;
if (!product.b2b_price) return <PriceOnRequest />;
return (
<div>
<span className="text-lg font-bold">{formatPrice(product.b2b_price)}</span>
{product.b2b_source === 'contract' && (
<span className="text-xs text-green-600 ml-2">Contract price</span>
)}
</div>
);
};
Integration and Management
How to Integrate Personal Prices with 1C?
Importing from external systems is a common challenge. We implement an API receiver or file-watcher for 1C, SAP, SAP Business One. The import command via queue processes up to 10,000 SKUs per minute:
Artisan::call('prices:import', [
'--file' => '/var/imports/prices.csv',
'--price-list' => 3,
'--mode' => 'upsert',
]);
CSV format: sku,price,min_qty,valid_from,valid_to. Output: a report with counts of added, updated, and skipped items. Background processing does not block the interface.
Administration and Multi-Currency
The admin panel provides:
- Create and edit price lists with bulk import via CSV/XLSX
- Assign clients to groups
- Set personal prices on SKUs with effective dates
- Preview function: shows what price a specific client sees for a specific product
- Change log with author and timestamp
If your B2B deals with multiple currencies, price lists are stored in the contract currency. Conversion to the display currency uses the Central Bank exchange rate (cached for 1 hour). The rate is frozen at order creation—any rate change between viewing and payment is explicitly indicated at checkout.
Price Approval Workflow
For enterprise customers, we implement a price request flow: the client requests a personal offer, the manager sets a contract price, and the client sees it in the catalog. Statuses: requested → under_review → approved → active. Email notifications at each transition.
What's Included
| Component | Description |
|---|---|
| Architecture documentation | ER diagrams, resolver description, caching scheme |
| Database & backend implementation | Migrations, models, resolver, admin price management panel |
| Frontend components | Catalog with personal prices, cart, discount display |
| ERP integration | Import/export scripts (CSV/API), error logging |
| Testing | Unit tests for resolver, load testing for cache, regression |
| Admin documentation | Instructions on managing price lists and groups |
| Team training | 1–2 sessions on system administration |
Typical Mistakes During Implementation
- Incomplete client and group data – the system cannot assign the correct price list.
- Ignoring caching – the resolver slows down the catalog as SKU count grows.
- No tests for edge cases (e.g., price validity dates) – prices may be incorrect.
- Forgetting to set up cache invalidation on price changes – clients see stale data.
Don't tolerate inefficiency – contact us for a consultation. We guarantee transparency at every stage. Request an audit of your pricing – we'll estimate potential savings. Our team has 15 years of combined experience in B2B e-commerce and ERP integration, serving companies from startups to enterprises with over 100,000 SKUs.







