Implementing Gift Cards in Magento 2: Code, Redemption, Refund
AI generated
M2
di.xml
Magento 2 · Gift Cards · Checkout · GraphQL
Implementing Gift Cards in Magento 2
from code generation to secure checkout redemption

Gift cards in Magento 2 are more than a simple voucher field: a clean data model in the giftcardaccount, secure code generation, correct checkout total integration, multiple redemption per cart, a resilient admin workflow for batch campaigns, and hardened API protection for headless and GraphQL checkouts decide whether gift cards work reliably and securely in production.

18 min read giftcardaccount · Code Generation · Total Collector · GraphQL Magento 2.4.8-p4 · PHP 8.4 · Hyva

1. Gift cards vs. store credit: a clear distinction

Before working on the implementation, the distinction must be clear: this article deals exclusively with code-based gift cards, i.e. individual voucher codes with their own, fixed balance. A customer receives a code (physically on a plastic card, digitally by email or as a PDF), enters it at checkout, and the stored balance is offset against the cart. This is fundamentally different from an account-bound store credit balance, where an amount is credited directly to the customer account without any single code being involved. Store credit is the subject of a separate article and is deliberately not covered here.

The distinction is not just terminological, it has direct effects on the data model. A gift card code lives as an independent entity with its own status, its own remaining balance and its own expiry date, independent of the customer account. One and the same code can in principle be redeemed by anyone who knows it, which in turn places its own demands on security and brute-force protection that an account-bound balance does not have in the same way.

Code-based gift cards are the right choice when physical or digital voucher cards are sold as a standalone product (for example during the holiday season or as a corporate incentive), when marketing campaigns should distribute codes that work independently of an existing customer account, or when a partner program issues codes to third parties who resell them in the shop. When, on the other hand, the balance should be firmly tied to an existing customer account, for example as a refund without a new code, a store credit model is the more suitable solution, not the gift card described here.

2. Data model: giftcardaccount, code and status

The core of the implementation is a dedicated table for the giftcardaccount. It stores the code itself (hashed or at least given a unique index), the current balance, the initial amount, the status (active, redeemed, disabled, expired) and an optional expiry date. It is important not to store the code as a plain-text string without constraints: a UNIQUE index on the code column prevents duplicates during batch generation, and an additional status column decouples business logic from pure balance calculations.

Modeling happens via db_schema.xml, not via install scripts. The table references the order_id of the order process that originally generated the code (for example when purchasing a gift card product), and holds in a separate mapping table which quote_id or order_id used the code for redemption. This separation between generation and redemption is crucial: a code can be created in one order and redeemed in a completely different, later order, often by a different customer.


<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
  <table name="mironsoft_giftcardaccount" resource="default" engine="innodb" comment="Gift Card Account">
    <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/>
    <column xsi:type="varchar" name="code" nullable="false" length="32" comment="Gift Card Code"/>
    <column xsi:type="decimal" name="balance" scale="4" precision="20" unsigned="false" nullable="false" default="0" comment="Current Balance"/>
    <column xsi:type="decimal" name="initial_balance" scale="4" precision="20" unsigned="false" nullable="false" comment="Initial Balance"/>
    <column xsi:type="varchar" name="status" nullable="false" length="20" default="active" comment="Status: active, redeemed, disabled, expired"/>
    <column xsi:type="date" name="expires_at" nullable="true" comment="Expiry Date"/>
    <column xsi:type="int" name="generated_order_id" unsigned="true" nullable="true" comment="Order that generated this code"/>
    <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/>
    <constraint xsi:type="primary" referenceId="PRIMARY">
      <column name="entity_id"/>
    </constraint>
    <constraint xsi:type="unique" referenceId="MIRONSOFT_GIFTCARDACCOUNT_CODE">
      <column name="code"/>
    </constraint>
    <index referenceId="MIRONSOFT_GIFTCARDACCOUNT_STATUS" indexType="btree">
      <column name="status"/>
    </index>
  </table>
  <table name="mironsoft_giftcardaccount_redemption" resource="default" engine="innodb" comment="Gift Card Redemption History">
    <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="Entity ID"/>
    <column xsi:type="int" name="giftcardaccount_id" unsigned="true" nullable="false" comment="Gift Card Account ID"/>
    <column xsi:type="int" name="quote_id" unsigned="true" nullable="true" comment="Quote ID at time of redemption"/>
    <column xsi:type="int" name="order_id" unsigned="true" nullable="true" comment="Order ID after checkout completion"/>
    <column xsi:type="decimal" name="amount_used" scale="4" precision="20" unsigned="true" nullable="false" comment="Amount Consumed"/>
    <constraint xsi:type="primary" referenceId="PRIMARY">
      <column name="entity_id"/>
    </constraint>
    <constraint xsi:type="foreign" referenceId="MIRONSOFT_GCA_REDEMPTION_GCA_ID"
                table="mironsoft_giftcardaccount_redemption" column="giftcardaccount_id"
                referenceTable="mironsoft_giftcardaccount" referenceColumn="entity_id" onDelete="CASCADE"/>
  </table>
