promo 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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