spree_api 5.4.0.rc2 → 5.4.0.rc4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78b4c09d45771aeb527cc5f623c07cca20e2920d2d083e7641573b8b95cadbf2
4
- data.tar.gz: 2dc7b84ce1f3e9b59fa3a6bc8c874d4488d8480841839765a5af4a8d21dcf30f
3
+ metadata.gz: 6509e525568a3b8ea64e7f0f798f4aa97269c1f050d8df3f8a0674fc53001935
4
+ data.tar.gz: 38bb74377c55de6b865eb28a1014fce6e5744714f44e8bf9484403e92d935044
5
5
  SHA512:
6
- metadata.gz: 412d63a74d08c348e9f76fd366552dc2af424ed3b42971896e3fdbe7e38c886592f5680207eb1805700f4e12c98a368428f50f4c389ee79953218729acd9c159
7
- data.tar.gz: 4034da7fbd869926562e842632d12b2bc39eb3338987fc60904e6c08b1a4905d621735c713d0fe212a2319125fdcdb11616872479b0d18d8f5d280456f5cf9b1
6
+ metadata.gz: d748f15cd7c17a7c1bf1f9bc0bec2b22e4326c8a5088ab6989e5904ea2c315f0dad52035f6407a2995728e35ce2cf2b1adfb35f48a025d13007e658f4ee16ad1
7
+ data.tar.gz: c0c909f704cbdd2088b15fde911e7ceb4b919ea233c2145406bdac76e2e615046cd2b82a54c0992a4bdb518930123cd3e9c5b2a2fb4eb31d05bf98232a177cb7
@@ -26,7 +26,8 @@ module Spree
26
26
 
27
27
  # Render the cart as JSON using the cart serializer.
28
28
  def render_cart(status: :ok)
29
- render json: Spree.api.cart_serializer.new(@cart.reload, params: serializer_params).to_h, status: status
29
+ @cart = @cart.remove_out_of_stock_items!
30
+ render json: Spree.api.cart_serializer.new(@cart, params: serializer_params).to_h, status: status
30
31
  end
31
32
 
32
33
  # Render the order as JSON using the order serializer (for complete action).
@@ -48,6 +48,11 @@ module Spree
48
48
  payment_processing_error: 'payment_processing_error',
49
49
  gateway_error: 'gateway_error',
50
50
 
51
+ # Gift card errors
52
+ gift_card_not_found: 'gift_card_not_found',
53
+ gift_card_expired: 'gift_card_expired',
54
+ gift_card_already_redeemed: 'gift_card_already_redeemed',
55
+
51
56
  # Digital download errors
52
57
  attachment_missing: 'attachment_missing',
53
58
  download_unauthorized: 'download_unauthorized',
@@ -2,6 +2,9 @@ module Spree
2
2
  module Api
3
3
  module V3
4
4
  class BaseController < ActionController::API
5
+ # API v3 uses flat params — disable Rails' automatic parameter wrapping
6
+ wrap_parameters false
7
+
5
8
  include ActiveStorage::SetCurrent
6
9
  include CanCan::ControllerAdditions
7
10
  include Spree::Core::ControllerHelpers::StrongParameters
@@ -3,14 +3,14 @@ module Spree
3
3
  module V3
4
4
  module Store
5
5
  module Carts
6
- class CouponCodesController < Store::BaseController
6
+ class DiscountCodesController < Store::BaseController
7
7
  include Spree::Api::V3::CartResolvable
8
8
  include Spree::Api::V3::OrderLock
9
9
 
10
10
  before_action :find_cart!
11
11
 
12
- # POST /api/v3/store/carts/:cart_id/coupon_codes
13
- # Apply a coupon code to the cart
12
+ # POST /api/v3/store/carts/:cart_id/discount_codes
13
+ # Apply a discount code to the cart
14
14
  def create
15
15
  with_order_lock do
16
16
  @cart.coupon_code = permitted_params[:code]
@@ -25,9 +25,9 @@ module Spree
25
25
  end
26
26
  end
27
27
 
28
- # DELETE /api/v3/store/carts/:cart_id/coupon_codes/:id
29
- # Remove a coupon code from the cart
30
- # :id is the coupon code string (e.g., SAVE10)
28
+ # DELETE /api/v3/store/carts/:cart_id/discount_codes/:id
29
+ # Remove a discount code from the cart
30
+ # :id is the discount code string (e.g., SAVE10)
31
31
  def destroy
32
32
  with_order_lock do
33
33
  coupon_handler.remove(params[:id])
@@ -43,7 +43,7 @@ module Spree
43
43
  private
44
44
 
45
45
  def coupon_handler
