spree_core 5.4.0.beta6 → 5.4.0.beta8

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/base_helper.rb +5 -7
  3. data/app/jobs/spree/payments/handle_webhook_job.rb +22 -0
  4. data/app/mailers/spree/base_mailer.rb +1 -1
  5. data/app/models/concerns/spree/user_methods.rb +16 -0
  6. data/app/models/spree/allowed_origin.rb +44 -0
  7. data/app/models/spree/asset.rb +121 -3
  8. data/app/models/spree/cart_promotion.rb +7 -0
  9. data/app/models/spree/checkout/default_requirements.rb +51 -0
  10. data/app/models/spree/checkout/registry.rb +112 -0
  11. data/app/models/spree/checkout/requirement.rb +49 -0
  12. data/app/models/spree/checkout/requirements.rb +56 -0
  13. data/app/models/spree/checkout/step.rb +52 -0
  14. data/app/models/spree/image/configuration/active_storage.rb +2 -14
  15. data/app/models/spree/image.rb +2 -78
  16. data/app/models/spree/legacy_user.rb +1 -0
  17. data/app/models/spree/line_item.rb +3 -3
  18. data/app/models/spree/order/checkout.rb +18 -0
  19. data/app/models/spree/order.rb +3 -0
  20. data/app/models/spree/order_promotion.rb +2 -0
  21. data/app/models/spree/payment_method.rb +34 -0
  22. data/app/models/spree/payment_session.rb +18 -0
  23. data/app/models/spree/product.rb +45 -34
  24. data/app/models/spree/shipment.rb +1 -0
  25. data/app/models/spree/store.rb +32 -0
  26. data/app/models/spree/variant.rb +21 -12
  27. data/app/services/spree/cart/create.rb +3 -30
  28. data/app/services/spree/carts/complete.rb +46 -0
  29. data/app/services/spree/carts/create.rb +32 -0
  30. data/app/services/spree/carts/update.rb +115 -0
  31. data/app/services/spree/{cart → carts}/upsert_items.rb +19 -23
  32. data/app/services/spree/payments/handle_webhook.rb +58 -0
  33. data/app/services/spree/seeds/all.rb +1 -0
  34. data/app/services/spree/seeds/allowed_origins.rb +14 -0
  35. data/app/views/spree/shared/_mailer_logo.html.erb +1 -1
  36. data/config/locales/en.yml +23 -2
  37. data/db/migrate/20260315000000_create_spree_allowed_origins.rb +14 -0
  38. data/db/migrate/20260315100000_add_product_media_support.rb +21 -0
  39. data/lib/spree/core/configuration.rb +3 -0
  40. data/lib/spree/core/dependencies.rb +4 -2
  41. data/lib/spree/core/version.rb +1 -1
  42. data/lib/spree/core.rb +1 -0
  43. data/lib/spree/permitted_attributes.rb +5 -1
  44. data/lib/spree/testing_support/factories/allowed_origin_factory.rb +8 -0
  45. data/lib/spree/testing_support/factories/asset_factory.rb +6 -9
  46. data/lib/spree/testing_support/factories/image_factory.rb +3 -1
  47. data/lib/spree/testing_support/factories/order_factory.rb +3 -0
  48. data/lib/tasks/images.rake +11 -11
  49. data/lib/tasks/products.rake +4 -2
  50. metadata +21 -6
  51. data/app/services/spree/orders/update.rb +0 -121
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1e118b3f3198080c2006abcd4956ae76608cf6a6a99ed7f16df2380839fe98f
4
- data.tar.gz: 8f05ff3e72814788a15c7c145ea62f1f9efb603b5dcd18fcedcd07774274db7a
3
+ metadata.gz: d8297e501dfb71ba02dbd003c3f2a6fa035585a07cca4cf3f7e039eb92bb55c6
4
+ data.tar.gz: 87546d3659afc8573cf33cd9b3b2ceac6e351f7f8e30969626991c7c39da8773
5
5
  SHA512:
