B2B Dealer Portals: Tier Pricing, Credit Limits, Bulk
A B2B dealer portal looks like an online store from the outside, but the hard parts are things a retail checkout never deals with: every buyer can see a different price for the same SKU, orders arrive as 80-line spreadsh
A B2B dealer portal looks like an online store from the outside, but the hard parts are things a retail checkout never deals with: every buyer can see a different price for the same SKU, orders arrive as 80-line spreadsheets, and the most important check at checkout isn't the card. It's whether this dealer is still inside their credit limit. Get pricing and credit wrong and the portal creates more phone calls for your sales team than it removes.
I run Loop Origin, and my team built IvoryTex, a bilingual B2B wholesale platform with tiered pricing, minimum order quantities and buyer approval on Laravel 12, Vue 3 and PostgreSQL. I wrote this with help from AI tools and checked every query and claim myself. The code below is simplified from patterns we use in production.
What does a B2B dealer ordering portal need?
Direct answer: A dealer portal needs company accounts with role-based users, server-side price resolution (contract price, then tier price list, then base price), MOQ and case-pack rules, a credit exposure check before order submission, fast bulk entry (quick order, CSV upload, reorder), optional approval chains, and self-service invoices and statements synced with your ERP.
Accounts are companies, not people
In retail, one user is one customer. In B2B, the customer is a company with several people who do different things:
| Role | Can browse prices | Can build cart | Can submit order | Can see invoices and statements |
|---|---|---|---|---|
| Buyer | Yes | Yes | Up to an order limit | No |
| Purchasing manager | Yes | Yes | Yes, approves buyers' carts | Optional |
| Finance | Yes | No | No | Yes |
| Account admin | Yes | Yes | Yes | Yes, manages users |
Price lists, credit limits, payment terms and ship-to addresses belong to the company account. Users inherit them. This sounds obvious, but I've seen portals bolted onto retail platforms where every buyer at the same dealer had a separately configured discount, and they drifted apart within months.
Onboarding is also different. IvoryTex buyers apply with business details, a tax ID and expected order volume, and an admin approves them manually before wholesale prices unlock. Until approval, the account sees nothing it can't already see as a guest.
Price resolution: one function, server-side
The rule I enforce on every dealer portal: the browser never decides a price. It receives a resolved price from the server for the authenticated company, and the server recomputes it at submission.
Precedence, from most to least specific:
| Priority | Source | Example | Typical owner |
|---|---|---|---|
| 1 | Contract price for this company and SKU | Dealer 1042 pays 410.00 for SKU TX-220 until 31 Dec | Sales, per negotiation |
| 2 | Quantity break on the company's tier | Wholesale tier: 10+ units at 455.00 | Pricing team |
| 3 | Tier price list | Wholesale tier: 480.00 | Pricing team |
| 4 | Base list price | 560.00 | Catalog |
In PostgreSQL, one query resolves it, so the cart, product page and order validation all use the same logic:
SELECT COALESCE(
(SELECT cp.price FROM contract_prices cp
WHERE cp.company_id = :company AND cp.sku_id = :sku
AND now() BETWEEN cp.valid_from AND cp.valid_to),
(SELECT tb.price FROM tier_breaks tb
WHERE tb.tier_id = :tier AND tb.sku_id = :sku AND tb.min_qty <= :qty
ORDER BY tb.min_qty DESC LIMIT 1),
(SELECT tp.price FROM tier_prices tp
WHERE tp.tier_id = :tier AND tp.sku_id = :sku),
(SELECT s.base_price FROM skus s WHERE s.id = :sku)
) AS unit_price;
For catalog pages with hundreds of SKUs, run the set-based version of the same logic with joins, and index each table on its lookup key (company_id, sku_id and tier_id, sku_id, min_qty). Column order in those indexes matters; I went through why in composite indexes in PostgreSQL and MySQL.
Store the resolved unit price and its source on each order line. When a dealer disputes an invoice three months later, "contract price, agreement #88" ends the conversation in one minute.
MOQ, case packs and order rules
Wholesale catalogs carry rules that retail doesn't:
- MOQ per SKU (minimum 12 units) and sometimes per order value (minimum 50,000 per order).
- Case-pack multiples: sold in packs of 6, so 14 is invalid.
- Tier-specific thresholds: approved wholesale buyers may get different minimums from smaller retail accounts, as on IvoryTex.
Validate these at three points: the quantity input (instant feedback), the cart (clear message before checkout), and the server on submission (the only check that counts).
The credit limit check
Most dealers buy on terms, Net 30 or Net 60. The question at checkout is not "does the card work" but "would this order push the account past its limit?"
Credit exposure is more than unpaid invoices:
exposure = open AR (invoiced, unpaid)
+ orders submitted but not yet invoiced
+ this order
available = credit_limit - exposure
Teams that only count invoiced AR miss the second line, and a dealer can place five large orders in a week before the first invoice posts. The check needs a lock so two simultaneous orders from the same company can't both pass:
DB::transaction(function () use ($order) {
$account = CreditAccount::where('company_id', $order->company_id)
->lockForUpdate()->firstOrFail();
$exposure = $account->open_ar + $account->uninvoiced_orders + $order->total;
if ($account->on_hold || $exposure > $account->credit_limit) {
$order->update(['status' => 'credit_review']);
event(new OrderNeedsCreditReview($order));
return;
}
$account->increment('uninvoiced_orders', $order->total);
$order->update(['status' => 'submitted']);
});
Two design choices matter here. Orders over the limit go to credit_review instead of being rejected, because finance often approves them after a payment. And open_ar is synced from the ERP or accounting system, which remains the source of truth for invoices. Treat that sync like a ledger, with idempotent updates and an audit trail, the same discipline as marketplace payment ledgers.
Bulk ordering that dealers actually use
If ordering in the portal is slower than emailing a spreadsheet to their sales rep, dealers will keep emailing. Three entry modes cover most of it:
- Quick order grid: type or paste SKU and quantity pairs, with live price and stock per line.
- CSV upload: validate every row, show errors inline (unknown SKU, below MOQ, not a case multiple), and let the dealer fix and resubmit without starting over.
- Reorder: copy any past order into the cart, re-resolving today's prices, not the old ones. IvoryTex supports one-click reorder from order history because wholesale buying is repetitive.
Build, platform B2B features, or custom?
| Option | Strengths | Limits to check | Fits when |
|---|---|---|---|
| B2B features of a hosted commerce platform | Fast start, hosted checkout, apps | Plan tier needed for B2B features, depth of credit and approval logic, ERP sync options | Standard tiers, card or simple terms |
| Off-the-shelf B2B portal software | Built-in quoting and accounts | Per-seat or per-order fees, customization limits | Mid-size catalog, common workflows |
| Custom portal on your stack | Exact pricing precedence, credit rules, ERP fit | You own hosting, security and maintenance | Complex contracts, credit terms, unusual rules |
Check current plans and pricing directly with vendors; they change often. The decision usually comes down to how unusual your pricing and credit rules are. The 3-year cost framework for custom vs off-the-shelf software is how I run that comparison with clients.
Failure modes I design against
- Prices computed in the frontend and trusted at submission.
- Credit checks without locking, letting parallel orders overshoot the limit.
- Stale AR data from a sync that silently stopped. Alert when the last successful sync is older than a few hours.
- Shared logins at a dealer, which destroy the audit trail. Make adding users self-service for the account admin.
Launch checklist
- Model companies, users, roles and ship-to addresses.
- Implement one server-side price resolution function and store the price source on each line.
- Enforce MOQ and case packs on input, cart and server.
- Compute exposure including uninvoiced orders, with a row lock.
- Route over-limit orders to credit review, not rejection.
- Ship quick order, CSV upload and reorder.
- Sync invoices and AR from the ERP with monitoring.
- Give dealers self-service invoices, statements and order status.
Frequently asked questions
Should dealers see stock levels?
Usually yes, as bands (in stock, low, backorder) rather than exact counts, unless your agreements say otherwise.
Where should credit limits live, the portal or the ERP?
The limit and AR usually belong in the ERP or accounting system. The portal keeps a synced copy plus uninvoiced orders, and does the real-time check.
How do I handle sales reps ordering on behalf of dealers?
Let reps act on behalf of a company with their own login, and record both identities on the order.
Can a dealer portal support quotes?
Yes. A quote is a cart with reserved prices and an expiry. Converting it to an order runs the same MOQ and credit checks.
How do I price a new dealer before contract terms exist?
Assign them a tier on approval. Contract prices can be added later without changing the resolution logic.
What should the first version include?
Accounts and roles, server-side pricing, MOQ rules, the credit check, quick order and reorder, and invoice download. Approval chains and CSV upload can follow.
Follow & Connect
- LinkedIn: Loop Origin
- X: Muhammad Ali Mughal (@AliMughal887)
- GitHub: Loop Origin (@LoopOrigin)
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.