46
- @coupon_handler ||= Spree.coupon_handler.new(@cart)
46
+ @coupon_handler ||= Spree.coupon_handler.new(@cart, enable_gift_cards: false)
47
47
  end
48
48
 
49
49
  def permitted_params
@@ -0,0 +1,72 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ module Carts
6
+ class GiftCardsController < Store::BaseController
7
+ include Spree::Api::V3::CartResolvable
8
+ include Spree::Api::V3::OrderLock
9
+
10
+ before_action :find_cart!
11
+
12
+ # POST /api/v3/store/carts/:cart_id/gift_cards
13
+ def create
14
+ with_order_lock do
15
+ gift_card = find_gift_card!
16
+ return unless gift_card
17
+
18
+ result = @cart.apply_gift_card(gift_card)
19
+
20
+ if result.success?
21
+ render_cart(status: :created)
22
+ else
23
+ render_service_error(result.error)
24
+ end
25
+ end
26
+ end
27
+
28
+ # DELETE /api/v3/store/carts/:cart_id/gift_cards/:id
29
+ def destroy
30
+ with_order_lock do
31
+ result = @cart.remove_gift_card
32
+
33
+ if result.success?
34
+ render_cart
35
+ else
36
+ render_service_error(result.error)
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def find_gift_card!
44
+ gift_card = @cart.store.gift_cards.find_by(code: permitted_params[:code]&.downcase)
45
+
46
+ if gift_card.nil?
47
+ render_error(code: ERROR_CODES[:gift_card_not_found], message: Spree.t(:gift_card_not_found), status: :not_found)
48
+ return
49
+ end
50
+
51
+ if gift_card.expired?
52
+ render_error(code: ERROR_CODES[:gift_card_expired], message: Spree.t(:gift_card_expired), status: :unprocessable_content)
53
+ return
54
+ end
55
+
56
+ if gift_card.redeemed?
57
+ render_error(code: ERROR_CODES[:gift_card_already_redeemed], message: Spree.t(:gift_card_already_redeemed), status: :unprocessable_content)
58
+ return
59
+ end
60
+
61
+ gift_card
62
+ end
63
+
64
+ def permitted_params
65
+ params.permit(:code)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -13,7 +13,7 @@ module Spree
13
13
 
14
14
  skip_before_action :authenticate_user
15
15
 
16
- # POST /api/v3/store/customer/password_resets
16
+ # POST /api/v3/store/password_resets
17
17
  def create
18
18
  redirect_url = params[:redirect_url]
19
19
 
@@ -41,7 +41,7 @@ module Spree
41
41
  render json: { message: Spree.t(:password_reset_requested, scope: :api) }, status: :accepted
42
42
  end
43
43
 
44
- # PATCH /api/v3/store/customer/password_resets/:id
44
+ # PATCH /api/v3/store/password_resets/:id
45
45
  def update
46
46
  user = Spree.user_class.find_by_password_reset_token(params[:id])
47
47
 
@@ -0,0 +1,34 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ class PoliciesController < Store::ResourceController
6
+ include Spree::Api::V3::HttpCaching
7
+
8
+ protected
9
+
10
+ def model_class
11
+ Spree::Policy
12
+ end
13
+
14
+ def serializer_class
15
+ Spree.api.policy_serializer
16
+ end
17
+
18
+ # Accept slug (e.g., return-policy) or prefixed ID (e.g., pol_abc123)
19
+ def find_resource
20
+ if params[:id].to_s.start_with?('pol_')
21
+ scope.find_by_prefix_id!(params[:id])
22
+ else
23
+ scope.friendly.find(params[:id])
24
+ end
25
+ end
26
+
27
+ def scope
28
+ super.order(:name)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Api
5
+ module V3
6
+ module Admin
7
+ class PriceHistorySerializer < V3::PriceHistorySerializer
8
+ typelize variant_id: :string,
9
+ price_id: :string,
10
+ compare_at_amount: [:string, nullable: true],
11
+ created_at: :string
12
+
13
+ attribute :variant_id do |price_history|
14
+ price_history.variant.prefixed_id
15
+ end
16
+
17
+ attribute :price_id do |price_history|
18
+ price_history.price.prefixed_id
19
+ end
20
+
21
+ attributes :compare_at_amount, created_at: :iso8601
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -18,7 +18,9 @@ module Spree
18
18
  gift_card_total: :string, display_gift_card_total: :string,
19
19
  covered_by_store_credit: :boolean,
20
20
  total: :string, display_total: :string,
21
+ amount_due: :string, display_amount_due: :string,
21
22
  shipping_eq_billing_address: :boolean,
23
+ warnings: 'Array<{code: string, message: string, line_item_id?: string, variant_id?: string}>',
22
24
  billing_address: { nullable: true }, shipping_address: { nullable: true },
