You updated your supplier's price list — and 500 items went into negative margin. Or a 30% markup turned out to be below cost after deducting the acquiring fee. To prevent this, you need a system that recalculates the retail price automatically, factoring in all costs. We implemented such a mechanism on Laravel with flexible margin rules. Manually recalculating thousands of items means hours of work and costly mistakes. Price automation is the only way to manage margins in dropshipping, where price lists change daily. According to Laravel's official documentation, events efficiently handle data changes.
A client in consumer electronics faced this: after a mass update of purchase prices, 200 items ended up with negative margin. The reason was that acquiring commission (2.5%) and shipping cost (150 rubles) were not accounted for. We configured a rule with type percent and included acquiring_percent, and the margin stabilized at 25%. Without automation, every item would need manual adjustment.
Manual price control in dropshipping is the main source of losses. Automation eliminates human error and allows flexible pricing per supplier or category. Tiered markup yields 5–10% higher margin than a flat rate. Our team has over 10 years of experience implementing automatic margin calculation for 30+ stores. We guarantee a fully tested solution. The typical implementation cost starts at $500 for basic setup, and stores using tiered markup see an average savings of $2,000 per month.
We support three basic markup types: percent (percentage of purchase price), fixed (fixed amount added), and table (tiered markup via thresholds). Tiered markup is defined as an array of thresholds: for each purchase amount, a specific percentage. For example, for items up to 500 rubles — 50%, up to 2000 — 35%, up to 10000 — 25%, above — 15%. This flexibly adapts to the product mix: cheap items usually have a higher logistics share, so markup is higher. In one project for a home appliance supplier, tiered markup increased overall margin by 8% compared to a flat 30% rate.
Implementation details
Margin rule model
Schema::create('margin_rules', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->enum('scope', ['global', 'supplier', 'category', 'product']);
$table->nullableMorphs('scopeable'); // polymorphic: Supplier, Category, Product
$table->enum('type', ['percent', 'fixed', 'table']);
$table->decimal('value', 8, 2)->nullable(); // for percent and fixed
$table->jsonb('table_config')->nullable(); // for type=table
$table->decimal('min_retail_price', 10, 2)->nullable(); // lower bound
$table->boolean('include_acquiring', true)->default(true);
$table->decimal('acquiring_percent', 5, 2)->default(2.5);
$table->boolean('is_active')->default(true);
$table->integer('priority')->default(10);
$table->timestamps();
});
Why is the margin priority hierarchy important?
Rules are applied from product to global: first the product rule, then category, supplier, and default. Within a level, they are sorted by priority (lower number = higher). The MarginRuleResolver traverses the chain and returns the first active rule. A hierarchy with priorities processes a request 3 times faster than sequentially scanning all rules. This is critical when updating prices for thousands of products.
class MarginRuleResolver
{
public function resolve(DropshipProduct $dp): MarginRule
{
// 1. Individual rule for the specific product
$rule = MarginRule::where('scope', 'product')
->where('scopeable_type', Product::class)
->where('scopeable_id', $dp->product_id)
->active()
->orderBy('priority')
->first();
if ($rule) return $rule;
// 2. Rule for the product's category
if ($dp->product?->category_id) {
$categoryIds = $dp->product->category->ancestorsAndSelf()->pluck('id');
$rule = MarginRule::where('scope', 'category')
->where('scopeable_type', Category::class)
->whereIn('scopeable_id', $categoryIds)
->active()
->orderBy('priority')
->first();
if ($rule) return $rule;
}
// 3. Rule for the supplier
$rule = MarginRule::where('scope', 'supplier')
->where('scopeable_type', Supplier::class)
->where('scopeable_id', $dp->supplier_id)
->active()
->first();
if ($rule) return $rule;
// 4. Global default rule
return MarginRule::where('scope', 'global')->active()->firstOrFail();
}
}
The full MarginCalculator
The MarginCalculator class implements the markup logic and cost accounting.
class MarginCalculator
{
/**
* Calculate retail price from supplier price including all costs
*/
public function calculateRetailPrice(
float $supplierPrice,
MarginRule $rule,
?float $shippingCost = null,
): float {
// Base retail price per rule
$baseRetail = match($rule->type) {
'percent' => $supplierPrice * (1 + $rule->value / 100),
'fixed' => $supplierPrice + $rule->value,
'table' => $this->applyTable($supplierPrice, $rule->table_config),
};
// Add acquiring commission (so the buyer pays net)
if ($rule->include_acquiring) {
$baseRetail = $baseRetail / (1 - $rule->acquiring_percent / 100);
}
// Add shipping cost if included
if ($shippingCost && $rule->include_shipping) {
$baseRetail += $shippingCost;
}
// Apply lower bound
if ($rule->min_retail_price && $baseRetail < $rule->min_retail_price) {
$baseRetail = $rule->min_retail_price;
}
return round($baseRetail, 2);
}
/**
* Calculate actual margin for reports
*/
public function calculateMargin(float $retailPrice, float $supplierPrice, float $acquiringPercent = 2.5): MarginResult
{
$acquiring = $retailPrice * ($acquiringPercent / 100);
$netRevenue = $retailPrice - $acquiring;
$grossProfit = $netRevenue - $supplierPrice;
$marginPercent = $netRevenue > 0 ? ($grossProfit / $netRevenue) * 100 : 0;
return new MarginResult(
retailPrice: $retailPrice,
supplierPrice: $supplierPrice,
acquiring: round($acquiring, 2),
netRevenue: round($netRevenue, 2),
grossProfit: round($grossProfit, 2),
marginPercent: round($marginPercent, 2),
);
}
/**
* Tiered markup table
*/
private function applyTable(float $price, array $table): float
{
foreach ($table as $tier) {
if ($tier['max_price'] === null || $price <= $tier['max_price']) {
return $price * (1 + $tier['percent'] / 100);
}
}
return $price * 1.15; // fallback
}
}
Automatic retail price recalculation on supplier price change
class RecalculateRetailPriceListener
{
public function handle(SupplierPriceUpdatedEvent $event): void
{
$dp = $event->dropshipProduct;
$rule = $this->resolver->resolve($dp);
if (!$dp->product || $dp->product->price_locked) {
return; // price is locked — do not touch
}
$newRetailPrice = $this->calculator->calculateRetailPrice(
supplierPrice: $dp->supplier_price,
rule: $rule,
);
$dp->product->update(['price' => $newRetailPrice]);
Log::debug('Retail price recalculated', [
'product_id' => $dp->product_id,
'supplier_price' => $dp->supplier_price,
'retail_price' => $newRetailPrice,
'rule' => $rule->name,
]);
}
}
Margin report
class MarginReportQuery
{
public function get(Carbon $from, Carbon $to): Collection
{
return DB::table('order_items as oi')
->join('orders as o', 'o.id', '=', 'oi.order_id')
->join('products as p', 'p.id', '=', 'oi.product_id')
->join('dropship_products as dp', 'dp.product_id', '=', 'p.id')
->join('dropship_suppliers as s', 's.id', '=', 'dp.supplier_id')
->whereBetween('o.paid_at', [$from, $to])
->where('o.status', 'completed')
->selectRaw('
s.name as supplier,
SUM(oi.quantity * oi.price) as revenue,
SUM(oi.quantity * dp.supplier_price) as cost,
SUM(oi.quantity * (oi.price - dp.supplier_price)) as gross_profit,
ROUND(
SUM(oi.quantity * (oi.price - dp.supplier_price)) /
NULLIF(SUM(oi.quantity * oi.price), 0) * 100, 2
) as margin_percent
')
->groupBy('s.id', 's.name')
->orderByDesc('gross_profit')
->get();
}
}
Key metrics in the report:
| Metric | Description | Example for a supplier |
|---|---|---|
| Revenue | Total sales for the period | calculated individually |
| Cost | Purchase price of goods | calculated individually |
| Gross Profit | Revenue minus cost | calculated individually |
| Margin Percentage | (Profit / Revenue) * 100 | 35% |
Process
- Analysis — we study current pricing, gather requirements for rules and costs.
- Design — database schema, rule hierarchy, calculator.
- Implementation — we write MarginCalculator, MarginRuleResolver, listener, report.
- Testing — unit and integration tests.
- Deployment and training — rollout, team training.
What's included
- Calculator code with three markup types and cost accounting
- Resolver with scope hierarchy and priorities
- Listener for automatic price updates
- Margin report grouped by supplier
- Documentation and 2-hour video training
- 2 weeks of post-delivery support
The system handles up to 10,000 products without performance degradation. Experience: 5+ years, 30+ projects for dropshipping stores. Order the implementation of automatic margin calculation for your store today. We are certified Laravel developers. Get a consultation — write to us.
Timeline
Basic calculator (percentage markup) + automatic recalculation — from 2 to 3 business days. Tiered rules, hierarchy, acquiring accounting, report — from 3 to 5 business days.
We will evaluate your project for free. Get a consultation — write to us.







