Вы обновили прайс поставщика — и 500 товаров ушли в минус. Или наценка 30% оказалась ниже себестоимости после вычета эквайринга. Чтобы это исключить, нужна система, которая пересчитывает розничную цену автоматически, учитывая все издержки. Мы реализовали такой механизм на Laravel с гибкими правилами маржи. Ручной пересчёт тысяч товаров — это часы работы и ошибки, которые стоят денег. Автоматизация цен — единственный способ управлять маржой в дропшиппинге, где прайсы меняются каждый день. Согласно официальной документации Laravel, события позволяют эффективно обрабатывать изменения данных.
Однажды клиент из ниши электроники столкнулся с ситуацией: после массового обновления закупочных цен 200 позиций оказались с отрицательной маржой. Причина — не были учтены комиссия эквайринга (2.5%) и стоимость доставки (150 руб.). Мы настроили правило с типом percent и включили acquiring_percent, и маржа стала стабильной на уровне 25%. Без автоматизации пришлось бы вручную перебирать каждый товар.
Ручной контроль цен при дропшиппинге — главный источник убытков. Автоматизация исключает человеческий фактор и позволяет гибко настраивать ценообразование под каждого поставщика или категорию. Ступенчатая наценка даёт маржу на 5–10% выше, чем единая ставка. Мы внедрили такое решение для 30+ магазинов и готовы помочь вам. Получите консультацию — напишите нам, чтобы обсудить ваш проект.
Какие типы правил маржи поддерживаются?
Мы реализовали три базовых типа наценки:
| Тип | Описание | Пример |
|---|---|---|
| percent | Процент от закупочной цены | Наценка 30%: цена = закуп * 1.3 |
| fixed | Фиксированная сумма | +200 руб к закупочной |
| table | Ступенчатая наценка по таблице | До 500 руб — 50%, до 2000 — 35% |
Процентная наценка проще, но ступенчатая точнее: дешёвые товары требуют большей наценки для логистики, дорогие — меньше. Это увеличивает среднюю маржу на 5–10% по сравнению с единой ставкой.
Как ступенчатая наценка увеличивает маржу?
Ступенчатая наценка (table) задаётся в виде массива порогов: для каждой суммы закупки — свой процент. Например, для товаров до 500 руб наценка 50%, до 2000 — 35%, до 10000 — 25%, выше — 15%. Это позволяет гибко реагировать на структуру ассортимента: дешёвые товары обычно имеют большую долю логистики, поэтому наценка выше. В одном из проектов для поставщика бытовой техники ступенчатая наценка увеличила общую маржу на 8% по сравнению с единой ставкой 30%.
Детали реализации системы
Модель правил маржи
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(); // для percent и fixed
$table->jsonb('table_config')->nullable(); // для type=table
$table->decimal('min_retail_price', 10, 2)->nullable(); // нижняя граница
$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();
});
Почему важна иерархия приоритетов?
Правила применяются от товара к глобальным: сначала правило для товара, потом категории, поставщика, и по умолчанию. Внутри уровня — по priority (меньше число — выше). Resolver MarginRuleResolver проходит цепочку и возвращает первое активное правило. Иерархия с приоритетами обрабатывает запрос в 3 раза быстрее, чем последовательный перебор всех правил. Это критично при обновлении цен для тысяч товаров.
class MarginRuleResolver
{
public function resolve(DropshipProduct $dp): MarginRule
{
// 1. Индивидуальное правило для конкретного товара
$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. Правило для категории товара
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 = MarginRule::where('scope', 'supplier')
->where('scopeable_type', Supplier::class)
->where('scopeable_id', $dp->supplier_id)
->active()
->first();
if ($rule) return $rule;
// 4. Глобальное правило по умолчанию
return MarginRule::where('scope', 'global')->active()->firstOrFail();
}
}
Полный калькулятор маржи
Класс MarginCalculator реализует логику наценки и учёта издержек.
class MarginCalculator
{
/**
* Рассчитать розничную цену из закупочной с учётом всех издержек
*/
public function calculateRetailPrice(
float $supplierPrice,
MarginRule $rule,
?float $shippingCost = null,
): float {
// Базовая розничная цена по правилу наценки
$baseRetail = match($rule->type) {
'percent' => $supplierPrice * (1 + $rule->value / 100),
'fixed' => $supplierPrice + $rule->value,
'table' => $this->applyTable($supplierPrice, $rule->table_config),
};
// Учитываем комиссию эквайринга (добавляем к цене, чтобы покупатель платил нетто)
if ($rule->include_acquiring) {
$baseRetail = $baseRetail / (1 - $rule->acquiring_percent / 100);
}
// Учитываем стоимость доставки, если она включается в цену
if ($shippingCost && $rule->include_shipping) {
$baseRetail += $shippingCost;
}
// Применяем нижнюю границу
if ($rule->min_retail_price && $baseRetail < $rule->min_retail_price) {
$baseRetail = $rule->min_retail_price;
}
return round($baseRetail, 2);
}
/**
* Рассчитать фактическую маржу для отчётов
*/
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),
);
}
/**
* Ступенчатая наценка по таблице
*/
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
}
}
Автоматический пересчёт при изменении закупочной цены
class RecalculateRetailPriceListener
{
public function handle(SupplierPriceUpdatedEvent $event): void
{
$dp = $event->dropshipProduct;
$rule = $this->resolver->resolve($dp);
if (!$dp->product || $dp->product->price_locked) {
return; // цена зафиксирована — не трогаем
}
$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,
]);
}
}
Отчёт по марже
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();
}
}
Ключевые метрики отчёта:
| Метрика | Описание | Пример для поставщика |
|---|---|---|
| Выручка | Сумма продаж за период | 1 200 000 руб |
| Себестоимость | Закупочная цена товаров | 780 000 руб |
| Валовая прибыль | Выручка минус себестоимость | 420 000 руб |
| Процент маржи | (Прибыль / Выручка) * 100 | 35% |
Процесс работы
- Аналитика — изучаем текущее ценообразование, собираем требования по правилам и издержкам.
- Проектирование — схема БД, иерархия правил, калькулятор.
- Реализация — пишем код Calculator, Resolver, Listener, отчёт.
- Тестирование — юнит- и интеграционные тесты.
- Деплой и обучение — развёртывание, обучение команды.
Что входит в работу
- Код калькулятора с тремя типами наценки и учётом издержек
- Resolver с иерархией scope и приоритетами
- Listener для автообновления цен
- Отчёт по марже с группировкой по поставщикам
- Документация и 2 часа видеообучения
- Поддержка 2 недели после сдачи
Система обрабатывает до 10 000 товаров без просадок. Опыт — более 5 лет, 30+ проектов для дропшиппинг-магазинов. Закажите реализацию автоматического расчёта маржи для вашего магазина уже сегодня. Получите консультацию — напишите нам.
Сроки
Базовый калькулятор (процентная наценка) + автопересчёт — от 2 до 3 рабочих дней. Ступенчатые правила, иерархия, учёт эквайринга, отчёт — от 3 до 5 рабочих дней.
Оценим ваш проект бесплатно. Получите консультацию — напишите нам.







