Setting up a full refund 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
    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 Full Refunds on 1C-Bitrix

A full refund is the cancellation of an entire payment and the return of the full order amount to the buyer. Technically, this is simpler than a partial refund, but it contains several nuances: the time window for cancellation, the difference between cancelling a pre-authorization and refunding charged funds, and the mandatory refund receipt when a cash register is connected.

Cancellation vs. Refund: What Is the Difference

Cancellation (void/cancel) — only possible before the bank has settled the transaction (usually before the end of the business day). Funds are returned instantly because they were not actually charged. In two-stage payments, this is the cancellation of the pre-authorization.

Refund (refund) — after a settled transaction. The funds are first charged, then a reverse transfer is initiated. The time for the amount to be credited to the buyer is 3–10 business days, depending on the bank.

Implementation 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');

        // Full refund = the total payment amount
        $fullAmount = $payment->getSum();

        // Validate: refund amount must not exceed the payment amount
        if ($refundableSum > $fullAmount) {
            $result->addError(new \Bitrix\Main\Error(
                'Refund amount (' . $refundableSum . ') exceeds payment amount (' . $fullAmount . ')'
            ));
            return $result;
        }

        try {
            // Try cancellation first (faster for the buyer)
            $cancelled = $this->tryCancel($externalId);

            if (!$cancelled) {
                // If cancellation is not possible — full refund via refund API
                $this->gateway->createRefund([
                    'payment_id' => $externalId,
                    'amount'     => [
                        'value'    => number_format($refundableSum, 2, '.', ''),
                        'currency' => $payment->getField('CURRENCY'),
                    ],
                ]);
            }

            // Update payment status in 1C-Bitrix
            $payment->setPaid('N');
            $payment->setField('PS_STATUS', 'REFUNDED');
            $payment->save();

            $result->setOperationType(
                \Bitrix\Sale\PaySystem\ServiceResult::MONEY_LEAVING
            );
        } catch (\Exception $e) {
            $result->addError(new \Bitrix\Main\Error($e->getMessage()));
        }

        return $result;
    }

    private function tryCancel(string $externalId): bool
    {
        try {
            $this->gateway->cancelPayment($externalId);
            return true;
        } catch (\Exception $e) {
            // If cancellation is not possible (transaction already settled) — return false
            return false;
        }
    }
}

Full Refund via YooKassa

// Simple full refund
$payment = $yookassaClient->getPaymentInfo($psInvoiceId);
$fullAmount = $payment->getAmount()->getValue();

$refund = $yookassaClient->createRefund([
    'payment_id' => $psInvoiceId,
    'amount'     => ['value' => $fullAmount, 'currency' => 'RUB'],
    'receipt'    => $fullRefundReceipt,  // required with a fiscal cash register
], uniqid('', true));

Refund Receipt (Fiscal Law Compliance)

For a full refund, the receipt must contain all items from the original payment with the payment_mode: full_refund attribute:

// Get items from the original order
$order  = \Bitrix\Sale\Order::load($orderId);
$basket = $order->getBasket();

$refundItems = [];
foreach ($basket->getOrderableItems() as $item) {
    $refundItems[] = [
        'description'     => $item->getField('NAME'),
        'quantity'        => $item->getQuantity(),
        'amount'          => ['value' => number_format($item->getPrice(), 2, '.', ''), 'currency' => 'RUB'],
        'vat_code'        => getVatCode($item),
        'payment_subject' => 'commodity',
        'payment_mode'    => 'full_refund',
    ];
}

// Add delivery
$delivery = $order->getDeliveryPrice();
if ($delivery > 0) {
    $refundItems[] = [
        'description'     => 'Delivery',
        'quantity'        => 1,
        'amount'          => ['value' => number_format($delivery, 2, '.', ''), 'currency' => 'RUB'],
        'vat_code'        => 1,
        'payment_subject' => 'service',
        'payment_mode'    => 'full_refund',
    ];
}

Order Status After Refund

After a successful refund, the order status must be updated:

// Change order status to "Cancelled" or "Refunded"
$order->setField('STATUS_ID', 'VD');  // "Refund" status code — configurable
$order->save();

// Return items to stock if required
if ($returnToStock) {
    $shipmentCollection = $order->getShipmentCollection();
    foreach ($shipmentCollection as $shipment) {
        if (!$shipment->isSystem()) {
            $shipment->setField('CANCELED', 'Y');
        }
    }
    $order->save();
}

Timeline

Task Duration
refund method in handler + status update 1 day
Refund receipt for fiscal law compliance 1 day
Testing cancellation and refund scenarios 0.5 day