6
- metadata.gz: b6b0596bb455d278630cf6df3e1927034c703f15e4319c856cec8eaa798e871ec671597a3bf07f3cb20846b775a460f77df8e828184fc2b1200c4a394760c361
7
- data.tar.gz: c3693f84de9a1b1c26e3572f8c350a498050765242453b3b2a95d3a6956d06476a54db89ec3f21c0ba4bd0eea27edad7da2a2b1ada5f79d9a1eaf70a44084a28
6
+ metadata.gz: 8983ae8234112c480dc54e302bd7d56557b11418af19a0ca6d65aa182ed06195c828ae69dc122209c23057149febb28b4c9e410a460e9dba2b09978240788997
7
+ data.tar.gz: e62cdc311fe8a2a256442b38e84df024e5c145ef47dc3c2cf5981e923bf5e3558c3b6937c3a64d101eb0783f5c493fdfbc3e75db004740d45599fa9d8ddfc80a
@@ -114,10 +114,8 @@ module Spree
114
114
 
115
115
  base_url = if options[:relative]
116
116
  ''
117
- elsif store.respond_to?(:formatted_custom_domain) && store.formatted_custom_domain.present?
118
- store.formatted_custom_domain
119
117
  else
120
- store.formatted_url
118
+ store.storefront_url
121
119
  end
122
120
 
123
121
  localize = if options[:locale].present?
@@ -151,15 +149,15 @@ module Spree
151
149
  # we should always try to render image of the default variant
152
150
  # same as it's done on PDP
153
151
  def default_image_for_product(product)
154
- Spree::Deprecation.warn('BaseHelper#default_image_for_product is deprecated and will be removed in Spree 5.5. Please use product.default_image instead')
152
+ Spree::Deprecation.warn('BaseHelper#default_image_for_product is deprecated and will be removed in Spree 6.0. Please use product.primary_media instead')
155
153
 
156
- product.default_image
154
+ product.primary_media
157
155
  end
158
156
 
159
157
  def default_image_for_product_or_variant(product_or_variant)
160
- Spree::Deprecation.warn('BaseHelper#default_image_for_product_or_variant is deprecated and will be removed in Spree 5.5. Please use product_or_variant.default_image instead')
158
+ Spree::Deprecation.warn('BaseHelper#default_image_for_product_or_variant is deprecated and will be removed in Spree 6.0. Please use product_or_variant.primary_media instead')
161
159
 
162
- product_or_variant.default_image
160
+ product_or_variant.primary_media
163
161
  end
164
162
 
165
163
  def base_cache_key
