Розробка кешбек-платформи
Кешбек-платформа з'єднує покупців, магазини-партнери та платіжну обробку. Покупець робить покупку через партнерське посилання або карту, платформа отримує комісію від магазину та повертає частину покупцю. Технічно це система відстеження кліків/трансакцій, розрахунку винагород та управління виплатами.
Моделі відстеження
Існують два принципово різні механізми відстеження покупок:
Affiliate-трекінг — користувач переходить за спеціальним посиланням, робить покупку, магазин сповіщає платформу через postback або пікселі.
Card-linked — привʼязка платіжної карти, трансакції відстежуються через банківські API (Visa/Mastercard CLO, СБП). Потребує партнерства з банком.
Більшість платформ починає з affiliate.
Схема даних
CREATE TABLE partners (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE NOT NULL,
website VARCHAR(500) NOT NULL,
logo_url VARCHAR(500),
cashback_rate NUMERIC(5,2) NOT NULL, -- % від суми покупки
platform_rate NUMERIC(5,2) NOT NULL, -- повна комісія від партнера
status VARCHAR(20) NOT NULL DEFAULT 'active'
CHECK (status IN ('active','paused','terminated')),
tracking_url VARCHAR(500), -- шаблон посилання з {click_id}
network VARCHAR(50), -- admitad, cityads, власна
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE clicks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
partner_id UUID NOT NULL REFERENCES partners(id),
click_id VARCHAR(100) UNIQUE NOT NULL, -- передається в партнерську мережу
ip INET,
user_agent TEXT,
referrer VARCHAR(500),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
click_id UUID REFERENCES clicks(id),
user_id UUID REFERENCES users(id),
partner_id UUID NOT NULL REFERENCES partners(id),
order_id VARCHAR(200), -- ID замовлення в магазині
purchase_amount NUMERIC(15,2),
commission NUMERIC(15,2), -- отримано від партнера
cashback_amount NUMERIC(15,2), -- начислено користувачу
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','confirmed','cancelled','paid')),
hold_until DATE, -- дата, коли можна виплатити
source VARCHAR(50), -- 'postback','pixel','api'
raw_data JSONB, -- сирі дані від партнера
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE cashback_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID UNIQUE NOT NULL REFERENCES users(id),
balance NUMERIC(15,2) NOT NULL DEFAULT 0, -- доступно до виводу
pending NUMERIC(15,2) NOT NULL DEFAULT 0, -- на утримання
total_earned NUMERIC(15,2) NOT NULL DEFAULT 0,
total_withdrawn NUMERIC(15,2) NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE withdrawals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
amount NUMERIC(15,2) NOT NULL,
method VARCHAR(30) NOT NULL CHECK (method IN ('card','sbp','wallet','phone')),
destination VARCHAR(200) NOT NULL, -- номер карти/телефону
status VARCHAR(20) NOT NULL DEFAULT 'pending',
processed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Трекінгові посилання
import hashlib
import base64
from django.conf import settings
def generate_click_id(user_id: str, partner_id: str) -> str:
"""Унікальний click_id для відстеження"""
raw = f'{user_id}:{partner_id}:{timezone.now().timestamp()}'
return base64.urlsafe_b64encode(
hashlib.sha256(raw.encode()).digest()[:12]
).decode().rstrip('=')
def build_tracking_url(user, partner) -> str:
click_id = generate_click_id(str(user.id), str(partner.id))
# Зберегти клік
Click.objects.create(
user=user,
partner=partner,
click_id=click_id,
)
# Побудувати посилання з click_id
tracking_url = partner.tracking_url.replace('{click_id}', click_id)
# Приклад: https://ad.admitad.com/g/abc123/?subid={click_id}
# → https://ad.admitad.com/g/abc123/?subid=xK9mNpQr
return tracking_url
Endpoint для редиректу:
def click_redirect(request, partner_slug):
partner = get_object_or_404(Partner, slug=partner_slug, status='active')
user = request.user
if not user.is_authenticated:
# Зберегти намір, редирект на логін
request.session['pending_cashback_partner'] = partner_slug
return redirect('/login/?next=' + request.path)
url = build_tracking_url(user, partner)
# Аналітика
track_event.delay('cashback_click', {
'user_id': str(user.id),
'partner_id': str(partner.id),
})
return HttpResponseRedirect(url)
Postback від партнерських мереж
Admitad, CityAds та більшість CPA-мереж сповіщають через GET-запит (postback):
# GET /postback/admitad/?click_id={click_id}&order_id={order_id}&
# sale_amount={sale_amount}&commission={commission}&status={status}
def admitad_postback(request):
# Перевірити підпис (кожна мережа має свій алгоритм)
provided_sig = request.GET.get('sig')
click_id = request.GET.get('click_id')
expected_sig = hmac.new(
settings.ADMITAD_SECRET.encode(),
click_id.encode(),
hashlib.md5
).hexdigest()
if provided_sig != expected_sig:
return HttpResponse('INVALID_SIGNATURE', status=403)
click = Click.objects.filter(click_id=click_id).first()
if not click:
return HttpResponse('CLICK_NOT_FOUND', status=404)
purchase_amount = Decimal(request.GET.get('sale_amount', '0'))
commission = Decimal(request.GET.get('commission', '0'))
cashback_amount = commission * (click.partner.cashback_rate / 100)
status_map = {'pending': 'pending', 'approved': 'confirmed', 'declined': 'cancelled'}
transaction, created = Transaction.objects.get_or_create(
order_id=request.GET.get('order_id'),
partner=click.partner,
defaults={
'click': click,
'user': click.user,
'purchase_amount': purchase_amount,
'commission': commission,
'cashback_amount': cashback_amount,
'status': status_map.get(request.GET.get('status'), 'pending'),
'hold_until': date.today() + timedelta(days=click.partner.hold_days),
'source': 'postback',
'raw_data': dict(request.GET),
}
)
if not created:
# Оновити статус (approved → cancelled)
transaction.status = status_map.get(request.GET.get('status'), transaction.status)
transaction.save()
if transaction.status == 'confirmed':
credit_cashback.delay(str(transaction.id))
return HttpResponse('OK')
Начислення та виплата кешбеку
@shared_task
def credit_cashback(transaction_id: str):
"""Начислити кешбек після підтвердження трансакції"""
with transaction_lock(transaction_id):
txn = Transaction.objects.select_for_update().get(id=transaction_id)
if txn.status != 'confirmed':
return
account, _ = CashbackAccount.objects.select_for_update().get_or_create(
user=txn.user
)
account.pending += txn.cashback_amount
account.total_earned += txn.cashback_amount
account.save()
txn.status = 'credited'
txn.save()
notify_cashback_credited.delay(str(txn.user_id), float(txn.cashback_amount))
@shared_task
def release_held_cashback():
"""Щоденно: переводити підтверджений кешбек з pending у balance"""
today = date.today()
ready = Transaction.objects.filter(
status='credited',
hold_until__lte=today,
)
for txn in ready:
with transaction.atomic():
account = CashbackAccount.objects.select_for_update().get(user=txn.user)
account.pending -= txn.cashback_amount
account.balance += txn.cashback_amount
account.save()
txn.status = 'available'
txn.save()
Виплата через СБП
def request_withdrawal_sbp(user, amount: Decimal, phone: str):
account = CashbackAccount.objects.select_for_update().get(user=user)
if account.balance < amount:
raise InsufficientBalanceError()
if amount < Decimal('100'):
raise ValidationError('Мінімальна сума виводу 100 ₽')
account.balance -= amount
account.total_withdrawn += amount
account.save()
withdrawal = Withdrawal.objects.create(
user=user,
amount=amount,
method='sbp',
destination=phone,
status='pending',
)
# Відправити на платіжний шлюз (ЮKassa, Tinkoff, etc.)
process_sbp_payout.delay(str(withdrawal.id))
return withdrawal
Строк
MVP з affiliate-трекінгом, postback Admitad, особистим кабінетом та виплатою на карту: 6–8 тижнів. Повна платформа з кількома мережами, card-linked офертами, реферальною програмою та аналітичним дашбордом для партнерів: 4–5 місяців.