23
25
  gift_card: { nullable: true }
24
26
 
@@ -34,7 +36,9 @@ module Spree
34
36
  :discount_total, :display_discount_total,
35
37
  :tax_total, :display_tax_total, :included_tax_total, :display_included_tax_total,
36
38
  :additional_tax_total, :display_additional_tax_total, :total, :display_total,
37
- :delivery_total, :display_delivery_total,
39
+ :gift_card_total, :display_gift_card_total,
40
+ :amount_due, :display_amount_due,
41
+ :delivery_total, :display_delivery_total, :warnings,
38
42
  created_at: :iso8601, updated_at: :iso8601
39
43
 
40
44
  attribute :store_credit_total do |order|
@@ -45,8 +49,6 @@ module Spree
45
49
  order.display_total_applied_store_credit.to_s
46
50
  end
47
51
 
48
- attributes :gift_card_total, :display_gift_card_total
49
-
50
52
  attribute :covered_by_store_credit do |order|
51
53
  order.covered_by_store_credit?
52
54
  end
@@ -17,7 +17,9 @@ module Spree
17
17
  store_credit_total: :string, display_store_credit_total: :string,
18
18
  gift_card_total: :string, display_gift_card_total: :string,
19
19
  covered_by_store_credit: :boolean,
20
- total: :string, display_total: :string, completed_at: [:string, nullable: true],
20
+ total: :string, display_total: :string,
21
+ amount_due: :string, display_amount_due: :string,
22
+ completed_at: [:string, nullable: true],
21
23
  billing_address: { nullable: true }, shipping_address: { nullable: true },
22
24
  gift_card: { nullable: true }
23
25
 
@@ -28,6 +30,8 @@ module Spree
28
30
  :discount_total, :display_discount_total,
29
31
  :tax_total, :display_tax_total, :included_tax_total, :display_included_tax_total,
30
32
  :additional_tax_total, :display_additional_tax_total, :total, :display_total,
33
+ :gift_card_total, :display_gift_card_total,
34
+ :amount_due, :display_amount_due,
31
35
  :delivery_total, :display_delivery_total, :fulfillment_status, :payment_status,
32
36
  completed_at: :iso8601, created_at: :iso8601, updated_at: :iso8601
33
37
 
@@ -39,8 +43,6 @@ module Spree
39
43
  order.display_total_applied_store_credit.to_s
40
44
  end
41
45
 
42
- attributes :gift_card_total, :display_gift_card_total
43
-
44
46
  attribute :covered_by_store_credit do |order|
45
47
  order.covered_by_store_credit?
46
48
  end
@@ -0,0 +1,22 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ class PolicySerializer < BaseSerializer
5
+ typelize name: :string, slug: :string,
6
+ body: [:string, nullable: true], body_html: [:string, nullable: true]
7
+
8
+ attributes :name, :slug
9
+
10
+ attribute :body do |policy|
11
+ policy.body&.to_plain_text
12
+ end
13
+
14
+ attribute :body_html do |policy|
15
+ policy.body&.body&.to_s.to_s
16
+ end
17
+
18
+ attributes created_at: :iso8601, updated_at: :iso8601
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Api
5
+ module V3
6
+ class PriceHistorySerializer < BaseSerializer
7
+ typelize amount: :string,
8
+ amount_in_cents: :number,
9
+ display_amount: :string,
10
+ currency: :string,
11
+ recorded_at: :string
12
+
13
+ attributes :amount, :amount_in_cents, :currency
14
+
15
+ attribute :display_amount do |price_history|
16
+ price_history.display_amount
17
+ end
18
+
19
+ attribute :recorded_at do |price_history|
20
+ price_history.recorded_at&.iso8601
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -111,6 +111,14 @@ module Spree
111
111
  key: :metafields,
112
112
  resource: Spree.api.metafield_serializer,
113
113
  if: proc { expand?('metafields') }
114
+
115
+ typelize prior_price: ['PriceHistory', nullable: true]
116
+
117
+ attribute :prior_price,
118
+ if: proc { expand?('prior_price') } do |product|
119
+ record = price_in(product.default_variant)&.prior_price
120
+ Spree.api.price_history_serializer.new(record, params: params).to_h if record
121
+ end
114
122
  end
115
123
  end
116
124
  end
@@ -85,6 +85,14 @@ module Spree
85
85
  key: :metafields,
86
86
  resource: Spree.api.metafield_serializer,
87
87
  if: proc { expand?('metafields') }
88
+
89
+ typelize prior_price: ['PriceHistory', nullable: true]
90
+
91
+ attribute :prior_price,
92
+ if: proc { expand?('prior_price') } do |variant|
93
+ record = price_in(variant)&.prior_price
94
+ Spree.api.price_history_serializer.new(record, params: params).to_h if record
95
+ end
88
96
  end
