spree_api 5.4.0.beta3 → 5.4.0.beta4

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/error_handler.rb +2 -0
  3. data/app/controllers/concerns/spree/api/v3/http_caching.rb +7 -7
  4. data/app/controllers/concerns/spree/api/v3/order_lock.rb +42 -0
  5. data/app/controllers/concerns/spree/api/v3/resource_serializer.rb +7 -7
  6. data/app/controllers/spree/api/v3/resource_controller.rb +7 -1
  7. data/app/controllers/spree/api/v3/store/markets/countries_controller.rb +39 -0
  8. data/app/controllers/spree/api/v3/store/markets_controller.rb +42 -0
  9. data/app/controllers/spree/api/v3/store/orders/coupon_codes_controller.rb +18 -13
  10. data/app/controllers/spree/api/v3/store/orders/line_items_controller.rb +43 -36
  11. data/app/controllers/spree/api/v3/store/orders/shipments_controller.rb +9 -6
  12. data/app/controllers/spree/api/v3/store/orders/store_credits_controller.rb +18 -13
  13. data/app/controllers/spree/api/v3/store/orders_controller.rb +37 -28
  14. data/app/controllers/spree/api/v3/store/products_controller.rb +31 -12
  15. data/app/serializers/spree/api/v3/address_serializer.rb +3 -2
  16. data/app/serializers/spree/api/v3/admin/customer_serializer.rb +1 -1
  17. data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
  18. data/app/serializers/spree/api/v3/admin/product_serializer.rb +4 -4
  19. data/app/serializers/spree/api/v3/admin/taxon_serializer.rb +1 -1
  20. data/app/serializers/spree/api/v3/admin/taxonomy_serializer.rb +1 -1
  21. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +2 -2
  22. data/app/serializers/spree/api/v3/base_serializer.rb +9 -9
  23. data/app/serializers/spree/api/v3/country_serializer.rb +7 -18
  24. data/app/serializers/spree/api/v3/market_serializer.rb +23 -0
  25. data/app/serializers/spree/api/v3/order_serializer.rb +2 -2
  26. data/app/serializers/spree/api/v3/product_serializer.rb +8 -8
  27. data/app/serializers/spree/api/v3/taxon_serializer.rb +8 -8
  28. data/app/serializers/spree/api/v3/taxonomy_serializer.rb +6 -6
  29. data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
  30. data/app/serializers/spree/api/v3/wishlist_serializer.rb +1 -1
  31. data/app/services/spree/api/v3/filters_aggregator.rb +8 -21
  32. data/config/routes.rb +8 -0
  33. data/lib/spree/api/dependencies.rb +1 -0
  34. metadata +10 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00acdb80bb37236902858e0cf919b67cdbc8de40f54669d01db8b91ce87691d6
4
- data.tar.gz: 81f453d6ab3a5e4083263d67e6c383a3aa4a3f64e67f2077fda8a4ad6eec4e2f
3
+ metadata.gz: e8154a8dd735a006cee415621ff4d95bfdfb4f30935e139bcc72b3d665868ae3
4
+ data.tar.gz: d013be2887432f12fd2dc3440aa14d7fd5d831ebdedc4f814c93c6925dc6b450
5
5
  SHA512:
6
- metadata.gz: 56e2fbc855fd3dc4acc1655ec2cbfce11c89dbcc356404d4b19e872fa9c8e6d85f36525f7b9789de5f13c20b75b4b1c8415fdd56047005ad36cd418636850573
7
- data.tar.gz: 4879e2c6c20455ed8054afcc40653f2774412caed5f19de054655b70e616d98cedb50dd8a7698980044314e51b9a9ddef6eb4013ffbc3f8352b048cb95e128ca
6
+ metadata.gz: ec930921c59d1e904c643ae363efa3cd40c634567ab7a1daf696e35014efae10e6197c0b3f657d0ff537a05ed36b165e34316eb11a25a83fe2d5352793d0afb2
7
+ data.tar.gz: 92175956237d07cbf778b29d7bb2bd562f0e39cbef764d9f608e01c69c849fdc648727750f52a2370d228a8e79df7080d1c84e2802f585ac1ca2303a36d8acb6
@@ -23,6 +23,7 @@ module Spree
23
23
  order_cannot_transition: 'order_cannot_transition',
24
24
  order_empty: 'order_empty',
25
25
  order_invalid_state: 'order_invalid_state',
26
+ order_already_updated: 'order_already_updated',
26
27
 
27
28
  # Line item errors
28
29
  line_item_not_found: 'line_item_not_found',
@@ -192,6 +193,7 @@ module Spree
192
193
  )
193
194
  end
194
195
 
196
+
195
197
  private
196
198
 
197
199
  # Format validation errors for details field
@@ -12,7 +12,7 @@ module Spree
12
12
  extend ActiveSupport::Concern
13
13
 
14
14
  included do
15
- before_action :set_vary_headers
15
+ after_action :set_vary_headers
16
16
  end
17
17
 
18
18
  protected
@@ -69,15 +69,15 @@ module Spree
69
69
  private
70
70
 
71
71
  # Build a cache key for a collection
72
- # Includes: query params, pagination, includes, currency, locale
73
- # Strips order to avoid PostgreSQL errors with DISTINCT + subquery ORDER BY expressions
72
+ # Includes: latest updated_at, total count, query params, pagination, expand, currency, locale
74
73
  def collection_cache_key(collection)
75
74
  parts = [
76
- collection.reorder(nil).cache_key_with_version,
77
- params[:include],
78
- params[:q],
75
+ collection.map(&:updated_at).max&.to_i,
76
+ @pagy&.count,
77
+ params[:expand],
78
+ params[:q]&.to_json,
79
79
  params[:page],
80
- params[:per_page],
80
+ params[:limit],
81
81
  current_currency,
82
82
  current_locale
83
83
  ]
