Реалізація пріоритизації постачальників (ціна/наявність) при автонаповненні

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація пріоритизації постачальників (ціна/наявність) при автонаповненні
Складна
~5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація приоритизації постачальників (ціна/наявність) при автозаповненні

Коли один і той же товар є у кількох постачальників, потрібні правила: у кого замовляти, чия ціна йде в карточку, чиї дані вважати основними. Без явних правил приоритизації каталог перетворюється на хаос — ціни стрибають, фото змінюються при кожному імпорті, немає передбачуваності для покупця.

Рівні пріоритетів

Приоритизація працює на кількох рівнях одночасно:

Рівень Що визначає Приклад
Контент Чию назву, опис, фото використовувати Постачальник A має кращий контент
Ціна Яку ціну показувати покупцеві Мінімум серед постачальників з наявністю
Замовлення У кого фактично розміщувати замовлення Найдешевший, потім резерв
Наявність Як рахувати сумарний запас Сума, або тільки основного постачальника

Модель конфігурації пріоритетів

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 для 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            -- пріоритет правила (вище = важливіше)
);

-- Ранги постачальників у контексті правила
CREATE TABLE supplier_rule_ranks (
    rule_id         BIGINT REFERENCES supplier_priority_rules(id),
    supplier_id     INT REFERENCES suppliers(id),
    rank            SMALLINT NOT NULL,       -- 1 = найвищий пріоритет
    markup_pct      NUMERIC(5,2) DEFAULT 0, -- наценка до ціни постачальника
    is_content_src  BOOLEAN DEFAULT FALSE,   -- джерело контенту
    PRIMARY KEY (rule_id, supplier_id)
);

Стратегії ціноутворення

enum PriceStrategy: string
{
    case Min       = 'min';       // Мінімальна ціна серед постачальників з наявністю
    case Primary   = 'primary';   // Ціна основного постачальника
    case Markup    = 'markup';    // Базова ціна + наценка з правила
}

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
    {
        // Враховуємо наценку кожного постачальника при обчисленні мінімуму
        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
    {
        // Основний постачальник — перший за рангом з наявністю
        $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);
    }
}

Стратегії вибору джерела контенту

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
    {
        // Беремо постачальника з is_content_src = true, якщо він має пропозицію
        $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: перший за рангом з наявністю
        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
    {
        // Скоринг повноти контенту постачальника
        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;
    }
}

Застосування правил

class ProductSyncService
{
    public function syncProduct(Product $product): void
    {
        $rule = $this->ruleResolver->findApplicableRule($product);

        if (!$rule) return;

        // Ціна
        $newPrice = $this->priceResolver->resolve($product, $rule);

        // Наявність
        $newStock = match ($rule->stock_mode) {
            'sum'          => $product->offers->sum('stock'),
            'primary_only' => $this->getPrimaryOffer($product, $rule)?->stock ?? 0,
            'max'          => $product->offers->max('stock'),
        };

        // Контент
        $contentSupplierId = $this->contentResolver->resolveContentSupplier($product, $rule);

        $product->update([
            'price'             => $newPrice,
            'stock'             => $newStock,
            'content_supplier'  => $contentSupplierId,
        ]);

        if ($contentSupplierId) {
            $this->applySupplierContent($product, $contentSupplierId);
        }
    }
}

Розв'язання конфліктів при однакових цінах

Якщо кілька постачальників дають однакову ціну — порядок переваги:

  1. Постачальник з найменшим часом доставки (lead_time_days)
  2. Постачальник з більшим остатком
  3. Постачальник з найвищим рейтингом надійності (відсоток успішних замовлень)
  4. Ранг у таблиці supplier_rule_ranks

Інтерфейс управління правилами

В адміністративній панелі правила повинні бути налаштовними без розгортання:

  • Вибір області дії (глобально, по категорії, по бренду)
  • Drag-and-drop ранжування постачальників
  • Перемикачі стратегій (min/primary/markup)
  • Поле наценки для кожного постачальника
  • Тестування правила на конкретному товарі

Строки реалізації

  • Схема даних + моделі: 1 день
  • PriceResolver + ContentSourceResolver: 1–2 дні
  • ProductSyncService + Observer-тригери: 1 день
  • Інтерфейс управління правилами в админці: 2 дні
  • Тести + документація для оператора: 1 день

Разом: 6–7 робочих днів.