Developing an RMA (Return Merchandise Authorization) system on 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1189
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    813
  • 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
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

RMA (Return Merchandise Authorization) System Development on 1C-Bitrix

Returns without a system are chaos. The customer calls or writes to a general inbox, a manager manually searches for the order, negotiates the return, and sends instructions. Tracking the return status is impossible, and there is no analytics. An RMA system automates this process: the customer creates a return request through their personal account, the manager processes it in the admin panel, and the system tracks statuses. Bitrix has no built-in RMA — it is built on top of the sale module.

Legal Context

Product returns are governed by consumer protection legislation. Two main grounds:

  • Defective goods — defect, manufacturing fault. The claim period is within the warranty period.
  • Non-defective goods — "changed my mind." Only for non-food products, 14 days, the product must be unused.

In the RMA system, both grounds must be represented as separate request types with different fields and processes.

Data Model

RMA request table:

class RmaRequestTable extends \Bitrix\Main\ORM\Data\DataManager
{
    public static function getTableName(): string { return 'b_local_rma_request'; }

    public static function getMap(): array
    {
        return [
            new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
            new StringField('NUMBER'),              // RMA-YYYYMMDD-XXXX
            new IntegerField('ORDER_ID'),           // b_sale_order
            new IntegerField('USER_ID'),
            new EnumField('TYPE', ['values' => ['DEFECT', 'EXCHANGE', 'REFUND']]),
            new EnumField('STATUS', ['values' => ['NEW', 'UNDER_REVIEW', 'APPROVED', 'REJECTED', 'RETURNED', 'REFUNDED', 'CLOSED']]),
            new TextField('DESCRIPTION'),           // Customer-provided reason description
            new StringField('DEFECT_PHOTO_IDS'),    // JSON array of b_file IDs
            new StringField('REJECT_REASON'),       // Rejection reason (if REJECTED)
            new FloatField('REFUND_AMOUNT'),        // Amount to be refunded
            new IntegerField('RESPONSIBLE_ID'),     // Responsible manager
            new DatetimeField('CREATED_AT'),
            new DatetimeField('UPDATED_AT'),
            new DatetimeField('DEADLINE_AT'),       // Processing deadline (14 days)
        ];
    }
}

Return items:

class RmaItemTable extends \Bitrix\Main\ORM\Data\DataManager
{
    public static function getTableName(): string { return 'b_local_rma_item'; }

    public static function getMap(): array
    {
        return [
            new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
            new IntegerField('RMA_ID'),
            new IntegerField('ORDER_BASKET_ID'),    // b_sale_basket
            new IntegerField('PRODUCT_ID'),
            new StringField('PRODUCT_NAME'),
            new FloatField('QUANTITY'),
            new FloatField('PRICE'),
            new StringField('REASON_CODE'),         // DEFECTIVE, NOT_AS_DESCRIBED, CHANGED_MIND, WRONG_ITEM
            new StringField('CONDITION'),           // NEW, USED, DAMAGED
        ];
    }
}

Status history:

class RmaHistoryTable extends \Bitrix\Main\ORM\Data\DataManager
{
    public static function getTableName(): string { return 'b_local_rma_history'; }
    // RMA_ID, OLD_STATUS, NEW_STATUS, USER_ID, COMMENT, CREATED_AT
}

RMA Number Generation

class RmaNumberGenerator
{
    public static function generate(): string
    {
        $date   = date('Ymd');
        $lastId = RmaRequestTable::getList([
            'order'  => ['ID' => 'DESC'],
            'limit'  => 1,
            'select' => ['ID'],
        ])->fetch()['ID'] ?? 0;

        return 'RMA-' . $date . '-' . str_pad($lastId + 1, 4, '0', STR_PAD_LEFT);
    }
}

Creating a Request from the Personal Account

Page /personal/rma/create/?order_id=12345:

class RmaCreateComponent extends \CBitrixComponent
{
    public function executeComponent(): void
    {
        $orderId = (int)$this->arParams['ORDER_ID'];

        if (!$orderId || !$this->isOrderOwner($orderId)) {
            LocalRedirect('/personal/orders/');
            return;
        }

        // Load order items
        $order  = \Bitrix\Sale\Order::load($orderId);
        $basket = $order->getBasket();

        $items = [];
        foreach ($basket as $item) {
            // Check: has the return period expired?
            $daysSinceOrder = (new \DateTime())->diff(new \DateTime($order->getField('DATE_INSERT')))->days;
            if ($daysSinceOrder > 365) continue; // Skip items older than one year

            $items[] = [
                'BASKET_ID'    => $item->getId(),
                'PRODUCT_ID'   => $item->getProductId(),
                'NAME'         => $item->getField('NAME'),
                'QUANTITY'     => $item->getQuantity(),
                'PRICE'        => $item->getPrice(),
                'CAN_REFUND'   => $daysSinceOrder <= 14, // 14 days for return without defect
            ];
        }

        if ($this->request->isPost() && check_bitrix_sessid()) {
            $this->createRmaRequest($orderId, $items);
        }

        $this->arResult['ORDER'] = $order;
        $this->arResult['ITEMS'] = $items;
        $this->includeComponentTemplate();
    }

