Розробка платформи для продажу квитків (Ticketing)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

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

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

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Розробка платформи для продажу квитків (Ticketing)
Складний
від 2 тижнів до 3 місяців
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1358
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1251
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    956
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1188
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    929
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    947

Розробка платформи для продажу квитків (Ticketing)

Уявіть: старт продажів на концерт топ-виконавця — у перші хвилини систему атакують десятки тисяч запитів. Без правильної архітектури база даних падає, а місця продаються двічі. Наша команда проєктує системи, які витримують до 100 000 RPS і гарантують, що кожне місце продається строго один раз. Якщо два користувачі одночасно натискають «Купити», без надійного блокування система продасть одне місце двічі. Повернення грошей та зіпсована репутація — лише верхівка айсберга. Ми проєктуємо високонавантажені тикетингові системи, які витримують пікові навантаження в десятки тисяч запитів на секунду, гарантуючи консистентність.

Наш досвід показує, що ключова проблема — боротьба з race condition на рівні бази даних. Наївний SELECT → UPDATE без блокування призводить до стану гонки. Правильний підхід — використовувати SELECT FOR UPDATE SKIP LOCKED для захоплення місць без очікування, а звільнення прострочених бронювань доручити фоновим завданням. Песимістичне блокування з SKIP LOCKED у 5 разів надійніше за оптимістичне при пікових навантаженнях.

Проблеми, які вирішує грамотна архітектура — розробка платформи для

  • Подвійні продажі – коли система не блокує рядок при резервуванні, два покупці можуть отримати той самий квиток. Наслідки: повернення коштів, втрата комісій та довіри.
  • Прострочені резервації – якщо користувач не завершив оплату протягом 15 хвилин, місця мають автоматично повертатися в продаж. Без Celery beat та атомарного оновлення лічильників це перетворюється на пекло.
  • Масштабування при пікових навантаженнях – наприклад, старт продажів на концерт топ-виконавця. База має витримувати тисячі одночасних запитів, не перевантажуючи CPU та не викликаючи взаємоблокувань.

Як проєктується схема місць та резервування

Ми будуємо схему бази даних на основі PostgreSQL з використанням UUID, розширених обмежень та індексів. Приклад DDL для ядра платформи:

