spree_core 5.3.3 → 5.4.0.beta

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 (242) hide show
  1. checksums.yaml +4 -4
  2. data/app/finders/spree/orders/find_complete.rb +33 -2
  3. data/app/finders/spree/stores/find_default.rb +17 -0
  4. data/app/finders/spree/variants/visible_finder.rb +1 -0
  5. data/app/helpers/spree/addresses_helper.rb +1 -2
  6. data/app/helpers/spree/base_helper.rb +5 -4
  7. data/app/jobs/spree/api_key_touch_job.rb +9 -0
  8. data/app/models/concerns/spree/admin_user_methods.rb +32 -0
  9. data/app/models/concerns/spree/number_as_param.rb +5 -3
  10. data/app/models/concerns/spree/prefixed_id.rb +82 -0
  11. data/app/models/concerns/spree/product_scopes.rb +33 -18
  12. data/app/models/concerns/spree/publishable.rb +47 -47
  13. data/app/models/concerns/spree/{multi_store_resource.rb → store_scoped_resource.rb} +1 -10
  14. data/app/models/concerns/spree/stores/markets.rb +124 -0
  15. data/app/models/concerns/spree/user_methods.rb +7 -7
  16. data/app/models/concerns/spree/user_payment_source.rb +2 -0
  17. data/app/models/concerns/spree/user_roles.rb +1 -0
  18. data/app/models/spree/ability.rb +13 -0
  19. data/app/models/spree/address.rb +30 -0
  20. data/app/models/spree/adjustment.rb +2 -0
  21. data/app/models/spree/api_key.rb +47 -0
  22. data/app/models/spree/asset.rb +2 -0
  23. data/app/models/spree/authentication/strategies/base_strategy.rb +55 -0
  24. data/app/models/spree/authentication/strategies/email_password_strategy.rb +47 -0
  25. data/app/models/spree/base.rb +1 -0
  26. data/app/models/spree/calculator.rb +2 -0
  27. data/app/models/spree/country.rb +56 -0
  28. data/app/models/spree/coupon_code.rb +2 -0
  29. data/app/models/spree/credit_card.rb +2 -0
  30. data/app/models/spree/current.rb +9 -4
  31. data/app/models/spree/customer_group.rb +2 -0
  32. data/app/models/spree/customer_return.rb +2 -0
  33. data/app/models/spree/data_feed.rb +2 -0
  34. data/app/models/spree/digital.rb +2 -0
  35. data/app/models/spree/digital_link.rb +2 -0
  36. data/app/models/spree/export.rb +3 -2
  37. data/app/models/spree/fulfilment_changer.rb +8 -2
  38. data/app/models/spree/gateway/bogus.rb +60 -0
  39. data/app/models/spree/gateway_customer.rb +2 -0
  40. data/app/models/spree/gift_card.rb +2 -0
  41. data/app/models/spree/gift_card_batch.rb +2 -0
  42. data/app/models/spree/image.rb +18 -0
  43. data/app/models/spree/import.rb +5 -3
  44. data/app/models/spree/import_mapping.rb +2 -0
  45. data/app/models/spree/import_row.rb +2 -0
  46. data/app/models/spree/import_schemas/customers.rb +21 -0
  47. data/app/models/spree/imports/customers.rb +9 -0
  48. data/app/models/spree/integration.rb +2 -0
  49. data/app/models/spree/inventory_unit.rb +2 -0
  50. data/app/models/spree/invitation.rb +6 -6
  51. data/app/models/spree/legacy_admin_user.rb +31 -0
  52. data/app/models/spree/legacy_user.rb +19 -1
  53. data/app/models/spree/line_item.rb +15 -4
  54. data/app/models/spree/log_entry.rb +2 -0
  55. data/app/models/spree/market.rb +83 -0
  56. data/app/models/spree/market_country.rb +25 -0
  57. data/app/models/spree/metafield.rb +2 -0
  58. data/app/models/spree/metafield_definition.rb +2 -0
  59. data/app/models/spree/newsletter_subscriber.rb +2 -0
  60. data/app/models/spree/option_type.rb +2 -0
  61. data/app/models/spree/option_value.rb +2 -0
  62. data/app/models/spree/order/address_book.rb +2 -1
  63. data/app/models/spree/order.rb +73 -45
  64. data/app/models/spree/payment.rb +6 -1
  65. data/app/models/spree/payment_capture_event.rb +2 -0
  66. data/app/models/spree/payment_method.rb +59 -1
  67. data/app/models/spree/payment_session.rb +109 -0
  68. data/app/models/spree/payment_sessions/bogus.rb +4 -0
  69. data/app/models/spree/payment_setup_session.rb +79 -0
  70. data/app/models/spree/payment_source.rb +2 -0
  71. data/app/models/spree/permission_sets/default_customer.rb +19 -0
  72. data/app/models/spree/policy.rb +2 -0
  73. data/app/models/spree/post.rb +2 -0
  74. data/app/models/spree/post_category.rb +2 -0
  75. data/app/models/spree/price.rb +26 -2
  76. data/app/models/spree/price_list.rb +2 -0
  77. data/app/models/spree/price_rule.rb +2 -0
  78. data/app/models/spree/price_rules/market_rule.rb +19 -0
  79. data/app/models/spree/product/slugs.rb +1 -1
  80. data/app/models/spree/product.rb +41 -29
  81. data/app/models/spree/promotion/rules/country.rb +1 -1
  82. data/app/models/spree/promotion.rb +3 -1
  83. data/app/models/spree/promotion_action.rb +2 -0
  84. data/app/models/spree/promotion_category.rb +2 -0
  85. data/app/models/spree/promotion_rule.rb +2 -0
  86. data/app/models/spree/prototype.rb +2 -0
  87. data/app/models/spree/refund.rb +2 -0
  88. data/app/models/spree/refund_reason.rb +2 -0
  89. data/app/models/spree/reimbursement/credit.rb +2 -0
  90. data/app/models/spree/reimbursement.rb +2 -0
  91. data/app/models/spree/reimbursement_type.rb +2 -0
  92. data/app/models/spree/report.rb +3 -1
  93. data/app/models/spree/return_authorization.rb +2 -0
  94. data/app/models/spree/return_authorization_reason.rb +2 -0
  95. data/app/models/spree/return_item.rb +2 -0
  96. data/app/models/spree/role.rb +2 -0
  97. data/app/models/spree/shipment.rb +11 -1
  98. data/app/models/spree/shipping_category.rb +2 -0
  99. data/app/models/spree/shipping_method.rb +2 -0
  100. data/app/models/spree/shipping_method_category.rb +2 -0
  101. data/app/models/spree/shipping_rate.rb +2 -0
  102. data/app/models/spree/state_change.rb +2 -0
  103. data/app/models/spree/stock_item.rb +2 -0
  104. data/app/models/spree/stock_location.rb +2 -0
  105. data/app/models/spree/stock_movement.rb +2 -0
  106. data/app/models/spree/stock_transfer.rb +2 -1
  107. data/app/models/spree/store.rb +110 -179
  108. data/app/models/spree/store_credit.rb +2 -0
  109. data/app/models/spree/store_credit_category.rb +2 -0
  110. data/app/models/spree/store_credit_event.rb +2 -0
  111. data/app/models/spree/store_credit_type.rb +2 -0
  112. data/app/models/spree/store_product.rb +2 -0
  113. data/app/models/spree/tax_category.rb +2 -0
  114. data/app/models/spree/tax_rate.rb +2 -0
  115. data/app/models/spree/taxon.rb +4 -1
  116. data/app/models/spree/taxon_image.rb +8 -0
  117. data/app/models/spree/taxon_rule.rb +2 -0
  118. data/app/models/spree/taxonomy.rb +2 -0
  119. data/app/models/spree/user_identity.rb +81 -0
  120. data/app/models/spree/variant.rb +30 -18
  121. data/app/models/spree/webhook_delivery.rb +2 -0
  122. data/app/models/spree/webhook_endpoint.rb +2 -0
  123. data/app/models/spree/wished_item.rb +15 -0
  124. data/app/models/spree/wishlist.rb +2 -8
  125. data/app/models/spree/zone.rb +2 -6
  126. data/app/presenters/spree/filters/price_presenter.rb +1 -0
  127. data/app/presenters/spree/filters/price_range_presenter.rb +1 -0
  128. data/app/presenters/spree/filters/quantified_price_range_presenter.rb +1 -0
  129. data/app/presenters/spree/product_summary_presenter.rb +1 -0
  130. data/app/services/spree/addresses/helper.rb +22 -3
  131. data/app/services/spree/cart/associate.rb +19 -6
  132. data/app/services/spree/checkout/select_shipping_method.rb +13 -1
  133. data/app/services/spree/classifications/reposition.rb +5 -0
  134. data/app/services/spree/data_feeds/google/rss.rb +3 -1
  135. data/app/services/spree/imports/row_processors/customer.rb +70 -0
  136. data/app/services/spree/orders/approve.rb +5 -3
  137. data/app/services/spree/orders/cancel.rb +9 -4
  138. data/app/services/spree/products/prepare_nested_attributes.rb +1 -1
  139. data/app/services/spree/sample_data/import_runner.rb +54 -0
  140. data/app/services/spree/sample_data/loader.rb +78 -0
  141. data/app/services/spree/seeds/admin_user.rb +2 -3
  142. data/app/services/spree/seeds/all.rb +1 -0
  143. data/app/services/spree/seeds/api_keys.rb +16 -0
  144. data/app/services/spree/seeds/stores.rb +2 -4
  145. data/app/sorters/spree/orders/sort.rb +4 -0
  146. data/app/subscribers/spree/product_metrics_subscriber.rb +4 -4
  147. data/app/views/spree/addresses/_form.html.erb +1 -2
  148. data/config/brakeman.ignore +120 -0
  149. data/config/locales/en.yml +20 -1
  150. data/db/migrate/20250923141900_create_spree_user_identities.rb +17 -0
  151. data/db/migrate/20260123000000_create_spree_api_keys.rb +19 -0
  152. data/db/migrate/20260131000000_add_thumbnail_id_to_spree_variants_and_products.rb +9 -0
  153. data/db/migrate/20260213000000_create_spree_payment_sessions.rb +27 -0
  154. data/db/migrate/20260218000000_create_spree_payment_setup_sessions.rb +24 -0
  155. data/db/migrate/20260220000000_create_spree_markets.rb +29 -0
  156. data/db/sample_data/customers.csv +21 -0
  157. data/db/sample_data/metafield_definitions.rb +7 -0
  158. data/db/sample_data/orders.rb +131 -0
  159. data/db/sample_data/payment_methods.rb +17 -0
  160. data/db/sample_data/posts.rb +7 -0
  161. data/db/sample_data/products.csv +1083 -0
  162. data/db/sample_data/promotions.rb +8 -0
  163. data/db/sample_data/shipping_methods.rb +39 -0
  164. data/lib/generators/spree/authentication/devise/devise_generator.rb +2 -2
  165. data/lib/generators/spree/authentication/dummy/dummy_generator.rb +54 -0
  166. data/lib/generators/spree/authentication/dummy/templates/authentication_helpers.rb.tt +52 -0
  167. data/lib/generators/spree/authentication/dummy/templates/create_spree_admin_users.rb.tt +33 -0
  168. data/lib/generators/spree/dummy/dummy_generator.rb +1 -1
  169. data/lib/spree/core/configuration.rb +1 -0
  170. data/lib/spree/core/controller_helpers/common.rb +6 -0
  171. data/lib/spree/core/controller_helpers/currency.rb +5 -0
  172. data/lib/spree/core/controller_helpers/order.rb +5 -1
  173. data/lib/spree/core/controller_helpers/store.rb +1 -1
  174. data/lib/spree/core/dependencies.rb +1 -1
  175. data/lib/spree/core/engine.rb +17 -4
  176. data/lib/spree/core/pricing/context.rb +6 -3
  177. data/lib/spree/core/product_filters.rb +14 -0
  178. data/lib/spree/core/query_filters/comparable.rb +4 -0
  179. data/lib/spree/core/query_filters/text.rb +4 -0
  180. data/lib/spree/core/version.rb +1 -1
  181. data/lib/spree/core.rb +4 -4
  182. data/lib/spree/database_type_utilities.rb +7 -0
  183. data/lib/spree/events.rb +17 -10
  184. data/lib/spree/money.rb +2 -9
  185. data/lib/spree/permitted_attributes.rb +19 -7
  186. data/lib/spree/testing_support/capybara_config.rb +2 -2
  187. data/lib/spree/testing_support/common_rake.rb +15 -4
  188. data/lib/spree/testing_support/factories/api_key_factory.rb +19 -0
  189. data/lib/spree/testing_support/factories/custom_domain_factory.rb +7 -5
  190. data/lib/spree/testing_support/factories/import_factory.rb +12 -0
  191. data/lib/spree/testing_support/factories/market_factory.rb +35 -0
  192. data/lib/spree/testing_support/factories/order_factory.rb +3 -1
  193. data/lib/spree/testing_support/factories/payment_method_factory.rb +2 -0
  194. data/lib/spree/testing_support/factories/payment_session_factory.rb +47 -0
  195. data/lib/spree/testing_support/factories/payment_setup_session_factory.rb +31 -0
  196. data/lib/spree/testing_support/factories/price_rule_factory.rb +10 -0
  197. data/lib/spree/testing_support/factories/user_identity_factory.rb +15 -0
  198. data/lib/spree/testing_support/store.rb +3 -2
  199. data/lib/spree/webhooks.rb +7 -7
  200. data/lib/tasks/images.rake +20 -0
  201. data/lib/tasks/markets.rake +40 -0
  202. data/lib/tasks/sample_data.rake +15 -0
  203. data/spec/fixtures/files/customers_import.csv +4 -0
  204. metadata +99 -59
  205. data/LICENSE.md +0 -57
  206. data/app/finders/spree/stores/find_current.rb +0 -28
  207. data/app/models/spree/custom_domain.rb +0 -59
  208. data/app/serializers/spree/events/asset_serializer.rb +0 -22
  209. data/app/serializers/spree/events/base_serializer.rb +0 -61
  210. data/app/serializers/spree/events/customer_return_serializer.rb +0 -20
  211. data/app/serializers/spree/events/digital_link_serializer.rb +0 -20
  212. data/app/serializers/spree/events/digital_serializer.rb +0 -18
  213. data/app/serializers/spree/events/export_serializer.rb +0 -22
  214. data/app/serializers/spree/events/gift_card_batch_serializer.rb +0 -24
  215. data/app/serializers/spree/events/gift_card_serializer.rb +0 -29
  216. data/app/serializers/spree/events/image_serializer.rb +0 -9
  217. data/app/serializers/spree/events/import_row_serializer.rb +0 -23
  218. data/app/serializers/spree/events/import_serializer.rb +0 -24
  219. data/app/serializers/spree/events/invitation_serializer.rb +0 -28
  220. data/app/serializers/spree/events/line_item_serializer.rb +0 -31
  221. data/app/serializers/spree/events/newsletter_subscriber_serializer.rb +0 -21
  222. data/app/serializers/spree/events/order_serializer.rb +0 -39
  223. data/app/serializers/spree/events/payment_serializer.rb +0 -24
  224. data/app/serializers/spree/events/post_category_serializer.rb +0 -20
  225. data/app/serializers/spree/events/post_serializer.rb +0 -26
  226. data/app/serializers/spree/events/price_serializer.rb +0 -22
  227. data/app/serializers/spree/events/product_serializer.rb +0 -24
  228. data/app/serializers/spree/events/promotion_serializer.rb +0 -32
  229. data/app/serializers/spree/events/refund_serializer.rb +0 -23
  230. data/app/serializers/spree/events/reimbursement_serializer.rb +0 -22
  231. data/app/serializers/spree/events/report_serializer.rb +0 -23
  232. data/app/serializers/spree/events/return_authorization_serializer.rb +0 -22
  233. data/app/serializers/spree/events/return_item_serializer.rb +0 -27
  234. data/app/serializers/spree/events/shipment_serializer.rb +0 -24
  235. data/app/serializers/spree/events/stock_item_serializer.rb +0 -22
  236. data/app/serializers/spree/events/stock_movement_serializer.rb +0 -22
  237. data/app/serializers/spree/events/stock_transfer_serializer.rb +0 -22
  238. data/app/serializers/spree/events/store_credit_serializer.rb +0 -30
  239. data/app/serializers/spree/events/user_serializer.rb +0 -18
  240. data/app/serializers/spree/events/variant_serializer.rb +0 -34
  241. data/app/serializers/spree/events/wished_item_serializer.rb +0 -20
  242. data/app/serializers/spree/events/wishlist_serializer.rb +0 -22
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class Variant < Spree.base_class
3
+ has_prefix_id :variant
4
+
3
5
  acts_as_paranoid
