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 |