@@ -0,0 +1,22 @@
1
+ module Spree
2
+ module Payments
3
+ class HandleWebhookJob < Spree::BaseJob
4
+ queue_as Spree.queues.payment_webhooks
5
+
6
+ retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
7
+ retry_on ActiveRecord::LockWaitTimeout, wait: 5.seconds, attempts: 3
8
+ discard_on ActiveRecord::RecordNotFound
9
+
10
+ def perform(payment_method_id:, action:, payment_session_id:)
11
+ payment_method = Spree::PaymentMethod.find(payment_method_id)
12
+ payment_session = Spree::PaymentSession.find(payment_session_id)
13
+
14
+ Spree::Dependencies.payments_handle_webhook_service.constantize.call(
15
+ payment_method: payment_method,
16
+ action: action.to_sym,
17
+ payment_session: payment_session
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
@@ -37,7 +37,7 @@ module Spree
37
37
  # this is only a fail-safe solution if developer didn't set this in environment files
38
38
  # http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views
39
39
  def ensure_default_action_mailer_url_host(store_url = nil)
40
- host_url = store_url.presence || current_store.try(:url_or_custom_domain)
40
+ host_url = store_url.presence || current_store.try(:storefront_url)
41
41
 
42
42
  return if host_url.blank?
43
43
 
@@ -18,6 +18,13 @@ module Spree
18
18
  # Enable lifecycle events for user models
19
19
  publishes_lifecycle_events
20
20
 
21
+ # Password reset token (Rails 7.1+ signed token, no DB column needed)
22
+ # Token auto-invalidates when password changes (salt changes)
23
+ # Expiration is configurable via Spree::Config.customer_password_reset_expires_in (in minutes)
24
+ generates_token_for :password_reset, expires_in: Spree::Config.customer_password_reset_expires_in.minutes do
25
+ password_salt&.last(10) || encrypted_password&.last(10)
26
+ end
27
+
21
28
  # we need to have this callback before any dependent: :destroy associations
22
29
  # https://github.com/rails/rails/issues/3458
23
30
  before_validation :clone_billing_address, if: :use_billing?
@@ -35,6 +42,7 @@ module Spree
35
42
  has_many :promotion_rule_users, class_name: 'Spree::PromotionRuleUser', foreign_key: :user_id, dependent: :destroy
36
43
  has_many :promotion_rules, through: :promotion_rule_users, class_name: 'Spree::PromotionRule'
37
44
  has_many :orders, foreign_key: :user_id, class_name: 'Spree::Order'
45
+ has_many :carts, -> { incomplete }, foreign_key: :user_id, class_name: 'Spree::Order'
38
46
  has_many :completed_orders, -> { complete }, foreign_key: :user_id, class_name: 'Spree::Order'
39
47
  has_many :store_credits, class_name: 'Spree::StoreCredit', foreign_key: :user_id, dependent: :destroy
40
48
  has_many :wishlists, class_name: 'Spree::Wishlist', foreign_key: :user_id, dependent: :destroy
@@ -58,6 +66,14 @@ module Spree
58
66
  #
59
67
  attr_accessor :confirm_email, :terms_of_service
60
68
 
69
+ def self.find_by_password_reset_token(token)
70
+ find_by_token_for(:password_reset, token)
71
+ end
72
+
73
+ def self.find_by_password_reset_token!(token)
74
+ find_by_token_for!(:password_reset, token)
75
+ end
76
+
61
77
  def self.multi_search(query)
62
78
  sanitized_query = sanitize_query_for_multi_search(query)
63
79
  return none if query.blank?
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ class AllowedOrigin < Spree.base_class
5
+ has_prefix_id :ao
6
+
7
+ include Spree::SingleStoreResource
8
+
9
+ belongs_to :store, class_name: 'Spree::Store'
10
+
11
+ validates :store, :origin, presence: true
12
+ validates :origin, uniqueness: { scope: [:store_id, *spree_base_uniqueness_scope] }
13
+ validate :origin_must_be_valid_http_url
14
+
15
+ private
16
+
17
+ def origin_must_be_valid_http_url
18
+ return if origin.blank?
19
+
20
+ uri = URI.parse(origin)
21
+
22
+ unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
23
+ errors.add(:origin, :invalid)
24
+ return
25
+ end
26
+
27
+ if uri.host.blank?
28
+ errors.add(:origin, :invalid)
29
+ return
30
+ end
31
+
32
+ # Origins must not have a path, query, or fragment
33
+ if uri.path.present? && uri.path != '/'
34
+ errors.add(:origin, :must_be_origin_only)
35
+ end
36
+
37
+ if uri.query.present? || uri.fragment.present?
38
+ errors.add(:origin, :must_be_origin_only)
39
+ end
40
+ rescue URI::InvalidURIError
41
+ errors.add(:origin, :invalid)
42
+ end
43
+ end
44
+ end
@@ -1,20 +1,42 @@
1
1
  module Spree
2
2
  class Asset < Spree.base_class
3
- has_prefix_id :asset # Stripe: file_
3
+ has_prefix_id :media
4
4
 
5
5
  include Support::ActiveStorage
6
+ include Rails.application.routes.url_helpers
7
+ include Spree::ImageMethods # legacy, will be removed in Spree 6
6
8
  include Spree::Metafields
7
9
  include Spree::Metadata
8
10
 
11
+ # Legacy styles support (was in Spree::Image::Configuration::ActiveStorage)
12
+ # @deprecated Will be removed in Spree 6
13
+ def self.styles
14
+ @styles ||= Spree::Config.product_image_variant_sizes.transform_values do |dimensions|
15
+ "#{dimensions[0]}x#{dimensions[1]}>"
16
+ end
17
+ end
18
+
19
+ def default_style
20
+ :small
21
+ end
22
+
9
23
  publishes_lifecycle_events
10
24
 
11
25
  EXTERNAL_URL_METAFIELD_KEY = 'external.url'
26
+ MEDIA_TYPES = %w[image video external_video].freeze
27
+
28
+ after_initialize { self.media_type ||= 'image' }
12
29
 
13
30
  belongs_to :viewable, polymorphic: true, touch: true
14
31
  acts_as_list scope: [:viewable_id, :viewable_type]
15
32
 
16
33
  delegate :key, :attached?, :variant, :variable?, :blob, :filename, :variation, to: :attachment
17
34
 
35
+ validates :media_type, inclusion: { in: MEDIA_TYPES }
36
+ validates :attachment, attached: true, content_type: Rails.application.config.active_storage.web_image_content_types,
37
+ if: -> { media_type == 'image' }
38
+ validates :external_video_url, presence: true, if: -> { media_type.in?(%w[video external_video]) }
39
+
18
40
  WEBP_SAVER_OPTIONS = {
19
41
  strip: true,
20
42
  quality: 75,
@@ -43,14 +65,46 @@ module Spree
43
65
 
44
66
  default_scope { includes(attachment_attachment: :blob) }
45
67
 
68
+ # STI was disabled in Spree::Image, keep it disabled here
69
+ self.inheritance_column = nil
70
+
46
71
  store_accessor :private_metadata, :session_uploaded_assets_uuid
47
72
  scope :with_session_uploaded_assets_uuid, lambda { |uuid|
48
73
  where(session_id: uuid)
49
74
  }
50
75
  scope :with_external_url, ->(url) { url.present? ? with_metafield_key_value(EXTERNAL_URL_METAFIELD_KEY, url.strip) : none }
51
76
 
77
+ # Callbacks merged from Spree::Image
78
+ after_commit :touch_product_variants, if: :should_touch_product_variants?, on: :update
79
+ after_commit :update_viewable_thumbnail_on_create, on: :create
80
+ after_commit :update_viewable_thumbnail_on_destroy, on: :destroy
81
+ after_commit :update_viewable_thumbnail_on_reorder, on: :update, if: :saved_change_to_position?
82
+ after_commit :update_viewable_thumbnail_on_viewable_change, on: :update, if: :saved_change_to_viewable_id?
83
+
84
+ after_create :increment_viewable_media_count
85
+ after_destroy :decrement_viewable_media_count
86
+
52
87
  def product
53
- @product ||= viewable_type == 'Spree::Variant' ? viewable&.product : nil
88
+ @product ||= case viewable_type
89
+ when 'Spree::Variant' then viewable&.product
90
+ when 'Spree::Product' then viewable
91
+ end
92
+ end
93
+
94
+ def focal_point
95
+ return nil if focal_point_x.nil? || focal_point_y.nil?
96
+
97
+ { x: focal_point_x, y: focal_point_y }
98
+ end
99
+
100
+ def focal_point=(point)
101
+ if point.nil?
102
+ self.focal_point_x = nil
103
+ self.focal_point_y = nil
104
+ else
105
+ self.focal_point_x = point[:x]
106
+ self.focal_point_y = point[:y]
107
+ end
54
108
  end
55
109
 
56
110
  def external_url
@@ -66,7 +120,71 @@ module Spree
66
120
  end
67
121
 
68
122
  def event_prefix
69
- 'asset'
123
+ 'media'
124
+ end
125
+
126
+ # @deprecated
127
+ def styles
128
+ Spree::Deprecation.warn("Asset#styles is deprecated and will be removed in Spree 6.0. Please use active storage variants with cdn_image_url")
129
+
130
+ self.class.styles.map do |_, size|
131
+ width, height = size.chop.split('x').map(&:to_i)
132
+
133
+ {
134
+ url: generate_url(size: size),
135
+ size: size,
136
+ width: width,
137
+ height: height
138
+ }
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def touch_product_variants
145
+ viewable.product.variants.touch_all
146
+ end
147
+
148
+ def should_touch_product_variants?
149
+ viewable.is_a?(Spree::Variant) &&
150
+ viewable.is_master? &&
151
+ viewable.product.has_variants? &&
152
+ saved_change_to_position?
153
+ end
154
+
155
+ def increment_viewable_media_count
156
+ case viewable_type
157
+ when 'Spree::Variant'
158
+ Spree::Variant.increment_counter(:media_count, viewable_id)
159
+ Spree::Product.increment_counter(:media_count, viewable.product_id)
160
+ when 'Spree::Product'
161
+ Spree::Product.increment_counter(:media_count, viewable_id)
162
+ end
163
+ end
164
+
165
+ def decrement_viewable_media_count
166
+ case viewable_type
167
+ when 'Spree::Variant'
168
+ Spree::Variant.decrement_counter(:media_count, viewable_id)
169
+ Spree::Product.decrement_counter(:media_count, viewable.product_id)
170
+ when 'Spree::Product'
171
+ Spree::Product.decrement_counter(:media_count, viewable_id)
172
+ end
70
173
  end
174
+
175
+ def update_viewable_thumbnail
176
+ case viewable_type
177
+ when 'Spree::Variant'
178
+ viewable.update_thumbnail!
179
+ viewable.product.update_thumbnail!
180
+ when 'Spree::Product'
181
+ viewable.update_thumbnail!
182
+ end
183
+ end
184
+
185
+ alias update_viewable_thumbnail_on_create update_viewable_thumbnail
186
+ alias update_viewable_thumbnail_on_destroy update_viewable_thumbnail
187
+ alias update_viewable_thumbnail_on_reorder update_viewable_thumbnail
188
+ alias update_viewable_thumbnail_on_viewable_change update_viewable_thumbnail
71
189
  end
72
190
  end
@@ -0,0 +1,7 @@
1
+ module Spree
2
+ # Cart-facing view of an OrderPromotion.
3
+ # Same table, different prefix ID (cp_) for the Cart API.
4
+ class CartPromotion < OrderPromotion
5
+ has_prefix_id :cpromo
6
+ end
7
+ end
@@ -0,0 +1,51 @@
1
+ module Spree
2
+ module Checkout
3
+ # Built-in checkout requirements that map to the standard Spree checkout flow.
4
+ #
5
+ # Checks line items, email, shipping address, shipping method, and payment.
6
+ # In Spree 6 these same checks will read from the +Cart+ model instead of
7
+ # the state machine — the API contract stays identical.
8
+ #
9
+ # @see Requirements
10
+ class DefaultRequirements
11
+ # @param order [Spree::Order]
12
+ def initialize(order)
13
+ @order = order
14
+ end
15
+
16
+ # @return [Array<Hash{Symbol => String}>] unmet default requirements as
17
+ # +{ step:, field:, message: }+ hashes
18
+ def call
19
+ [].tap do |r|
20
+ r << req('cart', 'line_items', Spree.t('checkout_requirements.line_items_required')) unless @order.line_items.any?
21
+ r << req('address', 'email', Spree.t('checkout_requirements.email_required')) unless @order.email.present?
22
+ r << req('address', 'ship_address', Spree.t('checkout_requirements.ship_address_required')) if @order.requires_ship_address? && @order.ship_address.blank?
23
+ r << req('delivery', 'shipping_method', Spree.t('checkout_requirements.shipping_method_required')) if delivery_required? && !shipping_method_selected?
24
+ r << req('payment', 'payment', Spree.t('checkout_requirements.payment_required')) if payment_required? && !payment_satisfied?
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def delivery_required?
31
+ @order.has_checkout_step?('delivery') && @order.delivery_required?
32
+ end
33
+
34
+ def shipping_method_selected?
35
+ @order.shipments.any? && @order.shipments.all? { |s| s.shipping_method.present? }
36
+ end
37
+
38
+ def payment_required?
39
+ @order.has_checkout_step?('payment') && @order.payment_required?
40
+ end
41
+
42
+ def payment_satisfied?
43
+ @order.payments.valid.any?
44
+ end
45
+
46
+ def req(step, field, message)
47
+ { step: step, field: field, message: message }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,112 @@
1
+ module Spree
2
+ module Checkout
3
+ # Global registry for custom checkout steps and requirements.
4
+ #
5
+ # Provides a composable extension point so developers can add, remove, or
6
+ # reorder checkout steps and attach extra requirements to existing steps —
7
+ # all without subclassing or monkey-patching.
8
+ #
9
+ # Registered steps and requirements are evaluated by {Requirements} at
10
+ # serialization time to produce the +requirements+ array on the Cart API.
11
+ #
12
+ # @example Add a custom step
13
+ # Spree::Checkout::Registry.register_step(
14
+ # name: :loyalty,
15
+ # before: :payment,
16
+ # satisfied: ->(order) { order.loyalty_verified? },
17
+ # requirements: ->(order) { [{ step: 'loyalty', field: 'loyalty_number', message: 'Required' }] }
18
+ # )
19
+ #
20
+ # @example Add a requirement to an existing step
21
+ # Spree::Checkout::Registry.add_requirement(
22
+ # step: :payment,
23
+ # field: :po_number,
24
+ # message: 'PO number is required',
25
+ # satisfied: ->(order) { order.po_number.present? }
26
+ # )
27
+ #
28
+ # @example Remove a step
29
+ # Spree::Checkout::Registry.remove_step(:loyalty)
30
+ class Registry
31
+ class << self
32
+ # Register a new custom checkout step.
33
+ #
34
+ # @param name [String, Symbol] unique step identifier
35
+ # @param satisfied [Proc] lambda accepting an order, returns true when step is complete
36
+ # @param requirements [Proc] lambda accepting an order, returns Array of requirement hashes
37
+ # @param options [Hash] additional options forwarded to {Step} (+:before+, +:after+, +:applicable+)
38
+ # @return [Array<Step>] the updated steps list
39
+ def register_step(name:, satisfied:, requirements:, **options)
40
+ steps << Step.new(name: name, satisfied: satisfied, requirements: requirements, **options)
41
+ end
42
+
43
+ # Add an extra requirement to an existing checkout step.
44
+ #
45
+ # @param step [String, Symbol] checkout step this requirement belongs to
46
+ # @param field [String, Symbol] field identifier
47
+ # @param message [String] human-readable validation message
48
+ # @param satisfied [Proc] lambda accepting an order, returns true when met
49
+ # @param options [Hash] additional options forwarded to {Requirement} (+:applicable+)
50
+ # @return [Array<Requirement>] the updated requirements list
51
+ def add_requirement(step:, field:, message:, satisfied:, **options)
52
+ requirements << Requirement.new(step: step, field: field, message: message, satisfied: satisfied, **options)
53
+ end
54
+
55
+ # Remove a previously registered step by name.
56
+ #
57
+ # @param name [String, Symbol] step name to remove
58
+ # @return [Array<Step>] the updated steps list
59
+ def remove_step(name)
60
+ steps.reject! { |s| s.name == name.to_s }
61
+ end
62
+
63
+ # Remove a previously registered requirement by step and field.
64
+ #
65
+ # @param step [String, Symbol] checkout step the requirement belongs to
66
+ # @param field [String, Symbol] field identifier
67
+ # @return [Array<Requirement>] the updated requirements list
68
+ def remove_requirement(step:, field:)
69
+ requirements.reject! { |r| r.step == step.to_s && r.field == field.to_s }
70
+ end
71
+
72
+ # Returns steps sorted by +before+/+after+ constraints relative to the checkout flow.
73
+ #
74
+ # The sort order is derived from {Spree::Order.checkout_step_names} so it
75
+ # stays in sync with any customizations to the checkout state machine.
76
+ # Steps with +before:+/+after:+ anchors are ordered by the anchor's position;
77
+ # steps without constraints are appended at the end.
78
+ #
79
+ # @return [Array<Step>] steps in display order
80
+ def ordered_steps
81
+ return steps if steps.empty?
82
+
83
+ step_order = Spree::Order.checkout_step_names.map(&:to_s)
84
+ positioned, unpositioned = steps.partition { |s| s.before || s.after }
85
+
86
+ sorted = positioned.sort_by do |s|
87
+ anchor = s.before || s.after
88
+ idx = step_order.index(anchor)
89
+ # before: inserts just before the anchor, after: just after
90
+ idx ? (s.before ? idx - 0.5 : idx + 0.5) : Float::INFINITY
91
+ end
92
+
93
+ sorted + unpositioned
94
+ end
95
+
96
+ # @return [Array<Step>] all registered steps
97
+ def steps = (@steps ||= [])
98
+
99
+ # @return [Array<Requirement>] all registered requirements
100
+ def requirements = (@requirements ||= [])
101
+
102
+ # Clear all registered steps and requirements. Intended for testing.
103
+ #
104
+ # @return [void]
105
+ def reset!
106
+ @steps = []
107
+ @requirements = []
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,49 @@
1
+ module Spree
2
+ module Checkout
3
+ # Value object representing a single additional requirement registered via {Registry}.
4
+ #
5
+ # Unlike {Step}, a Requirement attaches extra validation to an *existing* checkout step
6
+ # rather than introducing a new step.
7
+ #
8
+ # @example Require a PO number for B2B orders at the payment step
9
+ # Spree::Checkout::Registry.add_requirement(
10
+ # step: :payment,
11
+ # field: :po_number,
12
+ # message: 'PO number is required for business accounts',
13
+ # satisfied: ->(order) { order.po_number.present? },
14
+ # applicable: ->(order) { order.account&.business? }
15
+ # )
16
+ class Requirement
17
+ # @return [String] checkout step this requirement belongs to
18
+ attr_reader :step
19
+
20
+ # @return [String] field identifier (e.g. +"po_number"+, +"tax_id"+)
21
+ attr_reader :field
22
+
23
+ # @return [String] human-readable message shown when the requirement is not met
24
+ attr_reader :message
25
+
26
+ # @param step [String, Symbol] checkout step this requirement belongs to
27
+ # @param field [String, Symbol] field identifier
28
+ # @param message [String] human-readable validation message
29
+ # @param satisfied [Proc] lambda accepting an order, returns true when met
30
+ # @param applicable [Proc] lambda accepting an order, returns true when this requirement applies
31
+ # (defaults to always applicable)
32
+ def initialize(step:, field:, message:, satisfied:, applicable: ->(_) { true })
33
+ @step = step.to_s
34
+ @field = field.to_s
35
+ @message = message
36
+ @satisfied_proc = satisfied
37
+ @applicable_proc = applicable
38
+ end
39
+
40
+ # @param order [Spree::Order]
41
+ # @return [Boolean] whether the requirement has been met
42
+ def satisfied?(order) = @satisfied_proc.call(order)
43
+
44
+ # @param order [Spree::Order]
45
+ # @return [Boolean] whether this requirement applies to the given order
46
+ def applicable?(order) = @applicable_proc.call(order)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,56 @@
1
+ module Spree
2
+ module Checkout
3
+ # Aggregates all checkout requirements for an order.
4
+ #
5
+ # Combines built-in checks from {DefaultRequirements} with custom steps and
6
+ # requirements registered in {Registry}. The resulting array of hashes is
7
+ # exposed on the Cart API as the +requirements+ attribute.
8
+ #
9
+ # Each requirement hash has the shape:
10
+ # { step: String, field: String, message: String }
11
+ #
12
+ # @example
13
+ # reqs = Spree::Checkout::Requirements.new(order)
14
+ # reqs.call # => [{ step: "address", field: "email", message: "Email address is required" }]
15
+ # reqs.met? # => false
16
+ class Requirements
17
+ # @param order [Spree::Order]
18
+ def initialize(order)
19
+ @order = order
20
+ end
21
+
22
+ # @return [Array<Hash{Symbol => String}>] all unmet requirements
23
+ def call
24
+ default + from_registered_steps + from_additional_requirements
25
+ end
26
+
27
+ # @return [Boolean] true when all requirements are satisfied
28
+ def met?
29
+ call.empty?
30
+ end
31
+
32
+ private
33
+
34
+ # @return [Array<Hash>] built-in checkout requirements
35
+ def default
36
+ DefaultRequirements.new(@order).call
37
+ end
38
+
39
+ # @return [Array<Hash>] requirements from unsatisfied registered steps
40
+ def from_registered_steps
41
+ Registry.ordered_steps
42
+ .select { |s| s.applicable?(@order) }
43
+ .reject { |s| s.satisfied?(@order) }
44
+ .flat_map { |s| s.requirements(@order) }
45
+ end
46
+
47
+ # @return [Array<Hash>] requirements from unsatisfied registered requirements
48
+ def from_additional_requirements
49
+ Registry.requirements
50
+ .select { |r| r.applicable?(@order) }
51
+ .reject { |r| r.satisfied?(@order) }
52
+ .map { |r| { step: r.step, field: r.field, message: r.message } }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,52 @@
1
+ module Spree
2
+ module Checkout
3
+ # Value object representing a custom checkout step registered via {Registry}.
4
+ #
5
+ # @example Register a loyalty step before payment
6
+ # Spree::Checkout::Registry.register_step(
7
+ # name: :loyalty,
8
+ # before: :payment,
9
+ # satisfied: ->(order) { order.loyalty_verified? },
10
+ # requirements: ->(order) { [{ step: 'loyalty', field: 'loyalty_number', message: 'Enter loyalty number' }] }
11
+ # )
12
+ class Step
13
+ # @return [String] step name
14
+ attr_reader :name
15
+
16
+ # @return [String, nil] name of the checkout step this should be placed after
17
+ attr_reader :after
18
+
19
+ # @return [String, nil] name of the checkout step this should be placed before
20
+ attr_reader :before
21
+
22
+ # @param name [String, Symbol] unique step identifier
23
+ # @param satisfied [Proc] lambda accepting an order, returns true when the step is complete
24
+ # @param requirements [Proc] lambda accepting an order, returns Array of requirement hashes
25
+ # (+{ step:, field:, message: }+) describing what is still needed
26
+ # @param applicable [Proc] lambda accepting an order, returns true when this step applies
27
+ # (defaults to always applicable)
28
+ # @param after [String, Symbol, nil] place this step after the named checkout step
29
+ # @param before [String, Symbol, nil] place this step before the named checkout step
30
+ def initialize(name:, satisfied:, requirements:, applicable: ->(_) { true }, after: nil, before: nil)
31
+ @name = name.to_s
32
+ @after = after&.to_s
33
+ @before = before&.to_s
34
+ @satisfied_proc = satisfied
35
+ @requirements_proc = requirements
36
+ @applicable_proc = applicable
37
+ end
38
+
39
+ # @param order [Spree::Order]
40
+ # @return [Boolean] whether the step's conditions have been met
41
+ def satisfied?(order) = @satisfied_proc.call(order)
42
+
43
+ # @param order [Spree::Order]
44
+ # @return [Array<Hash{Symbol => String}>] outstanding requirement hashes (+{ step:, field:, message: }+)
45
+ def requirements(order) = @requirements_proc.call(order)
46
+
47
+ # @param order [Spree::Order]
48
+ # @return [Boolean] whether this step applies to the given order
49
+ def applicable?(order) = @applicable_proc.call(order)
50
+ end
51
+ end
52
+ end
@@ -1,22 +1,10 @@
1
+ # @deprecated This module is now a no-op. All logic has been moved to Spree::Asset.
2
+ # Will be removed in Spree 6.0.
1
3
  module Spree
2
4
  class Image < Asset
3
5
  module Configuration
4
6
  module ActiveStorage
5
7
  extend ActiveSupport::Concern
6
-
7
- included do
8
- # Returns image styles derived from Spree::Config.product_image_variant_sizes
9
- # Format: { variant_name: 'WxH>' } for API compatibility
10
- def self.styles
11
- @styles ||= Spree::Config.product_image_variant_sizes.transform_values do |dimensions|
12
- "#{dimensions[0]}x#{dimensions[1]}>"
13
- end
14
- end
15
-
16
- def default_style
17
- :small
18
- end
19
- end
20
8
  end
21
9
  end
22
10
  end