Розробка системи подарункових сертифікатів для інтернет-магазину

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

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

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

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

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка системи подарункових сертифікатів для інтернет-магазину
Середня
~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

Розробка системи подарункових сертифікатів для E-Commerce

Подарунковий сертифікат — це передоплачений кредит, прив'язаний до унікального коду. Технічно це гібрид платіжного інструменту та промокоду: у нього є баланс, який списується при покупках, термін дії та можливість часткового використання. Неправильна реалізація веде до дір у обліку, подвійному використанню або витіку балансу.

Типи сертифікатів

По номіналу:

  • Фіксований номінал (500, 1000, 2000 руб.) — проста генерація та облік
  • Довільний номінал — покупатель вводить суму при оформленні

По джерелу:

  • Продані — покупатель заплатив гроші, отримав код
  • Промо / подарункові від магазину — виданяються вручну або автоматично (день народження, повернення лояльності)

По використанню:

  • Одноразові — списується повний номінал при першому застосуванні
  • Багаторазові з залишком — залишок зберігається до вичерпання терміну

Схема даних

CREATE TABLE gift_certificates (
    id              BIGSERIAL PRIMARY KEY,
    code            VARCHAR(50) NOT NULL UNIQUE,
    type            VARCHAR(20) NOT NULL DEFAULT 'sold', -- sold | promo
    initial_amount  NUMERIC(12,2) NOT NULL,
    balance         NUMERIC(12,2) NOT NULL,
    currency        CHAR(3) NOT NULL DEFAULT 'RUB',
    purchased_by    BIGINT REFERENCES users(id),
    recipient_email VARCHAR(255),
    recipient_name  VARCHAR(255),
    personal_message TEXT,
    is_active       BOOLEAN NOT NULL DEFAULT true,
    issued_at       TIMESTAMP NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMP,
    order_id        BIGINT REFERENCES orders(id) -- заказ, яким куплений
);

CREATE TABLE gift_certificate_usages (
    id              BIGSERIAL PRIMARY KEY,
    certificate_id  BIGINT NOT NULL REFERENCES gift_certificates(id),
    order_id        BIGINT NOT NULL REFERENCES orders(id),
    amount_used     NUMERIC(12,2) NOT NULL,
    balance_before  NUMERIC(12,2) NOT NULL,
    balance_after   NUMERIC(12,2) NOT NULL,
    used_at         TIMESTAMP NOT NULL DEFAULT NOW()
);

Таблиця gift_certificate_usages — незмінний журнал. Поточний баланс у gift_certificates.balance — денормалізований кеш, завжди відновлюємий з журналу.

Генерація кодів

Код повинен бути:

  • Унікальним та непередбачуваним (не sequential ID)
  • Зручним для ручного введення (без схожих символів: 0/O, 1/I/l)
  • Коротким, але достатньо ентропійним
class GiftCertificateCodeGenerator
{
    private const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    private const SEGMENT_LENGTH = 4;
    private const SEGMENTS = 4;

    public function generate(): string
    {
        do {
            $code = $this->makeCode();
        } while (GiftCertificate::where('code', $code)->exists());

        return $code;
    }

    private function makeCode(): string
    {
        $segments = [];
        for ($i = 0; $i < self::SEGMENTS; $i++) {
            $segment = '';
            for ($j = 0; $j < self::SEGMENT_LENGTH; $j++) {
                $segment .= self::ALPHABET[random_int(0, strlen(self::ALPHABET) - 1)];
            }
            $segments[] = $segment;
        }
        return implode('-', $segments); // ABCD-EF3H-K7MN-PQRT
    }
}

32 символи алфавіту, 4 сегменти по 4 символи = 32^16 ≈ 10^24 варіантів. Брутфорс неможливий, колізія практично виключена.

Застосування сертифіката при оформленні замовлення

Атомарне списання — ключова вимога:

