Setting up a partial refund in 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
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • 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
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Setting Up Partial Refunds on 1C-Bitrix

A partial refund is needed when a buyer has declined one item from an order, part of the goods could not be shipped, or a service was rendered incompletely. Unlike a full refund, a partial refund requires precise calculation of the amount and — when an online cash register is connected — the submission of a correction receipt. The standard 1C-Bitrix sale module supports refunds but requires additional configuration for correct interaction with the acquiring service.

Refund Architecture in 1C-Bitrix

A refund in 1C-Bitrix is a \Bitrix\Sale\PaymentCollection::createRefund() object or a direct call to the acquiring service API. Two approaches:

Via the sale module — create a refund through the payment handler. Advantage: automatic order status update and logging. Disadvantage: the handler must support the refund method.

Directly via the acquiring service API — call the refund API independently of 1C-Bitrix, then manually update the status. Advantage: greater control. Disadvantage: state synchronization is required.

Implementing the Refund Method in the Handler

class MyGatewayHandler extends \Bitrix\Sale\PaySystem\ServiceHandler
    implements \Bitrix\Sale\PaySystem\IRefund
{
    public function refund(
        \Bitrix\Sale\Payment $payment,
        $refundableSum
    ): \Bitrix\Sale\PaySystem\ServiceResult {

        $result = new \Bitrix\Sale\PaySystem\ServiceResult();

        $externalId = $payment->getField('PS_INVOICE_ID');
        if (!$externalId) {
            $result->addError(new \Bitrix\Main\Error('External payment ID not found'));
            return $result;
        }

        try {
            $refundResult = $this->gateway->createRefund([
                'payment_id'  => $externalId,
                'amount'      => [
                    'value'    => number_format($refundableSum, 2, '.', ''),
                    'currency' => $payment->getField('CURRENCY'),
                ],
                'description' => 'Partial refund for order ' . $payment->getOrderId(),
            ]);

            $result->setOperationType(
                \Bitrix\Sale\PaySystem\ServiceResult::MONEY_LEAVING
            );
            $result->setData(['refund_id' => $refundResult['id']]);
        } catch (\Exception $e) {
            $result->addError(new \Bitrix\Main\Error($e->getMessage()));
        }

        return $result;
    }
}

Partial Refund via YooKassa

// Refund a specific amount from a paid payment
$refund = $yookassaClient->createRefund([
    'payment_id'  => $psInvoiceId,
    'amount'      => [
        'value'    => number_format($refundAmount, 2, '.', ''),
        'currency' => 'RUB',
    ],
    'description' => 'Item return: ' . $itemName,
    'receipt'     => $refundReceipt,  // required with a fiscal cash register
], uniqid('', true));

Refund Receipt (Fiscal Law Compliance)

When an online cash register is connected, a partial refund requires a receipt of type refund. The receipt items include only those goods/services being refunded:

$refundReceipt = [
    'customer' => ['email' => $buyer->getEmail()],
    'items'    => [],
];

foreach ($returnedItems as $item) {
    $refundReceipt['items'][] = [
        'description'     => $item['name'],
        'quantity'        => $item['quantity'],
        'amount'          => [
            'value'    => number_format($item['price'], 2, '.', ''),
            'currency' => 'RUB',
        ],
        'vat_code'        => $item['vat_code'],
        'payment_subject' => 'commodity',
        'payment_mode'    => 'full_refund',  // for a refund receipt
    ];
}

The total of items in the refund receipt must exactly match the amount in the refund request — this is verified by the payment system.

Initiating a Refund from the Admin Panel

In the 1C-Bitrix admin panel, the "Refund" button on the order page (/bitrix/admin/sale_order_detail.php) creates an amount input form. On submission, the method \Bitrix\Sale\PaymentCollection::createRefund() is called:

$order = \Bitrix\Sale\Order::load($orderId);
$paymentCollection = $order->getPaymentCollection();

foreach ($paymentCollection as $payment) {
    if ($payment->isPaid() && $payment->getField('PS_INVOICE_ID') === $externalId) {
        $paymentService = \Bitrix\Sale\PaySystem\Manager::getObjectById(
            $payment->getPaymentSystemId()
        );
        $result = $paymentService->refund($payment, $refundAmount);
        break;
    }
}

Timeline

Task Duration
Implementing the refund method in the handler 1 day
Refund receipt for fiscal law compliance 1 day
UI in the admin panel 0.5 day