Реалізація Split-платежів (розщеплення оплати) на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Split-платежів (розщеплення оплати) на сайті
Складна
~3-5 робочих днів
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Split-платежів (розщеплення оплати) на веб-сайті

Split-платіж — це розбивка однієї транзакції покупця на декількох одержувачів одночасно. Типові сценарії: маркетплейс, де частина суми йде продавцю, частина — платформі; booking-сервіс з комісією агрегатора; підписка з revenue share між партнерами. Без правильної архітектури це перетворюється на ручну бухгалтерію з регулярними помилками і спорами.

Реалізація займає від 5 до 14 робочих днів залежно від кількості одержувачів, логіки розподілу і використовуваного платіжного провайдера.

Моделі розщеплення

Два принципово різні підходи, вибір між ними визначає все остальне.

Charge + Transfer (Stripe) — гроші приходять на мастер-аккаунт платформи, потім вручну (через API) переводяться на connected accounts. Платформа несе відповідальність за KYC продавців, комплаенс і можливі повернення.

Direct Charge — клієнт платить прямо продавцю, платформа отримує application fee. Продавець сам проходить KYC. Менше відповідальності, але менше контролю.

Для більшості маркетплейсів на ранній стадії Charge + Transfer простіша — менше юридичної складності при онбордингу продавців.

Stripe: реалізація Charge + Transfer

Stripe Connect — де-факто стандарт для split-платежів. Спочатку створюємо PaymentIntent на повну суму:

$paymentIntent = \Stripe\PaymentIntent::create([
    'amount' => $order->total_cents,
    'currency' => 'eur',
    'payment_method_types' => ['card'],
    'metadata' => [
        'order_id' => $order->id,
        'split_recipients' => json_encode($order->recipients),
    ],
]);

Після успішного платежу — подія payment_intent.succeeded у webhook. В обробнику виконуємо трансферти:

public function handlePaymentSucceeded(array $payload): void
{
    $intent = $payload['data']['object'];
    $recipients = json_decode($intent['metadata']['split_recipients'], true);

    foreach ($recipients as $recipient) {
        \Stripe\Transfer::create([
            'amount' => $recipient['amount_cents'],
            'currency' => $intent['currency'],
            'destination' => $recipient['stripe_account_id'],
            'transfer_group' => $intent['transfer_group'],
            'source_transaction' => $intent['charges']['data'][0]['id'],
        ]);
    }
}

transfer_group пов'язує всі трансферти з вихідним платежом — критично для правильного рефанду. source_transaction гарантує, що трансферт виконується тільки з коштів конкретного платежу, а не з загального балансу.

Зберігання конфігурації розщеплення

Правила split зберігаються в БД, не в коді — інакше кожна зміна комісії вимагає деплоя:

CREATE TABLE split_rules (
    id          bigserial PRIMARY KEY,
    entity_type varchar(50)    NOT NULL, -- 'seller', 'partner', 'platform'
    entity_id   bigint         NOT NULL,
    rule_type   varchar(20)    NOT NULL, -- 'percentage', 'fixed', 'remainder'
    value       numeric(10, 4) NOT NULL,
    priority    int            NOT NULL DEFAULT 0,
    currency    char(3),
    active      boolean        NOT NULL DEFAULT true,
    created_at  timestamptz    NOT NULL DEFAULT now()
);

Розрахунок долей перед створенням трансфертів:

class SplitCalculator
{
    public function calculate(int $totalCents, array $rules): array
    {
        $allocated = 0;
        $result = [];

        // Спочатку фіксовані суми
        foreach ($rules as $rule) {
            if ($rule['rule_type'] === 'fixed') {
                $result[] = ['recipient' => $rule['entity_id'], 'amount' => $rule['value']];
                $allocated += $rule['value'];
            }
        }

        // Потім процентні
        foreach ($rules as $rule) {
            if ($rule['rule_type'] === 'percentage') {
                $amount = (int) round($totalCents * $rule['value'] / 100);
                $result[] = ['recipient' => $rule['entity_id'], 'amount' => $amount];
                $allocated += $amount;
            }
        }

        // Остача платформі або last-in-line одержувачу
        $remainder = $totalCents - $allocated;
        foreach ($rules as $rule) {
            if ($rule['rule_type'] === 'remainder') {
                $result[] = ['recipient' => $rule['entity_id'], 'amount' => $remainder];
                break;
            }
        }

        return $result;
    }
}

remainder правило всесте повинно бути рівно одне — захист від помилок округлення. Сума долей обов'язкова повинна збігатися з total до копійки.

Повернення при split

Повернення при розщепленому платежі — найбільш болісне місце. Stripe автоматично не відзиває трансферти при рефанде на PaymentIntent — це потрібно робити явно:

public function refund(Order $order, int $refundCents): void
{
    // 1. Рефанд основного платежу
    \Stripe\Refund::create([
        'payment_intent' => $order->stripe_payment_intent_id,
        'amount' => $refundCents,
        'refund_application_fee' => true,
    ]);

    // 2. Реверс трансфертів пропорційно
    $ratio = $refundCents / $order->total_cents;
    foreach ($order->transfers as $transfer) {
        $reverseAmount = (int) round($transfer->amount_cents * $ratio);
        \Stripe\Transfer::createReversal($transfer->stripe_transfer_id, [
            'amount' => $reverseAmount,
            'refund_application_fee' => true,
        ]);
    }
}

Якщо на аккаунті одержувача недостатньо коштів для реверсу (наприклад, він уже вивів гроші), Stripe повертає помилку. Потрібна логіка дебетування — окремий сценарій з manual intervention.

Альтернативи Stripe

CloudPayments (для СНГ) підтримує split через механізм Receipt з кількома одержувачами, але API менш гнучкий. YooKassa має вбудований split для маркетплейсів через Deal API — створюється deal, до нього прив'язуються виплати. Fondy та LiqPay пропонують split через партнерські договори, конфігурація на стороні провайдера, не через API.

Моніторинг і погодження

Кожен день запускається reconciliation job — порівняння сум трансфертів в БД з реальними трансфертами у Stripe через API:

$stripeTransfers = \Stripe\Transfer::all([
    'created' => ['gte' => $yesterday->timestamp, 'lt' => $today->timestamp],
    'limit' => 100,
]);

$dbTransfers = Transfer::whereDate('created_at', $yesterday)->get()->keyBy('stripe_id');

foreach ($stripeTransfers->autoPagingIterator() as $transfer) {
    if (!isset($dbTransfers[$transfer->id])) {
        Log::critical('Untracked transfer', ['stripe_id' => $transfer->id, 'amount' => $transfer->amount]);
    }
}

Розбіжності йдуть в алерт. Це не паранойя — webhook-и іноді губляться, особливо при деплоях у момент транзакції.

Податкові та юридичні аспекти

Розщеплення платежу не звільняє платформу від фіскальних обов'язків — у більшості юрисдикцій платформа є податковим агентом. У Росії це означає передачу даних про виплати в ФНС, в ЄС — DAC7 reporting. Це потрібно враховувати при проектуванні схеми split ще на старті — переробка пізніше коштує дорожче.