solidus_sale_pricing 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +25 -0
  6. data/LICENSE +26 -0
  7. data/README.md +173 -0
  8. data/Rakefile +21 -0
  9. data/app/assets/javascripts/spree/backend/solidus_sale_pricing.js +2 -0
  10. data/app/assets/javascripts/spree/frontend/solidus_sale_pricing.js +2 -0
  11. data/app/assets/stylesheets/spree/backend/solidus_sale_pricing.css +13 -0
  12. data/app/assets/stylesheets/spree/frontend/solidus_sale_pricing.css +4 -0
  13. data/app/controllers/spree/admin/product_controller_decorator.rb +10 -0
  14. data/app/controllers/spree/admin/sale_prices_controller.rb +73 -0
  15. data/app/helpers/spree/base_helper_decorator.rb +21 -0
  16. data/app/models/spree/calculator/dollar_amount_sale_price_calculator.rb +12 -0
  17. data/app/models/spree/calculator/percent_off_sale_price_calculator.rb +12 -0
  18. data/app/models/spree/price_decorator.rb +93 -0
  19. data/app/models/spree/product_decorator.rb +49 -0
  20. data/app/models/spree/sale_price.rb +55 -0
  21. data/app/models/spree/variant_decorator.rb +65 -0
  22. data/app/overrides/add_sale_price_to_product_view.rb +12 -0
  23. data/app/overrides/add_sale_product_admin_tabs.rb +6 -0
  24. data/app/views/spree/admin/products/_sale_products.html.erb +3 -0
  25. data/app/views/spree/admin/sale_prices/_form.html.erb +31 -0
  26. data/app/views/spree/admin/sale_prices/edit.html.erb +17 -0
  27. data/app/views/spree/admin/sale_prices/index.html.erb +54 -0
  28. data/app/views/spree/admin/sale_prices/new.html.erb +15 -0
  29. data/bin/rails +7 -0
  30. data/config/locales/en.yml +34 -0
  31. data/config/routes.rb +10 -0
  32. data/db/migrate/20160622203615_add_spree_create_sale_prices_table.rb +19 -0
  33. data/lib/generators/solidus_sale_pricing/install/install_generator.rb +31 -0
  34. data/lib/solidus_sale_pricing.rb +8 -0
  35. data/lib/solidus_sale_pricing/engine.rb +22 -0
  36. data/lib/solidus_sale_pricing/factories.rb +6 -0
  37. data/lib/solidus_sale_pricing/version.rb +18 -0
  38. data/solidus_sale_pricing.gemspec +38 -0
  39. data/spec/controllers/sale_prices_controller_spec.rb +56 -0
  40. data/spec/factories/sale_price.rb +14 -0
  41. data/spec/models/sale_price_spec.rb +5 -0
  42. data/spec/spec_helper.rb +95 -0
  43. metadata +262 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 43f2552daf03ea9d96228ae50b12181d542a6ddc
