spree_core 3.4.6 → 3.5.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/app/assets/javascripts/spree.js.coffee +1 -1
- data/app/helpers/spree/base_helper.rb +4 -0
- data/app/models/concerns/spree/named_type.rb +1 -1
- data/app/models/concerns/spree/user_methods.rb +21 -4
- data/app/models/concerns/spree/user_reporting.rb +2 -2
- data/app/models/spree/address.rb +6 -12
- data/app/models/spree/adjustable/adjustments_updater.rb +2 -1
- data/app/models/spree/country.rb +2 -1
- data/app/models/spree/line_item.rb +8 -2
- data/app/models/spree/log_entry.rb +1 -1
- data/app/models/spree/order.rb +8 -6
- data/app/models/spree/order/checkout.rb +1 -0
- data/app/models/spree/order_contents.rb +20 -12
- data/app/models/spree/order_inventory.rb +24 -12
- data/app/models/spree/payment/processing.rb +2 -2
- data/app/models/spree/preferences/preferable.rb +1 -1
- data/app/models/spree/product/scopes.rb +1 -1
- data/app/models/spree/promotion.rb +15 -1
- data/app/models/spree/promotion/rules/option_value.rb +13 -5
- data/app/models/spree/promotion/rules/product.rb +2 -1
- data/app/models/spree/promotion/rules/taxon.rb +3 -1
- data/app/models/spree/promotion_action_line_item.rb +3 -0
- data/app/models/spree/promotion_handler/promotion_duplicator.rb +52 -0
- data/app/models/spree/refund.rb +1 -1
- data/app/models/spree/reimbursement.rb +1 -1
- data/app/models/spree/reimbursement/reimbursement_type_engine.rb +7 -18
- data/app/models/spree/reimbursement_performer.rb +3 -7
- data/app/models/spree/reimbursement_type/original_payment.rb +2 -2
- data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +3 -7
- data/app/models/spree/reimbursement_type/store_credit.rb +2 -10
- data/app/models/spree/shipment.rb +10 -4
- data/app/models/spree/stock/availability_validator.rb +1 -1
- data/app/models/spree/stock/packer.rb +1 -1
- data/app/models/spree/stock/splitter/backordered.rb +5 -7
- data/app/models/spree/stock/splitter/base.rb +1 -0
- data/app/models/spree/stock/splitter/shipping_category.rb +9 -16
- data/app/models/spree/stock/splitter/weight.rb +18 -20
- data/app/models/spree/stock_transfer.rb +2 -1
- data/app/models/spree/store_credit_category.rb +13 -0
- data/app/models/spree/taxon.rb +7 -0
- data/app/models/spree/variant.rb +1 -1
- data/app/validators/email_validator.rb +7 -0
- data/config/locales/en.yml +18 -27
- data/db/default/spree/states.rb +9 -27
- data/db/migrate/20150128032538_remove_environment_from_tracker.rb +2 -0
- data/db/migrate/20171004223836_remove_icon_from_taxons.rb +8 -0
- data/db/migrate/20180222133746_add_unique_index_on_spree_promotions_code.rb +6 -0
- data/lib/generators/spree/dummy_model/dummy_model_generator.rb +23 -0
- data/lib/generators/spree/dummy_model/templates/migration.rb.tt +10 -0
- data/lib/generators/spree/dummy_model/templates/model.rb.tt +6 -0
- data/lib/spree/core/controller_helpers/auth.rb +1 -1
- data/lib/spree/core/controller_helpers/common.rb +4 -0
- data/lib/spree/core/controller_helpers/order.rb +6 -5
- data/lib/spree/core/engine.rb +10 -10
- data/lib/spree/core/environment_extension.rb +3 -0
- data/lib/spree/core/importer/order.rb +1 -1
- data/lib/spree/core/validators/email.rb +1 -0
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/money.rb +1 -5
- data/lib/spree/permitted_attributes.rb +1 -1
- data/lib/spree/testing_support/capybara_ext.rb +16 -13
- data/lib/spree/testing_support/common_rake.rb +4 -1
- data/lib/spree/testing_support/factories/inventory_unit_factory.rb +7 -0
- data/lib/spree/testing_support/factories/taxon_factory.rb +1 -1
- data/spree_core.gemspec +1 -1
- data/vendor/assets/javascripts/jsuri.js +458 -2
- metadata +13 -7
- data/app/models/spree/tracker.rb +0 -25
- data/lib/spree/testing_support/factories/tracker_factory.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2114cd601e774d50896839767298308294adc60e
|
4
|
+
data.tar.gz: be32dd8365af3332edbf8b878493bb6787b66f8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e95baff0a25bbff9bd76cb6952303234735a063f32f6c1df521fe75f7eb8cea28b5fff67bc92da889051b70530798051afaa08c4bfdbea025fa659b2024d0149
|
7
|
+
data.tar.gz: 44cdda9fbf073099a0b588170ccaa2f49e35065d58c582826657d74d4e1b4b65a7fc3df4b2d6e650da5669bc9790e57b836766b91e73ee3b5f3c10daf6b5d1db
|
@@ -4,7 +4,7 @@ module Spree
|
|
4
4
|
|
5
5
|
included do
|
6
6
|
scope :active, -> { where(active: true) }
|
7
|
-
default_scope { order("LOWER(#{table_name}.name)") }
|
7
|
+
default_scope { order(Arel.sql("LOWER(#{table_name}.name)")) }
|
8
8
|
|
9
9
|
validates :name, presence: true, uniqueness: { case_sensitive: false }
|
10
10
|
end
|
@@ -9,7 +9,11 @@ module Spree
|
|
9
9
|
included do
|
10
10
|
# we need to have this callback before any dependent: :destroy associations
|
11
11
|
# https://github.com/rails/rails/issues/3458
|
12
|
+
before_validation :clone_billing_address, if: :use_billing?
|
12
13
|
before_destroy :check_completed_orders
|
14
|
+
after_destroy :nullify_approver_id_in_approved_orders
|
15
|
+
|
16
|
+
attr_accessor :use_billing
|
13
17
|
|
14
18
|
has_many :role_users, class_name: 'Spree::RoleUser', foreign_key: :user_id, dependent: :destroy
|
15
19
|
has_many :spree_roles, through: :role_users, class_name: 'Spree::Role', source: :role
|
@@ -39,10 +43,6 @@ module Spree
|
|
39
43
|
first
|
40
44
|
end
|
41
45
|
|
42
|
-
def analytics_id
|
43
|
-
id
|
44
|
-
end
|
45
|
-
|
46
46
|
def total_available_store_credit
|
47
47
|
store_credits.reload.to_a.sum(&:amount_remaining)
|
48
48
|
end
|
@@ -52,5 +52,22 @@ module Spree
|
|
52
52
|
def check_completed_orders
|
53
53
|
raise Spree::Core::DestroyWithOrdersError if orders.complete.present?
|
54
54
|
end
|
55
|
+
|
56
|
+
def nullify_approver_id_in_approved_orders
|
57
|
+
Spree::Order.where(approver_id: id).update_all(approver_id: nil)
|
58
|
+
end
|
59
|
+
|
60
|
+
def clone_billing_address
|
61
|
+
if bill_address && ship_address.nil?
|
62
|
+
self.ship_address = bill_address.clone
|
63
|
+
else
|
64
|
+
ship_address.attributes = bill_address.attributes.except('id', 'updated_at', 'created_at')
|
65
|
+
end
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
def use_billing?
|
70
|
+
use_billing.in?([true, 'true', '1'])
|
71
|
+
end
|
55
72
|
end
|
56
73
|
end
|
data/app/models/spree/address.rb
CHANGED
@@ -10,6 +10,10 @@ module Spree
|
|
10
10
|
'TO', 'TV', 'UG', 'AE', 'VU', 'YE', 'ZW'
|
11
11
|
].freeze
|
12
12
|
|
13
|
+
# we're not freezing this on purpose so developers can extend and manage
|
14
|
+
# those attributes depending of the logic of their applications
|
15
|
+
EXCLUDED_KEYS_FOR_COMPARISION = %w(id updated_at created_at)
|
16
|
+
|
13
17
|
belongs_to :country, class_name: 'Spree::Country'
|
14
18
|
belongs_to :state, class_name: 'Spree::State', optional: true
|
15
19
|
|
@@ -31,12 +35,7 @@ module Spree
|
|
31
35
|
self.whitelisted_ransackable_attributes = %w[firstname lastname company]
|
32
36
|
|
33
37
|
def self.build_default
|
34
|
-
country
|
35
|
-
Spree::Country.find(Spree::Config[:default_country_id])
|
36
|
-
rescue
|
37
|
-
Spree::Country.first
|
38
|
-
end
|
39
|
-
new(country: country)
|
38
|
+
new(country: Spree::Country.default)
|
40
39
|
end
|
41
40
|
|
42
41
|
def self.default(user = nil, kind = 'bill')
|
@@ -47,11 +46,6 @@ module Spree
|
|
47
46
|
end
|
48
47
|
end
|
49
48
|
|
50
|
-
# Can modify an address if it's not been used in an order (but checkouts controller has finer control)
|
51
|
-
# def editable?
|
52
|
-
# new_record? || (shipments.empty? && checkouts.empty?)
|
53
|
-
# end
|
54
|
-
|
55
49
|
def full_name
|
56
50
|
"#{firstname} #{lastname}".strip
|
57
51
|
end
|
@@ -62,7 +56,7 @@ module Spree
|
|
62
56
|
|
63
57
|
def same_as?(other)
|
64
58
|
return false if other.nil?
|
65
|
-
attributes.except(
|
59
|
+
attributes.except(*EXCLUDED_KEYS_FOR_COMPARISION) == other.attributes.except(*EXCLUDED_KEYS_FOR_COMPARISION)
|
66
60
|
end
|
67
61
|
|
68
62
|
alias same_as same_as?
|
@@ -7,10 +7,11 @@ module Spree
|
|
7
7
|
|
8
8
|
def initialize(adjustable)
|
9
9
|
@adjustable = adjustable
|
10
|
-
adjustable.reload if shipment? && adjustable.persisted?
|
10
|
+
adjustable.reload if shipment? && adjustable && adjustable.persisted?
|
11
11
|
end
|
12
12
|
|
13
13
|
def update
|
14
|
+
return unless @adjustable
|
14
15
|
return unless @adjustable.persisted?
|
15
16
|
|
16
17
|
totals = {
|
data/app/models/spree/country.rb
CHANGED
@@ -18,7 +18,8 @@ module Spree
|
|
18
18
|
|
19
19
|
def self.default
|
20
20
|
country_id = Spree::Config[:default_country_id]
|
21
|
-
|
21
|
+
default = find_by(id: country_id) if country_id.present?
|
22
|
+
default || find_by(iso: 'US') || first
|
22
23
|
end
|
23
24
|
|
24
25
|
def <=>(other)
|
@@ -23,7 +23,7 @@ module Spree
|
|
23
23
|
validates_with Stock::AvailabilityValidator
|
24
24
|
validate :ensure_proper_currency, if: -> { order.present? }
|
25
25
|
|
26
|
-
before_destroy :
|
26
|
+
before_destroy :verify_order_inventory_before_destroy, if: -> { order.has_checkout_step?('delivery') }
|
27
27
|
|
28
28
|
before_destroy :destroy_inventory_units
|
29
29
|
|
@@ -116,7 +116,9 @@ module Spree
|
|
116
116
|
def update_price_from_modifier(currency, opts)
|
117
117
|
if currency
|
118
118
|
self.currency = currency
|
119
|
-
|
119
|
+
# variant.price_in(currency).amount can be nil if
|
120
|
+
# there's no price for this currency
|
121
|
+
self.price = (variant.price_in(currency).amount || 0) +
|
120
122
|
variant.price_modifier_amount_in(currency, opts)
|
121
123
|
else
|
122
124
|
self.price = variant.price +
|
@@ -131,6 +133,10 @@ module Spree
|
|
131
133
|
end
|
132
134
|
|
133
135
|
def verify_order_inventory
|
136
|
+
Spree::OrderInventory.new(order, self).verify(target_shipment, is_updated: true)
|
137
|
+
end
|
138
|
+
|
139
|
+
def verify_order_inventory_before_destroy
|
134
140
|
Spree::OrderInventory.new(order, self).verify(target_shipment)
|
135
141
|
end
|
136
142
|
|
@@ -4,7 +4,7 @@ module Spree
|
|
4
4
|
|
5
5
|
# Fix for #1767
|
6
6
|
# If a payment fails, we want to make sure we keep the record of it failing
|
7
|
-
after_rollback :save_anyway
|
7
|
+
after_rollback :save_anyway, if: proc { !Rails.env.test? }
|
8
8
|
|
9
9
|
def save_anyway
|
10
10
|
Spree::LogEntry.create!(source: source, details: details)
|
data/app/models/spree/order.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'spree/core/validators/email'
|
2
1
|
require 'spree/order/checkout'
|
3
2
|
|
4
3
|
module Spree
|
@@ -51,7 +50,7 @@ module Spree
|
|
51
50
|
remove_transition from: :delivery, to: :confirm
|
52
51
|
end
|
53
52
|
|
54
|
-
self.whitelisted_ransackable_associations = %w[shipments user promotions bill_address ship_address line_items]
|
53
|
+
self.whitelisted_ransackable_associations = %w[shipments user promotions bill_address ship_address line_items store]
|
55
54
|
self.whitelisted_ransackable_attributes = %w[completed_at email number state payment_state shipment_state total considered_risky]
|
56
55
|
|
57
56
|
attr_reader :coupon_code
|
@@ -155,7 +154,7 @@ module Spree
|
|
155
154
|
scope :incomplete, -> { where(completed_at: nil) }
|
156
155
|
|
157
156
|
# shows completed orders first, by their completed_at date, then uncompleted orders by their created_at
|
158
|
-
scope :reverse_chronological, -> { order('spree_orders.completed_at IS NULL', completed_at: :desc, created_at: :desc) }
|
157
|
+
scope :reverse_chronological, -> { order(Arel.sql('spree_orders.completed_at IS NULL'), completed_at: :desc, created_at: :desc) }
|
159
158
|
|
160
159
|
# Use this method in other gems that wish to register their own custom logic
|
161
160
|
# that should be called after Order#update
|
@@ -314,6 +313,10 @@ module Spree
|
|
314
313
|
Spree::TaxRate.adjust(self, shipments) if shipments.any?
|
315
314
|
end
|
316
315
|
|
316
|
+
def create_shipment_tax_charge!
|
317
|
+
Spree::TaxRate.adjust(self, shipments) if shipments.any?
|
318
|
+
end
|
319
|
+
|
317
320
|
def update_line_item_prices!
|
318
321
|
transaction do
|
319
322
|
line_items.each(&:update_price)
|
@@ -500,9 +503,8 @@ module Spree
|
|
500
503
|
|
501
504
|
def apply_free_shipping_promotions
|
502
505
|
Spree::PromotionHandler::FreeShipping.new(self).activate
|
503
|
-
shipments.each { |shipment| Adjustable::AdjustmentsUpdater.update(shipment) }
|
504
|
-
|
505
|
-
persist_totals
|
506
|
+
shipments.each { |shipment| Spree::Adjustable::AdjustmentsUpdater.update(shipment) }
|
507
|
+
update_with_updater!
|
506
508
|
end
|
507
509
|
|
508
510
|
# Clean shipments and make order back to address state
|
@@ -100,6 +100,7 @@ module Spree
|
|
100
100
|
before_transition to: :delivery, do: :create_proposed_shipments
|
101
101
|
before_transition to: :delivery, do: :ensure_available_shipping_rates
|
102
102
|
before_transition to: :delivery, do: :set_shipments_cost
|
103
|
+
before_transition to: :delivery, do: :create_shipment_tax_charge!
|
103
104
|
before_transition from: :delivery, do: :apply_free_shipping_promotions
|
104
105
|
end
|
105
106
|
|
@@ -8,19 +8,25 @@ module Spree
|
|
8
8
|
|
9
9
|
def add(variant, quantity = 1, options = {})
|
10
10
|
timestamp = Time.current
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
ActiveRecord::Base.transaction do
|
12
|
+
line_item = add_to_line_item(variant, quantity, options)
|
13
|
+
options[:line_item_created] = true if timestamp <= line_item.created_at
|
14
|
+
after_add_or_remove(line_item, options)
|
15
|
+
end
|
14
16
|
end
|
15
17
|
|
16
18
|
def remove(variant, quantity = 1, options = {})
|
17
|
-
|
18
|
-
|
19
|
+
ActiveRecord::Base.transaction do
|
20
|
+
line_item = remove_from_line_item(variant, quantity, options)
|
21
|
+
after_add_or_remove(line_item, options)
|
22
|
+
end
|
19
23
|
end
|
20
24
|
|
21
25
|
def remove_line_item(line_item, options = {})
|
22
|
-
|
23
|
-
|
26
|
+
ActiveRecord::Base.transaction do
|
27
|
+
line_item.destroy!
|
28
|
+
after_add_or_remove(line_item, options)
|
29
|
+
end
|
24
30
|
end
|
25
31
|
|
26
32
|
def update_cart(params)
|
@@ -29,11 +35,13 @@ module Spree
|
|
29
35
|
# Update totals, then check if the order is eligible for any cart promotions.
|
30
36
|
# If we do not update first, then the item total will be wrong and ItemTotal
|
31
37
|
# promotion rules would not be triggered.
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
38
|
+
ActiveRecord::Base.transaction do
|
39
|
+
persist_totals
|
40
|
+
PromotionHandler::Cart.new(order).activate
|
41
|
+
order.ensure_updated_shipments
|
42
|
+
order.payments.store_credits.checkout.destroy_all
|
43
|
+
persist_totals
|
44
|
+
end
|
37
45
|
true
|
38
46
|
else
|
39
47
|
false
|
@@ -17,17 +17,19 @@ module Spree
|
|
17
17
|
# In case shipment is passed the stock location should only unstock or
|
18
18
|
# restock items if the order is completed. That is so because stock items
|
19
19
|
# are always unstocked when the order is completed through +shipment.finalize+
|
20
|
-
def verify(shipment = nil)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
20
|
+
def verify(shipment = nil, is_updated: false)
|
21
|
+
return unless order.completed? || shipment.present?
|
22
|
+
|
23
|
+
units_count = inventory_units.reload.sum(&:quantity)
|
24
|
+
line_item_changed = is_updated ? !line_item.saved_changes? : !line_item.changed?
|
25
|
+
|
26
|
+
if units_count < line_item.quantity
|
27
|
+
quantity = line_item.quantity - units_count
|
28
|
+
|
29
|
+
shipment = determine_target_shipment unless shipment
|
30
|
+
add_to_shipment(shipment, quantity)
|
31
|
+
elsif (units_count > line_item.quantity) || (units_count == line_item.quantity && line_item_changed)
|
32
|
+
remove(units_count, shipment)
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
@@ -92,6 +94,7 @@ module Spree
|
|
92
94
|
shipment_units = shipment.inventory_units_for_item(line_item, variant).reject(&:shipped?).sort_by(&:state)
|
93
95
|
|
94
96
|
removed_quantity = 0
|
97
|
+
removed_backordered = 0
|
95
98
|
|
96
99
|
shipment_units.each do |inventory_unit|
|
97
100
|
inventory_unit.quantity.times do
|
@@ -101,6 +104,7 @@ module Spree
|
|
101
104
|
else
|
102
105
|
inventory_unit.destroy
|
103
106
|
end
|
107
|
+
removed_backordered += 1 if inventory_unit.backordered?
|
104
108
|
removed_quantity += 1
|
105
109
|
end
|
106
110
|
inventory_unit.save! if inventory_unit.persisted?
|
@@ -110,7 +114,15 @@ module Spree
|
|
110
114
|
|
111
115
|
# removing this from shipment, and adding to stock_location
|
112
116
|
if order.completed?
|
113
|
-
shipment.stock_location.
|
117
|
+
current_on_hand = shipment.stock_location.count_on_hand(variant)
|
118
|
+
|
119
|
+
if current_on_hand.negative? && current_on_hand.abs < removed_backordered
|
120
|
+
shipment.stock_location.restock_backordered variant, current_on_hand.abs, shipment
|
121
|
+
else
|
122
|
+
shipment.stock_location.restock_backordered variant, removed_backordered, shipment
|
123
|
+
end
|
124
|
+
|
125
|
+
shipment.stock_location.restock variant, removed_quantity - removed_backordered, shipment
|
114
126
|
end
|
115
127
|
|
116
128
|
removed_quantity
|
@@ -29,7 +29,7 @@ module Spree
|
|
29
29
|
# a new pending payment record for the remaining amount to capture later.
|
30
30
|
def capture!(amount = nil)
|
31
31
|
return true if completed?
|
32
|
-
amount ||= money.
|
32
|
+
amount ||= money.money.cents
|
33
33
|
started_processing!
|
34
34
|
protect_from_connection_error do
|
35
35
|
# Standard ActiveMerchant capture usage
|
@@ -114,7 +114,7 @@ module Spree
|
|
114
114
|
|
115
115
|
def gateway_action(source, action, success_state)
|
116
116
|
protect_from_connection_error do
|
117
|
-
response = payment_method.send(action, money.
|
117
|
+
response = payment_method.send(action, money.money.cents,
|
118
118
|
source,
|
119
119
|
gateway_options)
|
120
120
|
handle_response(response, success_state, :failure)
|
@@ -24,7 +24,7 @@
|
|
24
24
|
#
|
25
25
|
# # Typecasting is performed on assignment
|
26
26
|
# s.preferred_temperature = '24'
|
27
|
-
# s.
|
27
|
+
# s.preferred_temperature # => 24
|
28
28
|
#
|
29
29
|
# # Modifications have been made to the .preferences hash
|
30
30
|
# s.preferences #=> {color: 'blue', temperature: 24}
|
@@ -23,7 +23,7 @@ module Spree
|
|
23
23
|
# We should not define price scopes here, as they require something slightly different
|
24
24
|
next if name.to_s.include?('master_price')
|
25
25
|
parts = name.to_s.match(/(.*)_by_(.*)/)
|
26
|
-
scope(name.to_s, -> { order("#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ? 'ASC' : 'DESC'}") })
|
26
|
+
scope(name.to_s, -> { order(Arel.sql("#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ? 'ASC' : 'DESC'}")) })
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
@@ -3,7 +3,7 @@ module Spree
|
|
3
3
|
MATCH_POLICIES = %w(all any)
|
4
4
|
UNACTIVATABLE_ORDER_STATES = ['complete', 'awaiting_return', 'returned']
|
5
5
|
|
6
|
-
attr_reader :eligibility_errors
|
6
|
+
attr_reader :eligibility_errors, :generate_code
|
7
7
|
|
8
8
|
belongs_to :promotion_category, optional: true
|
9
9
|
|
@@ -54,6 +54,12 @@ module Spree
|
|
54
54
|
order && !UNACTIVATABLE_ORDER_STATES.include?(order.state)
|
55
55
|
end
|
56
56
|
|
57
|
+
def generate_code=(generating_code)
|
58
|
+
if ActiveModel::Type::Boolean.new.cast(generating_code)
|
59
|
+
self.code = random_code
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
57
63
|
def expired?
|
58
64
|
!!(starts_at && Time.current < starts_at || expires_at && Time.current > expires_at)
|
59
65
|
end
|
@@ -227,5 +233,13 @@ module Spree
|
|
227
233
|
def expires_at_must_be_later_than_starts_at
|
228
234
|
errors.add(:expires_at, :invalid_date_range) if expires_at < starts_at
|
229
235
|
end
|
236
|
+
|
237
|
+
def random_code
|
238
|
+
coupon_code = loop do
|
239
|
+
random_token = SecureRandom.hex(4)
|
240
|
+
break random_token unless self.class.exists?(code: random_token)
|
241
|
+
end
|
242
|
+
coupon_code
|
243
|
+
end
|
230
244
|
end
|
231
245
|
end
|