@@ -0,0 +1,42 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module OrderLock
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ rescue_from ActiveRecord::Deadlocked, with: :handle_order_lock_conflict
9
+ rescue_from ActiveRecord::LockWaitTimeout, with: :handle_order_lock_conflict
10
+ end
11
+
12
+ private
13
+
14
+ def with_order_lock
15
+ order = @order || @parent
16
+
17
+ order.with_lock do
18
+ # Persist increment within the transaction so reloads inside yield see the new version
19
+ new_version = order.state_lock_version + 1
20
+ order.update_column(:state_lock_version, new_version)
21
+
22
+ yield
23
+
24
+ if performed? && response.status >= 400
25
+ # Operation failed — revert the increment
26
+ order.update_column(:state_lock_version, new_version - 1)
27
+ end
28
+ end
29
+ end
30
+
31
+ def handle_order_lock_conflict(exception)
32
+ Rails.error.report(exception, context: { order_id: (@order || @parent)&.id }, source: 'spree.api.v3')
33
+ render_error(
34
+ code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:order_already_updated],
35
+ message: Spree.t(:order_already_updated),
36
+ status: :conflict
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -23,17 +23,17 @@ module Spree
23
23
  store: current_store,
24
24
  user: current_user,
25
25
  locale: current_locale,
26
- includes: include_list
26
+ expand: expand_list
27
27
  }
28
28
  end
29
29
 
30
- # Parse include parameter into list
31
- # Supports: ?include=variants,images or ?includes=variants,images
32
- def include_list
33
- include_param = params[:include].presence || params[:includes].presence
34
- return [] unless include_param
30
+ # Parse expand parameter into list
31
+ # Supports: ?expand=variants,images
32
+ def expand_list
33
+ expand_param = params[:expand].presence
34
+ return [] unless expand_param
35
35
 
36
- include_param.to_s.split(',').map(&:strip)
36
+ expand_param.to_s.split(',').map(&:strip)
37
37
  end
38
38
  end
39
39
  end
@@ -103,6 +103,7 @@ module Spree
103
103
  end
104
104
 
105
105
  # Override in subclass to disable distinct (e.g., for custom sorting with computed columns)
106
+ # @return [Boolean] whether to apply distinct to the collection
106
107
  def collection_distinct?
107
108
  true
108
109
  end
@@ -112,6 +113,8 @@ module Spree
112
113
  collection
113
114
  end
114
115
 
116
+ # Override in subclass to specify collection includes
117
+ # @return [Array<Symbol>] the includes to apply to the collection
115
118
  def collection_includes
116
119
  []
117
120
  end
@@ -122,16 +125,19 @@ module Spree
122
125
  end
123
126
 
124
127
  # Pagination parameters
128
+ # @return [Integer] the current page number
125
129
  def page
126
130
  params[:page]&.to_i || 1
127
131
  end
128
132
 
133
+ # @return [Integer] the number of items per page
129
134
  def limit
130
- limit_param = params[:per_page]&.to_i || params[:limit]&.to_i || 25
135
+ limit_param = params[:limit]&.to_i || 25
131
136
  [limit_param, 100].min # Max 100 per page
132
137
  end
133
138
 
134
139
  # Metadata for collection responses
140
+ # @return [Hash] pagination metadata
135
141
  def collection_meta(_collection)
136
142
  return {} unless @pagy
137
143
 
@@ -0,0 +1,39 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ module Markets
6
+ class CountriesController < Store::BaseController
7
+ before_action :load_market
8
+
9
+ # GET /api/v3/store/markets/:market_id/countries
10
+ def index
11
+ countries = @market.countries.order(:name)
12
+
13
+ render json: {
14
+ data: countries.map { |country| serialize_country(country) }
15
+ }
16
+ end
17
+
18
+ # GET /api/v3/store/markets/:market_id/countries/:id
19
+ def show
20
+ country = @market.countries.find_by!(iso: params[:id].upcase)
21
+
22
+ render json: serialize_country(country)
23
+ end
24
+
25
+ private
26
+
27
+ def load_market
28
+ @market = current_store.markets.find_by_prefix_id!(params[:market_id])
29
+ end
30
+
31
+ def serialize_country(country)
32
+ Spree.api.country_serializer.new(country, params: serializer_params).to_h
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,42 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ class MarketsController < Store::BaseController
6
+ # GET /api/v3/store/markets
7
+ def index
8
+ markets = current_store.markets.includes(:countries).order(:position)
9
+
10
+ render json: {
11
+ data: markets.map { |market| serialize_market(market) }
12
+ }
13
+ end
14
+
15
+ # GET /api/v3/store/markets/:id
16
+ def show
17
+ market = current_store.markets.includes(:countries).find_by_prefix_id!(params[:id])
18
+
19
+ render json: serialize_market(market)
20
+ end
21
+
22
+ # GET /api/v3/store/markets/resolve?country=DE
23
+ def resolve
24
+ country_iso = params[:country]&.upcase
25
+ country = Spree::Country.find_by!(iso: country_iso)
26
+ market = current_store.market_for_country(country)
27
+
28
+ raise ActiveRecord::RecordNotFound unless market
29
+
30
+ render json: serialize_market(market)
31
+ end
32
+
33
+ private
34
+
35
+ def serialize_market(market)
36
+ Spree.api.market_serializer.new(market, params: serializer_params.merge(expand: expand_list + ['countries'])).to_h
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -5,6 +5,7 @@ module Spree
5
5
  module Orders
6
6
  class CouponCodesController < ResourceController
7
7
  include Spree::Api::V3::OrderConcern
8
+ include Spree::Api::V3::OrderLock
8
9
 
9
10
  before_action :authorize_order_access!
10
11
  skip_before_action :set_resource
@@ -12,14 +13,16 @@ module Spree
12
13
  # POST /api/v3/store/orders/:order_id/coupon_codes
13
14
  # Apply a coupon code to the order
14
15
  def create
15
- @parent.coupon_code = permitted_params[:code]
16
+ with_order_lock do
17
+ @parent.coupon_code = permitted_params[:code]
16
18
 
17
- coupon_handler.apply
19
+ coupon_handler.apply
18
20
 
