spree_api 5.6.0.rc3 → 5.6.0.rc5
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 +4 -4
- data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +54 -8
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +8 -3
- data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +5 -5
- data/app/controllers/spree/api/v3/admin/order_routing_rules_controller.rb +65 -0
- data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +5 -0
- data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +5 -0
- data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +5 -0
- data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/base_controller.rb +7 -2
- data/app/controllers/spree/api/v3/store/carts_controller.rb +6 -1
- data/app/controllers/spree/api/v3/store/channel_controller.rb +31 -0
- data/app/controllers/spree/api/v3/store/resource_controller.rb +12 -0
- data/app/jobs/spree/webhook_delivery_job.rb +6 -1
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +3 -4
- data/app/serializers/spree/api/v3/admin/order_routing_rule_serializer.rb +40 -0
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/cart_serializer.rb +8 -1
- data/app/serializers/spree/api/v3/channel_serializer.rb +9 -1
- data/app/serializers/spree/api/v3/customer_group_serializer.rb +15 -0
- data/app/serializers/spree/api/v3/customer_serializer.rb +8 -0
- data/app/services/spree/webhooks/deliver_webhook.rb +6 -2
- data/app/subscribers/spree/webhook_event_subscriber.rb +5 -2
- data/config/locales/en.yml +2 -0
- data/config/routes.rb +10 -0
- data/lib/spree/api/dependencies.rb +2 -0
- metadata +10 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eee15468ad97bc5af5ed27d4e0ed4f4149c8b325efe81d9ef05d91af84094a27
|
|
4
|
+
data.tar.gz: c5d97420fb0bf65903f944487e102a57f957babdb7119dc2406307c0d45b2634
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e7783e8de7f1681b747863d5f9d161e1c5b8d05e1c087b5454332868fa81caaf7900e421fb60dabcca24b1d8ffbb99abf68338b0e85f13d6f456708e8c35e40f
|
|
7
|
+
data.tar.gz: 30aad9e4cc778ae8d11b1527053cf1f96f4fc75846ee48422152939a050a2506537824c1ffbeafc0b53dd8bd7c9c927ff39f5014ef462b9581c2f66a177d6ee7
|
|
@@ -6,41 +6,87 @@ module Spree
|
|
|
6
6
|
# read channel context without threading it through method args.
|
|
7
7
|
#
|
|
8
8
|
# Resolution order:
|
|
9
|
-
# 1.
|
|
9
|
+
# 1. The authenticated publishable key's channel binding
|
|
10
|
+
# (+Spree::ApiKey#channel+) — server-assigned, cannot be overridden;
|
|
11
|
+
# a +X-Spree-Channel+ header naming a different channel is a 422
|
|
12
|
+
# 2. +X-Spree-Channel+ header value matched against +channels.code+ —
|
|
10
13
|
# or, if it looks like a prefixed ID (+ch_…+), against +channels.id+
|
|
11
14
|
# — scoped to the current store
|
|
12
|
-
#
|
|
15
|
+
# 3. +current_store.default_channel+
|
|
13
16
|
#
|
|
14
17
|
# The concern is a no-op if no channel matches — callers fall back to
|
|
15
18
|
# +Spree::Current.channel+'s store-default behavior.
|
|
19
|
+
#
|
|
20
|
+
# IMPORTANT: key-bound resolution requires +authenticate_api_key!+ to run
|
|
21
|
+
# BEFORE +set_current_channel+ — both Store API branches order their
|
|
22
|
+
# callbacks accordingly (prepended in Store::BaseController, inherited
|
|
23
|
+
# ahead of the include in Store::ResourceController).
|
|
16
24
|
module ChannelResolution
|
|
17
25
|
extend ActiveSupport::Concern
|
|
18
26
|
|
|
19
27
|
CHANNEL_HEADER = 'X-Spree-Channel'.freeze
|
|
20
28
|
|
|
21
29
|
included do
|
|
30
|
+
# Prepended: LocaleAndCurrency's set_locale/set_currency callbacks
|
|
31
|
+
# (registered up in V3::BaseController) consult Spree::Current's
|
|
32
|
+
# fallback chain, which must derive from the REQUEST's store — not
|
|
33
|
+
# the Store.default fallback — before they run.
|
|
34
|
+
prepend_before_action :set_current_store_context
|
|
22
35
|
before_action :set_current_channel
|
|
23
36
|
end
|
|
24
37
|
|
|
25
38
|
protected
|
|
26
39
|
|
|
27
40
|
def current_channel
|
|
28
|
-
@current_channel ||= channel_from_header || Spree::Current.channel
|
|
41
|
+
@current_channel ||= channel_from_api_key || channel_from_header || Spree::Current.channel
|
|
29
42
|
end
|
|
30
43
|
|
|
31
44
|
private
|
|
32
45
|
|
|
33
|
-
# Only write to Spree::Current when the header resolves a
|
|
34
|
-
# channel. The store-default fallback is handled lazily by
|
|
46
|
+
# Only write to Spree::Current when the key or header resolves a
|
|
47
|
+
# specific channel. The store-default fallback is handled lazily by
|
|
35
48
|
# +Spree::Current.channel+ itself, which avoids one query per
|
|
36
|
-
# header-less API request.
|
|
49
|
+
# unbound, header-less API request.
|
|
50
|
+
def set_current_store_context
|
|
51
|
+
Spree::Current.store = current_store
|
|
52
|
+
end
|
|
53
|
+
|
|
37
54
|
def set_current_channel
|
|
55
|
+
bound = channel_from_api_key
|
|
56
|
+
|
|
57
|
+
if bound
|
|
58
|
+
header = channel_from_header
|
|
59
|
+
if header && header.id != bound.id
|
|
60
|
+
render_error(
|
|
61
|
+
code: ErrorHandler::ERROR_CODES[:channel_mismatch],
|
|
62
|
+
message: Spree.t('api.errors.channel_mismatch', default: 'The requested channel does not match the channel this API key is bound to'),
|
|
63
|
+
status: :unprocessable_entity
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unless bound.active?
|
|
69
|
+
render_error(
|
|
70
|
+
code: ErrorHandler::ERROR_CODES[:channel_inactive],
|
|
71
|
+
message: Spree.t('api.errors.channel_inactive', default: 'The channel this API key is bound to is not active'),
|
|
72
|
+
status: :forbidden
|
|
73
|
+
)
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
38
78
|
# Memoize so a later +current_channel+ call (e.g. the storefront gate,
|
|
39
|
-
# serializer params) reuses this row instead of re-querying
|
|
40
|
-
@current_channel = channel_from_header
|
|
79
|
+
# serializer params) reuses this row instead of re-querying.
|
|
80
|
+
@current_channel = bound || channel_from_header
|
|
41
81
|
Spree::Current.channel = @current_channel if @current_channel
|
|
42
82
|
end
|
|
43
83
|
|
|
84
|
+
def channel_from_api_key
|
|
85
|
+
return nil unless respond_to?(:current_api_key, true)
|
|
86
|
+
|
|
87
|
+
current_api_key&.channel
|
|
88
|
+
end
|
|
89
|
+
|
|
44
90
|
def channel_from_header
|
|
45
91
|
value = request.headers[CHANNEL_HEADER].presence
|
|
46
92
|
return nil if value.blank?
|
|
@@ -21,6 +21,10 @@ module Spree
|
|
|
21
21
|
record_not_found: 'record_not_found',
|
|
22
22
|
resource_invalid: 'resource_invalid',
|
|
23
23
|
|
|
24
|
+
# Channel errors
|
|
25
|
+
channel_mismatch: 'channel_mismatch',
|
|
26
|
+
channel_inactive: 'channel_inactive',
|
|
27
|
+
|
|
24
28
|
# Cart errors
|
|
25
29
|
cart_not_found: 'cart_not_found',
|
|
26
30
|
cart_cannot_transition: 'cart_cannot_transition',
|
|
@@ -86,10 +86,15 @@ module Spree
|
|
|
86
86
|
self.class._scoped_resource
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
#
|
|
90
|
-
# `
|
|
89
|
+
# Maps the action to the scope kind. Consults the controller's
|
|
90
|
+
# `read_actions` (ResourceController's overridable list) when defined,
|
|
91
|
+
# so declaring a custom read-only action once — e.g. `types` — fixes
|
|
92
|
+
# both the CanCanCan action mapping and the required scope kind.
|
|
93
|
+
# Controllers outside the ResourceController hierarchy can still
|
|
94
|
+
# override `action_kind` directly (e.g. dashboard `analytics`).
|
|
91
95
|
def action_kind
|
|
92
|
-
|
|
96
|
+
read = respond_to?(:read_actions, true) ? read_actions : READ_ACTIONS
|
|
97
|
+
read.include?(action_name) ? 'read' : 'write'
|
|
93
98
|
end
|
|
94
99
|
|
|
95
100
|
# True when authorization derives from the API key's scopes rather
|
|
@@ -87,14 +87,14 @@ module Spree
|
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
# `key_type` and `
|
|
91
|
-
# Spree::ApiKey); update is limited to the human-facing `name`.
|
|
92
|
-
# them here keeps them out of mass assignment so a rename
|
|
93
|
-
# rather than 422.
|
|
90
|
+
# `key_type`, `scopes`, and `channel_id` are create-only (immutability
|
|
91
|
+
# lives on Spree::ApiKey); update is limited to the human-facing `name`.
|
|
92
|
+
# Stripping them here keeps them out of mass assignment so a rename
|
|
93
|
+
# returns 200 rather than 422.
|
|
94
94
|
def permitted_params
|
|
95
95
|
return params.permit(:name) if action_name == 'update'
|
|
96
96
|
|
|
97
|
-
params.permit(:name, :key_type, scopes: [])
|
|
97
|
+
params.permit(:name, :key_type, :channel_id, scopes: [])
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
private
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# CRUD for `Spree::OrderRoutingRule` STI subclasses, nested under the
|
|
6
|
+
# owning channel. Same shape as PromotionRulesController — the registry
|
|
7
|
+
# is `Spree.order_routing.rules` and the parent is a Channel.
|
|
8
|
+
class OrderRoutingRulesController < ResourceController
|
|
9
|
+
include Spree::Api::V3::Admin::SubclassedResource
|
|
10
|
+
|
|
11
|
+
scoped_resource :settings
|
|
12
|
+
|
|
13
|
+
subclassed_via -> { Spree.order_routing.rules },
|
|
14
|
+
unknown_type_error: 'unknown_order_routing_rule_type'
|
|
15
|
+
|
|
16
|
+
def types
|
|
17
|
+
authorize! :read, model_class
|
|
18
|
+
|
|
19
|
+
render json: { data: model_class.subclasses_with_preference_schema }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def model_class
|
|
25
|
+
Spree::OrderRoutingRule
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def serializer_class
|
|
29
|
+
Spree.api.admin_order_routing_rule_serializer
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def permitted_params
|
|
33
|
+
params.permit(:type, :active, :position, preferences: {})
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# `types` is read-only discovery — maps to the read scope + :show ability.
|
|
37
|
+
def read_actions
|
|
38
|
+
super + %w[types]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def scope
|
|
42
|
+
super.ordered
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def set_parent
|
|
46
|
+
return if action_name == 'types'
|
|
47
|
+
|
|
48
|
+
@parent = current_store.channels.accessible_by(current_ability, parent_ability_action)
|
|
49
|
+
.find_by_prefix_id!(params[:channel_id])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parent_association
|
|
53
|
+
:order_routing_rules
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def build_subclassed_resource(klass, attrs)
|
|
59
|
+
klass.new(attrs.merge(channel: @parent, store: @parent.store))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -51,6 +51,11 @@ module Spree
|
|
|
51
51
|
params.permit(:name, :description, :active, :storefront_visible, :auto_capture, :position, metadata: {}, preferences: {})
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# `types` is read-only discovery — maps to the read scope + :show ability.
|
|
55
|
+
def read_actions
|
|
56
|
+
super + %w[types]
|
|
57
|
+
end
|
|
58
|
+
|
|
54
59
|
private
|
|
55
60
|
|
|
56
61
|
# New payment methods are created through the current store.
|
|
@@ -55,6 +55,11 @@ module Spree
|
|
|
55
55
|
params.permit(:type, preferences: {})
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# `types`/`calculators` are read-only discovery — read scope + :show ability.
|
|
59
|
+
def read_actions
|
|
60
|
+
super + %w[types calculators]
|
|
61
|
+
end
|
|
62
|
+
|
|
58
63
|
def set_parent
|
|
59
64
|
return if %w[types calculators].include?(action_name)
|
|
60
65
|
|
|
@@ -14,8 +14,13 @@ module Spree
|
|
|
14
14
|
# +allow_guest_storefront_access!+.
|
|
15
15
|
include Spree::Api::V3::StorefrontGating
|
|
16
16
|
|
|
17
|
-
# Require publishable API key for all Store API requests
|
|
18
|
-
|
|
17
|
+
# Require publishable API key for all Store API requests. Prepended so
|
|
18
|
+
# the key is authenticated BEFORE ChannelResolution's
|
|
19
|
+
# +set_current_channel+ and the StorefrontGating 401 guard run —
|
|
20
|
+
# a channel-bound key must resolve its channel ahead of the gate, and
|
|
21
|
+
# this matches Store::ResourceController, where +authenticate_request!+
|
|
22
|
+
# is inherited ahead of both concerns.
|
|
23
|
+
prepend_before_action :authenticate_api_key!
|
|
19
24
|
end
|
|
20
25
|
end
|
|
21
26
|
end
|
|
@@ -141,8 +141,13 @@ module Spree
|
|
|
141
141
|
Spree.api.cart_serializer
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
+
# Channel-scoped: a storefront surface lists only its own channel's
|
|
145
|
+
# carts — without this, a blended DTC + wholesale storefront leaks
|
|
146
|
+
# the user's wholesale cart into the DTC surface (and vice versa).
|
|
147
|
+
# Explicit by-ID lookups (show/update) stay cross-channel so a shared
|
|
148
|
+
# checkout can load any cart the caller is authorized for.
|
|
144
149
|
def scope
|
|
145
|
-
current_store.carts.where(user: current_user).order(updated_at: :desc)
|
|
150
|
+
current_store.carts.where(user: current_user, channel: current_channel).order(updated_at: :desc)
|
|
146
151
|
end
|
|
147
152
|
|
|
148
153
|
private
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Store
|
|
5
|
+
# Exposes the channel context the request resolved to (bound key →
|
|
6
|
+
# X-Spree-Channel header → store default) so a storefront can read its
|
|
7
|
+
# own identity and access posture. Deliberately a singular resource:
|
|
8
|
+
# the Store API never enumerates a store's channels — that would leak
|
|
9
|
+
# the existence of gated surfaces (wholesale, POS) to anyone holding
|
|
10
|
+
# the publishable key.
|
|
11
|
+
class ChannelController < Store::BaseController
|
|
12
|
+
# A gated storefront must be able to read "this channel requires
|
|
13
|
+
# login" before authentication to render a sign-in wall instead of
|
|
14
|
+
# an error page.
|
|
15
|
+
allow_guest_storefront_access!
|
|
16
|
+
|
|
17
|
+
# GET /api/v3/store/channel
|
|
18
|
+
def show
|
|
19
|
+
render json: serialize_resource(current_channel)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def serializer_class
|
|
25
|
+
Spree.api.channel_serializer
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -9,6 +9,18 @@ module Spree
|
|
|
9
9
|
include Spree::Api::V3::ChannelResolution
|
|
10
10
|
include Spree::Api::V3::StorefrontGating
|
|
11
11
|
|
|
12
|
+
# The inherited +set_parent+/+set_resource+ callbacks were registered
|
|
13
|
+
# before the two concerns above, so they'd run resource lookups ahead
|
|
14
|
+
# of channel resolution (wrong channel scoping for X-Spree-Channel /
|
|
15
|
+
# key-bound requests) and ahead of the login gate (guests could probe
|
|
16
|
+
# resource existence on a gated channel via 404s). Re-register them so
|
|
17
|
+
# lookups always run inside the resolved channel context, behind the
|
|
18
|
+
# gate.
|
|
19
|
+
skip_before_action :set_parent
|
|
20
|
+
skip_before_action :set_resource
|
|
21
|
+
before_action :set_parent
|
|
22
|
+
before_action :set_resource, only: [:show, :update, :destroy]
|
|
23
|
+
|
|
12
24
|
protected
|
|
13
25
|
|
|
14
26
|
def authenticate_request!
|
|
@@ -13,10 +13,15 @@ module Spree
|
|
|
13
13
|
|
|
14
14
|
# Accept optional second argument for backward compatibility with jobs
|
|
15
15
|
# enqueued before this change was deployed.
|
|
16
|
-
|
|
16
|
+
#
|
|
17
|
+
# `payload_secrets` carries credentials withheld from the persisted payload
|
|
18
|
+
# so the outgoing request body stays complete. See
|
|
19
|
+
# {Spree::WebhookPayloadRedaction}.
|
|
20
|
+
def perform(delivery_id, _deprecated_secret_key = nil, payload_secrets: nil)
|
|
17
21
|
delivery = Spree::WebhookDelivery.find_by(id: delivery_id)
|
|
18
22
|
return if delivery.nil?
|
|
19
23
|
|
|
24
|
+
delivery.payload_secrets = payload_secrets
|
|
20
25
|
secret_key = delivery.webhook_endpoint.secret_key
|
|
21
26
|
Spree::Webhooks::DeliverWebhook.call(delivery: delivery, secret_key: secret_key)
|
|
22
27
|
end
|
|
@@ -18,12 +18,17 @@ module Spree
|
|
|
18
18
|
scopes: [:string, multi: true],
|
|
19
19
|
revoked_at: [:string, nullable: true],
|
|
20
20
|
last_used_at: [:string, nullable: true],
|
|
21
|
-
created_by_email: [:string, nullable: true]
|
|
21
|
+
created_by_email: [:string, nullable: true],
|
|
22
|
+
channel_id: [:string, nullable: true]
|
|
22
23
|
|
|
23
24
|
attributes :name, :key_type, :token_prefix, :scopes,
|
|
24
25
|
created_at: :iso8601, updated_at: :iso8601,
|
|
25
26
|
revoked_at: :iso8601, last_used_at: :iso8601
|
|
26
27
|
|
|
28
|
+
attribute :channel_id do |key|
|
|
29
|
+
key.channel&.prefixed_id
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
# Returned only on the create response — `plaintext_token` is held in
|
|
28
33
|
# memory on the model after `generate_token` and is never persisted
|
|
29
34
|
# for secret keys, so we serialize it whenever it's available rather
|
|
@@ -5,12 +5,11 @@ module Spree
|
|
|
5
5
|
# Serializes Spree::CustomerGroup for the admin pickers
|
|
6
6
|
# (e.g. promotion rule, customer-group filters). Surfaces only
|
|
7
7
|
# what the admin UI needs to display + select rows.
|
|
8
|
-
class CustomerGroupSerializer < V3::
|
|
9
|
-
typelize
|
|
10
|
-
description: 'string | null',
|
|
8
|
+
class CustomerGroupSerializer < V3::CustomerGroupSerializer
|
|
9
|
+
typelize description: 'string | null',
|
|
11
10
|
customers_count: :number
|
|
12
11
|
|
|
13
|
-
attributes :
|
|
12
|
+
attributes :description, :customers_count,
|
|
14
13
|
created_at: :iso8601, updated_at: :iso8601
|
|
15
14
|
|
|
16
15
|
# Members are paginated separately via `/customers?customer_group_id_in=…`
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module Api
|
|
5
|
+
module V3
|
|
6
|
+
module Admin
|
|
7
|
+
# Serializes Spree::OrderRoutingRule (and its STI subclasses) for the
|
|
8
|
+
# per-channel routing-rules editor. Same shape as PromotionRule so the
|
|
9
|
+
# frontend renders both with the same schema-driven preference form.
|
|
10
|
+
class OrderRoutingRuleSerializer < BaseSerializer
|
|
11
|
+
typelize type: :string,
|
|
12
|
+
channel_id: :string,
|
|
13
|
+
position: :number,
|
|
14
|
+
active: :boolean,
|
|
15
|
+
preferences: 'Record<string, unknown>',
|
|
16
|
+
preference_schema: "Array<{ key: string; type: string; default: unknown }>",
|
|
17
|
+
label: :string,
|
|
18
|
+
description: :string
|
|
19
|
+
|
|
20
|
+
attributes :position, :active
|
|
21
|
+
attributes created_at: :iso8601, updated_at: :iso8601
|
|
22
|
+
|
|
23
|
+
attribute :type do |rule|
|
|
24
|
+
rule.class.api_type
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attribute :channel_id do |rule|
|
|
28
|
+
rule.channel&.prefixed_id
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
attribute :preferences, &:serialized_preferences
|
|
32
|
+
attribute :preference_schema, &:serialized_preference_schema
|
|
33
|
+
|
|
34
|
+
attribute :label, &:human_name
|
|
35
|
+
attribute :description, &:human_description
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -21,6 +21,7 @@ module Spree
|
|
|
21
21
|
preferred_unit_system: :string,
|
|
22
22
|
preferred_storefront_access: :string,
|
|
23
23
|
preferred_guest_checkout: :boolean,
|
|
24
|
+
preferred_order_routing_strategy: :string,
|
|
24
25
|
metadata: 'Record<string, unknown>'
|
|
25
26
|
|
|
26
27
|
attributes :metadata,
|
|
@@ -39,6 +40,7 @@ module Spree
|
|
|
39
40
|
:preferred_unit_system,
|
|
40
41
|
:preferred_storefront_access,
|
|
41
42
|
:preferred_guest_checkout,
|
|
43
|
+
:preferred_order_routing_strategy,
|
|
42
44
|
created_at: :iso8601, updated_at: :iso8601
|
|
43
45
|
|
|
44
46
|
attribute :url, &:storefront_url
|
|
@@ -25,10 +25,15 @@ module Spree
|
|
|
25
25
|
|
|
26
26
|
attributes :event_name, :event_id, :response_code, :execution_time,
|
|
27
27
|
:error_type, :request_errors, :response_body, :success,
|
|
28
|
-
:payload,
|
|
29
28
|
created_at: :iso8601, updated_at: :iso8601,
|
|
30
29
|
delivered_at: :iso8601
|
|
31
30
|
|
|
31
|
+
# Redacted again on read: deliveries written before payload redaction
|
|
32
|
+
# shipped still hold live credentials in the column.
|
|
33
|
+
attribute :payload do |delivery|
|
|
34
|
+
Spree::WebhookPayloadRedaction.split(delivery.payload).first
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
attribute :webhook_endpoint_id do |delivery|
|
|
33
38
|
delivery.webhook_endpoint&.prefixed_id
|
|
34
39
|
end
|
|
@@ -5,7 +5,7 @@ module Spree
|
|
|
5
5
|
# Pre-purchase cart data with checkout progression info
|
|
6
6
|
class CartSerializer < BaseSerializer
|
|
7
7
|
typelize number: :string, current_step: :string, completed_steps: 'string[]', token: :string, email: [:string, nullable: true],
|
|
8
|
-
customer_note: [:string, nullable: true], market_id: [:string, nullable: true],
|
|
8
|
+
customer_note: [:string, nullable: true], market_id: [:string, nullable: true], channel_id: [:string, nullable: true],
|
|
9
9
|
currency: :string, locale: [:string, nullable: true], total_quantity: :number,
|
|
10
10
|
requirements: 'Array<{step: string, field: string, message: string}>',
|
|
11
11
|
item_total: [:string, nullable: true], display_item_total: [:string, nullable: true],
|
|
@@ -34,6 +34,13 @@ module Spree
|
|
|
34
34
|
order.market&.prefixed_id
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Which surface this cart belongs to — lets a multi-channel storefront
|
|
38
|
+
# verify a cookie-persisted cart against its own channel before
|
|
39
|
+
# adopting it (see the wholesale reference storefront's cart guard).
|
|
40
|
+
attribute :channel_id do |order|
|
|
41
|
+
order.channel&.prefixed_id
|
|
42
|
+
end
|
|
43
|
+
|
|
37
44
|
attributes :number, :token, :email, :customer_note,
|
|
38
45
|
:currency, :locale, :total_quantity, :warnings
|
|
39
46
|
|
|
@@ -5,9 +5,17 @@ module Spree
|
|
|
5
5
|
typelize name: :string,
|
|
6
6
|
code: :string,
|
|
7
7
|
active: :boolean,
|
|
8
|
-
default: :boolean
|
|
8
|
+
default: :boolean,
|
|
9
|
+
storefront_access: :string,
|
|
10
|
+
guest_checkout: :boolean
|
|
9
11
|
|
|
10
12
|
attributes :name, :code, :active, :default
|
|
13
|
+
|
|
14
|
+
# Resolved (channel → store fallback) values — what a client should act
|
|
15
|
+
# on, as opposed to the raw nullable preferences on the admin serializer.
|
|
16
|
+
attribute :storefront_access, &:resolved_storefront_access
|
|
17
|
+
|
|
18
|
+
attribute :guest_checkout, &:resolved_guest_checkout
|
|
11
19
|
end
|
|
12
20
|
end
|
|
13
21
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
# Minimal customer-group shape for the Store API. Exposed on
|
|
5
|
+
# `customers/me` so a storefront can branch on membership (e.g. an
|
|
6
|
+
# approved wholesale buyer) — pricing itself always resolves
|
|
7
|
+
# server-side.
|
|
8
|
+
class CustomerGroupSerializer < BaseSerializer
|
|
9
|
+
typelize name: :string
|
|
10
|
+
|
|
11
|
+
attributes :name
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -37,6 +37,14 @@ module Spree
|
|
|
37
37
|
store = params&.dig(:store) || Spree::Current.store
|
|
38
38
|
user.newsletter_subscriber(store)
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
# Membership signal for storefront branching (e.g. wholesale approval);
|
|
42
|
+
# scoped to the request store (params first — Spree::Current.store falls
|
|
43
|
+
# back to the DEFAULT store, wrong on sibling stores' domains) so other
|
|
44
|
+
# stores' memberships never leak.
|
|
45
|
+
many :customer_groups,
|
|
46
|
+
proc { |groups, params| groups.for_store(params&.dig(:store) || Spree::Current.store) },
|
|
47
|
+
resource: proc { Spree.api.customer_group_serializer }
|
|
40
48
|
end
|
|
41
49
|
end
|
|
42
50
|
end
|
|
@@ -56,7 +56,7 @@ module Spree
|
|
|
56
56
|
'X-Spree-Webhook-Timestamp' => webhook_timestamp.to_s,
|
|
57
57
|
'X-Spree-Webhook-Event' => @delivery.event_name
|
|
58
58
|
}
|
|
59
|
-
body =
|
|
59
|
+
body = payload_json
|
|
60
60
|
http_options = { open_timeout: TIMEOUT, read_timeout: TIMEOUT, verify_mode: ssl_verify_mode }
|
|
61
61
|
|
|
62
62
|
# SSRF protection is disabled in development so webhooks can reach
|
|
@@ -77,10 +77,14 @@ module Spree
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def generate_signature
|
|
80
|
-
payload_json = @delivery.payload.to_json
|
|
81
80
|
OpenSSL::HMAC.hexdigest('SHA256', @secret_key, "#{webhook_timestamp}.#{payload_json}")
|
|
82
81
|
end
|
|
83
82
|
|
|
83
|
+
# Memoized so the signature is computed over exactly the bytes sent.
|
|
84
|
+
def payload_json
|
|
85
|
+
@payload_json ||= @delivery.deliverable_payload.to_json
|
|
86
|
+
end
|
|
87
|
+
|
|
84
88
|
def webhook_timestamp
|
|
85
89
|
@webhook_timestamp ||= Time.current.to_i
|
|
86
90
|
end
|
|
@@ -56,13 +56,16 @@ module Spree
|
|
|
56
56
|
)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Live credentials go over the wire but never into the delivery log.
|
|
60
|
+
persisted_payload, secrets = Spree::WebhookPayloadRedaction.split(payload)
|
|
61
|
+
|
|
59
62
|
delivery = endpoint.webhook_deliveries.create!(
|
|
60
63
|
event_name: event.name,
|
|
61
64
|
event_id: event.id,
|
|
62
|
-
payload:
|
|
65
|
+
payload: persisted_payload
|
|
63
66
|
)
|
|
64
67
|
|
|
65
|
-
Spree::WebhookDeliveryJob.perform_later(delivery.id)
|
|
68
|
+
Spree::WebhookDeliveryJob.perform_later(delivery.id, payload_secrets: secrets)
|
|
66
69
|
rescue ActiveRecord::RecordNotUnique
|
|
67
70
|
# Race condition: another thread already created this delivery — safe to ignore
|
|
68
71
|
rescue StandardError => e
|
data/config/locales/en.yml
CHANGED
|
@@ -10,6 +10,8 @@ en:
|
|
|
10
10
|
invalid_taxonomy_id: Invalid taxonomy id.
|
|
11
11
|
must_specify_api_key: You must specify an API key.
|
|
12
12
|
errors:
|
|
13
|
+
channel_inactive: The channel this API key is bound to is not active.
|
|
14
|
+
channel_mismatch: The requested channel does not match the channel this API key is bound to.
|
|
13
15
|
guest_checkout_not_allowed: You must be signed in to complete checkout.
|
|
14
16
|
storefront_login_required: Authentication required to access this store.
|
|
15
17
|
negative_quantity: quantity is negative
|
data/config/routes.rb
CHANGED
|
@@ -20,6 +20,9 @@ Spree::Core::Engine.add_routes do
|
|
|
20
20
|
resources :currencies, only: [:index]
|
|
21
21
|
resources :locales, only: [:index]
|
|
22
22
|
|
|
23
|
+
# Current channel context (never a list — see Store::ChannelController)
|
|
24
|
+
resource :channel, only: [:show], controller: 'channel'
|
|
25
|
+
|
|
23
26
|
# Catalog
|
|
24
27
|
resources :products, only: [:index, :show] do
|
|
25
28
|
collection do
|
|
@@ -342,8 +345,15 @@ Spree::Core::Engine.add_routes do
|
|
|
342
345
|
post :add_products
|
|
343
346
|
post :remove_products
|
|
344
347
|
end
|
|
348
|
+
|
|
349
|
+
resources :order_routing_rules, only: [:index, :show, :create, :update, :destroy]
|
|
345
350
|
end
|
|
346
351
|
|
|
352
|
+
# Subclass discovery for the routing-rules editor, mirroring
|
|
353
|
+
# `/promotion_rules/types`. Top-level so the SPA can build the
|
|
354
|
+
# "Add rule" picker without a parent channel.
|
|
355
|
+
get 'order_routing_rules/types', to: 'order_routing_rules#types'
|
|
356
|
+
|
|
347
357
|
# Variants (top-level, for search/autocomplete across all products)
|
|
348
358
|
resources :variants, only: [:index, :show], concerns: :custom_fieldable
|
|
349
359
|
|
|
@@ -112,6 +112,7 @@ module Spree
|
|
|
112
112
|
fulfillment_serializer: 'Spree::Api::V3::FulfillmentSerializer',
|
|
113
113
|
address_serializer: 'Spree::Api::V3::AddressSerializer',
|
|
114
114
|
customer_serializer: 'Spree::Api::V3::CustomerSerializer',
|
|
115
|
+
customer_group_serializer: 'Spree::Api::V3::CustomerGroupSerializer',
|
|
115
116
|
country_serializer: 'Spree::Api::V3::CountrySerializer',
|
|
116
117
|
channel_serializer: 'Spree::Api::V3::ChannelSerializer',
|
|
117
118
|
product_publication_serializer: 'Spree::Api::V3::ProductPublicationSerializer',
|
|
@@ -201,6 +202,7 @@ module Spree
|
|
|
201
202
|
admin_admin_user_serializer: 'Spree::Api::V3::Admin::AdminUserSerializer',
|
|
202
203
|
admin_address_serializer: 'Spree::Api::V3::Admin::AddressSerializer',
|
|
203
204
|
admin_channel_serializer: 'Spree::Api::V3::Admin::ChannelSerializer',
|
|
205
|
+
admin_order_routing_rule_serializer: 'Spree::Api::V3::Admin::OrderRoutingRuleSerializer',
|
|
204
206
|
admin_product_publication_serializer: 'Spree::Api::V3::Admin::ProductPublicationSerializer',
|
|
205
207
|
admin_market_serializer: 'Spree::Api::V3::Admin::MarketSerializer',
|
|
206
208
|
admin_shipping_method_serializer: 'Spree::Api::V3::Admin::DeliveryMethodSerializer',
|
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.6.0.
|
|
4
|
+
version: 5.6.0.rc5
|
|
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-07-
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rswag-specs
|
|
@@ -72,14 +72,14 @@ dependencies:
|
|
|
72
72
|
requirements:
|
|
73
73
|
- - '='
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: 5.6.0.
|
|
75
|
+
version: 5.6.0.rc5
|
|
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: 5.6.0.
|
|
82
|
+
version: 5.6.0.rc5
|
|
83
83
|
description: Spree's API
|
|
84
84
|
email:
|
|
85
85
|
- hello@spreecommerce.org
|
|
@@ -142,6 +142,7 @@ files:
|
|
|
142
142
|
- app/controllers/spree/api/v3/admin/me_controller.rb
|
|
143
143
|
- app/controllers/spree/api/v3/admin/media_controller.rb
|
|
144
144
|
- app/controllers/spree/api/v3/admin/option_types_controller.rb
|
|
145
|
+
- app/controllers/spree/api/v3/admin/order_routing_rules_controller.rb
|
|
145
146
|
- app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb
|
|
146
147
|
- app/controllers/spree/api/v3/admin/orders/base_controller.rb
|
|
147
148
|
- app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb
|
|
@@ -189,6 +190,7 @@ files:
|
|
|
189
190
|
- app/controllers/spree/api/v3/store/carts/store_credits_controller.rb
|
|
190
191
|
- app/controllers/spree/api/v3/store/carts_controller.rb
|
|
191
192
|
- app/controllers/spree/api/v3/store/categories_controller.rb
|
|
193
|
+
- app/controllers/spree/api/v3/store/channel_controller.rb
|
|
192
194
|
- app/controllers/spree/api/v3/store/countries_controller.rb
|
|
193
195
|
- app/controllers/spree/api/v3/store/currencies_controller.rb
|
|
194
196
|
- app/controllers/spree/api/v3/store/customer/addresses_controller.rb
|
|
@@ -251,6 +253,7 @@ files:
|
|
|
251
253
|
- app/serializers/spree/api/v3/admin/newsletter_subscriber_serializer.rb
|
|
252
254
|
- app/serializers/spree/api/v3/admin/option_type_serializer.rb
|
|
253
255
|
- app/serializers/spree/api/v3/admin/option_value_serializer.rb
|
|
256
|
+
- app/serializers/spree/api/v3/admin/order_routing_rule_serializer.rb
|
|
254
257
|
- app/serializers/spree/api/v3/admin/order_serializer.rb
|
|
255
258
|
- app/serializers/spree/api/v3/admin/payment_method_serializer.rb
|
|
256
259
|
- app/serializers/spree/api/v3/admin/payment_serializer.rb
|
|
@@ -291,6 +294,7 @@ files:
|
|
|
291
294
|
- app/serializers/spree/api/v3/credit_card_serializer.rb
|
|
292
295
|
- app/serializers/spree/api/v3/currency_serializer.rb
|
|
293
296
|
- app/serializers/spree/api/v3/custom_field_serializer.rb
|
|
297
|
+
- app/serializers/spree/api/v3/customer_group_serializer.rb
|
|
294
298
|
- app/serializers/spree/api/v3/customer_return_serializer.rb
|
|
295
299
|
- app/serializers/spree/api/v3/customer_serializer.rb
|
|
296
300
|
- app/serializers/spree/api/v3/delivery_method_serializer.rb
|
|
@@ -380,9 +384,9 @@ licenses:
|
|
|
380
384
|
- BSD-3-Clause
|
|
381
385
|
metadata:
|
|
382
386
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
383
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.
|
|
387
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc5
|
|
384
388
|
documentation_uri: https://docs.spreecommerce.org/
|
|
385
|
-
source_code_uri: https://github.com/spree/spree/tree/v5.6.0.
|
|
389
|
+
source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc5
|
|
386
390
|
post_install_message:
|
|
387
391
|
rdoc_options: []
|
|
388
392
|
require_paths:
|