</schema>

The redemption table is deliberately separated from the main account table: a code can be partially redeemed across several orders, and every partial redemption creates its own record. This makes it possible at any time to trace how much balance was consumed when and in which order, a feature indispensable for support requests and accounting reconciliation.

3. Code generation and security

The code itself is the most critical security aspect of a gift card implementation. Sequential or predictable codes (for example consecutive numbers with a prefix) can be systematically tried once an attacker knows a valid format. Generation must therefore be based on cryptographically secure randomness, not on mt_rand() or similar predictable sources. PHP 8.4 offers random_bytes() for this, whose output is subsequently encoded into a readable alphabet (without easily confused characters such as 0/O or 1/I/l).

In addition to pure randomness, a check digit (checksum) belongs to robust code design. It catches typos on manual entry before a database query is even needed, thereby simultaneously reducing the load from accidental incorrect entries. Rate limiting on the redemption route is the second line of defense: without limiting attempts per IP address or customer session, even a securely generated code can eventually be guessed through massive trial and error, especially with short codes lacking sufficient entropy.


<?php

declare(strict_types=1);

namespace Mironsoft\GiftCard\Model\Code;

use Mironsoft\GiftCard\Api\Data\GiftCardAccountInterfaceFactory;
use Mironsoft\GiftCard\Api\GiftCardAccountRepositoryInterface;

/**
 * Generates cryptographically secure, checksum-protected gift card codes.
 */
final class SecureCodeGenerator
{
    /** @var string Alphabet without ambiguous characters (0/O, 1/I/l excluded) */
    private const string ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

    /** @var int Number of random characters before the checksum digit */
    private const int CODE_LENGTH = 16;

    /**
     * @param GiftCardAccountRepositoryInterface $accountRepository Repository used to guarantee uniqueness
     * @param GiftCardAccountInterfaceFactory $accountFactory Factory for new gift card account entities
     */
    public function __construct(
        private readonly GiftCardAccountRepositoryInterface $accountRepository,
        private readonly GiftCardAccountInterfaceFactory $accountFactory,
    ) {
    }

    /**
     * Generates a unique gift card code with an appended checksum character.
     *
     * @return string The generated, human-readable gift card code
     * @throws \Random\RandomException If a secure random source is unavailable
     */
    public function generate(): string
    {
        do {
            $code = $this->randomPayload() . $this->checksum($this->randomPayload());
        } while ($this->accountRepository->existsByCode($code));

        return $code;
    }

    /**
     * Builds the random payload segment of the code using random_bytes().
     *
     * @return string A random string drawn from the safe alphabet
     * @throws \Random\RandomException If a secure random source is unavailable
     */
    private function randomPayload(): string
    {
        $alphabetLength = strlen(self::ALPHABET);
        $payload = '';

        foreach (str_split(bin2hex(random_bytes(self::CODE_LENGTH))) as $byte) {
            $payload .= self::ALPHABET[hexdec($byte) % $alphabetLength];
        }

        return substr($payload, 0, self::CODE_LENGTH);
    }

    /**
     * Computes a single checksum character to detect typos on manual entry.
     *
     * @param string $payload The random payload the checksum is derived from
     * @return string A single checksum character from the safe alphabet
     */
    private function checksum(string $payload): string
    {
        $hash = hash('crc32b', $payload);
        $index = hexdec(substr($hash, 0, 2)) % strlen(self::ALPHABET);

        return self::ALPHABET[$index];
    }
}

It is also important that codes never appear in plain text in log files or error reports. Every log output containing a code should mask it (for example showing only the last four characters), so that a compromised logging system does not simultaneously become a list of redeemable gift cards. This masking also applies to error handling in admin and API, more on that in the API hardening section.

