Розробка системи персональних цін для B2B інтернет-магазину

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка системи персональних цін для B2B інтернет-магазину
Складна
~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

Розроблення системи персональних цін для B2B інтернет-магазину

B2B-магазин принципово відрізняється від B2C: кожен контрагент працює по індивідуальним умовам — своєю ціновою групою, персональними скидками на SKU, договірними цінами, відстрочками платежу. Публічного прайса може не існувати взагалі. Розроблення повноцінної B2B-ціновой системи — одна з найтрудомісткіших задач у e-commerce: від 10 до 20 робочих днів залежно від глибини інтеграції з ERP.

Архітектура ціноутворення

Система будується навколо кількох концепцій:

  • Price list (прайс-лист) — набір цін для групи клієнтів
  • Customer group — сегмент: розниця, оптовик, дилер, VIP
  • Contract price — індивідуальна ціна для конкретного контрагента на конкретний SKU
  • Volume tier — ступінчаста скидка за об'єм
  • Customer discount — персональна скидка в відсотках поверх прайс-листа

Пріоритет застосування цін (від вищого до нижчого):

  1. Contract price (персональна договірна ціна на SKU)
  2. Customer group price list
  3. Volume tier з прайс-листа
  4. Base retail price

Схема базі даних

CREATE TABLE price_lists (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255),
    currency VARCHAR(3) DEFAULT 'RUB',
    is_default BOOLEAN DEFAULT FALSE
);

CREATE TABLE price_list_items (
    id BIGSERIAL PRIMARY KEY,
    price_list_id BIGINT REFERENCES price_lists(id) ON DELETE CASCADE,
    product_id BIGINT NOT NULL,
    variant_id BIGINT,
    price NUMERIC(14,4) NOT NULL,
    min_qty INT DEFAULT 1
);

CREATE TABLE customer_groups (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255),
    price_list_id BIGINT REFERENCES price_lists(id),
    discount_percent NUMERIC(5,2) DEFAULT 0
);

CREATE TABLE customer_prices (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
    product_id BIGINT NOT NULL,
    variant_id BIGINT,
    price NUMERIC(14,4) NOT NULL,
    min_qty INT DEFAULT 1,
    valid_from DATE,
    valid_to DATE
);

Резолвер ціни

Ключовий клас, вичисляючий итогову ціну для конкретного користувача:

class B2BPriceResolver {
    public function resolve(int $productId, int $qty, User $customer): PriceResult {
        // 1. Персональна договірна ціна
        $contractPrice = CustomerPrice::where('user_id', $customer->id)
            ->where('product_id', $productId)
            ->where('min_qty', '<=', $qty)
            ->where(fn($q) => $q->whereNull('valid_from')->orWhere('valid_from', '<=', today()))
            ->where(fn($q) => $q->whereNull('valid_to')->orWhere('valid_to', '>=', today()))
            ->orderByDesc('min_qty')
            ->first();

        if ($contractPrice) {
            return new PriceResult($contractPrice->price, 'contract');
        }

        // 2. Прайс-лист групи з volume tier
        $group = $customer->customerGroup;
        if ($group?->priceList) {
            $tier = PriceListItem::where('price_list_id', $group->price_list_id)
                ->where('product_id', $productId)
                ->where('min_qty', '<=', $qty)
                ->orderByDesc('min_qty')
                ->first();

            if ($tier) {
                $price = $tier->price;
                if ($group->discount_percent > 0) {
                    $price *= (1 - $group->discount_percent / 100);
                }
                return new PriceResult(round($price, 4), 'price_list');
            }
        }

        // 3. Базова ціна
        $product = Product::find($productId);
        return new PriceResult($product->price, 'retail');
    }
}

Кеширование цін

Резолвер викликається при кожному відображенні каталога — для 100 товарів на сторінці це 100 вызванків. Без кешу це катастрофа продуктивності.

Стратегія: кешувати персональний прайс-лист користувача цілком при логіні, інвалідувати при зміні його ціннових умов:

$cacheKey = "b2b_prices:{$user->id}";
$prices = Cache::remember($cacheKey, 3600, function () use ($user) {
    return $this->buildUserPriceMap($user);
});

Cache::forget("b2b_prices:{$user->id}");

Для великих каталогів (50k+ SKU) прайс будується в фоновій задачі та зберігається в Redis Hash.

Відображення цін у каталозі

B2B-користувач видит тільки свої ціни, без публічного прайса. Неавторизований користувач видит заглушку «Увійдіть для перегляду цін» чи запит на реєстрацію. Це реалізується middleware:

const PriceDisplay = ({ product }: { product: Product }) => {
  const { user } = useAuth();

  if (!user) return <RequestAccessButton />;
  if (!product.b2b_price) return <PriceOnRequest />;

  return (
    <div>
      <span className="text-lg font-bold">{formatPrice(product.b2b_price)}</span>
      {product.b2b_source === 'contract' && (
        <span className="text-xs text-green-600 ml-2">Договірна ціна</span>
      )}
    </div>
  );
};

Управління цінами в admin-панелі

Admin-панель забезпечує:

  • Створення та редагування прайс-листів з масовим імпортом через CSV/XLSX
  • Призначення клієнтів на групи
  • Установку персональних цін на SKU з датами дії
  • Предпросмотр ціни: «Яку ціну видит клієнт X на товар Y?»
  • Журнал змін цін з указанням автора та часу

Імпорт з ERP

Для синхронізації з 1С, SAP чи іншею ERP реалізується API-приймач чи file-watcher:

Artisan::call('prices:import', [
    '--file'       => '/var/imports/prices_20240115.csv',
    '--price-list' => 3,
    '--mode'       => 'upsert',
]);

Формат CSV: sku,price,min_qty,valid_from,valid_to. Імпорт через чергу, результат — звіт з кількістю добавлених/оновлених/пропущених позицій.

Мультивалютність

Якщо B2B працює з кількома валютами, прайс-листи зберігаються у валюті контракту. Конвертація у валюту відображення по курсу ЦБ (кешується на 1 годину):

$displayPrice = $price * ExchangeRateService::getRate($priceList->currency, 'RUB');

Курс фіксується у момент створення заказу — зміна курсу між переглядом каталога та оплатою повинна бути явно обозначена на checkout.

Узгодження цін (price negotiation)

Для enterprise-сегменту реалізуємо flow запиту ціни: клієнт запитує персональне пропозицію, менеджер встановлює договірну ціну, клієнт видит її у своєму каталозі. Статусы: requested → under_review → approved → active. Сповіщення по email на кожному переходе.