Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Terrylow88's avatar

Discount codes table design

Hi all,

I have been wondering for long that, how a discount code database table design will look like, because i have to allow a product to have its own discount code with single uses or multiple uses, and each product may has a number of codes (maybe 1K, 10K, 100K and single/multiple usage), this will make primary key running out pretty fast.

wondering how you people handling this

thx for any opinion

0 likes
8 replies
mathewberry's avatar
Level 7

At my last company this is something similar to what we did, how you implement it is up to you! I suggest changing some things around, it's designed to suit multiple types of voucher code, multi-use vouchers etc... To key past your primary key constraint we used big integer primary keys.

Schema::create( 'vouchers', function ( Blueprint $table ) {
    $table->bigIncrements( 'id' );
    
    // The voucher code
    $table->string( 'code' )->nullable( );

    // The human readable voucher code name
    $table->string( 'name' );

    // The description of the voucher - Not necessary 
    $table->text( 'description' )->nullable( );

    // The number of uses currently
    $table->integer( 'uses' )->unsigned( )->nullable( );

    // The max uses this voucher has
    $table->integer( 'max_uses' )->unsigned()->nullable( );

    // How many times a user can use this voucher.
    $table->integer( 'max_uses_user' )->unsigned( )->nullable( );

    // The type can be: voucher, discount, sale. What ever you want.
    $table->tinyInteger( 'type' )->unsigned( );

    // The amount to discount by (in pennies) in this example.
    $table->integer( 'discount_amount' );

    // Whether or not the voucher is a percentage or a fixed price. 
    $table->boolean( 'is_fixed' )->default( true );
    
    // When the voucher begins
    $table->timestamp( 'starts_at' );

    // When the voucher ends
    $table->timestamp( 'expires_at' );

    // You know what this is...
    $table->timestamps( );

    // We like to horde data.
    $table->softDeletes( );
});
Schema::create( 'user_voucher', function ( Blueprint $table ) {
    $table->integer( 'user_id' )->unsigned( );
    $table->bigInteger( 'voucher_id' )->unsigned( );

    $table->unique( [ 'user_id', 'voucher_id' ] );
});
Schema::create( 'product_voucher', function ( Blueprint $table ) {
    $table->integer( 'product_id' )->unsigned( );
    $table->bigInteger( 'voucher_id' )->unsigned( );

    $table->unique( [ 'product_id', 'voucher_id' ] );
});

I hope this helps :)

11 likes
Terrylow88's avatar

@matberry yes, it is very helpful and intuitive, im really appreciated your suggestion, thanks :)

1 like
Hamedov's avatar

How will you check for max_uses_user if user_voucher table is unique by user_id and voucher_id ?

keyurdpatel's avatar

@mathewberry this is very helpful but i don't understood about "uses,max_uses,max_uses_user" so can you help on this, to explain how its work? please help me.

mathewberry's avatar

@hamedov that is a good point, this SQL format does not account for that scenario, however what I have done to combat this is; in my orders table I have a voucher_id, user_id and a small function on the Voucher model that checks if the user has the maximum usages when they next try to apply a voucher.

It looks something like this:

public function userReachedMaximumUses(User $user) 
{
    if ($this->max_uses_user <= 0) {
        return false;
    }

    return $this->orders()->where('user_id', $user->id)->count() < $this->max_uses_user;
}

This little function is only relevant to orders but you could also look into adding a used column on the user_voucher pivot which is a better approach for the long term.

@keyurdpatel The max_uses is the total number of times the voucher can be used, for example you may set max_uses to 6, then every time someone uses a voucher you will increment the uses column and check to before applying if uses is greater than or equal to the max_uses, if it is you don't apply.

2 likes
martinbean's avatar

@shoxton Because any monetary amount should be stored in the lowest common denominator in the database, i.e. pence or cents.

The reason being, computers are notoriously bad at floating point math. So when you’re dealing with monetary amounts you want to be working with accurate amounts. If you get say, a tax amount wrong, then you’re going to have a very angry government department contacting you.

6 likes

Please or to participate in this conversation.