solidus_core 2.1.1 → 2.2.0.beta1

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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +0 -1
  3. data/app/assets/config/solidus_core_manifest.js +1 -0
  4. data/app/assets/javascripts/spree.js.erb +72 -0
  5. data/app/helpers/spree/store_helper.rb +5 -0
  6. data/app/jobs/spree/promotion_code_batch_job.rb +24 -0
  7. data/app/mailers/spree/promotion_code_batch_mailer.rb +13 -0
  8. data/app/models/concerns/spree/calculated_adjustments.rb +1 -1
  9. data/app/models/concerns/spree/ordered_property_value_list.rb +2 -2
  10. data/app/models/concerns/spree/user_address_book.rb +4 -4
  11. data/app/models/concerns/spree/user_methods.rb +7 -0
  12. data/app/models/concerns/spree/user_payment_source.rb +12 -5
  13. data/app/models/spree/address.rb +14 -3
  14. data/app/models/spree/adjustment.rb +13 -1
  15. data/app/models/spree/app_configuration.rb +0 -19
  16. data/app/models/spree/base.rb +2 -0
  17. data/app/models/spree/credit_card.rb +34 -43
  18. data/app/models/spree/gateway/bogus.rb +1 -1
  19. data/app/models/spree/gateway.rb +6 -4
  20. data/app/models/spree/inventory_unit.rb +3 -2
  21. data/app/models/spree/order/checkout.rb +187 -273
  22. data/app/models/spree/order.rb +137 -71
  23. data/app/models/spree/order_contents.rb +1 -1
  24. data/app/models/spree/order_inventory.rb +11 -11
  25. data/app/models/spree/order_promotion.rb +2 -0
  26. data/app/models/spree/order_update_attributes.rb +1 -8
  27. data/app/models/spree/order_updater.rb +67 -63
  28. data/app/models/spree/payment.rb +0 -1
  29. data/app/models/spree/payment_create.rb +27 -7
  30. data/app/models/spree/payment_method/store_credit.rb +3 -3
  31. data/app/models/spree/payment_method.rb +4 -1
  32. data/app/models/spree/payment_source.rb +45 -0
  33. data/app/models/spree/product/scopes.rb +24 -24
  34. data/app/models/spree/product.rb +4 -4
  35. data/app/models/spree/promotion.rb +2 -0
  36. data/app/models/spree/promotion_code/batch_builder.rb +63 -0
  37. data/app/models/spree/promotion_code.rb +1 -0
  38. data/app/models/spree/promotion_code_batch.rb +25 -0
  39. data/app/models/spree/promotion_handler/cart.rb +2 -2
  40. data/app/models/spree/promotion_handler/coupon.rb +1 -2
  41. data/app/models/spree/promotion_handler/free_shipping.rb +32 -21
  42. data/app/models/spree/promotion_handler/page.rb +1 -1
  43. data/app/models/spree/reimbursement.rb +1 -1
  44. data/app/models/spree/return_authorization.rb +0 -28
  45. data/app/models/spree/return_item.rb +1 -1
  46. data/app/models/spree/shipment.rb +4 -4
  47. data/app/models/spree/shipping_method.rb +2 -2
  48. data/app/models/spree/shipping_rate.rb +1 -1
  49. data/app/models/spree/stock/availability_validator.rb +16 -17
  50. data/app/models/spree/stock/coordinator.rb +3 -3
  51. data/app/models/spree/stock/package.rb +1 -1
  52. data/app/models/spree/stock/quantifier.rb +5 -4
  53. data/app/models/spree/stock_location.rb +2 -2
  54. data/app/models/spree/store.rb +2 -2
  55. data/app/models/spree/store_credit.rb +1 -1
  56. data/app/models/spree/tax/tax_helpers.rb +3 -3
  57. data/app/models/spree/tax_rate.rb +7 -1
  58. data/app/models/spree/taxonomy.rb +1 -1
  59. data/app/models/spree/variant/scopes.rb +5 -5
  60. data/app/models/spree/variant/vat_price_generator.rb +8 -5
  61. data/app/models/spree/variant.rb +1 -0
  62. data/app/models/spree/wallet/add_payment_sources_to_wallet.rb +19 -10
  63. data/app/models/spree/wallet/default_payment_builder.rb +6 -6
  64. data/app/models/spree/wallet.rb +71 -0
  65. data/app/models/spree/wallet_payment_source.rb +17 -0
  66. data/app/models/spree/zone.rb +1 -1
  67. data/app/views/spree/carton_mailer/shipped_email.text.erb +1 -1
  68. data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_errored.text.erb +2 -0
  69. data/app/views/spree/promotion_code_batch_mailer/promotion_code_batch_finished.text.erb +2 -0
  70. data/app/views/spree/reimbursement_mailer/reimbursement_email.html.erb +0 -7
  71. data/app/views/spree/reimbursement_mailer/reimbursement_email.text.erb +0 -5
  72. data/app/views/spree/shared/_error_messages.html.erb +1 -1
  73. data/app/views/spree/shipment_mailer/shipped_email.html.erb +1 -1
  74. data/config/initializers/assets.rb +1 -1
  75. data/config/initializers/friendly_id.rb +1 -1
  76. data/config/locales/en.yml +50 -12
  77. data/db/default/spree/store_credit.rb +2 -1
  78. data/db/migrate/20130826062534_add_depth_to_spree_taxons.rb +4 -6
  79. data/db/migrate/20160420044191_create_spree_wallet_payment_sources.rb +23 -0
  80. data/db/migrate/20160420181916_migrate_credit_cards_to_wallet_payment_sources.rb +26 -0
  81. data/db/migrate/20161017102621_create_spree_promotion_code_batch.rb +36 -0
  82. data/db/migrate/20161129035810_add_index_to_spree_payments_number.rb +5 -0
  83. data/db/migrate/20170223235001_remove_spree_store_credits_column.rb +5 -0
  84. data/lib/generators/spree/dummy/templates/rails/application.rb +1 -1
  85. data/lib/generators/spree/dummy/templates/rails/test.rb +1 -1
  86. data/lib/generators/spree/install/install_generator.rb +6 -5
  87. data/lib/spree/core/controller_helpers/payment_parameters.rb +54 -0
  88. data/lib/spree/core/engine.rb +6 -9
  89. data/lib/spree/core/version.rb +1 -1
  90. data/lib/spree/core.rb +0 -1
  91. data/lib/spree/money.rb +18 -0
  92. data/lib/spree/permission_sets/default_customer.rb +1 -1
  93. data/lib/spree/permitted_attributes.rb +1 -1
  94. data/lib/spree/testing_support/authorization_helpers.rb +1 -0
  95. data/lib/spree/testing_support/capybara_ext.rb +13 -0
  96. data/lib/spree/testing_support/factories/order_factory.rb +5 -1
  97. data/lib/spree/testing_support/factories/payment_factory.rb +1 -1
  98. data/lib/spree/testing_support/factories/shipment_factory.rb +0 -1
  99. data/solidus_core.gemspec +3 -3
  100. data/spec/jobs/promotion_code_batch_job_spec.rb +65 -0
  101. data/spec/lib/calculated_adjustments_spec.rb +105 -1
  102. data/spec/lib/spree/core/testing_support/factories/order_factory_spec.rb +4 -1
  103. data/spec/lib/spree/core/testing_support/factories/payment_factory_spec.rb +8 -0
  104. data/spec/lib/spree/money_spec.rb +32 -0
  105. data/spec/lib/spree/permission_sets/default_customer_spec.rb +20 -0
  106. data/spec/mailers/promotion_code_batch_mailer_spec.rb +45 -0
  107. data/spec/models/spree/credit_card_spec.rb +86 -86
  108. data/spec/models/spree/gateway_spec.rb +3 -1
  109. data/spec/models/spree/inventory_unit_spec.rb +12 -4
  110. data/spec/models/spree/order/checkout_spec.rb +11 -32
  111. data/spec/models/spree/order/tax_spec.rb +2 -2
  112. data/spec/models/spree/order_contents_spec.rb +24 -1
  113. data/spec/models/spree/order_inventory_spec.rb +130 -83
  114. data/spec/models/spree/order_spec.rb +15 -117
  115. data/spec/models/spree/order_update_attributes_spec.rb +1 -44
  116. data/spec/models/spree/order_updater_spec.rb +10 -13
  117. data/spec/models/spree/payment_create_spec.rb +5 -1
  118. data/spec/models/spree/payment_method_spec.rb +16 -0
  119. data/spec/models/spree/payment_spec.rb +14 -8
  120. data/spec/models/spree/promotion_code/batch_builder_spec.rb +61 -0
  121. data/spec/models/spree/promotion_code_batch_spec.rb +58 -0
  122. data/spec/models/spree/promotion_code_spec.rb +4 -0
  123. data/spec/models/spree/promotion_spec.rb +3 -6
  124. data/spec/models/spree/return_authorization_spec.rb +0 -59
  125. data/spec/models/spree/shipment_spec.rb +4 -4
  126. data/spec/models/spree/stock/availability_validator_spec.rb +64 -9
  127. data/spec/models/spree/tax/item_adjuster_spec.rb +1 -2
  128. data/spec/models/spree/unit_cancel_spec.rb +0 -85
  129. data/spec/models/spree/user_spec.rb +3 -1
  130. data/spec/models/spree/variant/vat_price_generator_spec.rb +8 -2
  131. data/spec/models/spree/variant_spec.rb +16 -4
  132. data/spec/models/spree/wallet_payment_source_spec.rb +46 -0
  133. data/spec/models/spree/wallet_spec.rb +128 -0
  134. data/spec/support/concerns/payment_source.rb +64 -0
  135. metadata +51 -25
  136. data/app/assets/javascripts/spree.js.coffee.erb +0 -64
  137. data/app/models/spree/promotion_builder.rb +0 -55
  138. data/app/models/spree/promotion_code/code_builder.rb +0 -62
  139. data/config/initializers/premailer_assets.rb +0 -1
  140. data/lib/spree/core/unreturned_item_charger.rb +0 -106
  141. data/lib/tasks/exchanges.rake +0 -47
  142. data/spec/lib/spree/core/unreturned_item_charger_spec.rb +0 -126
  143. data/spec/lib/tasks/exchanges_spec.rb +0 -220
  144. data/spec/models/spree/promotion_builder_spec.rb +0 -120
  145. data/spec/models/spree/promotion_code/code_builder_spec.rb +0 -77
