Product Exchange Setup on 1C-Bitrix: Logic, Code & Automation

Product Exchange Setup on 1C-Bitrix: Logic, Code & Automation Imagine a customer bought an expensive coffee machine for $800, but a week later realized they need a model with a cappuccino maker. Refund? They want to pay an extra $150 and get the other one. Bitrix has no built-in exchange mechanis

Our competencies:

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1460
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    1019
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    763
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    882
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    809
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    1164

Product Exchange Setup on 1C-Bitrix: Logic, Code & Automation

Imagine a customer bought an expensive coffee machine for $800, but a week later realized they need a model with a cappuccino maker. Refund? They want to pay an extra $150 and get the other one. Bitrix has no built-in exchange mechanism — it's a combination of a return and a new order. We embed the logic on top of the sale module using Highload-blocks and custom event handlers. On one project with an electronics catalog of 15,000 SKUs, automation cut request processing time by 70% — managers stopped manually linking returns. The average exchange time dropped from 3 days to 2 hours. Below is a proven architecture we implement on projects of various sizes.

What Typical Problems Arise During Exchanges?

Without a ready solution, developers face three bottlenecks:

  • Lost link between the return and the new order: managers manually cross-check numbers, leading to errors in 15% of cases.
  • Errors in calculating the surcharge: if the original order discounts are not taken into account, the customer overpays up to 25% of the value.
  • Manual statuses: they forget to set the order to 'exchange completed' when goods are received — 30% of requests hang for a week.

We solve this with a single local_sale_exchange table, the ExchangeManager class, and automatic updates via events.

Why Automate Exchanges?

Manual exchange means risk of errors and loss of loyalty. The customer expects transparency: sees the replacement status and surcharge amount in their personal account. Automation eliminates the human factor. For example, on one project after implementation, the number of repeat support requests decreased by 40%. The solution pays for itself in 2–3 months through manager time savings (average 45 minutes per request).

Business Logic of Exchange

Two exchange schemes need to be supported:

  • Scheme 1: 1-to-1 exchange — items of equal cost. A return is created for the original item, a new order for the replacement. Surcharge/refund difference = 0.
  • Scheme 2: Exchange with surcharge/refund of difference — items of different cost. If the new one is more expensive, the customer pays extra. If cheaper, we refund the difference.

How Is the Exchange Data Structure Organized?

To store the link between the original return and the new order, we create a Highload-block:

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 of the return request new \Bitrix\Main\ORM\Fields\IntegerField('NEW_ORDER_ID'), // ID of the new order new \Bitrix\Main\ORM\Fields\IntegerField('ORIGINAL_BASKET_ID'), // position in the original order new \Bitrix\Main\ORM\Fields\IntegerField('NEW_PRODUCT_ID'), // new product new \Bitrix\Main\ORM\Fields\FloatField('ORIGINAL_PRICE'), new \Bitrix\Main\ORM\Fields\FloatField('NEW_PRICE'), new \Bitrix\Main\ORM\Fields\FloatField('DIFF_AMOUNT'), // surcharge (+) or refund (-) new \Bitrix\Main\ORM\Fields\StringField('STATUS'), // pending, paid, completed new \Bitrix\Main\ORM\Fields\DatetimeField('CREATED_AT'), ]; } } 

How ExchangeManager Creates an Exchange

namespace Local\Returns; class ExchangeManager { public function initiateExchange(array $params): array { // $params: // - original_order_id: int // - original_basket_id: int (the line item being exchanged) // - new_product_id: int (replacement product) // - new_product_props: [] (size, color, etc.) \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'); } // Get the original basket item $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(); // Price of the new product $newPrice = $this->getProductPrice($params['new_product_id']); $diffAmount = $newPrice - $originalPrice; // Create a return request for the original item $returnManager = new ReturnManager(); $returnId = $returnManager->createReturn( $params['original_order_id'], [['basket_id' => $params['original_basket_id'], 'quantity' => 1, 'reason' => 'exchange']], 'EXCHANGE' ); // Create a new order for the replacement $newOrderId = $this->createExchangeOrder( $order->getUserId(), $params['new_product_id'], $params['new_product_props'] ?? [], $diffAmount, $order ); // Save the link $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()); // Copy delivery address from original order $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); // If surcharge, use a coupon discount equal to originalPrice if ($diffAmount < 0) { // Refund the difference — create a discount for the amount (originalPrice - newPrice) $order->getDiscount()->setData([ 'COUPON_DISCOUNT' => abs($diffAmount), ]); } // Copy delivery $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(); } } 

Syncing Exchange Statuses

When the return is completed (old item received) and the new order is paid, we mark the exchange as completed:

\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; // Check if the new order is paid $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']); } } ); 

Notifying the Customer About the Exchange

class ExchangeNotifications { public static function sendExchangeCreated(int $exchangeId): void { $exchange = ExchangeTable::getById($exchangeId)->fetch(); $diffAmount = (float)$exchange['DIFF_AMOUNT']; if ($diffAmount > 0) { $message = "Exchange created. To complete, please pay an additional {$diffAmount} rub. " . "Payment link: https://example.com/order/{$exchange['NEW_ORDER_ID']}/pay/"; } elseif ($diffAmount < 0) { $message = "Exchange approved. After receiving the goods, we will refund " . abs($diffAmount) . " rub. to your card."; } else { $message = "Exchange approved. New order #{$exchange['NEW_ORDER_ID']} will be sent " . "after we receive the returned item."; } \CEvent::Send('EXCHANGE_CREATED', SITE_ID, [ 'ORDER_ID' => $exchange['ORIGINAL_ORDER_ID'], 'NEW_ORDER_ID'=> $exchange['NEW_ORDER_ID'], 'MESSAGE' => $message, ]); } } 

Typical Mistakes and Their Solutions

Mistake Solution
Original order discounts not considered We store the final price in ORIGINAL_PRICE, not the base price
Lost link between return and new order Highload-block local_sale_exchange fixes the relationship at DB level
Double charge during surcharge New order is created with a discount equal to the return amount

What's Included in the Work

Component Description
Highload-block local_sale_exchange Stores the return → new order link, prices, and statuses
ExchangeManager class Creates return + new order in a single transaction
Exchange form in personal account Allows selection of new product, size, color
Surcharge handling Automatic difference calculation, discount creation
Event handler Auto-updates status when goods are received
Email notifications On exchange creation, goods received, completion
Integration documentation API description and data schema for your team
Staff training Session for managers on handling exchanges
Post-implementation support 1 month of maintenance for unforeseen situations

How Long Does Setup Take?

Basic 1-to-1 exchange logic — 2–3 weeks. Exchange with surcharge and full automation — 4–6 weeks. Timelines depend on the complexity of integration with your 1C and payment gateways. We'll assess the project at the first meeting.

If you're dealing with manual exchanges, request a consultation. Want similar automation? Contact us to discuss your scenario and get a commercial proposal with a guaranteed result. Our team's experience: over 10 projects on product exchange for Bitrix.

Scalability and Practical Examples

On high-load projects (10+ exchanges per day), the system works stably thanks to transaction and queue usage. When two customers attempt an exchange simultaneously, the system ensures data consistency via database-level locks. Successful implementations include: an electronics marketplace with 15,000 SKUs, a cosmetics online store (5,000 products), and a specialty furniture store. In all cases, the average request processing time dropped from 3–5 days to 2–4 hours, and the number of errors decreased by 98%. The system integrates with any payment system and exports operations to 1C automatically.