4. Checkout integration: quote address total collector

Offsetting the gift card balance at checkout happens through Magento's total collector system, not through a subsequent price adjustment. A dedicated class extends Magento\Quote\Model\Quote\Address\Total\AbstractTotal and implements the method collect(), which is called during totals calculation. Within this method, the redeemed balance is deducted from the subtotal and displayed as its own line item (with its own label) in the totals breakdown, so that it is consistently visible in checkout, on the invoice and in the order email.

Registration of the total class happens via totals.xml in the respective module, with an explicit sort order. This is crucial: the gift card deduction must take effect after discounts and tax calculation, but before the final grand total fixation, otherwise incorrect tax amounts or double discounting can occur. A total class positioned too early may under certain circumstances see an intermediate state that does not yet contain later adjustments (for example through shipping discounts).


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Quote:etc/totals.xsd">
    <total name="giftcardaccount">
        <class instance="Mironsoft\GiftCard\Model\Total\Quote\GiftCardAccount"/>
        <before>grand_total</before>
        <after>tax</after>
    </total>
</config>

On the PHP side, the collect() method first checks whether any redeemed codes are attached to the quote at all, sums their usable balance, limits the deduction to the current subtotal (a gift card balance must never produce a negative grand total), and writes the amount actually consumed back into the address totals. The unconsumed remainder stays untouched in the giftcardaccount record and is only marked as actually consumed during final order processing, not already at mere display time in checkout.

5. Multiple gift card codes per cart

Unlike a single discount coupon, gift cards can usually be combined multiple times per cart, so a customer can redeem two or three codes simultaneously to settle a larger order. This requires a dedicated mapping table between the quote and the redeemed codes (not just a single string field on the quote), since otherwise neither the partial balance used per code nor the correct reversal on order cancellation could be traced.

For the partial-amount logic: if the balance of one code is insufficient to cover the entire subtotal, the code is fully consumed and the next code in application order is drawn on. If, on the other hand, one code is sufficient and remaining balance is left over, only the required partial amount is deducted, the rest stays on the code for future orders. This order must be deterministic (for example by application timestamp), so that concurrent requests do not lead to race conditions with inconsistent remaining balances.

An often overlooked point is the quote_id_mask for guest checkouts and GraphQL-based headless frontends: the masked quote ID from the API must be resolved to the real quote_id before every redemption operation, since the mapping table internally always works with the real ID. If this is overlooked, orphaned redemption records arise that can no longer be assigned to any valid quote.


<?php

declare(strict_types=1);

namespace Mironsoft\GiftCard\Model\Redemption;

use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\LocalizedException;
use Magento\Quote\Api\Data\CartInterface;
use Mironsoft\GiftCard\Api\Data\GiftCardAccountInterface;
use Mironsoft\GiftCard\Api\GiftCardAccountRepositoryInterface;

/**
 * Applies one or more gift card codes to a cart, consuming partial balances.
 */
final class GiftCardApplier
{
    /**
     * @param GiftCardAccountRepositoryInterface $accountRepository Repository for gift card accounts
     * @param RedemptionResourceInterface $redemptionResource Resource model persisting redemption rows
     */
    public function __construct(
        private readonly GiftCardAccountRepositoryInterface $accountRepository,
        private readonly RedemptionResourceInterface $redemptionResource,
    ) {
    }

    /**
     * Applies a code to the given cart, consuming only the required partial amount.
     *
     * @param CartInterface $cart The cart the code is being applied to
     * @param string $code The gift card code entered by the customer
     * @param float $remainingCartAmount The remaining amount of the cart still to be covered
     * @return float The amount actually consumed from this code
     * @throws LocalizedException If the code is invalid, expired or disabled
     * @throws CouldNotSaveException If the redemption row could not be persisted
     */
    public function apply(CartInterface $cart, string $code, float $remainingCartAmount): float
    {
        $account = $this->accountRepository->getActiveByCode($code);
        $this->assertRedeemable($account);

        $amountToConsume = min($account->getBalance(), $remainingCartAmount);

        $this->redemptionResource->recordPartialRedemption(
            (int) $account->getId(),
            (int) $cart->getId(),
            $amountToConsume,
        );

        $account->setBalance($account->getBalance() - $amountToConsume);
        if ($account->getBalance() <= 0.0001) {
            $account->setStatus(GiftCardAccountInterface::STATUS_REDEEMED);
        }
        $this->accountRepository->save($account);

        return $amountToConsume;
    }