19
- if coupon_handler.successful?
20
- render_order(status: :created)
21
- else
22
- render_errors(coupon_handler.error)
21
+ if coupon_handler.successful?
22
+ render_order(status: :created)
23
+ else
24
+ render_errors(coupon_handler.error)
25
+ end
23
26
  end
24
27
  end
25
28
 
@@ -27,15 +30,17 @@ module Spree
27
30
  # Remove a coupon code from the order
28
31
  # :id is the promotion prefix_id (e.g., promo_xxx)
29
32
  def destroy
30
- @resource = scope.find_by_prefix_id!(params[:id])
31
- coupon_code = @resource.code.presence || @resource.name
33
+ with_order_lock do
34
+ @resource = scope.find_by_prefix_id!(params[:id])
35
+ coupon_code = @resource.code.presence || @resource.name
32
36
 
33
- coupon_handler.remove(coupon_code)
37
+ coupon_handler.remove(coupon_code)
34
38
 
35
- if coupon_handler.successful?
36
- render_order
37
- else
38
- render_errors(coupon_handler.error)
39
+ if coupon_handler.successful?
40
+ render_order
41
+ else
42
+ render_errors(coupon_handler.error)
43
+ end
39
44
  end
40
45
  end
41
46
 
@@ -5,63 +5,70 @@ module Spree
5
5
  module Orders
6
6
  class LineItemsController < ResourceController
7
7
  include Spree::Api::V3::OrderConcern
8
+ include Spree::Api::V3::OrderLock
8
9
 
9
10
  skip_before_action :set_resource
10
11
  before_action :authorize_order_access!
11
12
 
12
13
  # POST /api/v3/store/orders/:order_id/line_items
13
14
  def create
14
- result = Spree.cart_add_item_service.call(
15
- order: @parent,
16
- variant: variant,
17
- quantity: permitted_params[:quantity] || 1,
18
- metadata: permitted_params[:metadata] || {},
19
- options: permitted_params[:options] || {}
20
- )
21
-
22
- if result.success?
23
- render_order(status: :created)
24
- else
25
- render_service_error(result.error, code: ERROR_CODES[:insufficient_stock])
15
+ with_order_lock do
16
+ result = Spree.cart_add_item_service.call(
17
+ order: @parent,
18
+ variant: variant,
19
+ quantity: permitted_params[:quantity] || 1,
20
+ metadata: permitted_params[:metadata] || {},
21
+ options: permitted_params[:options] || {}
22
+ )
23
+
24
+ if result.success?
25
+ render_order(status: :created)
26
+ else
27
+ render_service_error(result.error, code: ERROR_CODES[:insufficient_stock])
28
+ end
26
29
  end
27
30
  end
28
31
 
29
32
  # PATCH /api/v3/store/orders/:order_id/line_items/:id
30
33
  def update
31
- @line_item = scope.find_by_prefix_id!(params[:id])
32
-
33
- @line_item.metadata = @line_item.metadata.merge(permitted_params[:metadata].to_h) if permitted_params[:metadata].present?
34
-
35
- if permitted_params[:quantity].present?
36
- result = Spree.cart_set_item_quantity_service.call(
37
- order: @parent,
38
- line_item: @line_item,
39
- quantity: permitted_params[:quantity]
40
- )
41
-
42
- if result.success?
34
+ with_order_lock do
35
+ @line_item = scope.find_by_prefix_id!(params[:id])
36
+
37
+ @line_item.metadata = @line_item.metadata.merge(permitted_params[:metadata].to_h) if permitted_params[:metadata].present?
38
+
39
+ if permitted_params[:quantity].present?
40
+ result = Spree.cart_set_item_quantity_service.call(
41
+ order: @parent,
42
+ line_item: @line_item,
43
+ quantity: permitted_params[:quantity]
44
+ )
45
+
46
+ if result.success?
47
+ render_order
48
+ else
49
+ render_service_error(result.error, code: ERROR_CODES[:invalid_quantity])
50
+ end
51
+ elsif @line_item.changed?
52
+ @line_item.save!
43
53
  render_order
44
54
  else
45
- render_service_error(result.error, code: ERROR_CODES[:invalid_quantity])
55
+ render_order
46
56
  end
47
- elsif @line_item.changed?
48
- @line_item.save!
49
- render_order
50
- else
51
- render_order
52
57
  end
53
58
  end
54
59
 
55
60
  # DELETE /api/v3/store/orders/:order_id/line_items/:id
56
61
  def destroy
57
- @line_item = scope.find_by_prefix_id!(params[:id])
62
+ with_order_lock do
63
+ @line_item = scope.find_by_prefix_id!(params[:id])
58
64
 
59
- Spree.cart_remove_line_item_service.call(
60
- order: @parent,
61
- line_item: @line_item
62
- )
65
+ Spree.cart_remove_line_item_service.call(
66
+ order: @parent,
67
+ line_item: @line_item
68
+ )
63
69
 
64
- render_order
70
+ render_order
71
+ end
65
72
  end
66
73
 
67
74
  protected
@@ -5,6 +5,7 @@ module Spree
5
5
  module Orders
6
6
  class ShipmentsController < ResourceController
7
7
  include Spree::Api::V3::OrderConcern
8
+ include Spree::Api::V3::OrderLock
8
9
 
9
10
  before_action :authorize_order_access!
10
11
  skip_before_action :set_resource
@@ -12,13 +13,15 @@ module Spree
12
13
 
13
14
  # PATCH /api/v3/store/orders/:order_id/shipments/:id
14
15
  def update
15
- if permitted_params[:selected_shipping_rate_id].present?
16
- shipping_rate = @resource.shipping_rates.find_by_prefix_id!(permitted_params[:selected_shipping_rate_id])
17
- @resource.selected_shipping_rate_id = shipping_rate.id
18
- @resource.save!
16
+ with_order_lock do
17
+ if permitted_params[:selected_shipping_rate_id].present?
18
+ shipping_rate = @resource.shipping_rates.find_by_prefix_id!(permitted_params[:selected_shipping_rate_id])
19
+ @resource.selected_shipping_rate_id = shipping_rate.id
20
+ @resource.save!
21
+ end
22
+
23
+ render_order
19
24
  end