    private function createRmaRequest(int $orderId, array $items): void
    {
        $selectedItems = [];
        foreach ($this->request->getPost('items') as $basketId => $itemData) {
            if (empty($itemData['selected'])) continue;
            $selectedItems[] = [
                'ORDER_BASKET_ID' => $basketId,
                'QUANTITY'        => (float)$itemData['quantity'],
                'REASON_CODE'     => $itemData['reason'],
                'CONDITION'       => $itemData['condition'],
            ];
        }

        if (empty($selectedItems)) {
            $this->arResult['ERROR'] = 'Please select at least one item to return';
            return;
        }

        // Upload defect photos
        $photoIds = [];
        foreach ($_FILES['defect_photos']['tmp_name'] as $i => $tmpName) {
            if (is_uploaded_file($tmpName)) {
                $fileId = \CFile::SaveFile([
                    'name'     => $_FILES['defect_photos']['name'][$i],
                    'size'     => $_FILES['defect_photos']['size'][$i],
                    'tmp_name' => $tmpName,
                    'type'     => $_FILES['defect_photos']['type'][$i],
                ], 'rma');
                if ($fileId) $photoIds[] = $fileId;
            }
        }

        $addResult = RmaRequestTable::add([
            'NUMBER'         => RmaNumberGenerator::generate(),
            'ORDER_ID'       => $orderId,
            'USER_ID'        => $GLOBALS['USER']->GetID(),
            'TYPE'           => $this->request->getPost('type'),
            'STATUS'         => 'NEW',
            'DESCRIPTION'    => htmlspecialchars($this->request->getPost('description')),
            'DEFECT_PHOTO_IDS' => json_encode($photoIds),
            'CREATED_AT'     => new \Bitrix\Main\Type\DateTime(),
            'DEADLINE_AT'    => \Bitrix\Main\Type\DateTime::createFromPhp(new \DateTime('+14 days')),
        ]);

        $rmaId = $addResult->getId();
        foreach ($selectedItems as $item) {
            RmaItemTable::add(array_merge($item, ['RMA_ID' => $rmaId]));
        }

        // Notify manager
        $this->notifyManager($rmaId);

        $this->arResult['SUCCESS'] = true;
        $this->arResult['RMA_NUMBER'] = 'RMA-...' ;
    }
}

Administrative Processing

Page /local/admin/rma_list.php — a list of all RMA requests with filters by status, deadline, and responsible manager. Manager actions:

// Approve return
public function approve(int $rmaId, float $refundAmount, int $managerId): void
{
    RmaRequestTable::update($rmaId, [
        'STATUS'         => 'APPROVED',
        'REFUND_AMOUNT'  => $refundAmount,
        'RESPONSIBLE_ID' => $managerId,
        'UPDATED_AT'     => new \Bitrix\Main\Type\DateTime(),
    ]);

    RmaHistoryTable::add([
        'RMA_ID'     => $rmaId,
        'OLD_STATUS' => 'UNDER_REVIEW',
        'NEW_STATUS' => 'APPROVED',
        'USER_ID'    => $managerId,
        'COMMENT'    => 'Request approved',
    ]);

    // Initiate refund through the payment system
    $this->initiateRefund($rmaId, $refundAmount);

    // Notify customer
    $this->notifyClient($rmaId, 'APPROVED');
}

Payment Refund Integration

To refund money through the payment system — use the API of the same gateway through which the payment was accepted. Most payment gateways have a refund method:

// Example for YooKassa
$client = new \YooKassa\Client();
$client->setAuth($shopId, $secretKey);

$originalPaymentId = $this->getOriginalPaymentId($orderId); // from b_sale_pay_system_action
$refund = $client->createRefund([
    'payment_id' => $originalPaymentId,
    'amount'     => ['value' => $refundAmount, 'currency' => 'RUB'],
    'description' => 'Refund for RMA #' . $rmaNumber,
]);

After a successful refund — RMA status → REFUNDED, replenish warehouse stock (if the item was returned to the warehouse).

Development Timeline

Option Scope Timeline
Basic RMA Application form, list in personal account, statuses 8–12 days
With admin processing Manager interface, history, notifications 12–18 days
Full system + Payment refunds, warehouse integration 18–28 days