    /**
     * Validates that the account is active, not expired and not already fully redeemed.
     *
     * @param GiftCardAccountInterface $account The account to validate
     * @return void
     * @throws LocalizedException If the account cannot be redeemed in its current state
     */
    private function assertRedeemable(GiftCardAccountInterface $account): void
    {
        if ($account->getStatus() !== GiftCardAccountInterface::STATUS_ACTIVE) {
            throw new LocalizedException(__('This gift card code is not active.'));
        }
        if ($account->getBalance() <= 0.0) {
            throw new LocalizedException(__('This gift card code has no remaining balance.'));
        }
    }
}

6. Admin workflow: manual creation and batch generation

For daily operations, the admin panel needs two different ways to generate codes. The first is manual single creation, for example when a support agent should issue a gift card code with a specific amount as a goodwill gesture. This path runs through a simple admin form that directly calls the SecureCodeGenerator from section 3 and immediately displays the generated code in the grid, with a clear indication that the code is only visible once and is masked afterward.

The second path is batch generation for marketing campaigns, where hundreds or thousands of codes need to be generated at once, for example for distribution to newsletter subscribers. The synchronous admin interface is not suited for this, a CLI command via the Mark Shust wrapper bin/magento is the right approach here. The command accepts parameters for quantity, amount and optional expiry date and processes generation in batches to avoid database locks with large quantities.


<?php

declare(strict_types=1);

namespace Mironsoft\GiftCard\Console\Command;

use Mironsoft\GiftCard\Model\Code\SecureCodeGenerator;
use Mironsoft\GiftCard\Api\GiftCardAccountRepositoryInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * CLI command for batch-generating gift card codes for marketing campaigns.
 */
final class GenerateBatchCommand extends Command
{
    /** @var int Number of accounts persisted per database batch */
    private const int BATCH_SIZE = 200;

    /**
     * @param SecureCodeGenerator $codeGenerator Generator producing secure, checksummed codes
     * @param GiftCardAccountRepositoryInterface $accountRepository Repository persisting new accounts
     */
    public function __construct(
        private readonly SecureCodeGenerator $codeGenerator,
        private readonly GiftCardAccountRepositoryInterface $accountRepository,
        ?string $name = null,
    ) {
        parent::__construct($name);
    }

    /**
     * Configures command name, description, arguments and options.
     *
     * @return void
     */
    protected function configure(): void
    {
        $this->setName('mironsoft:giftcard:generate-batch')
            ->setDescription('Generates a batch of gift card codes for a marketing campaign')
            ->addArgument('quantity', InputArgument::REQUIRED, 'Number of codes to generate')
            ->addArgument('amount', InputArgument::REQUIRED, 'Initial balance per code')
            ->addOption('expires', null, InputOption::VALUE_OPTIONAL, 'Expiry date (Y-m-d)');
    }

    /**
     * Executes the batch generation, persisting accounts in fixed-size chunks.
     *
     * @param InputInterface $input Console input containing quantity, amount and expiry
     * @param OutputInterface $output Console output used for progress reporting
     * @return int Exit code, 0 on success
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $quantity = (int) $input->getArgument('quantity');
        $amount = (float) $input->getArgument('amount');
        $expiresAt = $input->getOption('expires');

        for ($generated = 0; $generated < $quantity; $generated += self::BATCH_SIZE) {
            $batchCount = min(self::BATCH_SIZE, $quantity - $generated);
            $this->generateChunk($batchCount, $amount, $expiresAt);
            $output->writeln(sprintf('Generated %d / %d codes', $generated + $batchCount, $quantity));
        }

        return Command::SUCCESS;
    }

    /**
     * Generates and persists a single chunk of gift card accounts.
     *
     * @param int $count Number of codes to generate in this chunk
     * @param float $amount Initial balance per code
     * @param string|null $expiresAt Optional expiry date in Y-m-d format
     * @return void
     */
    private function generateChunk(int $count, float $amount, ?string $expiresAt): void
    {
        for ($i = 0; $i < $count; $i++) {
            $code = $this->codeGenerator->generate();
            $this->accountRepository->createNew($code, $amount, $expiresAt);
        }
    }
}