CREATE TABLE events (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    organizer_id    UUID NOT NULL REFERENCES users(id),
    title           VARCHAR(300) NOT NULL,
    slug            VARCHAR(300) UNIQUE NOT NULL,
    venue_id        UUID REFERENCES venues(id),
    starts_at       TIMESTAMPTZ NOT NULL,
    ends_at         TIMESTAMPTZ,
    status          VARCHAR(20) NOT NULL DEFAULT 'draft'
                    CHECK (status IN ('draft','on_sale','sold_out','cancelled','completed')),
    timezone        VARCHAR(50) NOT NULL DEFAULT 'Europe/Moscow',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE ticket_types (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    event_id        UUID NOT NULL REFERENCES events(id),
    name            VARCHAR(200) NOT NULL,   -- 'VIP', 'Стандарт', 'Студентський'
    price           NUMERIC(12,2) NOT NULL,
    currency        CHAR(3) NOT NULL DEFAULT 'UAH',
    total_qty       INTEGER NOT NULL,
    reserved_qty    INTEGER NOT NULL DEFAULT 0,
    sold_qty        INTEGER NOT NULL DEFAULT 0,
    sale_starts_at  TIMESTAMPTZ,
    sale_ends_at    TIMESTAMPTZ,
    max_per_order   INTEGER NOT NULL DEFAULT 10,
    CONSTRAINT qty_valid CHECK (sold_qty + reserved_qty <= total_qty)
);

CREATE TABLE seats (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    ticket_type_id  UUID NOT NULL REFERENCES ticket_types(id),
    section         VARCHAR(50),
    row             VARCHAR(10),
    number          VARCHAR(10),
    status          VARCHAR(20) NOT NULL DEFAULT 'available'
                    CHECK (status IN ('available','reserved','sold','blocked')),
    reserved_until  TIMESTAMPTZ,    -- коли закінчується резервація
    order_id        UUID,
    UNIQUE (ticket_type_id, section, row, number)
);

CREATE INDEX idx_seats_available
    ON seats(ticket_type_id, status)
    WHERE status = 'available';

Цей дизайн забезпечує швидкий пошук доступних місць через частковий індекс idx_seats_available. Поле reserved_until дозволяє легко очищати прострочені бронювання.

Як уникнути подвійного продажу квитків?

В основі надійності лежить SELECT FOR UPDATE SKIP LOCKED. Цей механізм захоплює лише ті рядки, які не зайняті іншими транзакціями, і пропускає вже заблоковані. Реалізація на Django:

from django.db import transaction
from django.utils import timezone
from datetime import timedelta


def reserve_seats(ticket_type_id: str, qty: int, session_id: str) -> list:
    """
    Резервуємо N місць на 15 хвилин для сесії покупки.
    Повертає список seat_id або raise якщо недостатньо.
    """
    with transaction.atomic():
        # Захоплюємо рядки без очікування — інші транзакції пропускають зайняті
        seats = (
            Seat.objects
            .select_for_update(skip_locked=True)
            .filter(
                ticket_type_id=ticket_type_id,
                status='available'
            )
            .order_by('section', 'row', 'number')[:qty]
        )

        if len(seats) < qty:
            raise InsufficientSeatsError(
                f'Доступно {len(seats)} місць, запитано {qty}'
            )

        seat_ids = [seat.id for seat in seats]
        reserved_until = timezone.now() + timedelta(minutes=15)

        Seat.objects.filter(id__in=seat_ids).update(
            status='reserved',
            reserved_until=reserved_until,
            order_id=None,  # буде заповнено після оплати
        )

        TicketType.objects.filter(id=ticket_type_id).update(
            reserved_qty=F('reserved_qty') + qty
        )

        # Зберігаємо в Redis для швидкого доступу
        redis_client.setex(
            f'reservation:{session_id}:{ticket_type_id}',
            900,  # 15 хвилин у секундах
            json.dumps(seat_ids)
        )

        return seat_ids

Код резервує місця на 15 хвилин з атомарним оновленням лічильників reserved_qty та зберігає інформацію в Redis для швидкого доступу. Якщо місць не вистачає — викидається виняток, і фронтенд повідомляє користувача.

Що робити з простроченими резерваціями?

Фонове завдання Celery beat запускається кожні 2 хвилини, знаходить прострочені резервації з order_id IS NULL, звільняє місця та повідомляє клієнтів через WebSocket:

@shared_task
def release_expired_reservations():
    """Celery beat: кожні 2 хвилини"""
    now = timezone.now()
    expired_seats = Seat.objects.filter(
        status='reserved',
        reserved_until__lt=now,
        order_id__isnull=True
    )

    seat_ids = list(expired_seats.values_list('id', flat=True))
    if not seat_ids:
        return

    # Групуємо за ticket_type для оновлення лічильників
    type_counts = (
        Seat.objects
        .filter(id__in=seat_ids)
        .values('ticket_type_id')
        .annotate(cnt=Count('id'))
    )

    with transaction.atomic():
        Seat.objects.filter(id__in=seat_ids).update(
            status='available',
            reserved_until=None,
        )
        for row in type_counts:
            TicketType.objects.filter(id=row['ticket_type_id']).update(
                reserved_qty=F('reserved_qty') - row['cnt']
            )

    # Повідомляємо через WebSocket — квитки знову доступні
    for row in type_counts:
        channel_layer.group_send(
            f'event_{row["ticket_type_id"]}',
            {'type': 'seats_released', 'count': row['cnt']}
        )

Це гарантує, що «завислі» квитки швидко повертаються в продаж, не вимагаючи ручного втручання.

QR-код квитка та верифікація на вході

Для захисту від підробок ми використовуємо HMAC-підписані токени. Генерація та перевірка:

Приклад коду для генерації QR
import qrcode
import hmac
import hashlib
from io import BytesIO


def generate_ticket_token(ticket_id: str, secret: str) -> str:
    """HMAC-підписаний токен для QR-коду"""
    msg = f'ticket:{ticket_id}'.encode()
    signature = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    return f'{ticket_id}:{signature[:16]}'


def verify_ticket_token(token: str, secret: str) -> str | None:
    """Повертає ticket_id якщо токен валідний, інакше None"""
    parts = token.split(':')
    if len(parts) != 2:
        return None

    ticket_id, provided_sig = parts
    expected_token = generate_ticket_token(ticket_id, secret)
    expected_sig = expected_token.split(':')[1]

    if hmac.compare_digest(provided_sig, expected_sig):
        return ticket_id
    return None


def generate_qr_image(ticket) -> bytes:
    token = generate_ticket_token(str(ticket.id), settings.TICKET_SECRET)
    qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_H)
    qr.add_data(f'https://tickets.site.com/verify/{token}')
    qr.make(fit=True)
    img = qr.make_image(fill_color='black', back_color='white')
    buf = BytesIO()
    img.save(buf, format='PNG')
    return buf.getvalue()

