Implementing Supplier Prioritization (Price/Availability) During Auto-Population
When the same product exists with multiple suppliers, you need rules: who to order from, whose price goes into the card, whose data to consider primary. Without explicit prioritization rules, the catalog becomes chaos — prices jump, photos change with every import, no predictability for the buyer.
Priority Levels
Prioritization works on several levels simultaneously:
| Level | Determines | Example |
|---|---|---|
| Content | Whose name, description, photo to use | Supplier A has better content |
| Price | Which price to show the buyer | Minimum among suppliers with stock |
| Order | Who actually fulfills the order | Cheapest, then backup |
| Availability | How to count total stock | Sum, or only primary supplier |
Priority Configuration Model
CREATE TABLE supplier_priority_rules (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
scope_type VARCHAR(20) NOT NULL, -- 'global', 'category', 'brand', 'product'
scope_id BIGINT, -- NULL for global
price_strategy VARCHAR(30) NOT NULL, -- 'min', 'primary', 'markup'
content_mode VARCHAR(20) NOT NULL, -- 'primary_first', 'best_score'
order_mode VARCHAR(20) NOT NULL, -- 'cheapest', 'priority_rank', 'round_robin'
stock_mode VARCHAR(20) NOT NULL, -- 'sum', 'primary_only', 'max'
is_active BOOLEAN DEFAULT TRUE,
priority INT DEFAULT 0 -- rule priority (higher = more important)
);
-- Supplier ranks within rule context
CREATE TABLE supplier_rule_ranks (
rule_id BIGINT REFERENCES supplier_priority_rules(id),
supplier_id INT REFERENCES suppliers(id),
rank SMALLINT NOT NULL, -- 1 = highest priority
markup_pct NUMERIC(5,2) DEFAULT 0, -- markup on supplier price
is_content_src BOOLEAN DEFAULT FALSE, -- content source
PRIMARY KEY (rule_id, supplier_id)
);
Pricing Strategies
enum PriceStrategy: string
{
case Min = 'min'; // Minimum price among suppliers with stock
case Primary = 'primary'; // Primary supplier price
case Markup = 'markup'; // Base price + markup from rule
}
class PriceResolver
{
public function resolve(Product $product, PriorityRule $rule): ?float
{
$offers = $product->offers()
->where('stock', '>', 0)
->with('supplier')
->get();
return match ($rule->price_strategy) {
PriceStrategy::Min->value => $this->resolveMin($offers, $rule),
PriceStrategy::Primary->value => $this->resolvePrimary($offers, $rule),
PriceStrategy::Markup->value => $this->resolveWithMarkup($offers, $rule),
};
}
private function resolveMin(Collection $offers, PriorityRule $rule): ?float
{
// Account for each supplier's markup when calculating minimum
return $offers->map(function ($offer) use ($rule) {
$rank = $rule->ranks->firstWhere('supplier_id', $offer->supplier_id);
$markup = $rank?->markup_pct ?? 0;
return $offer->price * (1 + $markup / 100);
})->min();
}
private function resolvePrimary(Collection $offers, PriorityRule $rule): ?float
{
// Primary supplier — first by rank with stock
$rankedOffers = $offers->sortBy(function ($offer) use ($rule) {
$rank = $rule->ranks->firstWhere('supplier_id', $offer->supplier_id);
return $rank?->rank ?? PHP_INT_MAX;
});
$primaryOffer = $rankedOffers->first();
if (!$primaryOffer) return null;
$rank = $rule->ranks->firstWhere('supplier_id', $primaryOffer->supplier_id);
return $primaryOffer->price * (1 + ($rank?->markup_pct ?? 0) / 100);
}
}
Content Source Selection Strategies
class ContentSourceResolver
{
public function resolveContentSupplier(Product $product, PriorityRule $rule): ?int
{
return match ($rule->content_mode) {
'primary_first' => $this->primaryFirst($product, $rule),
'best_score' => $this->bestScore($product, $rule),
default => null,
};
}
private function primaryFirst(Product $product, PriorityRule $rule): ?int
{
// Take supplier with is_content_src = true, if they have an offer
$contentSupplierIds = $rule->ranks
->where('is_content_src', true)
->sortBy('rank')
->pluck('supplier_id');
foreach ($contentSupplierIds as $supplierId) {
if ($product->offers->firstWhere('supplier_id', $supplierId)) {
return $supplierId;
}
}
// Fallback: first by rank with stock
return $product->offers
->sortBy(fn($o) => $rule->ranks->firstWhere('supplier_id', $o->supplier_id)?->rank ?? 999)
->first()?->supplier_id;
}
private function bestScore(Product $product, PriorityRule $rule): ?int
{
// Score supplier content completeness
return $product->offers->sortByDesc(function ($offer) {
$sp = SupplierProduct::where([
'supplier_id' => $offer->supplier_id,
'external_id' => $offer->supplier_sku,
])->first();
if (!$sp) return 0;
$score = 0;
if (!empty($sp->attributes['description'])) $score += 30;
if (!empty($sp->attributes['images'])) $score += 25;
if (!empty($sp->attributes['brand'])) $score += 15;
if (mb_strlen($sp->name) > 50) $score += 10;
if (!empty($sp->attributes['specs'])) $score += 20;
return $score;
})->first()?->supplier_id;
}
}
Applying Rules
class ProductSyncService
{
public function syncProduct(Product $product): void
{
$rule = $this->ruleResolver->findApplicableRule($product);
if (!$rule) return;
// Price
$newPrice = $this->priceResolver->resolve($product, $rule);
// Availability
$newStock = match ($rule->stock_mode) {
'sum' => $product->offers->sum('stock'),
'primary_only' => $this->getPrimaryOffer($product, $rule)?->stock ?? 0,
'max' => $product->offers->max('stock'),
};
// Content
$contentSupplierId = $this->contentResolver->resolveContentSupplier($product, $rule);
$product->update([
'price' => $newPrice,
'stock' => $newStock,
'content_supplier' => $contentSupplierId,
]);
if ($contentSupplierId) {
$this->applySupplierContent($product, $contentSupplierId);
}
}
}
Conflict Resolution at Equal Prices
When multiple suppliers offer the same price, preference order:
- Supplier with shortest delivery time (
lead_time_days) - Supplier with larger stock
- Supplier with highest reliability rating (percentage of successful orders)
- Rank in
supplier_rule_rankstable
Rule Management Interface
In the admin panel, rules must be configurable without deployment:
- Select scope (globally, by category, by brand)
- Drag-and-drop supplier ranking
- Strategy toggles (min/primary/markup)
- Markup field for each supplier
- Test rule on specific product
Timeline
- Data schema + models: 1 day
- PriceResolver + ContentSourceResolver: 1–2 days
- ProductSyncService + Observer triggers: 1 day
- Rule management interface in admin: 2 days
- Tests + operator documentation: 1 day
Total: 6–7 business days.







