promo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --backtrace
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in promo.gemspec
4
+ gemspec
5
+
6
+ gem 'pry'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Pedro Ivo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,122 @@
1
+ # Promo
2
+
3
+ A gem to generate and use coupons and promocodes
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'promo'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install promo
18
+
19
+ ## Usage
20
+
21
+ First you need to create the migrations needed in the gem:
22
+
23
+ # rails generate promo:install
24
+
25
+ Objects must always be created through generate method instead using new.
26
+ Here you may define some options:
27
+
28
+ ```ruby
29
+ promo = Promo::Promocode.generate(options)
30
+
31
+ options.multiple: false
32
+ options.quantity: 1
33
+ options.type: :percentage
34
+ options.status: :valid
35
+ options.expires: 4.weeks
36
+ options.code: SecureRandom.hex(4)
37
+ options.product: Some model with has_many :promocodes, as: :product
38
+ ```
39
+
40
+ ### Associated models
41
+
42
+ #### Cart / Order
43
+ While in a ecommerce, you might have a Cart and Order models (or any correspondency). thus you must associate these models in order to accept the promocodes associated.
44
+
45
+ In a simple example, we might define a Cart model as:
46
+ ```ruby
47
+ class Cart < ActiveRecord::Base
48
+ has_many :cart_items, dependent: :destroy
49
+ has_one :promo_history, :class_name => 'Promo::History'
50
+ has_one :promocode, through: :promo_history
51
+ ...
52
+ end
53
+
54
+ ```
55
+
56
+ And then, after the checkout that Cart model suppose to be transformed (or coppied) to a Order object, something like:
57
+
58
+ ```ruby
59
+ class Order < ActiveRecord::Base
60
+ has_many :order_items, dependent: :destroy
61
+ has_one :promo_history, :class_name => 'Promo::History'
62
+ has_one :promocode, through: :promo_history
63
+ ...
64
+ end
65
+
66
+ ```
67
+
68
+ #### Products
69
+
70
+ As a promocode may be associated with any kind of product, in the model you want allow it, you must define as
71
+
72
+ ```ruby
73
+ class Product < ActiveRecord::Base
74
+ has_many :promocodes, as: :product
75
+
76
+ has_many :cart_item, as: :product
77
+ has_many :order_item, as: :product
78
+
79
+ has_many :carts, through: :cart_item
80
+ has_many :orders, through: :order_item
81
+ ...
82
+ end
83
+ ```
84
+
85
+ ### Then you can calculate the discount by
86
+
87
+ ```ruby
88
+ class Cart < ActiveRecord::Base
89
+ def recalculate
90
+ self.update_attribute(:total_value, cart_items.map{ |i| i.product.value }.reduce(:+))
91
+ self.update_attribute(:discount_value, Promo::Usage.discount_for(promocode: self.promocode, product_list: product_list))
92
+ self.update_attribute(:final_value, self.total_value-self.discount_value)
93
+ end
94
+ end
95
+ ```
96
+
97
+ ### Example application
98
+ I've created a sample application with a basic product/cart/order model to test the gem, and also might be used as base for any simple ecommerce:
99
+
100
+ http://github.com/x1s/promo-example-cart
101
+
102
+ ### Administration view
103
+
104
+ ```ruby
105
+ # if you require 'sinatra' you get the DSL extended to Object
106
+ gem 'sinatra', '>= 1.3.0', :require => nil
107
+ ```
108
+
109
+ Add the following to your config/routes.rb:
110
+
111
+ ```ruby
112
+ require 'promo/web'
113
+ mount Promo::Web => '/promo'
114
+ ```
115
+
116
+ ## Contributing
117
+
118
+ 1. Fork it ( http://github.com/x1s/promo/fork )
119
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
120
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
121
+ 4. Push to the branch (`git push origin my-new-feature`)
122
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,8 @@
1
+ promocode:
2
+ messages:
3
+ already_used: 'Promocode already used'
4
+ invalid: 'Invalid Promocode'
5
+ expired: 'This promocode has expired'
6
+ invalid_use: 'Invalid use of this promocode'
7
+ not_valid_for: 'This promocode is not valid for'
8
+ must_have_product: 'Product associated with this Promocode not found'
@@ -0,0 +1,8 @@
1
+ promocode:
2
+ messages:
3
+ already_used: 'Promocode já utilizado'
4
+ invalid: 'Promocode inválido'
5
+ expired: 'Este promocode já expirou'
6
+ invalid_use: 'Uso invalido do cupão'
7
+ not_valid_for: 'Este cupão não é valido para este uso'
8
+ must_have_product: 'O produto associado ao promocode não foi encontrado'
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module Promo
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ desc 'Creates the models to the file'
7
+
8
+ def self.source_root
9
+ @_promo_source_root ||= File.expand_path("../templates", __FILE__)
10
+ end
11
+
12
+ def create_migration_files
13
+ time = Time.now.strftime("%Y%m%d%H%M%S").to_i
14
+ template '1_create_promo_promocode.rb', File.join('db', 'migrate', "#{time}_create_promo_promocode.rb")
15
+ template '2_create_promo_history.rb', File.join('db', 'migrate', "#{time+1}_create_promo_history.rb")
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ class CreatePromoPromocode < ActiveRecord::Migration
2
+ def change
3
+ create_table :promo_promocodes do |t|
4
+ t.references :product, polymorphic: true, index: true
5
+ t.references :cart, polymorphic: true, index: true
6
+ t.references :order, polymorphic: true, index: true
7
+ t.integer :value, default: 0
8
+ t.integer :promo_type, null: false
9
+ t.integer :status, default: 1
10
+ t.integer :quantity, default: 1
11
+ t.integer :used, default: 0
12
+ t.datetime :expires, null: false
13
+ t.string :code, null: false, index: true
14
+ t.datetime :used_at
15
+
16
+ t.timestamps
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ class CreatePromoHistory < ActiveRecord::Migration
2
+ def change
3
+ create_table :promo_histories do |t|
4
+ t.references :cart, polymorphic: true, index: true
5
+ t.references :order, polymorphic: true, index: true
6
+ t.references :promo_promocode, index: true
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ require "promo/version"
2
+
3
+ require 'promo/promocode'
4
+ require 'promo/history'
5
+ require 'promo/usage'
6
+
7
+ module Promo
8
+ end
9
+
10
+ class PromocodeException < StandardError; end
11
+ class UsedPromocode < PromocodeException; end
12
+ class ExpiredPromocode < PromocodeException; end
13
+ class InvalidPromocode < PromocodeException; end
14
+ class InvalidPromoProduct < PromocodeException; end
15
+
@@ -0,0 +1,8 @@
1
+ module Promo
2
+ class History < ActiveRecord::Base
3
+ self.table_name = 'promo_histories'
4
+ belongs_to :cart
5
+ belongs_to :order
6
+ belongs_to :promocode, :class_name => 'Promo::Promocode', :foreign_key => "promo_promocode_id"
7
+ end
8
+ end
@@ -0,0 +1,150 @@
1
+ module Promo
2
+ class Promocode < ActiveRecord::Base
3
+ self.table_name = 'promo_promocodes'
4
+ # Forbide direct creation of objects
5
+ private_class_method :create
6
+
7
+ belongs_to :product, polymorphic: true
8
+
9
+ has_many :histories, :class_name => 'Promo::History', :foreign_key => "promo_promocode_id"
10
+ has_many :carts, through: :histories
11
+ has_many :orders, through: :histories
12
+
13
+ validates :code, uniqueness: true
14
+
15
+ STATUS = { valid: 0, expired: 1, invalid: 2, used: 3}
16
+ TYPE = { percentage: 1, fixed_value: 0 }
17
+
18
+ scope :last, -> { where(status: STATUS[:valid]).order(id: :desc).limit(10) }
19
+ scope :used, -> { where(status: STATUS[:used]).order(used_at: :desc) }
20
+ scope :invalid, -> { where(status: STATUS[:invalid]).order(used_at: :desc) }
21
+ scope :expired, -> { where("status = ? OR expires < ?", STATUS[:expired], Time.now).order(expires: :desc) }
22
+
23
+ # Objects must always be created through generate method instead using new.
24
+ # here you may define some options:
25
+ # options.multiple: false
26
+ # options.quantity: 1
27
+ # options.type: :percentage
28
+ # options.status: :valid
29
+ # options.expires: 4.weeks
30
+ # options.code: SecureRandom.hex(4)
31
+ # options.product: Some model with has_many :promocodes, as: :product
32
+ # options.product_type: Associate the promocode with a class of product (any product of that class)
33
+ def self.generate(options={})
34
+ options[:multiple] ||= false
35
+ options[:quantity] ||= 1
36
+ options[:quantity] = 1 if options[:quantity].to_i <= 0
37
+ options[:promo_type] ||= TYPE[:percentage]
38
+ options[:status] ||= STATUS[:valid]
39
+ options[:expires] ||= Time.now + 4.weeks
40
+
41
+ if options[:code].blank?
42
+ options[:code] = generate_code
43
+ generated_code = true
44
+ else
45
+ generated_code = false
46
+ options[:code] = generate_code(0,options[:code])
47
+ end
48
+
49
+ multiple = options[:multiple]
50
+ options.delete(:multiple)
51
+
52
+ if multiple
53
+ ret = []
54
+ many = options[:quantity].to_i
55
+ options[:quantity] = 1
56
+ many.times do |item|
57
+ opt = options.dup
58
+ opt[:code] = generate_code if generated_code
59
+ opt[:code] = generate_code(0,options[:code]+item.to_s) if !generated_code
60
+ ret << create!(opt)
61
+ end
62
+ return ret
63
+ end
64
+ create!(options)
65
+ end
66
+
67
+ #--------------------------
68
+
69
+ def use (options={})
70
+ is_valid? options
71
+
72
+ self.used += 1
73
+ self.status = STATUS[:used] if self.quantity == self.used
74
+ self.used_at = Time.now
75
+ save
76
+ self
77
+ end
78
+
79
+ def invalidate!
80
+ update_attributes(status: STATUS[:invalid], used_at: Time.now)
81
+ end
82
+
83
+ #--------------------------
84
+
85
+ def has_product?
86
+ !self.product.nil?
87
+ end
88
+
89
+ def is_percentage?
90
+ self.promo_type == TYPE[:percentage]
91
+ end
92
+
93
+ def is_fixed_value?
94
+ self.promo_type == TYPE[:fixed_value]
95
+ end
96
+
97
+ def is_expired?
98
+ return true if self.status == STATUS[:expired]
99
+ return false if self.expires > Time.now
100
+ update_attribute(:status, STATUS[:expired])
101
+ true
102
+ end
103
+
104
+
105
+ # Validates the use of this promocode
106
+ # Options:
107
+ # product_list: Array with products, mandatory when the promocode is associated with
108
+ # a specific product or a specific category of products
109
+ #
110
+ def is_valid?(options={})
111
+ raise UsedPromocode.new 'promocode.messages.already_used' if self.status == STATUS[:used]
112
+ raise InvalidPromocode.new 'promocode.messages.invalid' if self.status != STATUS[:valid]
113
+ raise ExpiredPromocode.new 'promocode.messages.expired' if is_expired?
114
+
115
+ # Validating use with a specific product associated
116
+ if self.has_product?
117
+ logger.debug "#------------ Promocode associated with a product"
118
+ raise InvalidPromocode.new 'promocode.messages.invalid_use' if options[:product_list].nil?
119
+ if self.product && !options[:product_list].include?(self.product)
120
+ logger.debug "#--------------- Product associated not found on the list"
121
+ raise InvalidPromoProduct.new 'promocode.messages.not_valid_for'
122
+ end
123
+ end
124
+
125
+ # Validating use with when a class of product is associated with the promocode
126
+ # not a specific product (no product_id defined)
127
+ if self.product_id.nil? && !self.product_type.nil?
128
+ logger.debug "#------------ Promocode associated with a class"
129
+ raise InvalidPromocode.new 'promocode.messages.invalid_use' if options[:product_list].nil?
130
+ if options[:product_list].none? { |p| p.class.to_s == self.product_type }
131
+ logger.debug "#--------------- Class associated not found on the list"
132
+ raise InvalidPromoProduct.new 'promocode.messages.must_have_course'
133
+ end
134
+ end
135
+ # Returns the promocode if it's valid
136
+ self
137
+ end
138
+
139
+ # Generate random codes
140
+ def self.generate_code(size=4,code="")
141
+ code = SecureRandom.hex(size) if code.empty?
142
+ code = code+SecureRandom.hex(size) unless code.empty?
143
+ # Validates if the code is already created, then add something to the name
144
+ if Promo::Promocode.where(code: code).first
145
+ code = code+SecureRandom.hex(1)
146
+ end
147
+ code
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,52 @@
1
+ module Promo
2
+ class Usage
3
+ class << self
4
+ # Validates the use of any promocode
5
+ # Options (Hash):
6
+ # code: string with the code used
7
+ # product_list: array with the products to be evaluated
8
+ def validate (options={})
9
+ promocode = Promo::Promocode.where(code: options[:code]).first
10
+ raise InvalidPromocode.new 'promocode.messages.invalid' if promocode.nil?
11
+ promocode.is_valid? options
12
+ end
13
+
14
+ # Calculates the discounts to any list of products
15
+ # Options (Hash):
16
+ # promocode: Pomo::Promocode object
17
+ # product_list: array with the products to be evaluated
18
+ def discount_for (options={})
19
+ return 0 if options[:promocode].nil?
20
+ promocode = options[:promocode]
21
+ product_list = options[:product_list]
22
+
23
+ return discount_for_product(promocode, product_list) if promocode.has_product?
24
+ if promocode.is_percentage?
25
+ calculate_percentage product_list.map(&:value).reduce(:+), promocode.value
26
+ else
27
+ promocode.value
28
+ end
29
+ end
30
+
31
+ # Calculates the dicount for a specific product in the list
32
+ # previously associated with the current promocode
33
+ # Parameters:
34
+ # promocode: Pomo::Promocode object
35
+ # product_list: array with the products to be evaluated
36
+ def discount_for_product promocode, product_list
37
+ product = promocode.product
38
+ return 0 unless product_list.include? product
39
+ if promocode.is_percentage?
40
+ calculate_percentage(product.value,promocode.value)
41
+ else
42
+ promocode.value
43
+ end
44
+ end
45
+
46
+ #calculates the percentage to a specific value
47
+ def calculate_percentage(value, percent)
48
+ (value * (percent.to_f/100)).to_i
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module Promo
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'promo/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "promo"
8
+ spec.version = Promo::VERSION
9
+ spec.authors = ["Pedro Ivo"]
10
+ spec.email = ["pedroivo@x1s.eti.br"]
11
+ spec.summary = %q{Create coupons and promocodes}
12
+ spec.description = %q{}
13
+ spec.homepage = "http://github.com/x1s"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ end
File without changes
File without changes
File without changes
File without changes
File without changes
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: promo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pedro Ivo
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-02-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.5'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.5'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: ''
63
+ email:
64
+ - pedroivo@x1s.eti.br
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - .rspec
71
+ - Gemfile
72
+ - LICENSE.txt
73
+ - README.md
74
+ - Rakefile
75
+ - config/locale/promo.en.yml
76
+ - config/locale/promo.pt.yml
77
+ - lib/generators/promo/install/install_generator.rb
78
+ - lib/generators/promo/install/templates/1_create_promo_promocode.rb
79
+ - lib/generators/promo/install/templates/2_create_promo_history.rb
80
+ - lib/promo.rb
81
+ - lib/promo/history.rb
82
+ - lib/promo/promocode.rb
83
+ - lib/promo/usage.rb
84
+ - lib/promo/version.rb
85
+ - promo.gemspec
86
+ - spec/lib/promo/history_spec.rb
87
+ - spec/lib/promo/promocode_spec.rb
88
+ - spec/lib/promo/usage_spec.rb
89
+ - spec/lib/promo_spec.rb
90
+ - spec/spec_helper.rb
91
+ homepage: http://github.com/x1s
92
+ licenses:
93
+ - MIT
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 1.8.23
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: Create coupons and promocodes
116
+ test_files:
117
+ - spec/lib/promo/history_spec.rb
118
+ - spec/lib/promo/promocode_spec.rb
119
+ - spec/lib/promo/usage_spec.rb
120
+ - spec/lib/promo_spec.rb
121
+ - spec/spec_helper.rb