Gift Certificates System for E-Commerce

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Gift Certificates System for E-Commerce
Medium
~3-5 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Development of Gift Certificate System for E-Commerce

Gift certificate is prepaid credit tied to unique code. Technically hybrid of payment instrument and promo code: has balance debited on purchases, expiration date, and partial use ability. Improper implementation leads to accounting holes, double use, or balance leakage.

Certificate Types

By Nominal:

  • Fixed nominal (500, 1000, 2000 rubles) — simple generation and accounting
  • Custom nominal — buyer enters amount at checkout

By Source:

  • Sold — customer paid, got code
  • Promo/gift from store — issued manually or automatically (birthday, loyalty return)

By Use:

  • Single-use — full nominal debited on first use
  • Multi-use with balance — balance stored until expiration

Data Schema

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) -- order that bought it
);

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()
);

Table gift_certificate_usages — immutable log. Current balance in gift_certificates.balance — denormalized cache, always recoverable from log.

Code Generation

Code must be:

  • Unique and unpredictable (not sequential ID)
  • Convenient for manual entry (no similar chars: 0/O, 1/I/l)
  • Short but entropy-sufficient
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-char alphabet, 4 segments of 4 chars = 32^16 ≈ 10^24 variants. Brute force impossible, collision virtually excluded.

Certificate Application at Checkout

Atomic deduction — key requirement:

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() excludes race condition on simultaneous use in two browser windows.

Refund on Order Cancellation

On return, balance restored:

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

        // Don't restore more than 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]);
        }
    });
}

If certificate expired — refund policy at store discretion: extend or refund money.

Certificate Purchase as Product

Certificate is special order item type. On order paid status, certificate auto-generated and sent:

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);
        }
    }
}

Certificate Email Design

Email with certificate is product itself. Minimum:

  • Beautiful HTML template with code in large font (copy-friendly)
  • Nominal amount
  • Expiration date
  • QR code or direct link for use
  • Personal message from sender

Certificate also available as PDF for self-printing. PDF generation via barryvdh/laravel-dompdf or puppeteer.

Balance in Personal Cabinet

If certificate tied to user account (not just code):

  • "My Certificates" tab with balance and usage history
  • Field to tie code to account
  • Auto-offer to apply available balance at checkout

Application Restrictions

Optional business rules:

  • Certificate applicable only to certain product categories
  • Minimum order amount for use
  • Can't pay for another certificate with certificate
  • Only one certificate per order (or multiple — configurable)

Implementation Timeline

  • Code generation + use + partial use: 3–4 days
  • Certificate purchase as product + auto-issue: 2 days
  • HTML/PDF email with design: 1–2 days
  • Personal cabinet + usage history: 2 days
  • Promo certificates (manual issue + automation): +1–2 days

Full system: 1.5–2 weeks.