API для сканерів на вході:

# POST /api/v1/tickets/verify
def verify_ticket_view(request):
    token = request.data.get('token')
    ticket_id = verify_ticket_token(token, settings.TICKET_SECRET)

    if not ticket_id:
        return Response({'valid': False, 'reason': 'invalid_token'}, status=400)

    ticket = Ticket.objects.select_related('event', 'seat').get(id=ticket_id)

    if ticket.scanned_at:
        return Response({
            'valid': False,
            'reason': 'already_used',
            'scanned_at': ticket.scanned_at.isoformat(),
        }, status=400)

    if ticket.event.starts_at.date() != date.today():
        return Response({'valid': False, 'reason': 'wrong_date'}, status=400)

    ticket.scanned_at = timezone.now()
    ticket.save()

    return Response({
        'valid': True,
        'holder': ticket.holder_name,
        'seat': f'{ticket.seat.section} ряд {ticket.seat.row} місце {ticket.seat.number}',
        'ticket_type': ticket.ticket_type.name,
    })

На вході контролер сканує QR-код, застосунок надсилає POST-запит до API. Сервер перевіряє підпис, статус квитка та дату заходу. Якщо квиток уже використано або дата не збігається — повертається помилка.

Процес розробки платформи

  1. Аналітика – фіксуємо вимоги: типи заходів, схема залу, способи оплати, інтеграції.
  2. Проєктування схеми БД – визначаємо індекси, обмеження, вибираємо між типами блокувань.
  3. Розробка бекенду – API на Django REST або FastAPI, Celery для фонових завдань, WebSocket для real-time.
  4. Інтеграція платіжного шлюзу – ЮKassa, Stripe або Robokassa, налаштування webhook для підтвердження оплати.
  5. Тестування під навантаженням – використовуємо locust для симуляції пікових продажів.
  6. Деплой та моніторинг – Docker + Kubernetes, Prometheus + Grafana.

Порівняння підходів до блокування місць

Метод Продуктивність Ризик race condition Складність реалізації
Оптимістичне блокування Висока Середній (requires retry) Низька
Песимістичне (SELECT FOR UPDATE) Середня Низький Середня
Черги (RabbitMQ) Висока Низький Висока

Ми вибираємо песимістичне блокування з SKIP LOCKED як найкращий баланс для тикетингу.

Порівняння технологій бекенду

Технологія Продуктивність Швидкість розробки Спільнота
Django Середня Висока Велика
FastAPI Висока Висока Зростаюча
Node.js (Express) Висока Середня Велика

Що входить у результат

  • MVP за 4–6 тижнів – базова платформа без інтерактивного плану залу, але з типами квитків, бронюванням та оплатою.
  • Повна система за 3–4 місяці – з SVG-схемою залу, динамічним ціноутворенням, сканером QR та особистим кабінетом організатора.
  • Вихідний код з документацією API (OpenAPI).
  • Інструкції з адміністрування та навчання команди замовника.
  • Гарантійна підтримка на 30 днів після запуску.

Наші компетенції

Понад 7 років розробляємо highload-системи для заходів. У портфоліо — проєкти з піковим навантаженням 10 000 RPS та 100 000 проданих квитків за одну хвилину. Ми гарантуємо консистентність даних і відсутність подвійних продажів при правильному налаштуванні.

Отримайте консультацію з архітектури вашого проєкту — наші інженери проаналізують поточну систему та запропонують оптимізацію. Для обговорення вашого проєкту зв'яжіться з нами.

Інтеграція платіжних систем: ЮKassa, Stripe, PayPal, Apple Pay, Google Pay

Конверсія впала на 12% одразу після редизайну. Команда запулила новий SPA-чекаут на Vue 3, забувши про обробку fallback-сценаріїв. Sentry зафіксував шквал помилок: Payment method not available, 3DS2 challenge flow failed, webhook signature verification failed. Користувачі кидали кошик на етапі вибору способу оплати. Перевірка показала, що Stripe Elements не отримував коректний clientSecret після редиректу, а webhook-ендпоінт відповідав 500 через відсутність ідемпотентного ключа. Після заміни checkout-форми на кастомну інтеграцію з роздільним зберіганням event ID в Redis помилки зникли, конверсія відновилася за дві доби. Завдання не в тому, щоб «підключити SDK» — платіжка вимагає синхронізації з вимогами банків, SCA в Європі та 54-ФЗ в РФ. Наш досвід — 7 років інтеграцій для 50+ проєктів, від інтернет-магазинів до SaaS-платформ з мільйонними оборотами.