4
+ data.tar.gz: d0507eccc04923af5cb7cc8c7b775d3feb8706ec
5
+ SHA512:
6
+ metadata.gz: a011d0c4fb2e6d7294b15f475eb8e9a4760e55773c4e797bc75ee18d89b49e95b898e29094eafb77eb3160b55fc8ef472aea7ea38f1fc95766f2fa88f73f7cdc
7
+ data.tar.gz: a5a39ca9f034a2be81c354df766adf22ce0b66925d8f5fc99325a39536e3ab2485de83de53b958af80bbe64b557c9b7562fc64f366a1dbccd1c204c8eee054de
@@ -0,0 +1,14 @@
1
+ \#*
2
+ *~
3
+ .#*
4
+ .DS_Store
5
+ .idea
6
+ .project
7
+ .sass-cache
8
+ coverage
9
+ Gemfile.lock
10
+ tmp
11
+ nbproject
12
+ pkg
13
+ *.swp
14
+ spec/dummy
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1 @@
1
+ 2.3.0
data/Gemfile ADDED
@@ -0,0 +1,25 @@
1
+ source 'https://rubygems.org'
2
+
3
+ branch = ENV.fetch('SOLIDUS_BRANCH', 'master')
4
+ gem 'solidus', github: 'solidusio/solidus', branch: branch
5
+ gem 'solidus_auth_devise'
6
+
7
+ if branch == 'master' || branch >= 'v2.0'
8
+ gem 'rails-controller-testing', group: :test
9
+ else
10
+ gem 'rails_test_params_backport', group: :test
11
+ gem 'rails', '~> 4.2.7'
12
+ end
13
+
14
+ gem 'pg'
15
+ gem 'mysql2'
16
+
17
+ group :development, :test do
18
+ gem 'pry-rails'
19
+ gem 'pry-byebug'
20
+ gem 'vcr'
21
+ gem 'webmock'
22
+ gem 'timecop'
23
+ end
24
+
25
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2016 [name of plugin creator]
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name Spree nor the names of its contributors may be used to
13
+ endorse or promote products derived from this software without specific
14
+ prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,173 @@
1
+ Solidus Sale Pricing
2
+ =======================
3
+
4
+ Based on https://github.com/jonathandean/spree-sale-pricing.
5
+
6
+ New changes
7
+ ===========
8
+
9
+ - Added backend interface
10
+ - Added missing methods
11
+
12
+ Solidus Sale Pricing
13
+ ==================
14
+
15
+ A Solidus extension (Rails Engine) that lets you set sale prices on products, either by a fixed sale price or a
16
+ percentage off of the original price. Sale prices have a start date, end date and enabled flag to allow you to schedule
17
+ sales, have a historical record of sale prices and put sales on hold.
18
+
19
+ Installing
20
+ ----------
21
+
22
+ In your `Gemfile` add the following for the latest released version:
23
+ ```ruby
24
+ gem 'solidus_sale_pricing'
25
+ ```
26
+
27
+ _OR_ to work from master:
28
+ ```ruby
29
+ gem 'solidus_sale_pricing', :git => 'git://github.com/jtapia/solidus_sale_pricing.git'
30
+ ```
31
+
32
+ Install the Gem:
33
+ ```sh
34
+ bundle install
35
+ ```
36
+
37
+ Copy the migrations in your app:
38
+ ```sh
39
+ bundle exec rake railties:install:migrations
40
+ ```
41
+
42
+ Run database migrations in your app:
43
+ ```sh
44
+ bundle exec rake db:migrate
45
+ ```
46
+
47
+ Usage
48
+ -----
49
+
50
+ Simple example assuming you have a product in your database with the price of $20 and you want to put it on sale
51
+ immediately for $10:
52
+ ```ruby
53
+ product = Spree::Product.first
54
+
55
+ puts product.price.to_f # => 20.0
56
+ puts product.on_sale? # => false
57
+
58
+ product.put_on_sale 10
59
+
60
+ puts product.price.to_f # => 10.0
61
+ puts product.original_price.to_f # => 20.0
62
+ puts product.on_sale? # => true
63
+ ```
64
+
65
+ By default it uses the supplied Spree::Calculator::DollarAmountSalePriceCalculator which essentially just returns the
66
+ value you give it as the sale price.
67
+
68
+ You can also give a certain percentage off by specifying that you want to use Spree::Calculator::PercentOffSalePriceCalculator.
69
+ Note that the percentage is given as a float between 0 and 1, not the integer amount from 0 to 100.
70
+ ```ruby
71
+ product.put_on_sale 0.2, "Spree::Calculator::PercentOffSalePriceCalculator"
72
+ puts product.price.to_f # => 16.0
73
+ ```
74
+
75
+ This extension gives you all of the below methods on both your Products and Variants. If accessed on the Product when reading values,
76
+ it will return values from your Master variant. If accessed on the Product when writing values, it will by default update
77
+ all variants including the master variants. If you change the all_variants parameter to false, it will only then write to
78
+ the master variant and leave the other variants untouched.
79
+
80
+ **price** Returns the sale price if currently on sale, the original price if not
81
+
82
+ **sale_price** Returns the sale price if currently on sale, nil if not
83
+
84
+ **original_price** Always returns the original price
85
+
86
+ **on_sale?** Return a boolean indication if it is currently on sale (enabled is set to true and we are currently within the active date range)
87
+
88
+ **put\_on\_sale(value[, ...])** Put this item on sale (see below sections for options and more information)
89
+
90
+ **create_sale** Alias of ```put_on_sale```
91
+
92
+ **active_sale** Returns the currently active sale (Spree::SalePrice object) that price and sale_price will use. If there is more than one potentially active sale, the one with the latest created_at timestamp is used. See the section on "Multiple active sales" for the reasoning behind that.
93
+
94
+ **current_sale** Alias of ```active_sale```
95
+
96
+ **next_active_sale** Currently returns the latest created Spree::SalePrice object (active or not.) The name is kind of misleading so it should probably be changed. We may also want to make this only return the latest created inactive sale, since that's kind of the original intention of it to be used inside of ```enable_sale``` and ```start_sale```. Needs more thought.
97
+
98
+ **next_current_sale** Alias of ```next_active_sale```
99
+
100
+ **enable_sale(all_variants = true)** Enable the sale returned by ```next_active_sale``` by setting that Spree::SalePrice object's enabled flag to true. Does not change the start and end dates so it does not necessary mean that the sale will then become active. Therefore, you can enable a sale in this manner and still have it not take effect on the site. (Use ```start_sale``` for that) _Note:_ The all_variants flag is only available on Spree::Product (not on Spree::Variant)
101
+
102
+ **disable_sale(all_variants = true)** Disable the sale returned by ```active_sale``` by setting that Spree::SalePrice object's enabled flag to false. This always makes the sale inactive, regardless of the date range. _Note:_ The all_variants flag is only available on Spree::Product (not on Spree::Variant)
103
+
104
+ **start_sale(end_time = nil, all_variants = true)** Start the sale returned by ```next_active_sale``` (and make it active) by setting that Spree::SalePrice object's enabled flag to true and ensuring that the current time is in between the start and end dates. _Note:_ The all_variants flag is only available on Spree::Product (not on Spree::Variant)
105
+
106
+ **stop_sale(all_variants = true)** Stop the sale returned by ```active_sale``` by setting that Spree::SalePrice object's enabled flag to false and the end date to the current time. _Note:_ The all_variants flag is only available on Spree::Product (not on Spree::Variant)
107
+
108
+ Since you have these methods available to both your products and variants, it is possible to put the product and all
109
+ variants on sale or just particular variants. See the explanation of put\_on\_sale below for more information.
110
+
111
+
112
+ Options for put\_on\_sale (create_sale)
113
+ ---------------------------------------
114
+ ```ruby
115
+ put_on_sale(value, calculator_type = "Spree::Calculator::DollarAmountSalePriceCalculator", all_variants = true, start_at = Time.now, end_at = nil, enabled = true)
116
+ ```
117
+ **value** (_float_)
118
+
119
+ This is either the sale price that you want to sell the product for (if using the default DollarAmountSalePriceCalculator)
120
+ or the float representation of the percentage off of the original price (between 0 and 1)
121
+
122
+ **calculator_type** (_string_) - Default: **"Spree::Calculator::DollarAmountSalePriceCalculator"**
123
+
124
+ Specify which calculator to use for determining the sale price. The default calculator will take the value as is and use it
125
+ as the sale price. You can also pass in another calculator value to determine the sale price differently, such as the
126
+ provided "Spree::Calculator::PercentOffSalePriceCalculator", which will take a given percentage off of the original
127
+ price.
128
+
129
+ **all_variants** (_boolean_) - Default: **true**
130
+
131
+ _Only for Spree::Product_. By default it set all of variants (including the master variant) for the product on sale. If you change this value to false
132
+ it will only put the master variant on sale. Only change this if you know the implications.
133
+
134
+ **start_at** (_DateTime or nil_) - Default: **Time.now**
135
+
136
+ Specify the date and time that the sale takes effect. By default it uses the current time. It can also be nil but it's not
137
+ recommended because for future reporting reasons you will probably want to know exactly when the sale started.
138
+
139
+ **end_at** (_DateTime or nil_) - Default: **nil**
140
+
141
+ Specify the end date of the sale or nil to keep the sale running indefinitely. For future reporting reasons it's recommended
142
+ to set this at the time you decide to deactivate the sale rather than just setting enabled to false.
143
+
144
+ **enabled** (_boolean_) - Default: **true**
145
+
146
+ Disable this sale temporarily by setting this to false (overrides the start_at and end_at range). It's not recommended to
147
+ use this to stop the sale when you decide to end it because it could impact future reporting needs. It's mainly intended
148
+ to keep the sale disabled while you are still working on it and it isn't quite ready, or if you need to disable temporarily
149
+ for some reason in the middle of a sale.
150
+
151
+ Multiple active sales
152
+ ---------------------
153
+
154
+ Technically you can have more than one active sale at a time. However, because
155
+ Solidus is going to use product.price or
156
+ variant.price throughout (with no additional parameters or means to identify a particular sale), we have to consistently
157
+ work with a single sale price so that the customer is always charged the same price as they see on the site. What we do then
158
+ is always take the last created sale price. We do this so that if you want to temporarily make a new sale to override a
159
+ currently running one, you just add a new active sale. Then when that new sale ends, the old sale will be in effect again
160
+ (provided it's still active, of course.) So you can add more than one active sale but only one will actually be used at
161
+ a given time.
162
+
163
+ Testing
164
+ -------
165
+
166
+ Tests are in progress, so there aren't any yet. I know, TDD, blah blah blah.
167
+
168
+ Be sure to bundle your dependencies and then create a dummy test app for the specs to run against.
169
+ ```sh
170
+ $ bundle
171
+ $ bundle exec rake test app
172
+ $ bundle exec rspec spec
173
+ ```
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/extension_rake'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default do
10
+ if Dir["spec/dummy"].empty?
11
+ Rake::Task[:test_app].invoke
12
+ Dir.chdir("../../")
13
+ end
14
+ Rake::Task[:spec].invoke
15
+ end
16
+
17
+ desc 'Generates a dummy app for testing'
18
+ task :test_app do
19
+ ENV['LIB_NAME'] = 'solidus_sale_pricing'
20
+ Rake::Task['extension:test_app'].invoke
21
+ end
@@ -0,0 +1,2 @@
1
+ // Placeholder manifest file.
2
+ // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/backend/all.js'
@@ -0,0 +1,2 @@
1
+ // Placeholder manifest file.
2
+ // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js'
@@ -0,0 +1,13 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css'
4
+ */
5
+
6
+ table th .actions [class*='fa-'].stop:hover, td .actions [class*='fa-'].stop:hover {
7
+ background-color: #C60F13;
8
+ color: #FFFFFF;
9
+ }
10
+
11
+ table th .actions [class*='fa-'].stop:active, td .actions [class*='fa-'].stop:active {
12
+ color: #C60F13;
13
+ }
@@ -0,0 +1,4 @@
1
+ /*
2
+ Placeholder manifest file.
3
+ the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css'
4
+ */
@@ -0,0 +1,10 @@
1
+ module Spree
2
+ module Admin
3
+ ProductsController.class_eval do
4
+ def create_sale
5
+ # if @product.create_sale()
6
+ # @relation_types = Spree::Product.relation_types
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,73 @@
1
+ module Spree
2
+ module Admin
3
+ class SalePricesController < ResourceController
4
+ before_action :load_data
5
+ before_action :load_sale_price, only: [:update, :destroy]
6
+
7
+ def index
8
+ @sale_prices = @product.sale_prices
9
+ end
10
+
11
+ def create
12
+ begin
13
+ @product.create_sale(sale_price_params)
14
+ flash[:success] = Spree.t(:sale_price_successfully_created)
15
+ redirect_to admin_product_sale_prices_path(@product)
16
+ rescue => e
17
+ flash[:error] = Spree.t(:error_on_create)
18
+ render :new
19
+ end
20
+ end
21
+
22
+ def update
23
+ if @sale_price.update(sale_price_params)
24
+ flash.now[:success] = Spree.t(:sale_price_successfully_updated)
25
+ else
26
+ flash.now[:error] = Spree.t(:error_on_update)
27
+ end
28
+
29
+ render :edit
30
+ end
31
+
32
+ def stop
33
+ if @product.stop_sale
34
+ flash[:success] = Spree.t(:sale_price_stopped)
35
+
36
+ respond_with(@product) do |format|
37
+ format.html { redirect_to admin_product_sale_prices_path(@product) }
38
+ format.js { redirect_to admin_product_sale_prices_path(@product) }
39
+ end
40
+ end
41
+ end
42
+
43
+ def enable
44
+ if @product.enable_sale
45
+ flash[:success] = Spree.t(:sale_price_enabled)
46
+
47
+ respond_with(@product) do |format|
48
+ format.html { redirect_to admin_product_sale_prices_path(@product) }
49
+ format.js { redirect_to admin_product_sale_prices_path(@product) }
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def sale_price_params
57
+ params.require(:sale_price).permit(permitted_sale_price_attributes)
58
+ end
59
+
60
+ def permitted_sale_price_attributes
61
+ [ :value, :start_at, :end_at, :caclulator_type ]
62
+ end
63
+
64
+ def load_data
65
+ @product ||= Spree::Product.friendly.find(params[:product_id])
66
+ end
67
+
68
+ def load_sale_price
69
+ @sale_price ||= Spree::SalePrice.find(params[:id])
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,21 @@
1
+ Spree::BaseHelper.class_eval do
2
+ def display_original_price(product_or_variant)
3
+ product_or_variant.original_price_in(current_currency).display_price.to_html
4
+ end
5
+
6
+ def display_discount_percent(product_or_variant, append_text = 'Off')
7
+ discount = product_or_variant.discount_percent_in current_currency
8
+
9
+ # number_to_percentage(discount, precision: 0).to_html
10
+
11
+ if discount > 0
12
+ return "#{number_to_percentage(discount, precision: 0).to_html} #{append_text}"
13
+ else
14
+ return ''
15
+ end
16
+ end
17
+
18
+ def format_date date
19
+ date.strftime('%Y-%m-%dT%H:%M:%S') if date
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ module Spree
2
+ class Calculator::DollarAmountSalePriceCalculator < Spree::Calculator
3
+ # TODO validate that the sale price is less than the original price
4
+ def self.description
5
+ 'Calculates the sale price for a Variant by returning the provided fixed sale price'
6
+ end
7
+
8
+ def compute(sale_price)
9
+ sale_price.value
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Spree
2
+ class Calculator::PercentOffSalePriceCalculator < Spree::Calculator
3
+ # TODO validate that the sale price is between 0 and 1
4
+ def self.description
5
+ 'Calculates the sale price for a Variant by taking off a percentage of the original price'
6
+ end
7
+
8
+ def compute(sale_price)
9
+ (1.0 - sale_price.value.to_f) * sale_price.variant.original_price.to_f
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,93 @@
1
+ Spree::Price.class_eval do
2
+ has_many :sale_prices
3
+
4
+ # TODO also accept a class reference for calculator type instead of only a string
5
+ def put_on_sale(attrs={})
6
+ new_sale(attrs).save!
7
+ end
8
+ alias :create_sale :put_on_sale
9
+
10
+ def new_sale(attrs={})
11
+ sale_price = sale_prices.new({
12
+ value: attrs[:value],
13
+ start_at: attrs[:start_at] || Time.now,
14
+ end_at: attrs[:end_at],
15
+ enabled: attrs[:enabled] || true
16
+ })
17
+ sale_price.calculator_type = attrs[:calculator_type] || 'Spree::Calculator::DollarAmountSalePriceCalculator'
18
+ sale_price
19
+ end
20
+
21
+ def active_sale
22
+ on_sale? ? first_sale(sale_prices.active) : nil
23
+ end
24
+ alias :current_sale :active_sale
25
+
26
+ def next_active_sale
27
+ sale_prices.present? ? first_sale(sale_prices) : nil
28
+ end
29
+ alias :next_current_sale :next_active_sale
30
+
31
+ def sale_price
32
+ on_sale? ? active_sale.price : nil
33
+ end
34
+
35
+ def sale_price=(value)
36
+ on_sale? ? active_sale.update_attribute(:value, value) : put_on_sale(value)
37
+ end
38
+
39
+ def discount_percent
40
+ on_sale? ? (1 - (sale_price / original_price)) * 100 : 0.0
41
+ end
42
+
43
+ def on_sale?
44
+ sale_prices.active.present? && first_sale(sale_prices.active).value != original_price
45
+ end
46
+
47
+ def original_price
48
+ self[:amount]
49
+ end
50
+
51
+ def original_price=(value)
52
+ self.price = value
53
+ end
54
+
55
+ def price
56
+ on_sale? ? sale_price : original_price
57
+ end
58
+
59
+ def amount
60
+ price
61
+ end
62
+
63
+ def enable_sale
64
+ return nil unless next_active_sale.present?
65
+ next_active_sale.enable
66
+ end
67
+
68
+ def disable_sale
69
+ return nil unless active_sale.present?
70
+ active_sale.disable
71
+ end
72
+
73
+ def start_sale(end_time = nil)
74
+ return nil unless next_active_sale.present?
75
+ next_active_sale.start(end_time)
76
+ end
77
+
78
+ def stop_sale
79
+ return nil unless active_sale.present?
80
+ active_sale.stop
81
+ end
82
+
83
+ def update_sale(attrs)
84
+ return nil unless active_sale.present?
85
+ active_sale.update(attrs)
86
+ end
87
+
88
+ private
89
+
90
+ def first_sale(scope)
91
+ scope.order("created_at DESC").first
92
+ end
93
+ end