20
-
21
- render_order
22
25
  end
23
26
 
24
27
  protected
@@ -5,6 +5,7 @@ module Spree
5
5
  module Orders
6
6
  class StoreCreditsController < Store::BaseController
7
7
  include Spree::Api::V3::OrderConcern
8
+ include Spree::Api::V3::OrderLock
8
9
 
9
10
  before_action :require_authentication!
10
11
  before_action :set_parent
@@ -12,26 +13,30 @@ module Spree
12
13
 
13
14
  # POST /api/v3/store/orders/:order_id/store_credits
14
15
  def create
15
- result = Spree.checkout_add_store_credit_service.call(
16
- order: @parent,
17
- amount: params[:amount].try(:to_f)
18
- )
16
+ with_order_lock do
17
+ result = Spree.checkout_add_store_credit_service.call(
18
+ order: @parent,
19
+ amount: params[:amount].try(:to_f)
20
+ )
19
21
 
20
- if result.success?
21
- render_order
22
- else
23
- render_service_error(result.error)
22
+ if result.success?
23
+ render_order
24
+ else
25
+ render_service_error(result.error)
26
+ end
24
27
  end
25
28
  end
26
29
 
27
30
  # DELETE /api/v3/store/orders/:order_id/store_credits
28
31
  def destroy
29
- result = Spree.checkout_remove_store_credit_service.call(order: @parent)
32
+ with_order_lock do
33
+ result = Spree.checkout_remove_store_credit_service.call(order: @parent)
30
34
 
31
- if result.success?
32
- render_order
33
- else
34
- render_service_error(result.error)
35
+ if result.success?
36
+ render_order
37
+ else
38
+ render_service_error(result.error)
39
+ end
35
40
  end
36
41
  end
37
42
  end
@@ -4,6 +4,7 @@ module Spree
4
4
  module Store
5
5
  class OrdersController < ResourceController
6
6
  include Spree::Api::V3::OrderConcern
7
+ include Spree::Api::V3::OrderLock
7
8
 
8
9
  # Skip base controller's set_resource and define our own complete list
9
10
  skip_before_action :set_resource
@@ -20,48 +21,56 @@ module Spree
20
21
  # }
21
22
  #
22
23
  def update
23
- result = Spree::Api::V3::Orders::Update.call(
24
- order: @order,
25
- params: order_params
26
- )
27
-
28
- if result.success?
29
- render json: serialize_resource(@order.reload)
30
- else
31
- render_service_error(result.error, code: ERROR_CODES[:validation_error])
24
+ with_order_lock do
25
+ result = Spree::Api::V3::Orders::Update.call(
26
+ order: @order,
27
+ params: order_params
28
+ )
29
+
30
+ if result.success?
31
+ render json: serialize_resource(@order.reload)
32
+ else
33
+ render_service_error(result.error, code: ERROR_CODES[:validation_error])
34
+ end
32
35
  end
33
36
  end
34
37
 
35
38
  # PATCH /api/v3/store/orders/:id/next
36
39
  def next
37
- result = Spree.checkout_next_service.call(order: @order)
38
-
39
- if result.success?
40
- render json: serialize_resource(@order)
41
- else
42
- render_service_error(result.error, code: ERROR_CODES[:order_cannot_transition])
40
+ with_order_lock do
41
+ result = Spree.checkout_next_service.call(order: @order)
42
+
43
+ if result.success?
44
+ render json: serialize_resource(@order)
45
+ else
46
+ render_service_error(result.error, code: ERROR_CODES[:order_cannot_transition])
47
+ end
43
48
  end
44
49
  end
45
50
 
46
51
  # PATCH /api/v3/store/orders/:id/advance
47
52
  def advance
48
- result = Spree.checkout_advance_service.call(order: @order)
49
-
50
- if result.success?
51
- render json: serialize_resource(@order)
52
- else
53
- render_service_error(result.error, code: ERROR_CODES[:order_cannot_transition])
53
+ with_order_lock do
54
+ result = Spree.checkout_advance_service.call(order: @order)
55
+
56
+ if result.success?
57
+ render json: serialize_resource(@order)
58
+ else
59
+ render_service_error(result.error, code: ERROR_CODES[:order_cannot_transition])
60
+ end
54
61
  end
55
62
  end
56
63
 
57
64
  # PATCH /api/v3/store/orders/:id/complete
58
65
  def complete
59
- result = Spree.checkout_complete_service.call(order: @order)
60
-
61
- if result.success?
62
- render json: serialize_resource(@order)
63
- else
64
- render_service_error(result.error, code: ERROR_CODES[:order_already_completed])
66
+ with_order_lock do
67
+ result = Spree.checkout_complete_service.call(order: @order)
68
+
69
+ if result.success?
70
+ render json: serialize_resource(@order)
71
+ else
72
+ render_service_error(result.error, code: ERROR_CODES[:order_already_completed])
73
+ end
65
74
  end
66
75
  end
67
76
 
@@ -117,7 +126,7 @@ module Spree
117
126
  [
118
127
  :id, :firstname, :lastname, :address1, :address2,
119
128
  :city, :zipcode, :phone, :company,
120
- :country_iso, :state_abbr, :state_name
129
+ :country_iso, :state_abbr, :state_name, :quick_checkout
121
130
  ]
122
131
  end
123
132
  end
@@ -3,9 +3,11 @@ module Spree
3
3
  module V3
4
4
  module Store
5
5
  class ProductsController < ResourceController
