Rails 3: Generate unique codes (coupons)
You can do something like this too:
chars = ('a'..'z').to_a + ('A'..'Z').to_a
def String.random_alphanumeric(size=16)
(0...size).collect { chars[Kernel.rand(chars.length)] }.join
end
But then you would have to compare against a database to make sure it is not used yet.
Generating unique, hard-to-guess coupon codes
The code needs to be unguessable, because the only verification you can perform before giving the user their reward is to check whether the code they entered exists in your list of "issued" codes.
That means the number of all possible codes in that format is much larger than the number of codes you want to issue,. Depending on how easy it is to simply try codes (think of a script trying repeatedly), then you might need all possible codes to outnumber issued codes by a factor of a million or a billion or more. This sounds high, but is possible in relatively short strings.
It also means that the codes that you use must be chosen as randomly as possible within all possible codes. This is necessary to avoid users figuring out that most valid codes start with "AAA" for example. More sophisticated users might spot that your "random" codes use a hackable random number generator (Ruby's default
rand()
is fast and statistically good for random data, but is hackable in this way, so don't use it).
The starting point for such a secure code would be the output from a cryptographic PRNG. Ruby has the securerandom
library, which you can use to get a raw code like this:
require 'securerandom'
SecureRandom.hex
# => "78c231af76a14ef9952406add6da5d42"
This code is long enough to cover any realistic number of vouchers (millions each for everyone on the planet), without any meaningful chance of repetition or being easy to guess. However, it is a bit awkward to type from a physical copy.
Once you know how to generate a random, practically unguessable code, your next problem is understanding user experience and deciding how much you can realistically compromise security in the name of usability. You need to bear in mind the value to the end user, and therefore how hard someone might try to get a valid code. I cannot answer that for you, but can make some general points about usability:
Avoid ambiguous characters. In print, it is sometimes difficult to see the difference between
1
,I
andl
for example. We often understand what it is supposed to be from context, but a randomised string of characters does not have this context. It would be a bad user experience to have to try several variations of a code by testing0
vsO
,5
vsS
etc.Use either lower case or upper case letters but not both. Case sensitivity will not be understood or followed by some %age of your users.
Accept variations when matching codes. Allow spaces and dashes. Perhaps even allow
0
andO
to mean the same thing. This is best done by processing the input text so it is in the right case, strip separator characters etc.In print, separate the code into a few small parts, it will be easier for the user to find their place in the string and type a few characters at once.
Don't make the code too long. I would suggest 12 characters, in 3 groups of 4.
Here's an interesting one - you may want to scan the code for possible rude words, or avoid the characters that would generate them. If your code contained only the characters
K
,U
,F
,C
, then there would be a high chance of offending a user. This isn't usually a concern because users do not see most computer secure codes, but these ones will be in print!
Putting that all together, this is how I might generate a usable code:
# Random, unguessable number as a base20 string
# .rjust(12, '0') covers for unlikely, but possible small numbers
# .reverse ensures we don't use first character (which may not take all values)
raw = SecureRandom.random_number( 2**80 ).to_s( 20 ).rjust(12, '0').reverse
# e.g. "3ecg4f2f3d2ei0236gi"
# Convert Ruby base 20 to better characters for user experience
long_code = raw.tr( '0123456789abcdefghij', '234679QWERTYUPADFGHX' )
# e.g. "6AUF7D4D6P4AH246QFH"
# Format the code for printing
short_code = long_code[0..3] + '-' + long_code[4..7] + '-' + long_code[8..11]
# e.g. "6AUF-7D4D-6P4A"
There are 20**12
valid codes in this format, which means you can issue a billion of your own codes, and there would be one in four million chance of a user simply guessing a correct one. In cryptography circles that would be very bad (this code is insecure against a fast local attack), but for a web form offering free burritos to registered users, and where you would notice a someone trying four million times with a script, it is ok.
Simple coupon generator in rails
You don't mention what sort of coupon format you require and I am sure there are a bunch of gems that do similar things. I guess one approach is to use a unique code that you can generate and then tag a user_id to the end of it to ensure uniqueness across many codes.
def generate_coupon_code(user_id)
characters = %w(A B C D E F G H J K L M P Q R T W X Y Z 1 2 3 4 5 6 7 8 9)
code = ''
4.times { code << characters.sample }
code << user_id.to_s
code
end
How to create a coupon code generator in rails 4
Here's a small example
letters = (0..9).to_a + ('a'..'z').to_a + ('A'..'Z').to_a # add or remove possibilities
letters.sample(10).join #or any length you want
How to test uniqueness of Coupon/Promo-Codes?
Rspec's and_return
method allows you to specify multiple return values that will be cycled through
For example you could write
PromoCode.stub(:generate).and_return('badcode1', 'badcode2', 'goodcode')
Which will cause the first call to generate to return 'badcode1', the second 'badcode2' etc... You can then check that the returned promocode was created with the correct code.
If you want to be race condition proof you'll want a database uniqueness constraint, so your code might actually want to be
def generate!
create!(...)
rescue ActiveRecord::RecordNotUnique
retry
end
In your spec you would stub the create! method to raise the first time but return a value the second time
Rails create multiple tokens for same record (multiple coupons for one offer)
It's hard to tell which parts of this you're asking for help on. Are you not sure how to generate unique coupon codes, not sure how to model it, not sure how to create multiple records at a time? I'll try to give a general answer of what I'd do if I were building this, and maybe that will be helpful to you.
I'd create two models, Offer
and Coupon
:
rails g model offer title:string user:references
rails g model coupon offer:references code:uuid
I don't know if you're using mysql or postgresql. I think uuid will only work on postgresql. If you're using mysql I guess make the code column a string instead. Or better yet, switch to postgresql. ;)
class Offer < ActiveRecord::Base
belongs_to :user
has_many :coupons
end
class Coupon < ActiveRecord::Base
belongs_to :coupon
before_create -> { self.code = SecureRandom.uuid }
end
I would index the database on your code
column and make it unique, and disallow code
from being nil
. Might as well make both columns disallow nil
, I'm a big believer in doing that wherever possible in the database:
class CreateCoupons < ActiveRecord::Migration
def change
create_table :coupons do |t|
t.references :offer, index: true, null: false
t.uuid :code, null: false
end
add_index :coupons, :code, unique: true
end
end
Normally when you create a coupon you'd just do:
offer.coupons.create!
And it would generate the code for you in the before_create
hook. But since you want to create like 100 of them at once, the simple way would be:
100.times { offer.coupons.create! }
But that's going to be kind of inefficient. I don't know of a way to get Rails to do an efficient INSERT of many records, so we're going to do it manually:
objs = []
100.times do
objs.push "(#{offer.id}, '#{SecureRandom.uuid}', '#{Time.now}', '#{Time.now}')"
end
Coupon.connection.execute("INSERT INTO coupons (offer_id, code, created_at, updated_at) VALUES #{objs.join(", ")}")
If there's a more Railsy way of doing this, someone please let us know.
Best way to create unique token in Rails?
-- Update EOY 2022 --
It's been some time since I answered this. So much so that I've not even taken a look at this answer for ~7 years. I have also seen this code used in many organizations that rely on Rails to run their business.
TBH, these days I wouldn't consider my earlier solution, or how Rails implemented it, a great one. Its uses callbacks which can be PITA to debug and is pessimistic in nature, even though there is a very low chance of collision for SecureRandom.urlsafe_base64
. This holds true for both long and short-lived tokens.
What I would suggest as a potentially better approach is to be optimistic about it. Set a unique constraint on the token in the database of choice and then just attempt to save it. If saving produces an exception, retry until it succeeds.
class ModelName < ActiveRecord::Base
def persist_with_random_token!(attempts = 10)
retries ||= 0
token = SecureRandom.urlsafe_base64(nil, false)
save!
rescue ActiveRecord::RecordNotUnique => e
raise if (retries += 1) > attempts
Rails.logger.warn("random token, unlikely collision number #{retries}")
token = SecureRandom.urlsafe_base64(16, false)
retry
end
end
What is the result of this?
- One query less as we are not checking for the existence of the token beforehand.
- Quite a bit faster, overall because of it.
- Not using callbacks, which makes debugging easier.
- There is a fallback mechanism if a collision happens.
- A log trace (metric) if a collision does happen
- Is it time to clean old tokens maybe,
- or have we hit the unlikely number of records when we need to go to
SecureRandom.urlsafe_base64(32, false)
?).
-- Update --
As of January 9th, 2015. the solution is now implemented in Rails 5 ActiveRecord's secure token implementation.
-- Rails 4 & 3 --
Just for future reference, creating safe random token and ensuring it's uniqueness for the model (when using Ruby 1.9 and ActiveRecord):
class ModelName < ActiveRecord::Base
before_create :generate_token
protected
def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless ModelName.exists?(token: random_token)
end
end
end
Edit:
@kain suggested, and I agreed, to replace begin...end..while
with loop do...break unless...end
in this answer because previous implementation might get removed in the future.
Edit 2:
With Rails 4 and concerns, I would recommend moving this to concern.
# app/models/model_name.rb
class ModelName < ActiveRecord::Base
include Tokenable
end
# app/models/concerns/tokenable.rb
module Tokenable
extend ActiveSupport::Concern
included do
before_create :generate_token
end
protected
def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless self.class.exists?(token: random_token)
end
end
end
Related Topics
Best Way to Generate Order Numbers for an Online Store
Profile a Rails Controller Action
How to Read a Barcode from an Image
Confusing Behaviour of Const_Get in Ruby
Ruby: Initialize() VS Class Body
How to Apply a Patch to Ruby on Rails
Running Ruby Unit Tests with Rake
How to Download a CSV File in Ruby on Rails
Installing Gem Fails with Permissions Error
Ruby Singleton Methods with (Class_Eval, Define_Method) VS (Instance_Eval, Define_Method)
How to Update Gems on Production Server
Rails: Ensure Only One Boolean Field Is Set to True at a Time
Ruby on Rails, Including a Module with Arguments
How to Make Object Instance a Hash Key in Ruby
How to Pass Arguments from the Parent Task to the Child Task in Rake