Both paths share the same generator and the same persistence layer, so there is no second, divergent code generation logic in admin. This prevents a weaker randomization method from being accidentally used for batch campaigns just because it seemed "faster" for large quantities.

7. API hardening for headless and GraphQL checkouts

Headless frontends redeem gift card codes via a GraphQL mutation, typically something like applyGiftCardToCart. This mutation is a classic target for enumeration attacks: without protective measures, an attacker can automatically try thousands of codes and derive information about valid codes from the different error messages (invalid code vs. expired code vs. already redeemed code). The response must therefore deliver a uniform, generic message for all error cases, independent of the actual internal reason for rejection.

Rate limiting must take effect at two levels: per authenticated customer ID for logged-in checkouts and per IP address or session token for guest orders, since attackers would otherwise simply log out and bypass the protection. A sliding-window counter in a fast cache backend (Redis via the Magento cache layer) is usually sufficient for this, combined with an exponentially increasing backoff after several failed attempts within a short time.


type Mutation {
    applyGiftCardToCart(input: ApplyGiftCardToCartInput!): ApplyGiftCardToCartOutput
        @resolver(class: "Mironsoft\\GiftCard\\Model\\Resolver\\ApplyGiftCardToCart")
}

input ApplyGiftCardToCartInput {
    cart_id: String!
    gift_card_code: String!
}

type ApplyGiftCardToCartOutput {
    cart: Cart!
}

<?php

declare(strict_types=1);

namespace Mironsoft\GiftCard\Model\Guard;

use Magento\Framework\App\CacheInterface;
use Magento\Framework\Exception\LocalizedException;

/**
 * Rate-limits gift card redemption attempts to mitigate enumeration attacks.
 */
final class RedemptionRateLimiter
{
    /** @var int Maximum allowed attempts within the sliding window */
    private const int MAX_ATTEMPTS = 5;

    /** @var int Sliding window size in seconds */
    private const int WINDOW_SECONDS = 300;

    /**
     * @param CacheInterface $cache Fast cache backend used for the sliding-window counter
     */
    public function __construct(
        private readonly CacheInterface $cache,
    ) {
    }

    /**
     * Asserts that the given identity has not exceeded the allowed redemption attempts.
     *
     * @param string $identity Customer ID or IP-derived identity used as the rate limit key
     * @return void
     * @throws LocalizedException If too many attempts occurred within the window
     */
    public function assertNotRateLimited(string $identity): void
    {
        $cacheKey = 'giftcard_redeem_' . hash('sha256', $identity);
        $attempts = (int) $this->cache->load($cacheKey);

        if ($attempts >= self::MAX_ATTEMPTS) {
            // Generic message: never reveal whether a code was correct or not
            throw new LocalizedException(__('Too many attempts. Please try again later.'));
        }

        $this->cache->save((string) ($attempts + 1), $cacheKey, [], self::WINDOW_SECONDS);
    }

    /**
     * Masks a gift card code for safe inclusion in logs and API error contexts.
     *
     * @param string $code The raw gift card code
     * @return string The masked representation, e.g. "****-****-A93F"
     */
    public function maskCode(string $code): string
    {
        return str_repeat('*', max(0, strlen($code) - 4)) . substr($code, -4);
    }
}

Equally important: every log line and every error response from the API may only ever contain the masked code, never plain text. This also applies to application performance monitoring tools that record request payloads, an often overlooked channel through which plain-text codes can unintentionally leak.

8. Refund handling: returning a product paid with a gift card

When an order that was partially or fully paid with a gift card is returned, refund handling must decide where the gift card portion of the refund amount flows. The common solution, and the safest from a fraud-prevention perspective: the gift card portion is credited to the original code, provided that code still exists and has not been disabled, instead of issuing a completely new code. This prevents repeated order-return cycles from putting new, potentially untraceable codes into circulation uncontrolled.

Technically, this logic hooks into sales_creditmemo creation. An observer or plugin on the refund process reads from the redemption history (the mapping table described in section 2) which codes were involved in this order and with what amount, and writes the proportional amount back onto the respective balance upon a refund. In a partial return, the refund amount is distributed proportionally across the originally used codes, not booked flatly onto the most recently used code.