6
- SORT_OPTIONS = {
7
- 'price-low-to-high' => :ascend_by_price,
8
- 'price-high-to-low' => :descend_by_price
6
+ # Sort values that require special scopes (not plain Ransack column sorts).
7
+ CUSTOM_SORT_SCOPES = {
8
+ 'price asc' => :ascend_by_price,
9
+ 'price desc' => :descend_by_price,
10
+ 'best_selling' => :by_best_selling
9
11
  }.freeze
10
12
 
11
13
  protected
@@ -48,24 +50,41 @@ module Spree
48
50
  !custom_sort_requested?
49
51
  end
50
52
 
51
- # Apply custom sorting scopes for price/best-selling
53
+ # Applies sorting from the unified `sort` param.
54
+ # Custom values ('price asc', 'best_selling') use product-specific scopes.
55
+ # Standard Ransack values ('name asc', 'created_at desc') are passed to q[s].
52
56
  def apply_collection_sort(collection)
53
- sort_by = params.dig(:q, :sort_by) || params[:sort_by]
54
- return collection unless sort_by.present?
57
+ sort_value = sort_param
58
+ return collection unless sort_value.present?
55
59
 
56
- return collection.distinct(false).reorder(nil).by_best_selling if sort_by == 'best-selling'
60
+ scope_method = CUSTOM_SORT_SCOPES[sort_value]
61
+ return collection unless scope_method
57
62
 
58
- scope_method = SORT_OPTIONS[sort_by]
59
- return collection.reorder(nil).send(scope_method) if scope_method.present?
63
+ sorted = collection.reorder(nil)
64
+ sort_value == 'best_selling' ? sorted.distinct(false).send(scope_method) : sorted.send(scope_method)
65
+ end
66
+
67
+ # Inject sort into ransack params when it's a standard Ransack sort
68
+ def ransack_params
69
+ rp = super
70
+ sort_value = sort_param
71
+
72
+ if sort_value.present? && !CUSTOM_SORT_SCOPES.key?(sort_value)
73
+ rp = rp.respond_to?(:to_unsafe_h) ? rp.to_unsafe_h : rp.dup
74
+ rp['s'] = sort_value
75
+ end
60
76
 
61
- collection
77
+ rp
62
78
  end
63
79
 
64
80
  private
65
81
 
82
+ def sort_param
83
+ params[:sort] || params.dig(:q, :s)
84
+ end
85
+
66
86
  def custom_sort_requested?
67
- sort_by = params.dig(:q, :sort_by) || params[:sort_by]
68
- SORT_OPTIONS.key?(sort_by)
87
+ CUSTOM_SORT_SCOPES.key?(sort_param)
69
88
  end
70
89
  end
71
90
  end
@@ -6,11 +6,12 @@ module Spree
6
6
  address1: [:string, nullable: true], address2: [:string, nullable: true],
7
7
  city: [:string, nullable: true], zipcode: [:string, nullable: true], phone: [:string, nullable: true],
8
8
  company: [:string, nullable: true], state_abbr: [:string, nullable: true], state_name: [:string, nullable: true],
9
- state_text: [:string, nullable: true], country_iso: :string, country_name: :string
9
+ state_text: [:string, nullable: true], country_iso: :string, country_name: :string,
10
+ quick_checkout: :boolean
10
11
 
11
12
  attributes :firstname, :lastname, :full_name, :address1, :address2,
12
13
  :city, :zipcode, :phone, :company, :country_name, :country_iso, :state_text,
13
- :state_abbr
14
+ :state_abbr, :quick_checkout
14
15
 
15
16
  # State name - used for countries without predefined states
16
17
  attribute :state_name do |address|
@@ -33,7 +33,7 @@ module Spree
33
33
 
34
34
  many :orders,
35
35
  resource: Spree.api.admin_order_serializer,
36
- if: proc { params[:includes]&.include?('orders') }
36
+ if: proc { params[:expand]&.include?('orders') }
37
37
 
38
38
  # TODO: Add store_credits association when Admin API is implemented
39
39
  end
@@ -36,7 +36,7 @@ module Spree
36
36
 
37
37
  one :user,
38
38
  resource: Spree.api.admin_customer_serializer,
39
- if: proc { params[:includes]&.include?('user') }
39
+ if: proc { params[:expand]&.include?('user') }
40
40
 
41
41
  # TODO: Add adjustments associations when Admin API is implemented
42
42
  end
@@ -26,20 +26,20 @@ module Spree
26
26
  # Admin uses admin variant serializer
27
27
  many :variants,
28
28
  resource: Spree.api.admin_variant_serializer,
29
- if: proc { params[:includes]&.include?('variants') }
29
+ if: proc { params[:expand]&.include?('variants') }
30
30
 
31
31
  one :default_variant,
32
32
  resource: Spree.api.admin_variant_serializer,
33
- if: proc { params[:includes]&.include?('default_variant') }
33
+ if: proc { params[:expand]&.include?('default_variant') }
34
34
 
35
35
  one :master,
36
36
  key: :master_variant,
37
37
  resource: Spree.api.admin_variant_serializer,
38
- if: proc { params[:includes]&.include?('master_variant') }
38
+ if: proc { params[:expand]&.include?('master_variant') }
39
39
 
40
40
  many :metafields,
41
41
  resource: Spree.api.admin_metafield_serializer,
42
- if: proc { params[:includes]&.include?('metafields') }
42
+ if: proc { params[:expand]&.include?('metafields') }
43
43
  end
44
44
  end
45
45
  end
@@ -12,7 +12,7 @@ module Spree
12
12
 
13
13
  many :metafields,
14
14
  resource: Spree.api.admin_metafield_serializer,
15
- if: proc { params[:includes]&.include?('metafields') }
15
+ if: proc { params[:expand]&.include?('metafields') }
16
16
  end
17
17
  end
18
18
  end
@@ -7,7 +7,7 @@ module Spree
7
7
  class TaxonomySerializer < V3::TaxonomySerializer
8
8
  many :metafields,
9
9
  resource: Spree.api.admin_metafield_serializer,
10
- if: proc { params[:includes]&.include?('metafields') }
10
+ if: proc { params[:expand]&.include?('metafields') }
11
11
  end
12
12
  end
13
13
  end
@@ -30,11 +30,11 @@ module Spree
30
30
  # All prices for this variant (for admin management)
31
31
  many :prices,
32
32
  resource: Spree.api.admin_price_serializer,
33
- if: proc { params[:includes]&.include?('prices') }
33
+ if: proc { params[:expand]&.include?('prices') }
34
34
 
35
35
  many :metafields,
36
36
  resource: Spree.api.admin_metafield_serializer,
37
- if: proc { params[:includes]&.include?('metafields') }
37
+ if: proc { params[:expand]&.include?('metafields') }
38
38
 
39
39
  # TODO: Add stock_items association when Admin API is implemented
40
40
  end
@@ -33,24 +33,24 @@ module Spree
33
33
  params[:locale]
34
34
  end
35
35
 
36
- def includes
37
- @includes ||= Array(params[:includes] || [])
36
+ def expands
37
+ @expands ||= Array(params[:expand] || [])
38
38
  end
39
39
 
40
- # Check if an association should be included
41
- def include?(name)
42
- includes.include?(name.to_s)
40
+ # Check if an association should be expanded
41
+ def expand?(name)
42
+ expands.include?(name.to_s)
43
43
  end
44
44
 
45
- # Get nested includes for a given parent
46
- def nested_includes_for(parent)
45
+ # Get nested expands for a given parent
46
+ def nested_expands_for(parent)
47
47
  prefix = "#{parent}."
48
- includes.select { |i| i.start_with?(prefix) }.map { |i| i.sub(prefix, '') }
48
+ expands.select { |i| i.start_with?(prefix) }.map { |i| i.sub(prefix, '') }
49
49
  end
50
50
 
51
51
  # Build nested params for child serializers
52
52
  def nested_params(parent = nil)
53
- params.merge(includes: parent ? nested_includes_for(parent) : [])
53
+ params.merge(expand: parent ? nested_expands_for(parent) : [])
54
54
  end
55
55
 
56
56
  # Returns price for a variant using full Price List resolution
@@ -5,30 +5,19 @@ module Spree
5
5
  include Alba::Resource
6
6
  include Typelizer::DSL
7
7
 
8
- # ISO 3166-1 codes - iso is the identifier, no redundant id field
9
8
  typelize iso: :string, iso3: :string, name: :string,
10
- states_required: :boolean, zipcode_required: :boolean,
11
- currency: [:string, nullable: true],
12
- default_locale: [:string, nullable: true],
13
- supported_locales: [:string, multi: true]
9
+ states_required: :boolean, zipcode_required: :boolean
14
10
 
15
11
  attributes :iso, :iso3, :name, :states_required, :zipcode_required
16
12
 
17
- attribute :currency do |country|
18
- country.market_currency
19
- end
20
-
21
- attribute :default_locale do |country|
22
- country.market_locale
23
- end
24
-
25
- attribute :supported_locales do |country|
26
- country.market_supported_locales
27
- end
28
-
29
13
  many :states,
30
14
  resource: Spree.api.state_serializer,
31
- if: proc { params[:includes]&.include?('states') }
15
+ if: proc { params[:expand]&.include?('states') }
16
+
17
+ attribute :market, if: proc { params[:expand]&.include?('market') } do |country|
18
+ m = country.current_market
19
+ m ? Spree.api.market_serializer.new(m, params: params).to_h : nil
20
+ end
32
21
  end
33
22
  end
34
23
  end
@@ -0,0 +1,23 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ class MarketSerializer < BaseSerializer
5
+ typelize name: :string, currency: :string,
6
+ default_locale: :string,
7
+ supported_locales: [:string, multi: true],
8
+ tax_inclusive: :boolean,
9
+ default: :boolean
10
+
11
+ attributes :name, :currency, :default_locale, :tax_inclusive, :default
12
+
13
+ attribute :supported_locales do |market|
14
+ market.supported_locales_list
15
+ end
16
+
17
+ many :countries,
18
+ resource: Spree.api.country_serializer,
19
+ if: proc { expand?(:countries) }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -6,7 +6,7 @@ module Spree
6
6
  class OrderSerializer < BaseSerializer
7
7
  typelize number: :string, state: :string, token: :string, email: [:string, nullable: true],
8
8
  special_instructions: [:string, nullable: true], currency: :string, locale: [:string, nullable: true], item_count: :number,
9
- shipment_state: [:string, nullable: true], payment_state: [:string, nullable: true],
9
+ state_lock_version: :number, shipment_state: [:string, nullable: true], payment_state: [:string, nullable: true],
10
10
  item_total: :string, display_item_total: :string,
11
11
  ship_total: :string, display_ship_total: :string,
12
12
  adjustment_total: :string, display_adjustment_total: :string,
@@ -18,7 +18,7 @@ module Spree
18
18
  bill_address: { nullable: true }, ship_address: { nullable: true }
19
19
 
20
20
  attributes :number, :state, :token, :email, :special_instructions,
21
- :currency, :locale, :item_count, :shipment_state, :payment_state,
21
+ :currency, :locale, :item_count, :state_lock_version, :shipment_state, :payment_state,
22
22
  :item_total, :display_item_total, :ship_total, :display_ship_total,
23
23
  :adjustment_total, :display_adjustment_total, :promo_total, :display_promo_total,
24
24
  :tax_total, :display_tax_total, :included_tax_total, :display_included_tax_total,
@@ -71,37 +71,37 @@ module Spree
71
71
  many :variant_images,
72
72
  key: :images,
73
73
  resource: Spree.api.image_serializer,
74
- if: proc { params[:includes]&.include?('images') }
74
+ if: proc { params[:expand]&.include?('images') }
75
75
 
76
76
  many :variants,
77
77
  resource: Spree.api.variant_serializer,
78
- if: proc { params[:includes]&.include?('variants') }
78
+ if: proc { params[:expand]&.include?('variants') }
79
79
 
80
80
  one :default_variant,
81
81
  resource: Spree.api.variant_serializer,
82
- if: proc { params[:includes]&.include?('default_variant') }
82
+ if: proc { params[:expand]&.include?('default_variant') }
83
83
 
84
84
  one :master,
85
85
  key: :master_variant,
86
86
  resource: Spree.api.variant_serializer,
87
- if: proc { params[:includes]&.include?('master_variant') }
87
+ if: proc { params[:expand]&.include?('master_variant') }
88
88
 
89
89
  many :option_types,
90
90
  resource: Spree.api.option_type_serializer,
91
- if: proc { params[:includes]&.include?('option_types') }
91
+ if: proc { params[:expand]&.include?('option_types') }
92
92
 
93
93
  many :taxons,
94
94
  proc { |taxons, params|
95
95
  taxons.select { |t| t.taxonomy.store_id == params[:store].id }
96
96
  },
97
97
  resource: Spree.api.taxon_serializer,
98
- if: proc { params[:includes]&.include?('taxons') },
99
- params: { includes: [] }
98
+ if: proc { params[:expand]&.include?('taxons') },
99
+ params: { expand: [] }
100
100
 
101
101
  many :public_metafields,
102
102
  key: :metafields,
103
103
  resource: Spree.api.metafield_serializer,
104
- if: proc { params[:includes]&.include?('metafields') }
104
+ if: proc { params[:expand]&.include?('metafields') }
105
105
  end
106
106
  end
107
107
  end
@@ -51,27 +51,27 @@ module Spree
51
51
  end
52
52
 
53
53
  # Conditional associations
54
- # Note: We pass empty includes to nested taxons to prevent infinite recursion
54
+ # Note: We pass empty expand to nested taxons to prevent infinite recursion
55
55
  # (e.g., ancestors trying to load their own ancestors)
56
56
  one :parent,
57
57
  resource: Spree.api.taxon_serializer,
58
- if: proc { params[:includes]&.include?('parent') },
59
- params: { includes: [] }
58
+ if: proc { params[:expand]&.include?('parent') },
59
+ params: { expand: [] }
60
60
 
61
61
  many :children,
62
62
  resource: Spree.api.taxon_serializer,
63
- if: proc { params[:includes]&.include?('children') },
64
- params: { includes: [] }
63
+ if: proc { params[:expand]&.include?('children') },
64
+ params: { expand: [] }
65
65
 
66
66
  many :ancestors,
67
67
  resource: Spree.api.taxon_serializer,
68
- if: proc { params[:includes]&.include?('ancestors') },
69
- params: { includes: [] }
68
+ if: proc { params[:expand]&.include?('ancestors') },
69
+ params: { expand: [] }
70
70
 
71
71
  many :public_metafields,
72
72
  key: :metafields,
73
73
  resource: Spree.api.metafield_serializer,
74
- if: proc { params[:includes]&.include?('metafields') }
74
+ if: proc { params[:expand]&.include?('metafields') }
75
75
  end
76
76
  end
77
77
  end
@@ -12,21 +12,21 @@ module Spree
12
12
  end
13
13
 
14
14
  # Conditional associations
15
- # Note: We pass empty includes to nested taxons to prevent infinite recursion
15
+ # Note: We pass empty expand to nested taxons to prevent infinite recursion
16
16
  one :root,
17
17
  resource: Spree.api.taxon_serializer,
18
- if: proc { params[:includes]&.include?('root') },
19
- params: { includes: [] }
18
+ if: proc { params[:expand]&.include?('root') },
19
+ params: { expand: [] }
20
20
 
21
21
  many :taxons,
22
22
  resource: Spree.api.taxon_serializer,
23
- if: proc { params[:includes]&.include?('taxons') },
24
- params: { includes: [] }
23
+ if: proc { params[:expand]&.include?('taxons') },
24
+ params: { expand: [] }
25
25
 
26
26
  many :public_metafields,
27
27
  key: :metafields,
28
28
  resource: Spree.api.metafield_serializer,
29
- if: proc { params[:includes]&.include?('metafields') }
29
+ if: proc { params[:expand]&.include?('metafields') }
30
30
  end
31
31
  end
32
32
  end
@@ -72,14 +72,14 @@ module Spree
72
72
  # Conditional associations
73
73
  many :images,
74
74
  resource: Spree.api.image_serializer,
75
- if: proc { params[:includes]&.include?('images') }
75
+ if: proc { params[:expand]&.include?('images') }
76
76
 
77
77
  many :option_values, resource: Spree.api.option_value_serializer
78
78
 
79
79
  many :public_metafields,
80
80
  key: :metafields,
81
81
  resource: Spree.api.metafield_serializer,
82
- if: proc { params[:includes]&.include?('metafields') }
82
+ if: proc { params[:expand]&.include?('metafields') }
83
83
  end
84
84
  end
85
85
  end
@@ -18,7 +18,7 @@ module Spree
18
18
  many :wished_items,
19
19
  key: :items,
20
20
  resource: Spree.api.wished_item_serializer,
21
- if: proc { params[:includes]&.include?('items') }
21
+ if: proc { params[:expand]&.include?('items') }
22
22
  end
23
23
  end
24
24
  end
@@ -2,16 +2,7 @@ module Spree
2
2
  module Api
3
3
  module V3
4
4
  class FiltersAggregator
5
- SORT_OPTION_IDS = %w[
6
- manual
7
- best-selling
8
- price-low-to-high
9
- price-high-to-low
10
- newest-first
11
- oldest-first
12
- name-a-z
13
- name-z-a
14
- ].freeze
5
+ SORT_OPTION_IDS = Spree::Taxon::SORT_ORDERS
15
6
 
16
7
  # @param scope [ActiveRecord::Relation] Base product scope (already filtered by store, availability, taxon, etc.)
17
8
  # @param currency [String] Currency for price range
@@ -43,11 +34,10 @@ module Spree
43
34
  end
44
35
 
45
36
  def sort_options
46
- SORT_OPTION_IDS.map do |id|
47
- { id: id, label: Spree.t("products_sort_options.#{id.underscore}") }
48
- end
37
+ SORT_OPTION_IDS.map { |id| { id: id } }
49
38
  end
50
39
 
40
+
51
41
  def price_filter
52
42
  # Remove ordering to avoid PostgreSQL DISTINCT + ORDER BY conflicts
53
43
  prices = Spree::Price.for_products(@scope.reorder(''), @currency)
@@ -58,7 +48,6 @@ module Spree
58
48
  {
59
49
  id: 'price',
60
50
  type: 'price_range',
61
- label: Spree.t(:price),
62
51
  min: min.to_f,
63
52
  max: max.to_f,
64
53
  currency: @currency
@@ -74,10 +63,9 @@ module Spree
74
63
  {
75
64
  id: 'availability',
76
65
  type: 'availability',
77
- label: Spree.t(:availability),
78
66
  options: [
79
- { id: 'in_stock', label: Spree.t(:in_stock), count: in_stock_count },
80
- { id: 'out_of_stock', label: Spree.t(:out_of_stock), count: out_of_stock_count }
67
+ { id: 'in_stock', count: in_stock_count },
68
+ { id: 'out_of_stock', count: out_of_stock_count }
81
69
  ]
82
70
  }
83
71
  end
@@ -90,8 +78,8 @@ module Spree
90
78
  {
91
79
  id: option_type.prefixed_id,
92
80
  type: 'option',
93
- label: option_type.presentation,
94
81
  name: option_type.name,
82
+ presentation: option_type.presentation,
95
83
  options: values.map { |ov| option_value_data(option_type, ov) }
96
84
  }
97
85
  end
@@ -110,8 +98,8 @@ module Spree
110
98
 
111
99
  {
112
100
  id: option_value.prefixed_id,
113
- label: option_value.presentation,
114
101
  name: option_value.name,
102
+ presentation: option_value.presentation,
115
103
  position: option_value.position,
116
104
  count: count
117
105
  }
@@ -135,7 +123,6 @@ module Spree
135
123
  {
136
124
  id: 'taxons',
137
125
  type: 'taxon',
138
- label: Spree.t(:category),
139
126
  options: child_taxons.map { |t| taxon_option_data(t) }
140
127
  }
141
128
  end
@@ -145,7 +132,7 @@ module Spree
145
132
 
146
133
  {
147
134
  id: taxon.prefixed_id,
148
- label: taxon.name,
135
+ name: taxon.name,
149
136
  permalink: taxon.permalink,
150
137
  count: count
151
138
  }
data/config/routes.rb CHANGED
@@ -11,6 +11,14 @@ Spree::Core::Engine.add_routes do
11
11
  # Store
12
12
  resource :store, only: [:show]
13
13
 
14
+ # Markets
15
+ resources :markets, only: [:index, :show] do
16
+ collection do
17
+ get :resolve
18
+ end
19
+ resources :countries, only: [:index, :show], controller: 'markets/countries'
20
+ end
21
+
14
22
  # Countries, Currencies, Locales (flat, market-aware)
15
23
  resources :countries, only: [:index, :show]
16
24
  resources :currencies, only: [:index]
@@ -107,6 +107,7 @@ module Spree
107
107
  address_serializer: 'Spree::Api::V3::AddressSerializer',
108
108
  customer_serializer: 'Spree::Api::V3::CustomerSerializer',
109
109
  country_serializer: 'Spree::Api::V3::CountrySerializer',
110
+ market_serializer: 'Spree::Api::V3::MarketSerializer',
110
111
  state_serializer: 'Spree::Api::V3::StateSerializer',
111
112
  store_serializer: 'Spree::Api::V3::StoreSerializer',
112
113
  wishlist_serializer: 'Spree::Api::V3::WishlistSerializer',
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.0.beta3
4
+ version: 5.4.0.beta4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-02 00:00:00.000000000 Z
11
+ date: 2026-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - '='
88
88
  - !ruby/object:Gem::Version
89
- version: 5.4.0.beta3
89
+ version: 5.4.0.beta4
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - '='
95
95
  - !ruby/object:Gem::Version
96
- version: 5.4.0.beta3
96
+ version: 5.4.0.beta4
97
97
  description: Spree's API
98
98
  email:
99
99
  - hello@spreecommerce.org
@@ -109,6 +109,7 @@ files:
109
109
  - app/controllers/concerns/spree/api/v3/jwt_authentication.rb
110
110
  - app/controllers/concerns/spree/api/v3/locale_and_currency.rb
111
111
  - app/controllers/concerns/spree/api/v3/order_concern.rb
112
+ - app/controllers/concerns/spree/api/v3/order_lock.rb
112
113
  - app/controllers/concerns/spree/api/v3/resource_serializer.rb
113
114
  - app/controllers/concerns/spree/api/v3/security_headers.rb
114
115
  - app/controllers/spree/api/v3/base_controller.rb
@@ -126,6 +127,8 @@ files:
126
127
  - app/controllers/spree/api/v3/store/customer/payment_setup_sessions_controller.rb
127
128
  - app/controllers/spree/api/v3/store/digitals_controller.rb
128
129
  - app/controllers/spree/api/v3/store/locales_controller.rb
130
+ - app/controllers/spree/api/v3/store/markets/countries_controller.rb
131
+ - app/controllers/spree/api/v3/store/markets_controller.rb
129
132
  - app/controllers/spree/api/v3/store/orders/coupon_codes_controller.rb
130
133
  - app/controllers/spree/api/v3/store/orders/line_items_controller.rb
131
134
  - app/controllers/spree/api/v3/store/orders/payment_methods_controller.rb
@@ -173,6 +176,7 @@ files:
173
176
  - app/serializers/spree/api/v3/invitation_serializer.rb
174
177
  - app/serializers/spree/api/v3/line_item_serializer.rb
175
178
  - app/serializers/spree/api/v3/locale_serializer.rb
179
+ - app/serializers/spree/api/v3/market_serializer.rb
176
180
  - app/serializers/spree/api/v3/metafield_serializer.rb
177
181
  - app/serializers/spree/api/v3/newsletter_subscriber_serializer.rb
178
182
  - app/serializers/spree/api/v3/option_type_serializer.rb
@@ -235,9 +239,9 @@ licenses:
235
239
  - BSD-3-Clause
236
240
  metadata:
237
241
  bug_tracker_uri: https://github.com/spree/spree/issues
238
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.beta3
242
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.beta4
239
243
  documentation_uri: https://docs.spreecommerce.org/
240
- source_code_uri: https://github.com/spree/spree/tree/v5.4.0.beta3
244
+ source_code_uri: https://github.com/spree/spree/tree/v5.4.0.beta4
241
245
  post_install_message:
242
246
  rdoc_options: []
243
247
  require_paths: