spree_core 3.4.6 → 3.5.0.rc1

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.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/app/assets/javascripts/spree.js.coffee +1 -1
  3. data/app/helpers/spree/base_helper.rb +4 -0
  4. data/app/models/concerns/spree/named_type.rb +1 -1
  5. data/app/models/concerns/spree/user_methods.rb +21 -4
  6. data/app/models/concerns/spree/user_reporting.rb +2 -2
  7. data/app/models/spree/address.rb +6 -12
  8. data/app/models/spree/adjustable/adjustments_updater.rb +2 -1
  9. data/app/models/spree/country.rb +2 -1
  10. data/app/models/spree/line_item.rb +8 -2
  11. data/app/models/spree/log_entry.rb +1 -1
  12. data/app/models/spree/order.rb +8 -6
  13. data/app/models/spree/order/checkout.rb +1 -0
  14. data/app/models/spree/order_contents.rb +20 -12
  15. data/app/models/spree/order_inventory.rb +24 -12
  16. data/app/models/spree/payment/processing.rb +2 -2
  17. data/app/models/spree/preferences/preferable.rb +1 -1
  18. data/app/models/spree/product/scopes.rb +1 -1
  19. data/app/models/spree/promotion.rb +15 -1
  20. data/app/models/spree/promotion/rules/option_value.rb +13 -5
  21. data/app/models/spree/promotion/rules/product.rb +2 -1
  22. data/app/models/spree/promotion/rules/taxon.rb +3 -1
  23. data/app/models/spree/promotion_action_line_item.rb +3 -0
  24. data/app/models/spree/promotion_handler/promotion_duplicator.rb +52 -0
  25. data/app/models/spree/refund.rb +1 -1
  26. data/app/models/spree/reimbursement.rb +1 -1
  27. data/app/models/spree/reimbursement/reimbursement_type_engine.rb +7 -18
  28. data/app/models/spree/reimbursement_performer.rb +3 -7
  29. data/app/models/spree/reimbursement_type/original_payment.rb +2 -2
  30. data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +3 -7
  31. data/app/models/spree/reimbursement_type/store_credit.rb +2 -10
  32. data/app/models/spree/shipment.rb +10 -4
  33. data/app/models/spree/stock/availability_validator.rb +1 -1
  34. data/app/models/spree/stock/packer.rb +1 -1
  35. data/app/models/spree/stock/splitter/backordered.rb +5 -7
  36. data/app/models/spree/stock/splitter/base.rb +1 -0
  37. data/app/models/spree/stock/splitter/shipping_category.rb +9 -16
  38. data/app/models/spree/stock/splitter/weight.rb +18 -20
  39. data/app/models/spree/stock_transfer.rb +2 -1
  40. data/app/models/spree/store_credit_category.rb +13 -0
  41. data/app/models/spree/taxon.rb +7 -0
  42. data/app/models/spree/variant.rb +1 -1
  43. data/app/validators/email_validator.rb +7 -0
  44. data/config/locales/en.yml +18 -27
  45. data/db/default/spree/states.rb +9 -27
  46. data/db/migrate/20150128032538_remove_environment_from_tracker.rb +2 -0
  47. data/db/migrate/20171004223836_remove_icon_from_taxons.rb +8 -0
  48. data/db/migrate/20180222133746_add_unique_index_on_spree_promotions_code.rb +6 -0
  49. data/lib/generators/spree/dummy_model/dummy_model_generator.rb +23 -0
  50. data/lib/generators/spree/dummy_model/templates/migration.rb.tt +10 -0
  51. data/lib/generators/spree/dummy_model/templates/model.rb.tt +6 -0
  52. data/lib/spree/core/controller_helpers/auth.rb +1 -1
  53. data/lib/spree/core/controller_helpers/common.rb +4 -0
  54. data/lib/spree/core/controller_helpers/order.rb +6 -5
  55. data/lib/spree/core/engine.rb +10 -10
  56. data/lib/spree/core/environment_extension.rb +3 -0
  57. data/lib/spree/core/importer/order.rb +1 -1
  58. data/lib/spree/core/validators/email.rb +1 -0
  59. data/lib/spree/core/version.rb +1 -1
  60. data/lib/spree/money.rb +1 -5
  61. data/lib/spree/permitted_attributes.rb +1 -1
  62. data/lib/spree/testing_support/capybara_ext.rb +16 -13
  63. data/lib/spree/testing_support/common_rake.rb +4 -1
  64. data/lib/spree/testing_support/factories/inventory_unit_factory.rb +7 -0
  65. data/lib/spree/testing_support/factories/taxon_factory.rb +1 -1
  66. data/spree_core.gemspec +1 -1
  67. data/vendor/assets/javascripts/jsuri.js +458 -2
  68. metadata +13 -7
  69. data/app/models/spree/tracker.rb +0 -25
  70. data/lib/spree/testing_support/factories/tracker_factory.rb +0 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 3186ecc9fcd800b68cd881887ef41fd1512a7b96215d601b013200f1b4c0e3c0