89
97
  end
90
98
  end
@@ -2,13 +2,17 @@ module Spree
2
2
  module Api
3
3
  module V3
4
4
  class FiltersAggregator
5
- # @param scope [ActiveRecord::Relation] Base product scope (already filtered by store, availability, category, etc.)
5
+ # @param scope [ActiveRecord::Relation] Base product scope (fully filtered, including option values)
6
6
  # @param currency [String] Currency for price range
7
7
  # @param category [Spree::Category, nil] Optional category for default_sort and category filtering context
8
- def initialize(scope:, currency:, category: nil)
8
+ # @param option_value_ids [Array<String>] Currently selected option value prefixed IDs (for disjunctive facet counts)
9
+ # @param scope_before_options [ActiveRecord::Relation] Scope before option value filters (for disjunctive counts)
10
+ def initialize(scope:, currency:, category: nil, option_value_ids: [], scope_before_options: nil)
9
11
  @scope = scope
10
12
  @currency = currency
11
13
  @category = category
14
+ @option_value_ids = option_value_ids
15
+ @scope_before_options = scope_before_options || scope
12
16
  end
13
17
 
14
18
  def call
@@ -78,25 +82,26 @@ module Spree
78
82
 
79
83
  def option_type_filters
80
84
  Spree::OptionType.filterable.includes(:option_values).order(:position).filter_map do |option_type|
81
- values = option_type.option_values.for_products(@scope).distinct.order(:position)
85
+ # Disjunctive: count against scope WITHOUT this option type's filter
86
+ count_scope = disjunctive_scope_for(option_type)
87
+ values = option_type.option_values.for_products(count_scope).distinct.order(:position)
82
88
  next if values.empty?
83
89
 
90
+ count_ids = count_scope.reorder('').distinct.pluck(:id)
91
+
84
92
  {
85
93
  id: option_type.prefixed_id,
86
94
  type: 'option',
87
95
  name: option_type.name,
88
96
  label: option_type.label,
89
- options: values.map { |ov| option_value_data(option_type, ov) }
97
+ options: values.map { |ov| option_value_data(count_ids, ov) }
90
98
  }
91
99
  end
92
100
  end
93
101
 
94
- def option_value_data(option_type, option_value)
95
- # Count products in scope that have this option value
96
- # We use a subquery approach to avoid GROUP BY conflicts when scope includes joins (like in_category)
97
- # Join directly to option_value_variants for efficiency (skips joining through option_values table)
102
+ def option_value_data(product_ids, option_value)
98
103
  count = Spree::Product
99
- .where(id: base_scope_product_ids)
104
+ .where(id: product_ids)
100
105
  .joins(:option_value_variants)
101
106
  .where(Spree::OptionValueVariant.table_name => { option_value_id: option_value.id })
102
107
  .distinct
@@ -111,8 +116,38 @@ module Spree
111
116
  }
112
117
  end
113
118
 
114
- def base_scope_product_ids
115
- @base_scope_product_ids ||= @scope.reorder('').distinct.pluck(:id)
119
+ # Returns the scope with all option type filters EXCEPT the given one applied.
120
+ # This gives disjunctive counts: selecting Blue still shows Red's true count.
121
+ def disjunctive_scope_for(option_type)
122
+ return @scope_before_options if grouped_selected_options.empty?
123
+
124
+ other_groups = grouped_selected_options.except(option_type.id)
125
+
126
+ # If this type has selections but no other types do, use scope before any option filters
127
+ return @scope_before_options if other_groups.empty?
128
+
129
+ # Rebuild: start from scope before options, apply only other option types
130
+ scope = @scope_before_options
131
+ other_groups.each_value do |ov_ids|
132
+ matching = Spree::Variant.where(deleted_at: nil)
133
+ .joins(:option_value_variants)
134
+ .where(Spree::OptionValueVariant.table_name => { option_value_id: ov_ids })
135
+ .select(:product_id)
136
+ scope = scope.where(id: matching)
137
+ end
138
+ scope
139
+ end
140
+
141
+ # Group selected option value IDs by option type (cached, single query)
142
+ def grouped_selected_options
143
+ @grouped_selected_options ||= begin
144
+ return {} if @option_value_ids.blank?
145
+
146
+ decoded = @option_value_ids.filter_map { |id| Spree::OptionValue.decode_prefixed_id(id) }
147
+ return {} if decoded.empty?
148
+
149
+ Spree::OptionValue.where(id: decoded).group_by(&:option_type_id).transform_values { |ovs| ovs.map(&:id) }
150
+ end
116
151
  end
117
152
 
118
153
  def category_filter