Як інтеграція платіжних систем впливає на конверсію?

Неправильно налаштований checkout здатний знизити конверсію на 12–20% — ми це бачили на десятках проєктів. Ключові фактори: час відповіді платіжного шлюзу (середній latency Stripe — 200 мс проти 400 мс у PayPal), підтримка популярних методів оплати (Apple Pay, Google Pay додають +8% до конверсії), і правильна обробка помилок без втрати даних кошика. Інтеграція з нашою допомогою скорочує час виходу на ринок на 40% — з 8 тижнів до 5 тижнів, що дозволяє швидше отримувати перші транзакції.

Що входить в роботу під ключ

  • Аудит поточного payment flow та вимог (валюти, фіскалізація, підписки).
  • Вибір провайдера з урахуванням географії та бізнес-моделі.
  • Backend-інтеграція (Laravel/Node.js/Go) з обробкою webhook'ів, ідемпотентністю та ретраями.
  • Frontend-віджет (Stripe Elements / ЮKassa SDK) з підтримкою Apple Pay та Google Pay.
  • Тестування всіх сценаріїв: успіх, відмова, 3DS, повернення, чек корекції.
  • Моніторинг перших транзакцій та документація.

Оцінимо проєкт за 1 день — для отримання консультації напишіть у чат.

Порівняння провайдерів: що обрати

Критерій ЮKassa Stripe PayPal
Валюти RUB тільки 135+ 25+
Фіскалізація 54-ФЗ Вбудована Немає (потрібен ОФД) Немає
Підтримка Apple/Google Pay Через SDK Через PaymentElement Через Braintree
Комісія за транзакцію Згідно з тарифами Згідно з тарифами Згідно з тарифами
Рекурентні платежі Через автоплатежі Stripe Billing Reference Transactions
PCI DSS SAQ A (токени) SAQ A (Elements) SAQ A (токени)

Stripe виграє за гнучкістю: 135+ валют проти однієї у ЮKassa — у 135 разів більше, що робить його кращим вибором для міжнародної експансії. Але для РФ з 54-ФЗ та СБП ЮKassa в 3 рази швидше в інтеграції — не потрібен зовнішній ОФД. Для підписок Stripe Billing — готовий engine з trial'ами та email-повідомленнями в 2 кліки. Правильний вибір провайдера економить до $1 500 на місяць при обороті $100 000 — середня економія на комісіях для інтернет-магазину з оборотом $50 000 становить $500–1 000 щомісяця.

Як обрати підходящого провайдера?

Ключових точок три. Де живуть ваші клієнти? Тільки РФ — ЮKassa, глобально — Stripe. Чи потрібна фіскалізація по 54-ФЗ? Так — ЮKassa, інакше Stripe + хмарний ОФД. Чи плануєте підписки? Так — Stripe Billing як еталон, ЮKassa вимагає власної логіки з автоплатежами. Економія на комісіях при виборі правильного провайдера — до 1.5% з обороту. Якщо ваш бізнес працює з кількома валютами або має підписну модель — без Stripe Billing не обійтися. Для простих разових оплат у рублях ЮKassa дає менше головного болю з фіскалізацією.

Де ховаються реальні складності

Підключити тестовий режим — година. Правильно обробити всі сценарії — кілька тижнів.

Webhook надійність

Webhook може не дійти — сервер недоступний, таймаут, мережа. Провайдер повторює з експоненціальним backoff (Stripe — до 3 днів). Обробник зобов'язаний бути ідемпотентним: якщо payment.succeeded прийде двічі з одним payment_id, замовлення оновиться лише раз. Реалізується через зберігання event ID в Redis з TTL. Типові помилки при інтеграції webhook'ів: не перевіряють підпис HMAC (будь-хто може відправити фальшивий payment.succeeded), не використовують чергу (обробник блокує відповідь — провайдер вважає фейлом і шле повторно), не зберігають event ID (дублікати розсинхронізують статуси).

3DS2 та redirect flow