4
- data.tar.gz: ba34e2b2b2ae15e75e45aea3b8d397459f91f2019895de3a8a51d20e059c5c7e
2
+ SHA1:
3
+ metadata.gz: 2114cd601e774d50896839767298308294adc60e
4
+ data.tar.gz: be32dd8365af3332edbf8b878493bb6787b66f8f
5
5
  SHA512:
6
- metadata.gz: fbabac3389a37851c3ca6b4c6814267f6d7117d8212b9035da4b9f5b9202d5b36b3b186b337a473cf80019115baba71236c1398d7599f865d43befd16a8d009f
7
- data.tar.gz: c3bbc7170c4173d76201321e8e5003e502e6b657b817cd21a44e0d324f775a102ef881ecfa6ce906235a7f74717a23eb2cd735b3ead142a817e198a2363a6680
6
+ metadata.gz: e95baff0a25bbff9bd76cb6952303234735a063f32f6c1df521fe75f7eb8cea28b5fff67bc92da889051b70530798051afaa08c4bfdbea025fa659b2024d0149
7
+ data.tar.gz: 44cdda9fbf073099a0b588170ccaa2f49e35065d58c582826657d74d4e1b4b65a7fc3df4b2d6e650da5669bc9790e57b836766b91e73ee3b5f3c10daf6b5d1db
@@ -4,7 +4,7 @@ class window.Spree
4
4
  jQuery(document).ready(callback)
5
5
 
6
6
  # fire ready callbacks also on turbolinks page change event
7
- jQuery(document).on 'page:load', ->
7
+ jQuery(document).on 'page:load turbolinks:load', ->
8
8
  callback(jQuery)
9
9
 
10
10
  @mountedAt: ->
@@ -84,6 +84,10 @@ module Spree
84
84
  v.options_text
85
85
  end
86
86
 
87
+ def frontend_available?
88
+ Spree::Core::Engine.frontend_available?
89
+ end
90
+
87
91
  private
88
92
 
89
93
  def create_product_image_tag(image, product, options, style)
@@ -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
@@ -8,11 +8,11 @@ module Spree
8
8
  end
9
9
 
10
10
  def order_count
11
- BigDecimal(orders.complete.size)
11
+ orders.complete.size
12
12
  end
13
13
 
14
14
  def average_order_value
15
- if order_count.to_i > 0
15
+ if order_count > 0
16
16
  lifetime_value / order_count
17
17
  else
18
18
  BigDecimal('0.00')
@@ -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 = begin
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('id', 'updated_at', 'created_at') == other.attributes.except('id', 'updated_at', 'created_at')
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 = {
@@ -18,7 +18,8 @@ module Spree
18
18
 
19
19
  def self.default
20
20
  country_id = Spree::Config[:default_country_id]
21
- country_id.present? ? find(country_id) : find_by!(iso: 'US')
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 :verify_order_inventory, if: -> { order.has_checkout_step?('delivery') }
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
- self.price = variant.price_in(currency).amount +
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)
@@ -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
- updater.update_shipment_total
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
- line_item = add_to_line_item(variant, quantity, options)
12
- options[:line_item_created] = true if timestamp <= line_item.created_at
13
- after_add_or_remove(line_item, options)
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
- line_item = remove_from_line_item(variant, quantity, options)
18
- after_add_or_remove(line_item, options)
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
- line_item.destroy!
23
- after_add_or_remove(line_item, options)
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
- persist_totals
33
- PromotionHandler::Cart.new(order).activate
34
- order.ensure_updated_shipments
35
- order.payments.store_credits.checkout.destroy_all
36
- persist_totals
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
- if order.completed? || shipment.present?
22
- units_count = inventory_units.reload.sum(&:quantity)
23
- if units_count < line_item.quantity
24
- quantity = line_item.quantity - units_count
25
-
26
- shipment = determine_target_shipment unless shipment
27
- add_to_shipment(shipment, quantity)
28
- elsif (units_count > line_item.quantity) || (units_count == line_item.quantity && !line_item.changed?)
29
- remove(units_count, shipment)
30
- end
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.restock variant, removed_quantity, shipment
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.amount_in_cents
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.amount_in_cents,
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.preferred_color # => 24
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