@@ -49,22 +49,31 @@ module Spree
49
49
  private
50
50
 
51
51
  def make_request
52
- SsrfFilter.post(
53
- @delivery.url,
54
- headers: {
55
- 'Content-Type' => 'application/json',
56
- 'User-Agent' => 'Spree-Webhooks/1.0',
57
- 'X-Spree-Webhook-Signature' => generate_signature,
58
- 'X-Spree-Webhook-Timestamp' => webhook_timestamp.to_s,
59
- 'X-Spree-Webhook-Event' => @delivery.event_name
60
- },
61
- body: @delivery.payload.to_json,
62
- http_options: {
63
- open_timeout: TIMEOUT,
64
- read_timeout: TIMEOUT,
65
- verify_mode: ssl_verify_mode
66
- }
67
- )
52
+ headers = {
53
+ 'Content-Type' => 'application/json',
54
+ 'User-Agent' => 'Spree-Webhooks/1.0',
55
+ 'X-Spree-Webhook-Signature' => generate_signature,
56
+ 'X-Spree-Webhook-Timestamp' => webhook_timestamp.to_s,
57
+ 'X-Spree-Webhook-Event' => @delivery.event_name
58
+ }
59
+ body = @delivery.payload.to_json
60
+ http_options = { open_timeout: TIMEOUT, read_timeout: TIMEOUT, verify_mode: ssl_verify_mode }
61
+
62
+ # SSRF protection is disabled in development so webhooks can reach
63
+ # localhost / host.docker.internal (the storefront running on the host).
64
+ if Rails.env.development?
65
+ uri = URI.parse(@delivery.url)
66
+ http = Net::HTTP.new(uri.host, uri.port)
67
+ http.use_ssl = uri.scheme == 'https'
68
+ http_options.each { |k, v| http.send(:"#{k}=", v) }
69
+
70
+ request = Net::HTTP::Post.new(uri.request_uri)
71
+ headers.each { |k, v| request[k] = v }
72
+ request.body = body
73
+ http.request(request)
74
+ else
75
+ SsrfFilter.post(@delivery.url, headers: headers, body: body, http_options: http_options)
76
+ end
68
77
  end
69
78
 
70
79
  def generate_signature
@@ -21,13 +21,15 @@ module Spree
21
21
  return unless Spree::Api::Config.webhooks_enabled
22
22
  return if event.store_id.blank?
23
23
 
24
- # Find all active endpoints for this store subscribed to this event
25
- endpoints = Spree::WebhookEndpoint.active.where(store_id: event.store_id).select { |endpoint| endpoint.subscribed_to?(event.name) }
24
+ # Only load the columns we need for matching and delivery
25
+ endpoints = Spree::WebhookEndpoint
26
+ .enabled
27
+ .where(store_id: event.store_id)
28
+ .select(:id, :subscriptions)
26
29
 
27
- return if endpoints.empty?
28
-
29
- # Queue delivery for each endpoint
30
30
  endpoints.each do |endpoint|
31
+ next unless endpoint.subscribed_to?(event.name)
32
+
31
33
  queue_delivery(endpoint, event)
32
34
  end
33
35
  rescue StandardError => e
@@ -38,17 +40,25 @@ module Spree
38
40
  private
39
41
 
40
42
  def queue_delivery(endpoint, event)
41
- # Build base payload (without delivery ID)
42
43
  payload = build_payload(event)
43
44
 
44
- # Create delivery record
45
+ # Deduplicate: skip if we already have a delivery for this event + endpoint
46
+ if event.id.present?
47
+ return if Spree::WebhookDelivery.exists?(
48
+ webhook_endpoint_id: endpoint.id,
49
+ event_id: event.id
50
+ )
51
+ end
52
+
45
53
  delivery = endpoint.webhook_deliveries.create!(
46
54
  event_name: event.name,
55
+ event_id: event.id,
47
56
  payload: payload
48
57
  )
49
58
 
50
- # Queue the delivery job
51
59
  Spree::WebhookDeliveryJob.perform_later(delivery.id)
60
+ rescue ActiveRecord::RecordNotUnique
61
+ # Race condition: another thread already created this delivery — safe to ignore
52
62
  rescue StandardError => e
53
63
  Rails.logger.error "[Spree Webhooks] Error queuing delivery for endpoint #{endpoint.id}: #{e.message}"
54
64
  Rails.error.report(e)
@@ -2,24 +2,24 @@
2
2
  en:
3
3
  spree:
4
4
  api:
5
+ current_password_invalid: Current password is invalid or missing
5
6
  gateway_error: 'There was a problem with the payment gateway: %{text}'
6
7
  invalid_api_key: Invalid API key (%{key}) specified.
