Разработка системы возвратов (RMA) для интернет-магазина
RMA (Return Merchandise Authorization) — формализованный процесс приёма и обработки возвратов. Без системы возвраты обрабатываются вручную через почту и мессенджеры, теряются, задерживаются и вызывают недовольство. Разработка RMA занимает 5–8 рабочих дней — это полноценный workflow с несколькими участниками.
Бизнес-логика возвратов
Перед разработкой необходимо зафиксировать правила:
- Срок подачи заявки на возврат (14 дней по закону РФ; магазин может расширить)
- Возвращаемые категории товаров (ПО, нижнее бельё, персонализированные товары — не возвращаются)
- Причины возврата (брак, не подошёл, пришёл не тот товар, передумал)
- Способы компенсации: возврат денег / обмен / кредит на счёт
- Нужна ли отправка товара обратно или достаточно фото
Схема данных
CREATE TABLE returns (
id BIGSERIAL PRIMARY KEY,
rma_number VARCHAR(20) UNIQUE NOT NULL, -- RMA-2024-001234
order_id BIGINT REFERENCES orders(id),
user_id BIGINT REFERENCES users(id),
status VARCHAR(30) NOT NULL DEFAULT 'pending',
-- pending → approved → items_received → resolved / rejected
reason VARCHAR(50) NOT NULL,
comment TEXT,
resolution VARCHAR(20), -- 'refund', 'exchange', 'store_credit'
refund_amount NUMERIC(12,2),
created_at TIMESTAMP DEFAULT NOW(),
resolved_at TIMESTAMP
);
CREATE TABLE return_items (
id BIGSERIAL PRIMARY KEY,
return_id BIGINT REFERENCES returns(id) ON DELETE CASCADE,
order_item_id BIGINT REFERENCES order_items(id),
quantity INT NOT NULL,
condition VARCHAR(30), -- 'unopened', 'opened', 'damaged'
photos JSONB DEFAULT '[]' -- массив URL фото
);
Форма заявки на возврат
Покупатель заполняет форму в личном кабинете. Шаги:
- Выбор заказа → выбор позиций и количества
- Указание причины для каждой позиции
- Загрузка фотографий (если требуется)
- Выбор способа компенсации
- Подтверждение и получение RMA-номера
const ReturnForm = ({ order }: { order: OrderDetail }) => {
const form = useForm<ReturnFormData>({
resolver: zodResolver(returnSchema),
defaultValues: { items: [], reason: '', resolution: 'refund' },
});
return (
<form onSubmit={form.handleSubmit(submitReturn)}>
<h2 className="font-semibold mb-4">Выберите товары для возврата</h2>
{order.items.map(item => (
<ReturnItemRow key={item.id} item={item} form={form} />
))}
<Select name="reason" label="Причина возврата" options={returnReasons} />
<Textarea name="comment" label="Комментарий (необязательно)" />
<PhotoUploader name="photos" maxFiles={5} />
<RadioGroup name="resolution" label="Способ компенсации">
<RadioItem value="refund">Возврат денег</RadioItem>
<RadioItem value="exchange">Обмен на другой товар</RadioItem>
<RadioItem value="store_credit">Кредит на счёт магазина</RadioItem>
</RadioGroup>
<Button type="submit">Отправить заявку</Button>
</form>
);
};
Загрузка фотографий
Фото подтверждают состояние товара и необходимы для возвратов по причине «брак» или «повреждение при доставке». Загрузка через S3-совместимое хранилище:
public function uploadPhoto(Request $request): JsonResponse
{
$request->validate([
'photo' => 'required|image|mimes:jpeg,png,webp|max:5120',
]);
$path = $request->file('photo')->store('returns/photos', 's3');
$url = Storage::disk('s3')->url($path);
return response()->json(['url' => $url]);
}
Максимум 5 фото, каждое до 5 МБ. Превью отображается сразу после загрузки.
Workflow обработки в admin-панели
Менеджер видит очередь заявок с фильтрами по статусу, дате, сумме. Для каждой заявки доступны действия:
-
Одобрить — статус
approved, покупатель получает инструкции по отправке -
Отклонить — статус
rejectedс обязательным комментарием -
Отметить товар получен — статус
items_received, старт проверки качества -
Провести возврат — инициировать рефанд через платёжного провайдера, статус
resolved
class ReturnController extends Controller
{
public function approve(Return $return, Request $request): void
{
$return->transitionTo(Approved::class);
$return->update(['approved_by' => $request->user()->id]);
Notification::send($return->user, new ReturnApproved($return));
// Письмо с инструкцией по отправке и адресом склада
}
public function processRefund(Return $return): void
{
$payment = $return->order->payment;
$this->paymentGateway->refund($payment->gateway_id, $return->refund_amount);
$return->transitionTo(Resolved::class);
$return->update(['resolved_at' => now()]);
Notification::send($return->user, new RefundProcessed($return));
}
}
Автоматическое одобрение
Для снижения нагрузки на поддержку — правила автоодобрения:
class AutoApprovalService
{
public function shouldAutoApprove(Return $return): bool
{
// Сумма заказа до 2000 ₽ и причина "не подошёл" — автоодобрение
return $return->order->total <= 2000
&& $return->reason === 'not_suitable'
&& $return->created_at->diffInDays($return->order->delivered_at) <= 14;
}
}
Настраивается в admin-панели, не в коде.
Возврат денег через платёжный провайдер
Рефанды проходят через API платёжного провайдера. Для ЮKassa:
$client = new \YooKassa\Client();
$client->setAuth($shopId, $secretKey);
$refund = $client->createRefund([
'payment_id' => $order->payment->yookassa_payment_id,
'amount' => ['value' => $return->refund_amount, 'currency' => 'RUB'],
'description' => "Возврат по RMA #{$return->rma_number}",
]);
Частичный возврат (только часть позиций) поддерживается стандартно — сумма refund_amount рассчитывается как сумма возвращаемых позиций с пропорциональным включением скидок.
Аналитика возвратов
Отчёт по возвратам в admin: процент возвратов по категориям, топ причин, среднее время обработки, сумма возвращённых средств за период. Это позволяет выявлять системные проблемы: если конкретный товар возвращается чаще 15% — сигнал к проверке описания, фото или качества.