@@ -5,7 +5,8 @@ module Spree
5
5
  # @param attributes [Hash,ActionController::Parameters] attributes which are assigned to the new payment
6
6
  # * :payment_method_id Id of payment method used for this payment
7
7
  # * :source_attributes Attributes used to build the source of this payment. Usually a {CreditCard}
8
- # * :existing_card_id (Integer) The id of an existing {CreditCard} object to use
8
+ # * :existing_card_id (Integer) Deprecated: The id of an existing {CreditCard} object to use
9
+ # * :wallet_payment_source_id (Integer): The id of a {WalletPaymentSource} to use
9
10
  # @param request_env [Hash] rack env of user creating the payment
10
11
  # @param payment [Payment] Internal use only. Instead of making a new payment, change the attributes for an existing one.
11
12
  def initialize(order, attributes, payment: nil, request_env: {})
@@ -27,7 +28,13 @@ module Spree
27
28
  @payment.attributes = @attributes
28
29
 
29
30
  if source_attributes[:existing_card_id].present?
31
+ Spree::Deprecation.warn(
32
+ "Passing existing_card_id to PaymentCreate is deprecated. Use wallet_payment_source_id instead.",
33
+ caller,
34
+ )
30
35
  build_existing_card
36
+ elsif source_attributes[:wallet_payment_source_id].present?
37
+ build_from_wallet_payment_source
31
38
  else
32
39
  build_source
33
40
  end
@@ -44,27 +51,40 @@ module Spree
44
51
  if source_attributes.present? && payment_method.try(:payment_source_class)
45
52
  payment.source = payment_method.payment_source_class.new(source_attributes)
46
53
  payment.source.payment_method_id = payment_method.id
47
- payment.source.user_id = order.user_id if order
54
+ if order && payment.source.respond_to?(:user=)
55
+ payment.source.user = order.user
56
+ end
48
57
  end
49
58
  end
50
59
 
60
+ def build_from_wallet_payment_source
61
+ wallet_payment_source_id = source_attributes.fetch(:wallet_payment_source_id)
62
+ raise(ActiveRecord::RecordNotFound) if order.user.nil?
63
+ wallet_payment_source = order.user.wallet.find(wallet_payment_source_id)
64
+ raise(ActiveRecord::RecordNotFound) if wallet_payment_source.nil?
65
+ build_from_payment_source(wallet_payment_source.payment_source)
66
+ end
67
+
51
68
  def build_existing_card
52
69
  credit_card = available_cards.find(source_attributes[:existing_card_id])
70
+ build_from_payment_source(credit_card)
71
+ end
53
72
 
73
+ def build_from_payment_source(payment_source)
54
74
  # FIXME: does this work?
55
75
  if source_attributes[:verification_value]
56
- credit_card.verification_value = source_attributes[:verification_value]
76
+ payment_source.verification_value = source_attributes[:verification_value]
57
77
  end
58
78
 
59
- payment.source = credit_card
60
- payment.payment_method_id = credit_card.payment_method_id
79
+ payment.source = payment_source
80
+ payment.payment_method_id = payment_source.payment_method_id
61
81
  end
62
82
 
63
83
  def available_cards
64
84
  if user_id = order.user_id
65
- CreditCard.where(user_id: user_id)
85
+ Spree::CreditCard.where(user_id: user_id)
66
86
  else
67
- CreditCard.none
87
+ Spree::CreditCard.none
68
88
  end
69
89
  end
70
90
  end
@@ -109,7 +109,7 @@ module Spree
109
109
 
110
110
  def handle_action(action, action_name, auth_code)
111
111
  # Find first event with provided auth_code
112
- store_credit = StoreCreditEvent.find_by_authorization_code(auth_code).try(:store_credit)
112
+ store_credit = Spree::StoreCreditEvent.find_by_authorization_code(auth_code).try(:store_credit)
113
113
 
114
114
  if store_credit.nil?
115
115
  ActiveMerchant::Billing::Response.new(false, Spree.t('store_credit.unable_to_find_for_action', auth_code: auth_code, action: action_name), {}, {})
@@ -119,8 +119,8 @@ module Spree
119
119
  end
120
120
 
121
121
  def auth_or_capture_event(auth_code)
122
- capture_event = StoreCreditEvent.find_by(authorization_code: auth_code, action: Spree::StoreCredit::CAPTURE_ACTION)
123
- auth_event = StoreCreditEvent.find_by(authorization_code: auth_code, action: Spree::StoreCredit::AUTHORIZE_ACTION)
122
+ capture_event = Spree::StoreCreditEvent.find_by(authorization_code: auth_code, action: Spree::StoreCredit::CAPTURE_ACTION)
123
+ auth_event = Spree::StoreCreditEvent.find_by(authorization_code: auth_code, action: Spree::StoreCredit::AUTHORIZE_ACTION)
124
124
  capture_event || auth_event
125
125
  end
126
126
  end
@@ -17,7 +17,10 @@ module Spree
17
17
  scope :active, -> { where(active: true) }
18
18
  scope :available_to_users, -> { where(available_to_users: true) }
19
19
  scope :available_to_admin, -> { where(available_to_admin: true) }
20
- scope :available_to_store, -> (store) { (store.present? && store.payment_methods.empty?) ? self : store.payment_methods }
20
+ scope :available_to_store, ->(store) do
21
+ raise ArgumentError, "You must provide a store" if store.nil?
22
+ store.payment_methods.empty? ? all : where(id: store.payment_method_ids)
23
+ end
21
24
 
22
25
  include Spree::Preferences::StaticallyConfigurable
23
26
 
@@ -0,0 +1,45 @@
1
+ module Spree
2
+ class PaymentSource < Spree::Base
3
+ self.abstract_class = true
4
+
5
+ belongs_to :payment_method
6
+
7
+ has_many :payments, as: :source
8
+ has_many :wallet_payment_sources, class_name: 'Spree::WalletPaymentSource', as: :payment_source, inverse_of: :payment_source
9
+
10
+ attr_accessor :imported
11
+
12
+ # @return [Array<String>] the actions available on this payment source
13
+ def actions
14
+ %w(capture void credit)
15
+ end
16
+
17
+ # @param payment [Spree::Payment] the payment we want to know if can be captured
18
+ # @return [Boolean] true when the payment is in the pending or checkout states
19
+ def can_capture?(payment)
20
+ payment.pending? || payment.checkout?
21
+ end
22
+
23
+ # @param payment [Spree::Payment] the payment we want to know if can be voided
24
+ # @return [Boolean] true when the payment is not failed or voided
25
+ def can_void?(payment)
26
+ !payment.failed? && !payment.void?
27
+ end
28
+
29
+ # Indicates whether its possible to credit the payment. Note that most
30
+ # gateways require that the payment be settled first which generally
31
+ # happens within 12-24 hours of the transaction.
32
+ #
33
+ # @param payment [Spree::Payment] the payment we want to know if can be credited
34
+ # @return [Boolean] true when the payment is completed and can be credited
35
+ def can_credit?(payment)
36
+ payment.completed? && payment.credit_allowed > 0
37
+ end
38
+
39
+ # Indicates whether this payment source can be used more than once. E.g. a
40
+ # credit card with a 'payment profile'.
41
+ def reusable?
42
+ false
43
+ end
44
+ end
45
+ end
@@ -46,12 +46,12 @@ module Spree
46
46
  # This scope selects products in taxon AND all its descendants
47
47
  # If you need products only within one taxon use
48
48
  #
49
- # Spree::Product.joins(:taxons).where(Taxon.table_name => { :id => taxon.id })
49
+ # Spree::Product.joins(:taxons).where(Taxon.table_name => { id: taxon.id })
50
50
  #
51
51
  # If you're using count on the result of this scope, you must use the
52
52
  # `:distinct` option as well:
53
53
  #
54
- # Spree::Product.in_taxon(taxon).count(:distinct => true)
54
+ # Spree::Product.in_taxon(taxon).count(distinct: true)
55
55
  #
56
56
  # This is so that the count query is distinct'd:
57
57
  #
@@ -84,12 +84,12 @@ module Spree
84
84
  # note that it can test for properties with NULL values, but not for absent values
85
85
  add_search_scope :with_property_value do |property, value|
86
86
  joins(:properties)
87
- .where("#{ProductProperty.table_name}.value = ?", value)
87
+ .where("#{Spree::ProductProperty.table_name}.value = ?", value)
88
88
  .where(property_conditions(property))
89
89
  end
90
90
 
91
91
  add_search_scope :with_option do |option|
92
- option_types = OptionType.table_name
92
+ option_types = Spree::OptionType.table_name
93
93
  conditions = case option
94
94
  when String then { "#{option_types}.name" => option }
95
95
  when OptionType then { "#{option_types}.id" => option.id }
@@ -100,10 +100,10 @@ module Spree
100
100
  end
101
101
 
102
102
  add_search_scope :with_option_value do |option, value|
103
- option_values = OptionValue.table_name
103
+ option_values = Spree::OptionValue.table_name
104
104
  option_type_id = case option
105
- when String then OptionType.find_by(name: option) || option.to_i
106
- when OptionType then option.id
105
+ when String then Spree::OptionType.find_by(name: option) || option.to_i
106
+ when Spree::OptionType then option.id
107
107
  else option.to_i
108
108
  end
109
109
 
@@ -117,7 +117,7 @@ module Spree
117
117
  add_search_scope :with do |value|
118
118
  includes(variants_including_master: :option_values).
119
119
  includes(:product_properties).
120
- where("#{OptionValue.table_name}.name = ? OR #{ProductProperty.table_name}.value = ?", value, value)
120
+ where("#{Spree::OptionValue.table_name}.name = ? OR #{Spree::ProductProperty.table_name}.value = ?", value, value)
121
121
  end
122
122
 
123
123
  # Finds all products that have a name containing the given words.
@@ -147,38 +147,38 @@ module Spree
147
147
  # there is alternative faster and more elegant solution, it has small drawback though,
148
148
  # it doesn stack with other scopes :/
149
149
  #
150
- # :joins => "LEFT OUTER JOIN (SELECT line_items.variant_id as vid, COUNT(*) as cnt FROM line_items GROUP BY line_items.variant_id) AS popularity_count ON variants.id = vid",
151
- # :order => 'COALESCE(cnt, 0) DESC'
150
+ # joins: "LEFT OUTER JOIN (SELECT line_items.variant_id as vid, COUNT(*) as cnt FROM line_items GROUP BY line_items.variant_id) AS popularity_count ON variants.id = vid",
151
+ # order: 'COALESCE(cnt, 0) DESC'
152
152
  add_search_scope :descend_by_popularity do
153
153
  joins(:master).
154
154
  order(%{
155
155
  COALESCE((
156
156
  SELECT
157
- COUNT(#{LineItem.quoted_table_name}.id)
157
+ COUNT(#{Spree::LineItem.quoted_table_name}.id)
158
158
  FROM
159
- #{LineItem.quoted_table_name}
159
+ #{Spree::LineItem.quoted_table_name}
160
160
  JOIN
161
- #{Variant.quoted_table_name} AS popular_variants
161
+ #{Spree::Variant.quoted_table_name} AS popular_variants
162
162
  ON
163
- popular_variants.id = #{LineItem.quoted_table_name}.variant_id
163
+ popular_variants.id = #{Spree::LineItem.quoted_table_name}.variant_id
164
164
  WHERE
165
- popular_variants.product_id = #{Product.quoted_table_name}.id
165
+ popular_variants.product_id = #{Spree::Product.quoted_table_name}.id
166
166
  ), 0) DESC
167
167
  })
168
168
  end
169
169
 
170
170
  add_search_scope :not_deleted do
171
- where("#{Product.quoted_table_name}.deleted_at IS NULL or #{Product.quoted_table_name}.deleted_at >= ?", Time.current)
171
+ where("#{Spree::Product.quoted_table_name}.deleted_at IS NULL or #{Spree::Product.quoted_table_name}.deleted_at >= ?", Time.current)
172
172
  end
173
173
 
174
174
  # Can't use add_search_scope for this as it needs a default argument
175
175
  def self.available(available_on = nil)
176
- joins(master: :prices).where("#{Product.quoted_table_name}.available_on <= ?", available_on || Time.current)
176
+ joins(master: :prices).where("#{Spree::Product.quoted_table_name}.available_on <= ?", available_on || Time.current)
177
177
  end
178
178
  search_scopes << :available
179
179
 
180
180
  add_search_scope :taxons_name_eq do |name|
181
- group("spree_products.id").joins(:taxons).where(Taxon.arel_table[:name].eq(name))
181
+ group("spree_products.id").joins(:taxons).where(Spree::Taxon.arel_table[:name].eq(name))
182
182
  end
183
183
 
184
184
  def self.distinct_by_product_ids(sort_order = nil)
@@ -210,13 +210,13 @@ module Spree
210
210
  private
211
211
 
212
212
  def price_table_name
213
- Price.quoted_table_name
213
+ Spree::Price.quoted_table_name
214
214
  end
215
215
 
216
216
  # specifically avoid having an order for taxon search (conflicts with main order)
217
217
  def prepare_taxon_conditions(taxons)
218
218
  ids = taxons.map { |taxon| taxon.self_and_descendants.pluck(:id) }.flatten.uniq
219
- joins(:taxons).where("#{Taxon.table_name}.id" => ids)
219
+ joins(:taxons).where("#{Spree::Taxon.table_name}.id" => ids)
220
220
  end
221
221
 
222
222
  # Produce an array of keywords for use in scopes.
@@ -228,14 +228,14 @@ module Spree
228
228
  end
229
229
 
230
230
  def get_taxons(*ids_or_records_or_names)
231
- taxons = Taxon.table_name
231
+ taxons = Spree::Taxon.table_name
232
232
  ids_or_records_or_names.flatten.map { |t|
233
233
  case t
234
- when Integer then Taxon.find_by(id: t)
234
+ when Integer then Spree::Taxon.find_by(id: t)
235
235
  when ActiveRecord::Base then t
236
236
  when String
237
- Taxon.find_by(name: t) ||
238
- Taxon.where("#{taxons}.permalink LIKE ? OR #{taxons}.permalink = ?", "%/#{t}/", "#{t}/").first
237
+ Spree::Taxon.find_by(name: t) ||
238
+ Spree::Taxon.where("#{taxons}.permalink LIKE ? OR #{taxons}.permalink = ?", "%/#{t}/", "#{t}/").first
239
239
  end
240
240
  }.compact.flatten.uniq
241
241
  end
@@ -91,7 +91,7 @@ module Spree
91
91
  validates :name, presence: true
92
92
  validates :price, presence: true, if: proc { Spree::Config[:require_master_price] }
93
93
  validates :shipping_category_id, presence: true
94
- validates :slug, length: { minimum: 3 }, uniqueness: { allow_blank: true }
94
+ validates :slug, presence: true, uniqueness: { allow_blank: true }
95
95
 
96
96
  attr_accessor :option_values_hash
97
97
 
@@ -110,7 +110,7 @@ module Spree
110
110
 
111
111
  # @return [Spree::TaxCategory] tax category for this product, or the default tax category
112
112
  def tax_category
113
- super || TaxCategory.find_by(is_default: true)
113
+ super || Spree::TaxCategory.find_by(is_default: true)
114
114
  end
115
115
 
116
116
  # Ensures option_types and product_option_types exist for keys in
@@ -232,8 +232,8 @@ module Spree
232
232
  def set_property(property_name, property_value)
233
233
  ActiveRecord::Base.transaction do
234
234
  # Works around spree_i18n https://github.com/spree/spree/issues/301
235
- property = Property.create_with(presentation: property_name).find_or_create_by(name: property_name)
236
- product_property = ProductProperty.where(product: self, property: property).first_or_initialize
235
+ property = Spree::Property.create_with(presentation: property_name).find_or_create_by(name: property_name)
236
+ product_property = Spree::ProductProperty.where(product: self, property: property).first_or_initialize
237
237
  product_property.value = property_value
238
238
  product_property.save!
239
239
  end
@@ -19,6 +19,8 @@ module Spree
19
19
  has_many :codes, class_name: "Spree::PromotionCode", inverse_of: :promotion, dependent: :destroy
20
20
  alias_method :promotion_codes, :codes
21
21
 
22
+ has_many :promotion_code_batches, class_name: "Spree::PromotionCodeBatch", dependent: :destroy
23
+
22
24
  accepts_nested_attributes_for :promotion_actions, :promotion_rules
23
25
 
24
26
  validates_associated :rules
@@ -0,0 +1,63 @@
1
+ class ::Spree::PromotionCode::BatchBuilder
2
+ attr_reader :promotion_code_batch
3
+ delegate :promotion, :number_of_codes, :base_code, to: :promotion_code_batch
4
+
5
+ class_attribute :random_code_length, :batch_size, :sample_characters
6
+ self.random_code_length = 6
7
+ self.batch_size = 1_000
8
+ self.sample_characters = ('a'..'z').to_a + (2..9).to_a.map(&:to_s)
9
+
10
+ def initialize(promotion_code_batch)
11
+ @promotion_code_batch = promotion_code_batch
12
+ end
13
+
14
+ def build_promotion_codes
15
+ generate_random_codes
16
+ promotion_code_batch.update!(state: "completed")
17
+ rescue => e
18
+ promotion_code_batch.update!(
19
+ error: e.inspect,
20
+ state: "failed"
21
+ )
22
+ raise e
23
+ end
24
+
25
+ private
26
+
27
+ def generate_random_codes
28
+ total_batches = (number_of_codes.to_f / self.class.batch_size).ceil
29
+
30
+ total_batches.times do |i|
31
+ codes_for_current_batch = Set.new
32
+ codes_to_generate = [self.class.batch_size, number_of_codes - i * batch_size].min
33
+
34
+ while codes_for_current_batch.size < codes_to_generate
35
+ new_codes = Array.new(codes_to_generate) { generate_random_code }.to_set
36
+ codes_for_current_batch += get_unique_codes(new_codes)
37
+ end
38
+
39
+ codes_for_current_batch.map do |value|
40
+ promotion.codes.build(value: value, promotion_code_batch: promotion_code_batch)
41
+ end
42
+
43
+ promotion.save!
44
+
45
+ # We have to reload the promotion because otherwise all promotion codes
46
+ # we are creating will remain in memory. Doing a reload will remove all
47
+ # codes from memory.
48
+ promotion.reload
49
+ end
50
+ end
51
+
52
+ def generate_random_code
53
+ suffix = Array.new(self.class.random_code_length) do
54
+ sample_characters.sample
55
+ end.join
56
+
57
+ "#{base_code}_#{suffix}"
58
+ end
59
+
60
+ def get_unique_codes(code_set)
61
+ code_set - Spree::PromotionCode.where(value: code_set.to_a).pluck(:value)
62
+ end
63
+ end
@@ -1,5 +1,6 @@
1
1
  class Spree::PromotionCode < Spree::Base
2
2
  belongs_to :promotion, inverse_of: :codes
3
+ belongs_to :promotion_code_batch, class_name: "Spree::PromotionCodeBatch"
3
4
  has_many :adjustments
4
5
 
5
6
  validates :value, presence: true, uniqueness: { allow_blank: true }
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ class PromotionCodeBatch < ActiveRecord::Base
3
+ class CantProcessStartedBatch < StandardError
4
+ end
5
+
6
+ belongs_to :promotion, class_name: "Spree::Promotion"
7
+ has_many :promotion_codes, class_name: "Spree::PromotionCode", dependent: :destroy
8
+
9
+ validates :number_of_codes, numericality: { greater_than: 0 }
10
+ validates_presence_of :base_code, :number_of_codes
11
+
12
+ def finished?
13
+ state == "completed"
14
+ end
15
+
16
+ def process
17
+ if state == "pending"
18
+ update!(state: "processing")
19
+ PromotionCodeBatchJob.perform_later(self)
20
+ else
21
+ raise CantProcessStartedBatch.new("Batch #{id} already started")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -35,13 +35,13 @@ module Spree
35
35
  end
36
36
 
37
37
  def connected_order_promotions
38
- Promotion.active.includes(:promotion_rules).
38
+ Spree::Promotion.active.includes(:promotion_rules).
39
39
  joins(:order_promotions).
40
40
  where(spree_orders_promotions: { order_id: order.id }).readonly(false).to_a
41
41
  end
42
42
 
43
43
  def sale_promotions
44
- Promotion.where(apply_automatically: true).active.includes(:promotion_rules)
44
+ Spree::Promotion.where(apply_automatically: true).active.includes(:promotion_rules)
45
45
  end
46
46
 
47
47
  def promotion_code(promotion)
@@ -95,8 +95,7 @@ module Spree
95
95
  discount ||= order.adjustments.promotion.detect(&detector)
96
96
 
97
97
  if discount && discount.eligible
98
- order.update_totals
99
- order.persist_totals
98
+ order.update!
100
99
  set_success_code :coupon_code_applied
101
100
  elsif order.promotions.with_coupon_code(order.coupon_code)
102
101
  # if the promotion exists on an order, but wasn't found above,
@@ -10,38 +10,49 @@ module Spree
10
10
  end
11
11
 
12
12
  def activate
13
- promotions.each do |promotion|
14
- if order_promotion = existing_order_promotion(promotion)
15
- promotion.activate(
16
- order: order,
17
- promotion_code: order_promotion.promotion_code,
18
- )
19
- elsif promotion.apply_automatically?
20
- promotion.activate(order: order)
21
- end
13
+ connected_promotions.each do |order_promotion|
14
+ order_promotion.promotion.activate(
15
+ order: order,
16
+ promotion_code: order_promotion.promotion_code,
17
+ )
18
+ end
19
+
20
+ not_connected_automatic_promotions.each do |promotion|
21
+ promotion.activate(order: order)
22
22
  end
23
23
  end
24
24
 
25
25
  private
26
26
 
27
- def promotions
28
- Spree::Promotion.
27
+ def not_connected_automatic_promotions
28
+ automatic_promotions - connected_promotions.map(&:promotion)
29
+ end
30
+
31
+ def automatic_promotions
32
+ @automatic_promotions ||= active_free_shipping_promotions.
33
+ where(apply_automatically: true).
34
+ to_a.
35
+ uniq
36
+ end
37
+
38
+ def connected_promotions
39
+ @connected_promotions ||= order.order_promotions.
40
+ joins(:promotion).
41
+ includes(:promotion).
42
+ merge(active_free_shipping_promotions).
43
+ to_a.
44
+ uniq
45
+ end
46
+
47
+ def active_free_shipping_promotions
48
+ Spree::Promotion.all.
29
49
  active.
30
50
  joins(:promotion_actions).
31
51
  merge(
32
52
  Spree::PromotionAction.of_type(
33
53
  Spree::Promotion::Actions::FreeShipping
34
54
  )
35
- ).
36
- distinct
37
- end
38
-
39
- def existing_order_promotion(promotion)
40
- @lookup ||= order.order_promotions.map do |order_promotion|
41
- [order_promotion.promotion_id, order_promotion]
42
- end.to_h
43
-
44
- @lookup[promotion.id]
55
+ )
45
56
  end
46
57
  end
47
58
  end
@@ -17,7 +17,7 @@ module Spree
17
17
  private
18
18
 
19
19
  def promotion
20
- @promotion ||= Promotion.active.find_by(path: path)
20
+ @promotion ||= Spree::Promotion.active.find_by(path: path)
21
21
  end
22
22
  end
23
23
  end
@@ -38,7 +38,7 @@ module Spree
38
38
  # Refund.total_amount_reimbursed_for(reimbursement)
39
39
  # See the `reimbursement_generator` property regarding the generation of custom reimbursements.
40
40
  class_attribute :reimbursement_models
41
- self.reimbursement_models = [Refund, Reimbursement::Credit]
41
+ self.reimbursement_models = [Spree::Refund, Spree::Reimbursement::Credit]
42
42
 
43
43
  # The reimbursement_performer property should be set to an object that responds to the following methods:
44
44
  # - #perform
@@ -13,8 +13,6 @@ module Spree
13
13
 
14
14
  before_create :generate_number
15
15
 
16
- after_save :generate_expedited_exchange_reimbursements
17
-
18
16
  accepts_nested_attributes_for :return_items, allow_destroy: true
19
17
 
20
18
  validates :order, presence: true
@@ -22,11 +20,6 @@ module Spree
22
20
  validate :must_have_shipped_units, on: :create
23
21
  validate :no_previously_exchanged_inventory_units, on: :create
24
22
 
25
- # These are called prior to generating expedited exchanges shipments.
26
- # Should respond to a "call" method that takes the list of return items
27
- class_attribute :pre_expedited_exchange_hooks
28
- self.pre_expedited_exchange_hooks = []
29
-
30
23
  state_machine initial: :authorized do
31
24
  before_transition to: :canceled, do: :cancel_return_items
32
25
 
@@ -88,26 +81,5 @@ module Spree
88
81
  def cancel_return_items
89
82
  return_items.each { |item| item.cancel! if item.can_cancel? }
90
83
  end
91
-
92
- def generate_expedited_exchange_reimbursements
93
- return unless Spree::Config[:expedited_exchanges]
94
-
95
- items_to_exchange = return_items.select(&:exchange_required?)
96
- items_to_exchange.each(&:attempt_accept)
97
- items_to_exchange.select!(&:accepted?)
98
-
99
- return if items_to_exchange.blank?
100
-
101
- pre_expedited_exchange_hooks.each { |h| h.call items_to_exchange }
102
-
103
- reimbursement = Reimbursement.new(return_items: items_to_exchange, order: order)
104
-
105
- if reimbursement.save
106
- reimbursement.perform!
107
- else
108
- errors.add(:base, reimbursement.errors.full_messages)
109
- raise ActiveRecord::RecordInvalid.new(self)
110
- end
111
- end
112
84
  end
113
85
  end
@@ -124,7 +124,7 @@ module Spree
124
124
  transition to: :manual_intervention_required, from: [:accepted, :pending, :manual_intervention_required]
125
125
  end
126
126
 
127
- after_transition any => any, :do => :persist_acceptance_status_errors
127
+ after_transition any => any, do: :persist_acceptance_status_errors
128
128
  end
129
129
 
130
130
  # @param inventory_unit [Spree::InventoryUnit] the inventory for which we
@@ -72,7 +72,7 @@ module Spree
72
72
  self.whitelisted_ransackable_associations = ['order']
73
73
  self.whitelisted_ransackable_attributes = ['number']
74
74
 
75
- delegate :tax_category, to: :selected_shipping_rate, allow_nil: true
75
+ delegate :tax_category, :tax_category_id, to: :selected_shipping_rate, allow_nil: true
76
76
 
77
77
  def can_transition_from_pending_to_shipped?
78
78
  !requires_shipment?
@@ -136,9 +136,9 @@ module Spree
136
136
  def finalize!
137
137
  transaction do
138
138
  pending_units = inventory_units.select(&:pending?)
139
- pending_manifest = ShippingManifest.new(inventory_units: pending_units)
139
+ pending_manifest = Spree::ShippingManifest.new(inventory_units: pending_units)
140
140
  pending_manifest.items.each { |item| manifest_unstock(item) }
141
- InventoryUnit.finalize_units!(pending_units)
141
+ Spree::InventoryUnit.finalize_units!(pending_units)
142
142
  end
143
143
  end
144
144
 
@@ -391,7 +391,7 @@ module Spree
391
391
  end
392
392
 
393
393
  def ensure_can_destroy
394
- unless pending?
394
+ if shipped? || canceled?
395
395
  errors.add(:state, :cannot_destroy, state: state)
396
396
  throw :abort
397
397
  end
@@ -34,7 +34,7 @@ module Spree
34
34
  # Some extra care is needed with the having clause to ensure we are
35
35
  # counting distinct records of the join table. Otherwise a join could
36
36
  # cause this to return incorrect results.
37
- join_table = ShippingMethodCategory.arel_table
37
+ join_table = Spree::ShippingMethodCategory.arel_table
38
38
  having = join_table[:id].count(true).eq(shipping_category_ids.count)
39
39
  joins(:shipping_method_categories).
40
40
  where(spree_shipping_method_categories: { shipping_category_id: shipping_category_ids }).
@@ -46,7 +46,7 @@ module Spree
46
46
  # @return [ActiveRecord::Relation] shipping methods which are available
47
47
  # with the stock location or are marked available_to_all
48
48
  def self.available_in_stock_location(stock_location)
49
- smsl_table = ShippingMethodStockLocation.arel_table
49
+ smsl_table = Spree::ShippingMethodStockLocation.arel_table
50
50
 
51
51
  # We are searching for either a matching entry in the stock location join
52
52
  # table or available_to_all being true.
@@ -12,7 +12,7 @@ module Spree
12
12
  dependent: :destroy
13
13
 
14
14
  delegate :order, :currency, to: :shipment
15
- delegate :name, :tax_category, to: :shipping_method
15
+ delegate :name, :tax_category, :tax_category_id, to: :shipping_method
16
16
  delegate :code, to: :shipping_method, prefix: true
17
17
  alias_attribute :amount, :cost
18
18