7
8
  invalid_resource: Invalid resource. Please fix errors and try again.
8
9
  invalid_taxonomy_id: Invalid taxonomy id.
9
- current_password_invalid: Current password is invalid or missing
10
- password_reset_requested: If an account exists for that email, password reset instructions have been sent.
11
- password_reset_token_invalid: Password reset token is invalid or has expired.
12
- redirect_url_not_allowed: The redirect URL is not from an allowed origin for this store.
13
10
  must_specify_api_key: You must specify an API key.
14
11
  negative_quantity: quantity is negative
15
12
  order:
16
13
  could_not_transition: The order could not be transitioned. Please fix the errors and try again.
17
14
  insufficient_quantity: An item in your cart has become unavailable.
18
15
  invalid_shipping_method: Invalid shipping method specified.
16
+ password_reset_requested: If an account exists for that email, password reset instructions have been sent.
17
+ password_reset_token_invalid: Password reset token is invalid or has expired.
19
18
  payment:
20
19
  credit_over_limit: This payment can only be credited up to %{limit}. Please specify an amount less than or equal to this number.
21
20
  update_forbidden: This payment cannot be updated because it is %{state}.
22
- record_not_found: '%{model} not found'
21
+ record_not_found: "%{model} not found"
22
+ redirect_url_not_allowed: The redirect URL is not from an allowed origin for this store.
23
23
  resource_not_found: The resource you were looking for could not be found.
24
24
  shipment:
25
25
  cannot_ready: Cannot ready shipment.
@@ -27,10 +27,6 @@ en:
27
27
  shipment_transfer_success: Variants successfully transferred
28
28
  stock_location_required: A stock_location_id parameter must be provided in order to retrieve stock movements.
29
29
  unauthorized: You are not authorized to perform that action.
30
- v3:
31
- payments:
32
- session_required: This payment method requires a payment session. Use the payment sessions endpoint instead.
33
- method_unavailable: This payment method is not available for this order.
34
30
  v2:
35
31
  cart:
36
32
  no_coupon_code: No coupon code provided and the Order doesn't have any coupon code promotions applied
@@ -47,4 +43,8 @@ en:
47
43
  errors:
48
44
  the_wishlist_could_not_be_destroyed: The wishlist could not be destroyed.
49
45
  wrong_quantity: Quantity has to be greater than 0
46
+ v3:
47
+ payments:
48
+ method_unavailable: This payment method is not available for this order.
49
+ session_required: This payment method requires a payment session. Use the payment sessions endpoint instead.
50
50
  wrong_shipment_target: target shipment is the same as original shipment
data/config/routes.rb CHANGED
@@ -27,9 +27,7 @@ Spree::Core::Engine.add_routes do
27
27
  get :filters, to: 'products/filters#index'
28
28
  end
29
29
  end
30
- resources :categories, only: [:index, :show], id: /.+/ do
31
- resources :products, only: [:index], controller: 'categories/products'
32
- end
30
+ resources :categories, only: [:index, :show], id: /.+/
33
31
 
34
32
  # Carts
35
33
  resources :carts, only: [:index, :show, :create, :update, :destroy] do
@@ -38,7 +36,8 @@ Spree::Core::Engine.add_routes do
38
36
  post :complete
39
37
  end
40
38
  resources :items, only: [:create, :update, :destroy], controller: 'carts/items'
41
- resources :coupon_codes, only: [:create, :destroy], controller: 'carts/coupon_codes'
39
+ resources :discount_codes, only: [:create, :destroy], controller: 'carts/discount_codes'
40
+ resources :gift_cards, only: [:create, :destroy], controller: 'carts/gift_cards'
42
41
  resources :fulfillments, only: [:update], controller: 'carts/fulfillments'
43
42
  resources :payments, only: [:create], controller: 'carts/payments'
44
43
  resources :payment_sessions, only: [:create, :show, :update], controller: 'carts/payment_sessions' do
@@ -52,6 +51,12 @@ Spree::Core::Engine.add_routes do
52
51
  # Orders (single order lookup, guest-accessible via order token)
53
52
  resources :orders, only: [:show]
54
53
 
54
+ # Policies (return policy, privacy policy, terms of service, etc.)
55
+ resources :policies, only: [:index, :show]
56
+
57
+ # Password Resets (top-level, no auth required)
58
+ resources :password_resets, only: [:create, :update], controller: 'customer/password_resets'
59
+
55
60
  # Customers
56
61
  resources :customers, only: [:create]
57
62
 
@@ -60,8 +65,6 @@ Spree::Core::Engine.add_routes do
60
65
  get '/', action: :show, controller: '/spree/api/v3/store/customers'
61
66
  patch '/', action: :update, controller: '/spree/api/v3/store/customers'