class GiftCertificateService
{
    public function apply(string $code, Order $order, float $maxAmount): CertificateApplication
    {
        return DB::transaction(function () use ($code, $order, $maxAmount) {
            $cert = GiftCertificate::lockForUpdate()
                ->where('code', $code)
                ->where('is_active', true)
                ->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
                ->firstOrFail();

            if ($cert->balance <= 0) {
                throw new CertificateExhaustedException($code);
            }

            $amountToUse = min($cert->balance, $maxAmount);
            $balanceBefore = $cert->balance;

            $cert->decrement('balance', $amountToUse);

            if ($cert->balance == 0) {
                $cert->update(['is_active' => false]);
            }

            GiftCertificateUsage::create([
                'certificate_id' => $cert->id,
                'order_id'       => $order->id,
                'amount_used'    => $amountToUse,
                'balance_before' => $balanceBefore,
                'balance_after'  => $cert->balance,
            ]);

            return new CertificateApplication($cert, $amountToUse);
        });
    }
}

lockForUpdate() виключає race condition при одночасному застосуванні одного коду в двох вікнах браузера.

Повернення при скасуванні замовлення

При поверненні замовлення, оплаченого сертифікатом, баланс відновлюється:

public function refundTocertificate(GiftCertificateUsage $usage): void
{
    DB::transaction(function () use ($usage) {
        $cert = GiftCertificate::lockForUpdate()->find($usage->certificate_id);

        // Не відновлюємо більше initial_amount
        $refundAmount = min(
            $usage->amount_used,
            $cert->initial_amount - $cert->balance
        );

        $cert->increment('balance', $refundAmount);

        if (!$cert->is_active && $cert->balance > 0) {
            $cert->update(['is_active' => true]);
        }
    });
}

Якщо термін сертифіката вичерпаний — політика повернення на розсуд магазину: продовжити або повернути гроші.

Покупка сертифіката як товару

Сертифікат — особливий тип позиції у замовленні. При створенні замовлення у статусі "оплачено" автоматично генерується сертифікат та надсилається отримувачу:

class IssuePurchasedCertificates
{
    public function handle(OrderPaid $event): void
    {
        foreach ($event->order->items as $item) {
            if ($item->product->type !== 'gift_certificate') {
                continue;
            }

            $cert = GiftCertificate::create([
                'code'              => $this->generator->generate(),
                'initial_amount'    => $item->unit_price,
                'balance'           => $item->unit_price,
                'purchased_by'      => $event->order->user_id,
                'recipient_email'   => $item->meta['recipient_email'] ?? null,
                'recipient_name'    => $item->meta['recipient_name'] ?? null,
                'personal_message'  => $item->meta['message'] ?? null,
                'expires_at'        => now()->addYear(),
                'order_id'          => $event->order->id,
            ]);

            SendGiftCertificate::dispatch($cert);
        }
    }
}

Дизайн листа з сертифікатом

Лист з сертифікатом — це продукт сам по собі. Мінімум:

  • Красивий HTML-шаблон з кодом крупним шрифтом (copy-friendly)
  • Сума номіналу
  • Термін дії
  • QR-код або пряма ссилка для застосування
  • Персональне сообщение від відправника

Сертифікат також повинен бути доступен у PDF для самостійної розпечатки. Генерація PDF через barryvdh/laravel-dompdf або puppeteer.

Баланс у особистому кабінеті

Якщо сертифікат прив'язаний до аккаунту користувача (не тільки до коду):

  • Вкладка "Мої сертифікати" з балансом та історією використання
  • Поле для прив'язки коду до аккаунту
  • При оформленні замовлення — автоматичне пропозиція застосувати доступний баланс

Обмеження застосування

Додаткові бізнес-правила:

  • Сертифікат застосовується тільки до певних категорій товарів
  • Мінімальна сума замовлення для застосування
  • Не можна оплатити сертифікатом покупку іншого сертифіката
  • Тільки один сертифікат на замовлення (або кілька — конфігурюємо)

Сроки реалізації

  • Генерація кодів + застосування + часткове використання: 3–4 дня
  • Покупка сертифіката як товару + автовипуск: 2 дня
  • HTML/PDF лист з дизайном: 1–2 дня
  • Особистий кабінет + історія використання: 2 дня
  • Промо-сертифікати (ручна видача + автоматика): +1–2 дня

Повна система: 1,5–2 тижня.