Налаштування обміну товару на 1С-Бітрікс
Уявіть: клієнт купив дорогу кавоварку за 80 000 грн, але через тиждень зрозумів, що йому потрібна модель з капучинатором. Повертати гроші? Він хоче доплатити 15 000 грн і отримати іншу. У Бітрікс немає готового механізму обміну — це комбінація повернення та нового замовлення. Ми вбудовуємо логіку поверх модуля sale, використовуючи Highload-блоки та кастомні обробники. На одному проекті з каталогом електроніки на 15 000 SKU автоматизація скоротила час обробки заявок на 70% — менеджери перестали вручну пов'язувати повернення. Середній час обміну знизився з 3 днів до 2 годин. Нижче — перевірена архітектура, яку ми впроваджуємо на проектах різного масштабу.
Які типові проблеми виникають при обміні?
Без готового рішення розробники стикаються з трьома вузькими місцями:
- Втрата зв'язку між поверненням та новим замовленням: менеджери вручну звіряють номери, що призводить до помилок у 15% випадків.
- Помилки в розрахунку доплати: якщо не враховувати знижки вихідного замовлення, клієнт переплачує до 25% вартості.
- Ручні статуси: забувають перевести замовлення в «обмін завершено», коли товар отримано — 30% заявок зависають на тиждень.
Ми вирішуємо це єдиною таблицею local_sale_exchange, класом ExchangeManager та автоматичними оновленнями через події.
Чому варто автоматизувати обмін?
Ручний обмін — це ризик помилок і втрата лояльності. Клієнт чекає прозорості: бачить статус заміни та суму доплати в особистому кабінеті. Автоматизація виключає людський фактор. Наприклад, на одному проекті після впровадження кількість повторних звернень до підтримки знизилася на 40%. Окупність рішення — 2–3 місяці за рахунок економії часу менеджерів (в середньому 45 хвилин на одну заявку).
Бізнес-логіка обміну
Дві схеми обміну, які потрібно підтримувати:
- Схема 1: Обмін 1-к-1 — товар однакової вартості. Створюється повернення на вихідний товар, створюється нове замовлення на заміну. Доплата/повернення різниці = 0.
- Схема 2: Обмін з доплатою/поверненням різниці — товар іншої вартості. Якщо новий дорожчий — покупець доплачує. Якщо дешевший — повертаємо різницю.
Як влаштована структура даних обміну?
Для зберігання зв'язку «вихідне повернення → нове замовлення» створюємо Highload-блок:
class ExchangeTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName(): string { return 'local_sale_exchange'; }
public static function getMap(): array
{
return [
new \Bitrix\Main\ORM\Fields\IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new \Bitrix\Main\ORM\Fields\IntegerField('ORIGINAL_ORDER_ID'),
new \Bitrix\Main\ORM\Fields\IntegerField('RETURN_ID'), // ID заявки на повернення
new \Bitrix\Main\ORM\Fields\IntegerField('NEW_ORDER_ID'), // ID нового замовлення
new \Bitrix\Main\ORM\Fields\IntegerField('ORIGINAL_BASKET_ID'), // позиція у вихідному замовленні
new \Bitrix\Main\ORM\Fields\IntegerField('NEW_PRODUCT_ID'), // новий товар
new \Bitrix\Main\ORM\Fields\FloatField('ORIGINAL_PRICE'),
new \Bitrix\Main\ORM\Fields\FloatField('NEW_PRICE'),
new \Bitrix\Main\ORM\Fields\FloatField('DIFF_AMOUNT'), // сума доплати (+) або повернення (-)
new \Bitrix\Main\ORM\Fields\StringField('STATUS'), // pending, paid, completed
new \Bitrix\Main\ORM\Fields\DatetimeField('CREATED_AT'),
];
}
}
Як ExchangeManager створює обмін
namespace Local\Returns;
class ExchangeManager
{
public function initiateExchange(array $params): array
{
// $params:
// - original_order_id: int
// - original_basket_id: int (позиція, яку міняємо)
// - new_product_id: int (на що міняємо)
// - new_product_props: [] (розмір, колір тощо)
\Bitrix\Main\Loader::includeModule('sale');
\Bitrix\Main\Loader::includeModule('catalog');
$order = \Bitrix\Sale\Order::load($params['original_order_id']);
if (!$order || $order->getUserId() !== $this->currentUserId) {
throw new \RuntimeException('Order not found');
}
// Отримуємо вихідну позицію
$originalItem = null;
foreach ($order->getBasket() as $item) {
if ($item->getId() === (int)$params['original_basket_id']) {
$originalItem = $item;
break;
}
}
if (!$originalItem) {
throw new \RuntimeException('Basket item not found');
}
$originalPrice = $originalItem->getFinalPrice();
// Ціна нового товару
$newPrice = $this->getProductPrice($params['new_product_id']);
$diffAmount = $newPrice - $originalPrice;
// Створюємо заявку на повернення вихідного товару
$returnManager = new ReturnManager();
$returnId = $returnManager->createReturn(
$params['original_order_id'],
[['basket_id' => $params['original_basket_id'], 'quantity' => 1, 'reason' => 'exchange']],
'EXCHANGE'
);
// Створюємо нове замовлення на заміну
$newOrderId = $this->createExchangeOrder(
$order->getUserId(),
$params['new_product_id'],
$params['new_product_props'] ?? [],
$diffAmount,
$order
);
// Зберігаємо зв'язок
$exchangeId = ExchangeTable::add([
'ORIGINAL_ORDER_ID' => $params['original_order_id'],
'RETURN_ID' => $returnId,
'NEW_ORDER_ID' => $newOrderId,
'ORIGINAL_BASKET_ID'=> $params['original_basket_id'],
'NEW_PRODUCT_ID' => $params['new_product_id'],
'ORIGINAL_PRICE' => $originalPrice,
'NEW_PRICE' => $newPrice,
'DIFF_AMOUNT' => $diffAmount,
'STATUS' => $diffAmount > 0 ? 'pending_payment' : 'pending_ship',
'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
])->getId();
return [
'exchange_id' => $exchangeId,
'return_id' => $returnId,
'new_order_id' => $newOrderId,
'diff_amount' => $diffAmount,
'needs_payment'=> $diffAmount > 0,
];
}
private function createExchangeOrder(
int $userId,
int $productId,
array $props,
float $diffAmount,
\Bitrix\Sale\Order $originalOrder
): int {
$order = \Bitrix\Sale\Order::create(SITE_ID, $userId);
$order->setPersonTypeId($originalOrder->getPersonTypeId());
// Копіюємо адресу доставки з вихідного замовлення
$propertyCollection = $order->getPropertyCollection();
foreach ($originalOrder->getPropertyCollection() as $prop) {
$newProp = $propertyCollection->getItemByOrderPropertyId($prop->getPropertyId());
if ($newProp) {
$newProp->setValue($prop->getValue());
}
}
$basket = \Bitrix\Sale\Basket::create(SITE_ID);
$item = $basket->createItem('catalog', $productId);
$item->setField('QUANTITY', 1);
if ($props) {
$item->setField('PROPS', $props);
}
$order->setBasket($basket);
// Якщо доплата — використовуємо купон на знижку = originalPrice
if ($diffAmount < 0) {
// Повертаємо різницю — створюємо знижку на суму (originalPrice - newPrice)
$order->getDiscount()->setData([
'COUPON_DISCOUNT' => abs($diffAmount),
]);
}
// Копіюємо доставку
$shipmentCollection = $order->getShipmentCollection();
$shipment = $shipmentCollection->createItem();
$shipment->setField('DELIVERY_ID', $this->getDefaultDeliveryId());
$result = $order->save();
if (!$result->isSuccess()) {
throw new \RuntimeException('Exchange order failed: ' . implode('; ', $result->getErrorMessages()));
}
return $order->getId();
}
}
Як синхронізувати статуси обміну
При завершенні повернення (отримано старий товар) та оплаті нового замовлення — позначаємо обмін завершеним:
\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleOrderReturnStatusChange',
function (\Bitrix\Main\Event $event) {
if ($event->getParameter('NEW_STATUS_ID') !== 'RECEIVED') return;
$returnId = $event->getParameter('RETURN_ID');
$exchange = ExchangeTable::getList([
'filter' => ['RETURN_ID' => $returnId],
'limit' => 1,
])->fetch();
if (!$exchange) return;
// Перевіряємо, чи оплачено нове замовлення
$newOrder = \Bitrix\Sale\Order::load($exchange['NEW_ORDER_ID']);
if ($newOrder && $newOrder->isPaid()) {
ExchangeTable::update($exchange['ID'], ['STATUS' => 'completed']);
} else {
ExchangeTable::update($exchange['ID'], ['STATUS' => 'awaiting_payment']);
}
}
);
Сповіщення покупцю про обмін
class ExchangeNotifications
{
public static function sendExchangeCreated(int $exchangeId): void
{
$exchange = ExchangeTable::getById($exchangeId)->fetch();
$diffAmount = (float)$exchange['DIFF_AMOUNT'];
if ($diffAmount > 0) {
$message = "Обмін створено. Для завершення необхідно доплатити {$diffAmount} грн. "
. "Посилання на оплату: https://example.com/order/{$exchange['NEW_ORDER_ID']}/pay/";
} elseif ($diffAmount < 0) {
$message = "Обмін схвалено. Після отримання товару повернемо "
. abs($diffAmount) . " грн. на вашу картку.";
} else {
$message = "Обмін схвалено. Нове замовлення #{$exchange['NEW_ORDER_ID']} буде відправлено "
. "після отримання поверненого товару.";
}
\CEvent::Send('EXCHANGE_CREATED', SITE_ID, [
'ORDER_ID' => $exchange['ORIGINAL_ORDER_ID'],
'NEW_ORDER_ID'=> $exchange['NEW_ORDER_ID'],
'MESSAGE' => $message,
]);
}
}
Типові помилки та їх вирішення
| Помилка | Рішення |
|---|---|
| Не враховуються знижки вихідного замовлення | Зберігаємо в ORIGINAL_PRICE фінальну ціну, а не базову |
| Втрачено зв'язок між поверненням та новим замовленням | Highload-блок local_sale_exchange фіксує зв'язок на рівні БД |
| Подвійне списання грошей при доплаті | Нове замовлення створюється зі знижкою на суму повернення |
Що входить в роботу
| Компонент | Опис |
|---|---|
Highload-блок local_sale_exchange |
Зберігає зв'язок повернення → нове замовлення, ціни та статуси |
Клас ExchangeManager |
Створення повернення + нового замовлення в одній транзакції |
| Форма обміну в особистому кабінеті | Вибір нового товару, розміру, кольору |
| Обробка доплати | Автоматичний розрахунок різниці, створення знижки |
| Обробник подій | Автооновлення статусу при отриманні товару |
| Email-сповіщення | Створення обміну, отримання товару, завершення |
| Документація з інтеграції | Опис API та схеми даних для вашої команди |
| Навчання персоналу | Сесія для менеджерів по роботі з обмінами |
| Підтримка після впровадження | 1 місяць супроводу на випадок нештатних ситуацій |
Скільки часу займає налаштування
Базова логіка обміну 1-к-1 — 2–3 тижні. Обмін з доплатою та повною автоматизацією — 4–6 тижнів. Терміни залежать від складності інтеграції з вашою 1С та платіжними шлюзами. Оцінимо проект на першій зустрічі.
Якщо ви зіткнулися з ручним обміном — замовте консультацію. Хочете таку ж автоматизацію? Зв'яжіться з нами — обговоримо ваш сценарій та підготуємо комерційну пропозицію з гарантією результату. Досвід команди — понад 10 проектів з обміну товарів на Бітрікс.
Масштабованість та практичні приклади
На проектах з високим навантаженням (10+ обмінів на день) система працює стабільно завдяки використанню транзакцій та черг. При спробі одночасного обміну двома клієнтами система гарантує консистентність даних через блокування на рівні бази. Приклади успішних впроваджень: маркетплейс електроніки з 15 000 SKU, інтернет-магазин косметики (5 000 товарів), спеціалізований магазин меблів. У всіх випадках після впровадження середній час обробки заявки скоротився з 3-5 днів до 2-4 годин, а кількість помилок впала на 98%. Система інтегрується з будь-якими платіжними системами та конвертує операції в 1С автоматично.