62
67
 
63
- resources :password_resets, only: [:create, :update]
64
-
65
68
  resources :orders, only: [:index, :show]
66
69
  resources :addresses, only: [:index, :show, :create, :update, :destroy]
67
70
  resources :credit_cards, only: [:index, :show, :destroy]
@@ -94,6 +94,7 @@ module Spree
94
94
  # v3 serializers (API v3)
95
95
  credit_card_serializer: 'Spree::Api::V3::CreditCardSerializer',
96
96
  price_serializer: 'Spree::Api::V3::PriceSerializer',
97
+ price_history_serializer: 'Spree::Api::V3::PriceHistorySerializer',
97
98
  product_serializer: 'Spree::Api::V3::ProductSerializer',
98
99
  variant_serializer: 'Spree::Api::V3::VariantSerializer',
99
100
  media_serializer: 'Spree::Api::V3::MediaSerializer',
@@ -129,6 +130,7 @@ module Spree
129
130
  gift_card_serializer: 'Spree::Api::V3::GiftCardSerializer',
130
131
  currency_serializer: 'Spree::Api::V3::CurrencySerializer',
131
132
  locale_serializer: 'Spree::Api::V3::LocaleSerializer',
133
+ policy_serializer: 'Spree::Api::V3::PolicySerializer',
132
134
  metafield_serializer: 'Spree::Api::V3::MetafieldSerializer',
133
135
  shipping_category_serializer: 'Spree::Api::V3::ShippingCategorySerializer',
134
136
  tax_category_serializer: 'Spree::Api::V3::TaxCategorySerializer',
@@ -160,6 +162,7 @@ module Spree
160
162
  admin_product_serializer: 'Spree::Api::V3::Admin::ProductSerializer',
161
163
  admin_variant_serializer: 'Spree::Api::V3::Admin::VariantSerializer',
162
164
  admin_price_serializer: 'Spree::Api::V3::Admin::PriceSerializer',
165
+ admin_price_history_serializer: 'Spree::Api::V3::Admin::PriceHistorySerializer',
163
166
  admin_metafield_serializer: 'Spree::Api::V3::Admin::MetafieldSerializer',
164
167
  admin_category_serializer: 'Spree::Api::V3::Admin::CategorySerializer',
165
168
  admin_line_item_serializer: 'Spree::Api::V3::Admin::LineItemSerializer',
@@ -95,6 +95,17 @@ module Spree
95
95
  },
96
96
  required: %w[step field message]
97
97
  },
98
+ CartWarning: {
99
+ type: :object,
100
+ description: 'A warning about a cart issue (e.g., item removed due to stock change)',
101
+ properties: {
102
+ code: { type: :string, description: 'Machine-readable warning code', example: 'line_item_removed' },
103
+ message: { type: :string, description: 'Human-readable warning message', example: 'Blue T-Shirt was removed because it was sold out' },
104
+ line_item_id: { type: :string, nullable: true, description: 'Prefixed line item ID (when applicable)', example: 'li_abc123' },
105
+ variant_id: { type: :string, nullable: true, description: 'Prefixed variant ID (when applicable)', example: 'variant_abc123' }
106
+ },
107
+ required: %w[code message]
108
+ },
98
109
  FulfillmentManifestItem: {
99
110
  type: :object,
100
111
  description: 'An item within a fulfillment — which line item and how many units are in this fulfillment',
@@ -152,6 +163,14 @@ module Spree
152
163
  items: { '$ref' => '#/components/schemas/CheckoutRequirement' }
153
164
  }
154
165
  end
166
+
167
+ warn_key = props.key?('warnings') ? 'warnings' : :warnings
168
+ if props[warn_key]
169
+ props[warn_key] = {
170
+ type: :array,
171
+ items: { '$ref' => '#/components/schemas/CartWarning' }
172
+ }
173
+ end
155
174
  end
156
175
 
157
176
  # Typelizer cannot represent Array<{...}> inline object types in OpenAPI,
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.rc2
4
+ version: 5.4.0.rc4
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-23 00:00:00.000000000 Z
11
+ date: 2026-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -72,28 +72,28 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 0.10.0
75
+ version: 0.11.0
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 0.10.0
82
+ version: 0.11.0
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: spree_core
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - '='
88
88
  - !ruby/object:Gem::Version
89
- version: 5.4.0.rc2
89
+ version: 5.4.0.rc4
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.rc2
96
+ version: 5.4.0.rc4
97
97
  description: Spree's API
98
98
  email:
99
99
  - hello@spreecommerce.org
@@ -121,14 +121,14 @@ files:
121
121
  - app/controllers/spree/api/v3/resource_controller.rb