4
6
  acts_as_list scope: :product
5
7
 
@@ -47,6 +49,7 @@ module Spree
47
49
  has_many :option_values, through: :option_value_variants, dependent: :destroy, class_name: 'Spree::OptionValue'
48
50
 
49
51
  has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'Spree::Image'
52
+ belongs_to :thumbnail, class_name: 'Spree::Image', optional: true
50
53
 
51
54
  has_many :prices,
52
55
  class_name: 'Spree::Price',
@@ -91,36 +94,33 @@ module Spree
91
94
  scope :backorderable, -> { left_joins(:stock_items).where(spree_stock_items: { backorderable: true }) }
92
95
  scope :in_stock_or_backorderable, -> { in_stock.or(backorderable) }
93
96
 
94
- scope :eligible, -> {
95
- where(is_master: false).or(
96
- where(
97
- product_id: Spree::Variant.
98
- select(:product_id).
99
- group(:product_id).
100
- having("COUNT(#{Spree::Variant.table_name}.id) = 1")
97
+ scope :eligible, lambda {
98
+ joins(:product).where(
99
+ arel_table[:is_master].eq(false).or(
100
+ Spree::Product.arel_table[:variant_count].eq(0)
101
101
  )
102
102
  )
103
103
  }
104
104
 
105
- scope :not_discontinued, -> do
105
+ scope :not_discontinued, lambda {
106
106
  where(
107
107
  arel_table[:discontinue_on].eq(nil).or(
108
108
  arel_table[:discontinue_on].gteq(Time.current)
109
109
  )
110
110
  )
111
- end
111
+ }
112
112
 
113
113
  scope :not_deleted, -> { where("#{Spree::Variant.quoted_table_name}.deleted_at IS NULL") }
114
114
 
115
- scope :for_currency_and_available_price_amount, ->(currency = nil) do
115
+ scope :for_currency_and_available_price_amount, lambda { |currency = nil|
116
116
  currency ||= Spree::Store.default.default_currency
117
117
  joins(:prices).where("#{Spree::Price.table_name}.currency = ?", currency).where("#{Spree::Price.table_name}.amount IS NOT NULL").distinct
118
- end
118
+ }
119
119
 
120
- scope :active, ->(currency = nil) do
120
+ scope :active, lambda { |currency = nil|
121
121
  not_discontinued.not_deleted.
122
122
  for_currency_and_available_price_amount(currency)
123
- end
123
+ }
124
124
 
125
125
  scope :with_option_value, lambda { |option_name, option_value|
126
126
  option_type_ids = OptionType.where(name: option_name).ids
@@ -187,7 +187,8 @@ module Spree
187
187
  )
188
188
 
189
189
  self.whitelisted_ransackable_associations = %w[option_values product tax_category prices default_price]
190
- self.whitelisted_ransackable_attributes = %w[weight depth width height sku discontinue_on is_master cost_price cost_currency track_inventory deleted_at]
190
+ self.whitelisted_ransackable_attributes = %w[weight depth width height sku discontinue_on is_master cost_price cost_currency track_inventory
191
+ deleted_at]
191
192
  self.whitelisted_ransackable_scopes = %i(product_name_or_sku_cont search_by_product_name_or_sku)
192
193
 
193
194
  def self.product_name_or_sku_cont(query)
@@ -258,9 +259,13 @@ module Spree
258
259
  # @return [String] the options text of the variant
259
260
  def options_text
260
261
  @options_text ||= if option_values.loaded?
261
- option_values.sort_by { |ov| ov.option_type.position }.map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
262
+ option_values.sort_by do |ov|
263
+ ov.option_type.position
264
+ end.map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
262
265
  else
263
- option_values.includes(:option_type).joins(:option_type).order("#{Spree::OptionType.table_name}.position").map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ')
266
+ option_values.includes(:option_type).joins(:option_type).order("#{Spree::OptionType.table_name}.position").map do |ov|
267
+ "#{ov.option_type.presentation}: #{ov.presentation}"
268
+ end.to_sentence(words_connector: ', ', two_words_connector: ', ')
264
269
  end
265
270
  end
266
271
 
@@ -293,10 +298,17 @@ module Spree
293
298
  image_count.positive?
294
299
  end
295
300
 
296
- # Returns default Image for Variant, falling back to product's default image.
301
+ # Returns default Image for Variant.
297
302
  # @return [Spree::Image, nil]
298
303
  def default_image
299
- @default_image ||= has_images? ? images.first : product.default_image
304
+ thumbnail
305
+ end
306
+
307
+ # Updates the thumbnail_id to the first image by position.
308
+ # Called when images are added, removed, or reordered.
309
+ def update_thumbnail!
310
+ first_image = images.order(:position).first
311
+ update_column(:thumbnail_id, first_image&.id)
300
312
  end
301
313
 
302
314
  # Returns first Image for Variant.
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Spree
4
4
  class WebhookDelivery < Spree.base_class
5
+ has_prefix_id :whd
6
+
5
7
  belongs_to :webhook_endpoint, class_name: 'Spree::WebhookEndpoint'
6
8
  delegate :url, to: :webhook_endpoint
7
9
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Spree
4
4
  class WebhookEndpoint < Spree.base_class
5
+ has_prefix_id :whe # Stripe: we_
6
+
5
7
  acts_as_paranoid
6
8
 
7
9
  include Spree::SingleStoreResource
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class WishedItem < Spree.base_class
3
+ has_prefix_id :wi # Spree-specific: wished item
4
+
3
5
  extend DisplayMoney
4
6
  money_methods :total, :price
5
7
 
@@ -14,6 +16,19 @@ module Spree
14
16
  validates :variant, uniqueness: { scope: [:wishlist] }
15
17
  validates :quantity, numericality: { only_integer: true, greater_than: 0 }
16
18
 
19
+ # This is a workaround to allow the variant_id to be set with a prefixed ID
20
+ # in the API.
21
+ #
22
+ # @param id [String] the prefixed ID of the variant
23
+ def variant_id=(id)
24
+ if id.to_s.include?('_')
25
+ decoded = Spree::Variant.decode_prefixed_id(id)
26
+ super(decoded)
27
+ else
28
+ super(id)
29
+ end
30
+ end
31
+
17
32
  def price(currency)
18
33
  variant.amount_in(currency[:currency])
19
34
  end
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class Wishlist < Spree.base_class
3
+ has_prefix_id :wl # Spree-specific: wishlist
4
+
3
5
  include Spree::SingleStoreResource
4
6
 
5
7
  publishes_lifecycle_events
@@ -24,10 +26,6 @@ module Spree
24
26
  wished_items.exists?(variant_id: variant_id)
25
27
  end
26
28
 
27
- def to_param
28
- token
29
- end
30
-
31
29
  # returns the number of wished items in the wishlist
32
30
  #
33
31
  # @return [Integer]
@@ -42,10 +40,6 @@ module Spree
42
40
  @variant_ids ||= wished_items.pluck(:variant_id)
43
41
  end
44
42
 
45
- def self.get_by_param(param)
46
- find_by(token: param)
47
- end
48
-
49
43
  private
50
44
 
51
45
  def ensure_default_exists_and_is_unique
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class Zone < Spree.base_class
3
+ has_prefix_id :zone
4
+
3
5
  include Spree::UniqueName
4
6
 
5
7
  with_options dependent: :destroy, inverse_of: :zone do
@@ -18,7 +20,6 @@ module Spree
18
20
 
19
21
  after_save :remove_defunct_members
20
22
  after_save :remove_previous_default, if: %i[default_tax? saved_change_to_default_tax?]
21
- before_destroy :nullify_checkout_zone
22
23
 
23
24
  alias members zone_members
24
25
  accepts_nested_attributes_for :zone_members, allow_destroy: true, reject_if: proc { |a| a['zoneable_id'].blank? }
@@ -198,10 +199,5 @@ module Spree
198
199
  end
199
200
  end
200
201
 
201
- def nullify_checkout_zone
202
- if id == Spree::Store.current.checkout_zone_id
203
- Spree::Store.current.update(checkout_zone_id: nil)
204
- end
205
- end
206
202
  end
207
203
  end
@@ -2,6 +2,7 @@ module Spree
2
2
  module Filters
3
3
  class PricePresenter
4
4
  def initialize(amount:, currency:)
5
+ Spree::Deprecation.warn('Spree::Filters::PricePresenter is deprecated and will be removed in Spree 5.5.')
5
6
  @amount = amount.to_i
6
7
  @currency = currency
7
8
  end
@@ -11,6 +11,7 @@ module Spree
11
11
  end
12
12
 
13
13
  def initialize(min_price:, max_price:)
14
+ Spree::Deprecation.warn('Spree::Filters::PriceRangePresenter is deprecated and will be removed in Spree 5.5.')
14
15
  @min_price = min_price
15
16
  @max_price = max_price
16
17
  end
@@ -7,6 +7,7 @@ module Spree
7
7
  ].freeze
8
8
 
9
9
  def initialize(price:, quantifier:)
10
+ Spree::Deprecation.warn('Spree::Filters::QuantifiedPriceRangePresenter is deprecated and will be removed in Spree 5.5.')
10
11
  if ALLOWED_QUANTIFIERS.exclude?(quantifier.to_sym)
11
12
  raise ArgumentError, "quantifier must be one of: #{ALLOWED_QUANTIFIERS.join(', ')}"
12
13
  end
@@ -3,6 +3,7 @@ module Spree
3
3
  include Rails.application.routes.url_helpers
4
4
 
5
5
  def initialize(product)
6
+ Spree::Deprecation.warn('Spree::ProductSummaryPresenter is deprecated and will be removed in Spree 5.5.')
6
7
  @product = product
7
8
  end
8
9
 
@@ -21,13 +21,32 @@ module Spree
21
21
  end
22
22
 
23
23
  def fill_state_id(params)
24
- state_name = params[:state_name]
25
- return params unless state_name.present?
24
+ # Always extract state_abbr - it's not a model attribute
25
+ state_abbr = params.delete(:state_abbr)
26
26
 
27
27
  country ||= Spree::Country.find(params[:country_id]) if params[:country_id].present?
28
28
  return params unless country
29
29
 
30
- params[:state_id] = country.states.find_by(name: state_name)&.id
30
+ # Support state_abbr (state abbreviation code, e.g., "CA", "NY")
31
+ if state_abbr.present?
32
+ params[:state_id] = country.states.find_by(abbr: state_abbr)&.id
33
+ return params
34
+ end
35
+
36
+ # Support state_name - try to find matching state
37
+ if params[:state_name].present?
38
+ state = country.states.find_by(name: params[:state_name])
39
+ if state
40
+ # Found a matching state, use state_id
41
+ params[:state_id] = state.id
42
+ params.delete(:state_name)
43
+ elsif country.states_required?
44
+ # States required but not found - clear state_name so validation fails
45
+ params.delete(:state_name)
46
+ end
47
+ # If states not required and no match found, keep state_name as-is
48
+ end
49
+
31
50
  params
32
51
  end
33
52
 
@@ -3,13 +3,26 @@ module Spree
3
3
  class Associate
4
4
  prepend Spree::ServiceModule::Base
5
5
 
6
- def call(guest_order:, user:)
7
- if guest_order.user.nil?
8
- guest_order.associate_user!(user)
9
- success(guest_order)
10
- else
11
- failure(guest_order, 'Already assigned to a user')
6
+ def call(guest_order:, user:, override_email: true, guest_only: false)
7
+ return failure(guest_order, 'Already assigned to a user') if guest_only && guest_order.user.present? && guest_order.user != user
8
+
9
+ guest_order.user = user
10
+ guest_order.email = user.email if override_email
11
+ guest_order.bill_address ||= user.bill_address
12
+ guest_order.ship_address ||= user.ship_address
13
+
14
+ changes = guest_order.slice(*Spree::Order::ASSOCIATED_USER_ATTRIBUTES)
15
+
16
+ # immediately persist the changes we just made, but don't use save
17
+ # since we might have an invalid address associated
18
+ ActiveRecord::Base.connected_to(role: :writing) do
19
+ Spree::Order.unscoped.where(id: guest_order.id).update_all(changes)
12
20
  end
21
+
22
+ # Manually publish update event since update_all bypasses callbacks
23
+ guest_order.publish_event('order.updated') if changes.present?
24
+
25
+ success(guest_order)
13
26
  end
14
27
  end
15
28
  end
@@ -1,9 +1,18 @@
1
1
  module Spree
2
2
  module Checkout
3
+ # @deprecated This service is deprecated and will be removed in Spree 6.0.
4
+ # Order totals are now automatically updated via a callback on ShippingRate
5
+ # when selected_shipping_rate_id is set on a Shipment.
3
6
  class SelectShippingMethod
4
7
  prepend Spree::ServiceModule::Base
5
8
 
6
9
  def call(order:, params:)
10
+ Spree::Deprecation.warn(
11
+ "#{self.class.name} is deprecated and will be removed in Spree 6.0. " \
12
+ "Order totals are now automatically updated via ShippingRate callback when " \
13
+ "selected_shipping_rate_id is set on a Shipment.",
14
+ caller_locations(2)
15
+ )
7
16
  if params[:shipment_id].present?
8
17
  shipment = order.shipments.valid.find_by(id: params[:shipment_id])
9
18
  return failure(:shipment_not_found) if shipment.nil?
@@ -26,6 +35,9 @@ module Spree
26
35
  end
27
36
  end
28
37
 
38
+ # Note: selected_shipping_rate_id= now automatically updates order totals,
39
+ # but we still need update_with_updater! here because Checkout::Advance
40
+ # expects the full updater to run (including state updates) when this service succeeds.
29
41
  order.update_with_updater!
30
42
 
31
43
  success(order)
@@ -41,8 +53,8 @@ module Spree
41
53
  )
42
54
  end
43
55
 
56
+ # selected_shipping_rate_id= now automatically calls update_amounts and updates order totals
44
57
  shipment.selected_shipping_rate_id = selected_shipping_rate.id
45
- shipment.update_amounts
46
58
 
47
59
  success(shipment)
48
60
  end
@@ -1,9 +1,14 @@
1
1
  module Spree
2
2
  module Classifications
3
+ # @deprecated This service is deprecated and will be removed in Spree 5.5.
3
4
  class Reposition
4
5
  prepend Spree::ServiceModule::Base
5
6
 
6
7
  def call(classification:, position:)
8
+ Spree::Deprecation.warn(
9
+ "#{self.class.name} is deprecated and will be removed in Spree 5.5.",
10
+ caller_locations(2)
11
+ )
7
12
  if position.is_a?(String) && !position.match(/^\d+$/)
8
13
  return failure(nil, I18n.t('errors.messages.not_a_number'))
9
14
  end
@@ -18,7 +18,9 @@ module Spree
18
18
  result = products_list.call(store)
19
19
  if result.success?
20
20
  result.value[:products].find_each do |product|
21
- product.variants.active.find_each do |variant|
21
+ product.variants_including_master.active.find_each do |variant|
22
+ next if variant.is_master? && product.has_variants?
23
+
22
24
  add_variant_information_to_xml(xml, product, variant)
23
25
  end
24
26
  end
@@ -0,0 +1,70 @@
1
+ module Spree
2
+ module Imports
3
+ module RowProcessors
4
+ class Customer < Base
5
+ def process!
6
+ user = find_or_initialize_user
7
+ assign_user_attributes(user)
8
+ assign_address(user) if address_fields_present?
9
+ user.save!
10
+ user
11
+ end
12
+
13
+ private
14
+
15
+ def find_or_initialize_user
16
+ email = attributes['email'].to_s.strip.downcase
17
+ raise ArgumentError, 'Email is required' if email.blank?
18
+
19
+ Spree.user_class.find_or_initialize_by(email: email)
20
+ end
21
+
22
+ def assign_user_attributes(user)
23
+ user.first_name = attributes['first_name'].strip if attributes['first_name'].present?
24
+ user.last_name = attributes['last_name'].strip if attributes['last_name'].present?
25
+ user.phone = attributes['phone'].strip if attributes['phone'].present?
26
+ user.accepts_email_marketing = to_boolean(attributes['accepts_email_marketing']) if attributes['accepts_email_marketing'].present?
27
+ user.tag_list = attributes['tags'] if attributes['tags'].present?
28
+
29
+ if user.new_record?
30
+ password = SecureRandom.hex(16)
31
+ user.password = password
32
+ user.password_confirmation = password
33
+ end
34
+ end
35
+
36
+ def assign_address(user)
37
+ address = user.bill_address || user.build_bill_address
38
+ address.firstname = attributes['first_name'].presence || user.first_name
39
+ address.lastname = attributes['last_name'].presence || user.last_name
40
+ address.company = attributes['company'].strip if attributes['company'].present?
41
+ address.address1 = attributes['address1'].strip if attributes['address1'].present?
42
+ address.address2 = attributes['address2'].strip if attributes['address2'].present?
43
+ address.city = attributes['city'].strip if attributes['city'].present?
44
+ address.zipcode = attributes['zip'].strip if attributes['zip'].present?
45
+ address.phone = attributes['phone'].presence || user.phone
46
+
47
+ if attributes['country_code'].present?
48
+ address.country = Spree::Country.find_by(iso: attributes['country_code'].strip.upcase)
49
+ end
50
+
51
+ if attributes['province_code'].present? && address.country
52
+ address.state = address.country.states.find_by(abbr: attributes['province_code'].strip)
53
+ end
54
+
55
+ address.save!
56
+ user.bill_address = address
57
+ user.ship_address ||= address
58
+ end
59
+
60
+ def address_fields_present?
61
+ %w[address1 city country_code].any? { |f| attributes[f].present? }
62
+ end
63
+
64
+ def to_boolean(value)
65
+ value.to_s.strip.downcase.in?(%w[true yes 1 y])
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -4,11 +4,13 @@ module Spree
4
4
  prepend Spree::ServiceModule::Base
5
5
 
6
6
  def call(order:, approver: nil)
7
+ changes = { considered_risky: false, approved_at: Time.current }
7
8
  if approver.present?
8
- order.approved_by(approver)
9
- else
10
- order.approve!
9
+ changes[:approver_id] = approver.id
11
10
  end
11
+ order.update_columns(changes)
12
+
13
+ order.publish_event('order.approved')
12
14
  success(order.reload)
13
15
  rescue ActiveRecord::Rollback, ActiveRecord::RecordInvalid, StateMachines::InvalidTransition
14
16
  failure(order)
@@ -3,12 +3,17 @@ module Spree
3
3
  class Cancel
4
4
  prepend Spree::ServiceModule::Base
5
5
 
6
- def call(order:, canceler: nil)
7
- if canceler.present?
8
- order.canceled_by(canceler)
9
- else
6
+ def call(order:, canceler: nil, canceled_at: nil)
7
+ canceled_at ||= Time.current
8
+
9
+ order.transaction do
10
+ changes = { canceled_at: canceled_at }
11
+ changes[:canceler_id] = canceler.id if canceler.present?
12
+ order.update_columns(changes)
10
13
  order.cancel!
11
14
  end
15
+
16
+ order.publish_event('order.canceled')
12
17
  success(order.reload)
13
18
  rescue ActiveRecord::Rollback, ActiveRecord::RecordInvalid, StateMachines::InvalidTransition
14
19
  failure(order)
@@ -184,7 +184,7 @@ module Spree
184
184
  option_value_variant_params = {}
185
185
 
186
186
  option_value_params.each_with_index do |opt, index|
187
- option_type = Spree::OptionType.find_by(id: opt[:id]) if opt.fetch(:id)
187
+ option_type = Spree::OptionType.find_by_param(opt[:id]) if opt.fetch(:id)
188
188
  option_type ||= Spree::OptionType.where(name: opt[:name].parameterize).first_or_initialize do |o|
189
189
  o.name = o.presentation = opt[:name]
190
190
  o.position = opt[:position]
@@ -0,0 +1,54 @@
1
+ require 'csv'
2
+
3
+ module Spree
4
+ module SampleData
5
+ class ImportRunner
6
+ prepend Spree::ServiceModule::Base
7
+
8
+ def call(csv_path:, import_class:)
9
+ store = Spree::Store.default
10
+ admin = Spree.admin_user_class.first
11
+
12
+ raise 'No admin user found. Please run seeds first.' unless admin
13
+
14
+ import = import_class.new(
15
+ owner: store,
16
+ user: admin
17
+ )
18
+ import.number = import.generate_permalink(import_class)
19
+ import.attachment.attach(
20
+ io: File.open(csv_path),
21
+ filename: File.basename(csv_path),
22
+ content_type: 'text/csv'
23
+ )
24
+ import.save!(validate: false)
25
+ import.update_columns(status: 'processing')
26
+ import.create_mappings
27
+
28
+ row_number = 0
29
+ failed = 0
30
+
31
+ ::CSV.foreach(csv_path, headers: true) do |csv_row|
32
+ row_number += 1
33
+ import_row = import.rows.create!(
34
+ row_number: row_number,
35
+ data: csv_row.to_h.to_json,
36
+ status: 'pending'
37
+ )
38
+
39
+ begin
40
+ import_row.process!
41
+ rescue StandardError => e
42
+ failed += 1
43
+ puts "\n Warning: Row #{row_number} failed: #{e.message}"
44
+ end
45
+
46
+ print '.' if (row_number % 10).zero?
47
+ end
48
+
49
+ import.update!(status: 'completed')
50
+ puts "\n Processed #{row_number} rows (#{failed} failed)"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,78 @@
1
+ module Spree
2
+ module SampleData
3
+ class Loader
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call
7
+ Spree::Events.disable do
8
+ without_geocoding do
9
+ ensure_seeds_loaded
10
+
11
+ puts 'Loading sample configuration data...'
12
+ load_configuration_data
13
+
14
+ puts 'Loading sample metafield definitions...'
15
+ load_ruby_file('metafield_definitions')
16
+
17
+ puts 'Loading sample products...'
18
+ load_products
19
+
20
+ puts 'Loading sample customers...'
21
+ load_customers
22
+
23
+ puts 'Loading sample orders...'
24
+ load_ruby_file('orders')
25
+
26
+ puts 'Loading sample posts...'
27
+ load_ruby_file('posts')
28
+
29
+ puts 'Sample data loaded successfully!'
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def ensure_seeds_loaded
37
+ us = Spree::Country.find_by(iso: 'US')
38
+ return if us&.states&.any? && Spree::Store.default&.persisted?
39
+
40
+ puts 'Running seeds first...'
41
+ Spree::Seeds::All.call
42
+ end
43
+
44
+ def sample_data_path
45
+ @sample_data_path ||= Spree::Core::Engine.root.join('db', 'sample_data')
46
+ end
47
+
48
+ def load_configuration_data
49
+ load_ruby_file('shipping_methods')
50
+ load_ruby_file('payment_methods')
51
+ load_ruby_file('promotions')
52
+ end
53
+
54
+ def load_products
55
+ csv_path = sample_data_path.join('products.csv')
56
+ Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Products)
57
+ end
58
+
59
+ def load_customers
60
+ csv_path = sample_data_path.join('customers.csv')
61
+ Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Customers)
62
+ end
63
+
64
+ def load_ruby_file(name)
65
+ file = sample_data_path.join("#{name}.rb")
66
+ load file.to_s if file.exist?
67
+ end
68
+
69
+ def without_geocoding
70
+ previous = Spree::Config[:geocode_addresses]
71
+ Spree::Config[:geocode_addresses] = false
72
+ yield
73
+ ensure
74
+ Spree::Config[:geocode_addresses] = previous
75
+ end
76
+ end
77
+ end
78
+ end
@@ -14,9 +14,8 @@ module Spree
14
14
  )
15
15
  user.save!
16
16
 
17
- Spree::Store.all.each do |store|
18
- store.add_user(user)
19
- end
17
+ store = Spree::Store.default
18
+ store&.add_user(user) if store&.persisted?
20
19
  end
21
20
  end
22
21
  end
@@ -27,6 +27,7 @@ module Spree
27
27
 
28
28
  # add store resources
29
29
  PaymentMethods.call
30
+ ApiKeys.call
30
31
  end
31
32
  end
32
33
  end