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

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, 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

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

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

Типы сертификатов

По номиналу:

  • Фиксированный номинал (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]);
        }
    });
}

Если срок сертификата истёк — политика возврата на усмотрение магазина: можно продлить или вернуть деньгами.

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

Сертификат — особый тип позиции в заказе. При создании заказа в статусе «оплачен» автоматически генерируется сертификат и отправляется получателю:

// Слушатель события OrderPaid
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 недели.