122
122
  - app/controllers/spree/api/v3/store/auth_controller.rb
123
123
  - app/controllers/spree/api/v3/store/base_controller.rb
124
- - app/controllers/spree/api/v3/store/carts/coupon_codes_controller.rb
124
+ - app/controllers/spree/api/v3/store/carts/discount_codes_controller.rb
125
125
  - app/controllers/spree/api/v3/store/carts/fulfillments_controller.rb
126
+ - app/controllers/spree/api/v3/store/carts/gift_cards_controller.rb
126
127
  - app/controllers/spree/api/v3/store/carts/items_controller.rb
127
128
  - app/controllers/spree/api/v3/store/carts/payment_sessions_controller.rb
128
129
  - app/controllers/spree/api/v3/store/carts/payments_controller.rb
129
130
  - app/controllers/spree/api/v3/store/carts/store_credits_controller.rb
130
131
  - app/controllers/spree/api/v3/store/carts_controller.rb
131
- - app/controllers/spree/api/v3/store/categories/products_controller.rb
132
132
  - app/controllers/spree/api/v3/store/categories_controller.rb
133
133
  - app/controllers/spree/api/v3/store/countries_controller.rb
134
134
  - app/controllers/spree/api/v3/store/currencies_controller.rb
@@ -145,6 +145,7 @@ files:
145
145
  - app/controllers/spree/api/v3/store/markets/countries_controller.rb
146
146
  - app/controllers/spree/api/v3/store/markets_controller.rb
147
147
  - app/controllers/spree/api/v3/store/orders_controller.rb
148
+ - app/controllers/spree/api/v3/store/policies_controller.rb
148
149
  - app/controllers/spree/api/v3/store/products/filters_controller.rb
149
150
  - app/controllers/spree/api/v3/store/products_controller.rb
150
151
  - app/controllers/spree/api/v3/store/resource_controller.rb
@@ -177,6 +178,7 @@ files:
177
178
  - app/serializers/spree/api/v3/admin/payment_method_serializer.rb
178
179
  - app/serializers/spree/api/v3/admin/payment_serializer.rb
179
180
  - app/serializers/spree/api/v3/admin/payment_source_serializer.rb
181
+ - app/serializers/spree/api/v3/admin/price_history_serializer.rb
180
182
  - app/serializers/spree/api/v3/admin/price_serializer.rb
181
183
  - app/serializers/spree/api/v3/admin/product_serializer.rb
182
184
  - app/serializers/spree/api/v3/admin/refund_serializer.rb
@@ -223,6 +225,8 @@ files:
223
225
  - app/serializers/spree/api/v3/payment_session_serializer.rb
224
226
  - app/serializers/spree/api/v3/payment_setup_session_serializer.rb
225
227
  - app/serializers/spree/api/v3/payment_source_serializer.rb
228
+ - app/serializers/spree/api/v3/policy_serializer.rb
229
+ - app/serializers/spree/api/v3/price_history_serializer.rb
226
230
  - app/serializers/spree/api/v3/price_serializer.rb
227
231
  - app/serializers/spree/api/v3/product_serializer.rb
228
232
  - app/serializers/spree/api/v3/promotion_serializer.rb
@@ -269,9 +273,9 @@ licenses:
269
273
  - BSD-3-Clause
270
274
  metadata:
271
275
  bug_tracker_uri: https://github.com/spree/spree/issues
272
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.rc2
276
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.4.0.rc4
273
277
  documentation_uri: https://docs.spreecommerce.org/
274
- source_code_uri: https://github.com/spree/spree/tree/v5.4.0.rc2
278
+ source_code_uri: https://github.com/spree/spree/tree/v5.4.0.rc4
275
279
  post_install_message:
276
280
  rdoc_options: []
277
281
  require_paths:
@@ -1,37 +0,0 @@
1
- module Spree
2
- module Api
3
- module V3
4
- module Store
5
- module Categories
6
- class ProductsController < Store::ProductsController
7
- before_action :set_category
8
-
9
- protected
10
-
11
- def set_category
12
- @category = find_category
13
- end
14
-
15
- def scope
16
- super.in_category(@category)
17
- end
18
-
19
- private
20
-
21
- def find_category
22
- id = params[:category_id]
23
- category_scope = Spree::Category.for_store(current_store).accessible_by(current_ability, :show)
24
- category_scope = category_scope.i18n if Spree::Category.include?(Spree::TranslatableResource)
25
-
26
- if id.to_s.start_with?('ctg_')
27
- category_scope.find_by_prefix_id!(id)
28
- else
29
- find_with_fallback_default_locale { category_scope.i18n.find_by!(permalink: id) }
30
- end
31
- end
32
- end
33
- end
34
- end
35
- end
36
- end
37
- end