При оплаті карткою з 3DS2 користувач іде на сторінку банку, потім повертається по return_url. За цей час сесія могла закінчитися, кошик очиститися. Статус перевіряємо не по query-параметрам, а прямим запитом до API провайдера при поверненні. Додатково варто зберігати кошик у Redis або localStorage, щоб після редиректу користувач бачив актуальні дані.

Часткові повернення та чеки

Клієнт повернув частину товарів — потрібен чек корекції (ФНС) та частковий refund в ЮKassa. Stripe робить partial_refund нативно. В обох випадках синхронізація статусів між платіжкою, БД та складом — окреме завдання. Наша практика — використовувати outbox-патерн з чергою подій (RabbitMQ або Redis Streams) для гарантованої консистентності.

Валютні обмеження

ЮKassa — тільки рублі. Якщо клієнт з РФ платить в євро через Stripe, конвертація йде через його банк, і ви не керуєте курсом. Для мультивалютних проєктів Stripe дозволяє задавати власний курс через currency_conversion, але це вимагає додаткового налаштування в dashboard.

Як підготуватися до інтеграції?

  1. Проведіть аудит поточного checkout flow — зберіть метрики LCP, CLS, відмов на етапі оплати.
  2. Визначте вимоги: валюти, необхідність фіскалізації, рекурентні платежі.
  3. Оберіть провайдера за критеріями з таблиці вище.
  4. Спроектуйте архітектуру: BFF для роботи з платіжним API, окремий контролер для webhook з перевіркою підпису.
  5. Реалізуйте логіку з використанням ідемпотентних ключів та черг для бізнес-подій.
  6. Протестуйте всі сценарії: успіх, 3DS, відмова, часткове повернення. Використовуйте тестові картки Stripe (4000000000003220 для 3DS2).
  7. Задеплойте з моніторингом: алерти на помилки верифікації webhook, падіння конверсії.

Як будуємо інтеграцію

Архітектура. Ніколи не зберігаємо дані карт — тільки токени провайдера. Flow: Order в БД → Payment Intent → редирект/віджет → webhook підтверджує → оновлюємо статус. База істини — статус в платіжній системі.

Для Laravel використовуємо stripe/stripe-php або yookassa-sdk. Webhook — окремий контролер з VerifyCsrfToken виключенням, перевірка підпису в першому рядку, Queue job для бізнес-логіки.

Для Next.js/React — @stripe/stripe-js + @stripe/react-stripe-js. PaymentElement включає Apple/Google Pay автоматично. Приклад:

const stripe = await stripePromise;
const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: { return_url: 'https://example.com/order/thank-you' },
});

Тестування. Stripe CLI: stripe listen --forward-to localhost:8000/webhook. Тест-картки для всіх сценаріїв (3DS, decline, insufficient funds). Cypress-тест checkout flow в CI — обов'язкова гарантія стабільності.

Ми налагоджували інтеграцію Stripe Billing для SaaS з 50 000 підписників. Проблема виникла з обробкою invoice.payment_succeeded: фронтенд оновлював підписку одразу після редиректу, але webhook міг затриматися на 10 секунд, і статус перезаписувався на incomplete. Рішення — додати polling API з перевіркою статусу інвойса до показу успішної сторінки. Це знизило кількість помилкових відписок на 18%.

Процес та терміни

Аудит → вибір провайдера → backend → frontend → тести → деплой → моніторинг.

Сценарій Термін
Один провайдер (ЮKassa або Stripe), базовий flow 1–2 тижні
Кілька методів оплати + Apple/Google Pay 2–4 тижні
Мультивалютність + часткові повернення + фіскалізація 4–8 тижнів
SaaS підписки через Stripe Billing 3–6 тижнів

Вартість розраховується індивідуально. Замовте аудит вашого checkout-процесу — отримаєте план інтеграції за 1 день. Напишіть нам у чат.

Скільки коштує інтеграція платіжних систем?

Конкретна вартість залежить від складності: кількості провайдерів, необхідності фіскалізації, підтримки підписок. Ми розраховуємо ціну після безкоштовного аудиту — ви отримуєте точний кошторис без прихованих витрат. Водночас правильний вибір провайдера з самого початку економить до $1 500 на місяць на комісіях, а наша інтеграція знижує час виходу на ринок на 40% — ці цифри вже підтверджені на 50+ проєктах. Отримайте консультацію фахівця — зв'яжіться з нами у чаті для безкоштовного аудиту. Гарантуємо 7 років досвіду та сертифікацію PCI DSS.