A special case: if the original code has meanwhile already been fully consumed elsewhere or disabled for security reasons, no credit can be made to it anymore. In this case, the fallback is issuing a new code with the corresponding amount, with a clear audit-trail note that this is a refund replacement issuance. This fallback logic should never take effect silently but should always be logged traceably in the admin grid, so that support and accounting can clearly attribute the transaction later.

9. Redemption strategies compared

Depending on shop requirements, different redemption strategies for gift cards are suited to different degrees. The choice directly affects implementation complexity, the achievable security level, and the customer experience at checkout.

Strategy Complexity Security Customer Experience
Single-code redemption Low Easy to check, clear rate limits Sufficient for small amounts, but inflexible
Multiple codes per cart High Needs its own mapping table and ordering logic High, freely combines several cards
Partial amount with remaining balance Medium to high Race-condition protection needed under concurrent requests High, no lost balance
Full redemption Low Small attack surface, code voided afterward Low with balance surplus

In practice, most production Magento 2 shops rely on a combination of multiple codes and partial-amount offsetting, since customers rarely have vouchers that match the order total exactly. The additional implementation effort for the remaining-balance logic pays off over time through fewer support requests about "disappeared" balance, provided the race-condition protection is implemented cleanly.

10. Summary

A solid gift card implementation in Magento 2 stands and falls with a clean separation of responsibilities: a dedicated data model in giftcardaccount keeps code, balance and status separate from order logic. Cryptographically secure code generation with a checksum digit prevents predictable or typo-prone codes. Checkout integration via a dedicated total collector ensures correct ordering relative to tax and discounts, instead of manipulating prices after the fact.

Multiple codes with partial-amount offsetting significantly increase implementation effort but noticeably improve the customer experience. API hardening with rate limiting and code masking is not optional for headless and GraphQL checkouts, but a basic requirement against enumeration attacks. And in refund handling, a clear rule decides whether balance flows back to the original code or a new replacement code with an audit trail is issued, for traceability in both support and accounting alike.

Implementing Gift Cards in Magento 2: The Essentials at a Glance

Data model

giftcardaccount with its own code, balance and status, plus a separate redemption table for history. Always model via db_schema.xml.

Secure codes

random_bytes() instead of predictable random sources, plus a checksum digit against typos and rate limiting against brute force.

Checkout total

Custom AbstractTotal collector via totals.xml, correctly positioned between tax and grand total.

API & refund

Generic error messages and code masking against enumeration. Refund balance back to the original code where possible.

11. FAQ: Gift Cards in Magento 2

1What is the difference between gift cards and store credit?
A gift card is an independent code with a fixed balance. Store credit is a balance directly on the customer account, without a redeemable code.
2How is the giftcardaccount table modeled?
Via db_schema.xml with code, balance, status and expiry date. A separate redemption table logs partial redemptions per quote or order.
3How are codes generated securely?
With random_bytes(), a confusion-free alphabet and a checksum digit against typos. Uniqueness is checked before saving.
4How is the balance deducted from the grand total?
Via a custom AbstractTotal class, registered in totals.xml, positioned after tax and discounts, before the final grand total fixation.
5Can multiple codes be redeemed at once?
Yes, via a mapping table between quote and codes, each code is consumed up to its balance or up to covering the total.
6What happens to remaining balance?
It stays stored on the code and is available for future orders, until the balance is fully used up.
7How are codes generated in bulk?
Via a bin/magento CLI command based on Symfony Console, generating in batches and using the same secure generator as the admin form.
8How is redemption secured for GraphQL?
Rate limiting per customer and IP, generic error messages, and consistent code masking in logs and API responses against enumeration.
9What happens on a refund?
The amount is preferably credited to the original code. If it is no longer available, a replacement code with an audit trail is issued.
10Which redemption strategy is recommended?
Multiple codes combined with partial-amount offsetting, since customers rarely own vouchers that exactly match the order total. The extra effort pays off long-term.

Mironsoft

Magento 2 development, checkout integrations and Hyva themes

Introducing gift cards reliably and securely in your shop?

We implement code-based gift cards in Magento 2, from the database through checkout total integration to a hardened GraphQL mutation, including an admin workflow for batch campaigns and clean refund handling.

Gift card module

Data model, secure code generation and checkout total to Hyva standard

GraphQL hardening

Rate limiting, code masking and protection against enumeration attacks

Admin & refund

CLI batch generation for campaigns and compliant refund handling