spree_api 6.0.0.beta1 → 6.0.0.beta3
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/admin/embed_permissions.rb +55 -0
- data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +34 -5
- data/app/controllers/concerns/spree/api/v3/admin/stock_receipt_actions.rb +1 -1
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +11 -0
- data/app/controllers/concerns/spree/api/v3/current_password_confirmation.rb +8 -0
- data/app/controllers/concerns/spree/api/v3/orders/claim_actions.rb +2 -2
- data/app/controllers/concerns/spree/api/v3/orders/exchange_actions.rb +3 -3
- data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +15 -2
- data/app/controllers/concerns/spree/api/v3/orders/post_sale_actions.rb +1 -1
- data/app/controllers/concerns/spree/api/v3/orders/return_actions.rb +3 -3
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +14 -0
- data/app/controllers/concerns/spree/api/v3/store/storefront_products.rb +35 -0
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +9 -0
- data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +2 -2
- data/app/controllers/spree/api/v3/admin/base_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +8 -3
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +0 -14
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +29 -2
- data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +4 -1
- data/app/controllers/spree/api/v3/admin/invitations/acceptance_links_controller.rb +41 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +11 -0
- data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +14 -1
- data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +22 -1
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +22 -1
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +41 -9
- data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +31 -11
- data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +1 -19
- data/app/controllers/spree/api/v3/admin/prices_controller.rb +33 -2
- data/app/controllers/spree/api/v3/admin/products_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/resource_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/sellers/invitations/acceptance_links_controller.rb +37 -0
- data/app/controllers/spree/api/v3/admin/setup_controller.rb +9 -1
- data/app/controllers/spree/api/v3/admin/store_controller.rb +5 -0
- data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/translations_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +12 -0
- data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +27 -0
- data/app/controllers/spree/api/v3/base_controller.rb +22 -3
- data/app/controllers/spree/api/v3/resource_controller.rb +27 -2
- data/app/controllers/spree/api/v3/seller/auth_controller.rb +1 -1
- data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +5 -2
- data/app/controllers/spree/api/v3/seller/invitations/acceptance_links_controller.rb +34 -0
- data/app/controllers/spree/api/v3/seller/me_controller.rb +25 -2
- data/app/controllers/spree/api/v3/seller/orders_controller.rb +20 -25
- data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +1 -1
- data/app/controllers/spree/api/v3/seller/resource_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/carts/items_controller.rb +7 -4
- data/app/controllers/spree/api/v3/store/carts/payment_sessions_controller.rb +10 -1
- data/app/controllers/spree/api/v3/store/carts/payments_controller.rb +5 -1
- data/app/controllers/spree/api/v3/store/companies/members_controller.rb +10 -11
- data/app/controllers/spree/api/v3/store/companies/orders_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/products_controller.rb +6 -12
- data/app/controllers/spree/api/v3/store/resource_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/wishlist_items_controller.rb +11 -0
- data/app/controllers/spree/api/v3/webhooks/fulfillments_controller.rb +10 -6
- data/app/controllers/spree/api/v3/webhooks/payments_controller.rb +6 -2
- data/app/controllers/spree/api/v3/webhooks/payouts_controller.rb +6 -2
- data/app/jobs/spree/webhook_delivery_job.rb +18 -4
- data/app/serializers/spree/api/v3/admin/actor_serializer.rb +28 -0
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +14 -1
- data/app/serializers/spree/api/v3/admin/claim_serializer.rb +3 -4
- data/app/serializers/spree/api/v3/admin/exchange_serializer.rb +3 -4
- data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +6 -0
- data/app/serializers/spree/api/v3/admin/invitation_acceptance_link_serializer.rb +15 -0
- data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +0 -11
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +28 -1
- data/app/serializers/spree/api/v3/admin/order_group_serializer.rb +7 -0
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +7 -24
- data/app/serializers/spree/api/v3/admin/refund_serializer.rb +6 -0
- data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -3
- data/app/serializers/spree/api/v3/admin/saved_report_serializer.rb +3 -4
- data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +2 -3
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +10 -0
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +6 -2
- data/app/serializers/spree/api/v3/base_serializer.rb +22 -0
- data/app/serializers/spree/api/v3/gift_card_serializer.rb +3 -1
- data/app/serializers/spree/api/v3/line_item_serializer.rb +2 -1
- data/app/serializers/spree/api/v3/market_serializer.rb +5 -0
- data/app/serializers/spree/api/v3/product_filters_serializer.rb +3 -0
- data/app/serializers/spree/api/v3/seller/account_serializer.rb +19 -0
- data/app/serializers/spree/api/v3/seller/invitation_acceptance_link_serializer.rb +15 -0
- data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +1 -10
- data/app/services/spree/api/v3/filters_aggregator.rb +5 -2
- data/config/routes.rb +7 -0
- data/lib/spree/api/configuration.rb +19 -14
- data/lib/spree/api/dependencies.rb +4 -0
- data/lib/spree/api/engine.rb +6 -0
- data/lib/spree/api/openapi/schema_helper.rb +10 -4
- metadata +15 -6
|
@@ -26,7 +26,7 @@ module Spree
|
|
|
26
26
|
payment: payment,
|
|
27
27
|
amount: params[:amount],
|
|
28
28
|
reason: reason,
|
|
29
|
-
refunder:
|
|
29
|
+
refunder: current_actor,
|
|
30
30
|
# Names which order is being refunded. Only matters when the
|
|
31
31
|
# payment is shared by a split checkout, where it covers several
|
|
32
32
|
# and the payment cannot say which one this is for.
|
|
@@ -25,7 +25,7 @@ module Spree
|
|
|
25
25
|
#
|
|
26
26
|
# Editable fields only — status moves through the member actions.
|
|
27
27
|
def update
|
|
28
|
-
if @resource.update(
|
|
28
|
+
if @resource.update(update_attributes)
|
|
29
29
|
render json: serialize_resource(@resource.reload)
|
|
30
30
|
else
|
|
31
31
|
render_validation_error(@resource.errors)
|
|
@@ -55,6 +55,27 @@ module Spree
|
|
|
55
55
|
|
|
56
56
|
private
|
|
57
57
|
|
|
58
|
+
# The ids are resolved through the store rather than assigned raw,
|
|
59
|
+
# so another store's reason or warehouse reads as missing.
|
|
60
|
+
def update_attributes
|
|
61
|
+
attributes = permitted_params.to_h.except('reason_id', 'stock_location_id')
|
|
62
|
+
|
|
63
|
+
if params.key?(:reason_id)
|
|
64
|
+
attributes['reason'] =
|
|
65
|
+
(current_store.return_reasons.find_by_prefix_id!(params[:reason_id]) if params[:reason_id].present?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if params.key?(:stock_location_id)
|
|
69
|
+
attributes['stock_location'] =
|
|
70
|
+
if params[:stock_location_id].present?
|
|
71
|
+
current_store.stock_locations.accessible_by(current_ability, :show).
|
|
72
|
+
find_by_prefix_id!(params[:stock_location_id])
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
attributes
|
|
77
|
+
end
|
|
78
|
+
|
|
58
79
|
# Any of the store's warehouses the caller may see.
|
|
59
80
|
def stock_location_for_create
|
|
60
81
|
return nil if create_params[:stock_location_id].blank?
|
|
@@ -23,7 +23,7 @@ module Spree
|
|
|
23
23
|
result = Spree.order_create_service.call(
|
|
24
24
|
store: current_store,
|
|
25
25
|
customer: resolve_customer,
|
|
26
|
-
created_by:
|
|
26
|
+
created_by: current_actor,
|
|
27
27
|
params: order_create_params
|
|
28
28
|
)
|
|
29
29
|
|
|
@@ -52,6 +52,10 @@ module Spree
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# PATCH /api/v3/admin/orders/:id/complete
|
|
55
|
+
#
|
|
56
|
+
# An order holding several sellers' goods divides into one order per
|
|
57
|
+
# seller, so the group is what comes back — the orders it produced
|
|
58
|
+
# are nested inside it.
|
|
55
59
|
def complete
|
|
56
60
|
with_order_lock do
|
|
57
61
|
result = Spree.order_complete_service.call(
|
|
@@ -61,7 +65,7 @@ module Spree
|
|
|
61
65
|
)
|
|
62
66
|
|
|
63
67
|
if result.success?
|
|
64
|
-
render json:
|
|
68
|
+
render json: serialize_completion(result.value)
|
|
65
69
|
else
|
|
66
70
|
render_service_error(@resource.errors.presence || result.error, code: ERROR_CODES[:order_cannot_complete])
|
|
67
71
|
end
|
|
@@ -73,7 +77,7 @@ module Spree
|
|
|
73
77
|
with_order_lock do
|
|
74
78
|
result = Spree.order_cancel_workflow.call(
|
|
75
79
|
order: @resource,
|
|
76
|
-
canceler:
|
|
80
|
+
canceler: current_actor,
|
|
77
81
|
reason: cancel_reason,
|
|
78
82
|
note: params[:cancel_note].presence,
|
|
79
83
|
refund_payments: params[:refund_payments].to_b,
|
|
@@ -92,7 +96,7 @@ module Spree
|
|
|
92
96
|
# PATCH /api/v3/admin/orders/:id/approve
|
|
93
97
|
def approve
|
|
94
98
|
with_order_lock do
|
|
95
|
-
@resource.approved_by(
|
|
99
|
+
@resource.approved_by(current_actor)
|
|
96
100
|
render json: serialize_resource(@resource.reload)
|
|
97
101
|
end
|
|
98
102
|
end
|
|
@@ -149,12 +153,36 @@ module Spree
|
|
|
149
153
|
Spree.api.admin_order_serializer
|
|
150
154
|
end
|
|
151
155
|
|
|
156
|
+
# Completion answers with whatever it produced: one order, or the
|
|
157
|
+
# group a multi-seller order divided into.
|
|
158
|
+
#
|
|
159
|
+
# The group is rendered whole rather than through `filter_fields`: a
|
|
160
|
+
# `fields` list names an order's attributes, and narrowing a group by
|
|
161
|
+
# it can drop the very keys that say it is one.
|
|
162
|
+
def serialize_completion(record)
|
|
163
|
+
return serialize_resource(record.reload) unless record.is_a?(Spree::OrderGroup)
|
|
164
|
+
|
|
165
|
+
Spree.api.admin_order_group_serializer.new(completed_group(record), params: serializer_params).to_h
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Every child is rendered in full, so its associations are loaded once
|
|
169
|
+
# here rather than per order.
|
|
170
|
+
def completed_group(group)
|
|
171
|
+
Spree::OrderGroup.includes(
|
|
172
|
+
:customer, :ship_address, :bill_address,
|
|
173
|
+
{ payments: %i[payment_method source] },
|
|
174
|
+
{ orders: [:seller, :market, :gift_card, :customer, { line_items: { variant: :prices } }] }
|
|
175
|
+
).find(group.id)
|
|
176
|
+
end
|
|
177
|
+
|
|
152
178
|
# Override scope — Order uses SingleStoreResource (for_store).
|
|
153
|
-
# Variant prices are
|
|
154
|
-
# which this override bypasses; the
|
|
179
|
+
# Variant prices and each line's price list are named here rather
|
|
180
|
+
# than via scope_includes, which this override bypasses; the
|
|
181
|
+
# serializer reads both per row.
|
|
155
182
|
def scope
|
|
156
183
|
base = current_store.orders.accessible_by(current_ability, :show).
|
|
157
|
-
includes(line_items: { variant: :prices }).
|
|
184
|
+
includes(line_items: [{ variant: :prices }, { price_list: :catalog }]).
|
|
185
|
+
preload_associations_lazily
|
|
158
186
|
|
|
159
187
|
# Transient completion drafts (status draft + cart_id set) belong
|
|
160
188
|
# to in-flight checkouts, never to the admin. Admin drafts are the
|
|
@@ -190,7 +218,10 @@ module Spree
|
|
|
190
218
|
# only its id is reported and that comes off the order's own column.
|
|
191
219
|
# Variant prices ride along because the admin line-item serializer
|
|
192
220
|
# reads the base catalog price for every row (the negotiated-price
|
|
193
|
-
# comparison); without it each line costs its own price query.
|
|
221
|
+
# comparison); without it each line costs its own price query. Each
|
|
222
|
+
# line's price list and its catalog are named for the same reason —
|
|
223
|
+
# stated rather than left to ar_lazy_preload, which happens to cover
|
|
224
|
+
# them today.
|
|
194
225
|
def collection_includes
|
|
195
226
|
# `market` is the withdrawal deadline's other input. Fulfillments
|
|
196
227
|
# are loaded with their selected rate because the freight summary
|
|
@@ -198,7 +229,8 @@ module Spree
|
|
|
198
229
|
# fulfillment for a field that is nil on every parcel order.
|
|
199
230
|
[:customer, :channel, :seller, :external_references, :cancel_reason,
|
|
200
231
|
:market, { fulfillments: :selected_delivery_rate },
|
|
201
|
-
{ line_items: { variant: :prices }
|
|
232
|
+
{ line_items: [{ variant: :prices }, { price_list: :catalog }] },
|
|
233
|
+
{ po_document_attachment: :blob }]
|
|
202
234
|
end
|
|
203
235
|
|
|
204
236
|
# Read through the store's own vocabulary, so a reason belonging to
|
|
@@ -17,20 +17,10 @@ module Spree
|
|
|
17
17
|
|
|
18
18
|
# POST /api/v3/admin/password_resets
|
|
19
19
|
def create
|
|
20
|
-
redirect_url = params[:redirect_url]
|
|
21
|
-
|
|
22
|
-
# Validate redirect_url against allowed origins (secure by default).
|
|
23
|
-
# If no allowed origins are configured, redirect_url is silently ignored
|
|
24
|
-
# to prevent open redirect / token exfiltration attacks.
|
|
25
|
-
if redirect_url.present?
|
|
26
|
-
unless current_store.allowed_origins.exists? && current_store.allowed_origin?(redirect_url)
|
|
27
|
-
redirect_url = nil
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
20
|
user = Spree.admin_user_class.find_by(email: params[:email])
|
|
32
21
|
|
|
33
22
|
if user
|
|
23
|
+
redirect_url = trusted_redirect_url(user)
|
|
34
24
|
token = user.generate_token_for(:password_reset)
|
|
35
25
|
event_payload = { reset_token: token, email: user.email, store_id: current_store.prefixed_id }
|
|
36
26
|
event_payload[:redirect_url] = redirect_url if redirect_url.present?
|
|
@@ -71,6 +61,36 @@ module Spree
|
|
|
71
61
|
|
|
72
62
|
private
|
|
73
63
|
|
|
64
|
+
# The emailed link carries the reset token, so it may only point at
|
|
65
|
+
# the dashboard — never at a storefront origin, which anyone who can
|
|
66
|
+
# edit a store's allowed origins could add — and only when the
|
|
67
|
+
# account belongs to the store the request names. Anything else is
|
|
68
|
+
# ignored and the email links to the dashboard's own reset page.
|
|
69
|
+
#
|
|
70
|
+
# @return [String, nil]
|
|
71
|
+
def trusted_redirect_url(user)
|
|
72
|
+
redirect_url = params[:redirect_url].presence
|
|
73
|
+
return if redirect_url.blank?
|
|
74
|
+
return unless same_origin?(redirect_url, Spree::Stores::DashboardUrl.call(store: current_store))
|
|
75
|
+
return unless staff_of_current_store?(user)
|
|
76
|
+
|
|
77
|
+
redirect_url
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def same_origin?(url, other)
|
|
81
|
+
first = URI.parse(url.to_s)
|
|
82
|
+
second = URI.parse(other.to_s)
|
|
83
|
+
first.host.present? && [first.scheme, first.host, first.port] == [second.scheme, second.host, second.port]
|
|
84
|
+
rescue URI::InvalidURIError
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def staff_of_current_store?(user)
|
|
89
|
+
return false unless user.respond_to?(:spree_roles)
|
|
90
|
+
|
|
91
|
+
user.spree_roles.for_resource(current_store).exists?
|
|
92
|
+
end
|
|
93
|
+
|
|
74
94
|
def auth_response(user)
|
|
75
95
|
{
|
|
76
96
|
token: generate_jwt(user, audience: JWT_AUDIENCE_ADMIN),
|
|
@@ -122,7 +122,7 @@ module Spree
|
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
def permitted_params
|
|
125
|
-
|
|
125
|
+
normalize_params(
|
|
126
126
|
params.permit(
|
|
127
127
|
:name, :description, :position,
|
|
128
128
|
:starts_at, :ends_at, :match_policy,
|
|
@@ -132,24 +132,6 @@ module Spree
|
|
|
132
132
|
prices: [:id, :variant_id, :currency, :min_quantity, :amount, :compare_at_amount]
|
|
133
133
|
)
|
|
134
134
|
)
|
|
135
|
-
reject_foreign_variants(attrs)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# The price rows resolve variant ids with no store scoping, so a
|
|
139
|
-
# list in this store could otherwise be populated with another
|
|
140
|
-
# store's variants. Drop any id that isn't in the current store
|
|
141
|
-
# before assignment.
|
|
142
|
-
def reject_foreign_variants(attrs)
|
|
143
|
-
if attrs[:prices].present?
|
|
144
|
-
incoming = Array(attrs[:prices])
|
|
145
|
-
store_variant_ids = current_store.variants.where(id: incoming.map { |r| r[:variant_id] }.compact).
|
|
146
|
-
pluck(:id).map(&:to_s).to_set
|
|
147
|
-
attrs[:prices] = incoming.select do |row|
|
|
148
|
-
row[:variant_id].blank? || store_variant_ids.include?(row[:variant_id].to_s)
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
attrs
|
|
153
135
|
end
|
|
154
136
|
|
|
155
137
|
private
|
|
@@ -58,6 +58,10 @@ module Spree
|
|
|
58
58
|
# validations (docs/plans/6.0-volume-pricing.md). Each branch is
|
|
59
59
|
# guarded on the key its own failure carries, so a failure mode the
|
|
60
60
|
# service grows later is not reported under one of these names.
|
|
61
|
+
#
|
|
62
|
+
# `I18n.t` rather than `Spree.t` for these: the models raise them
|
|
63
|
+
# under `activerecord.errors`, and `Spree.t` would scope the lookup
|
|
64
|
+
# to `spree.` and miss the key.
|
|
61
65
|
if (invalid = result.error&.value.try(:[], :invalid_quantities))
|
|
62
66
|
return render_error(
|
|
63
67
|
code: 'invalid_min_quantity',
|
|
@@ -70,16 +74,34 @@ module Spree
|
|
|
70
74
|
if (negative = result.error&.value.try(:[], :invalid_amounts))
|
|
71
75
|
return render_error(
|
|
72
76
|
code: 'invalid_amount',
|
|
73
|
-
message:
|
|
77
|
+
message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.negative_price').upcase_first,
|
|
74
78
|
status: :unprocessable_content,
|
|
75
79
|
details: { rows: negative }
|
|
76
80
|
)
|
|
77
81
|
end
|
|
78
82
|
|
|
83
|
+
if (misplaced = result.error&.value.try(:[], :quantities_on_base_prices))
|
|
84
|
+
return render_error(
|
|
85
|
+
code: 'quantity_on_base_price',
|
|
86
|
+
message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.quantity_on_base_price').upcase_first,
|
|
87
|
+
status: :unprocessable_content,
|
|
88
|
+
details: { rows: misplaced }
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if (duplicates = result.error&.value.try(:[], :duplicate_quantities))
|
|
93
|
+
return render_error(
|
|
94
|
+
code: 'duplicate_min_quantity',
|
|
95
|
+
message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.duplicate_quantity').upcase_first,
|
|
96
|
+
status: :unprocessable_content,
|
|
97
|
+
details: { rows: duplicates }
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
79
101
|
if (over_cap = result.error&.value.try(:[], :over_cap))
|
|
80
102
|
return render_error(
|
|
81
103
|
code: 'too_many_breaks',
|
|
82
|
-
message:
|
|
104
|
+
message: I18n.t(
|
|
83
105
|
'activerecord.errors.models.spree/price.attributes.min_quantity.too_many_breaks',
|
|
84
106
|
count: Spree::Price::MAXIMUM_BREAKS_PER_VARIANT
|
|
85
107
|
).upcase_first,
|
|
@@ -88,6 +110,15 @@ module Spree
|
|
|
88
110
|
)
|
|
89
111
|
end
|
|
90
112
|
|
|
113
|
+
if (rising = result.error&.value.try(:[], :rising_ladders))
|
|
114
|
+
return render_error(
|
|
115
|
+
code: 'price_rises_with_quantity',
|
|
116
|
+
message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.price_rises_with_quantity').upcase_first,
|
|
117
|
+
status: :unprocessable_content,
|
|
118
|
+
details: { ladders: rising }
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
91
122
|
render json: result.value
|
|
92
123
|
end
|
|
93
124
|
|
|
@@ -301,7 +301,7 @@ module Spree
|
|
|
301
301
|
end
|
|
302
302
|
|
|
303
303
|
def search_provider
|
|
304
|
-
@search_provider ||= Spree::SearchProvider::Database.new(current_store)
|
|
304
|
+
@search_provider ||= Spree::SearchProvider::Database.new(current_store, audience: nil)
|
|
305
305
|
end
|
|
306
306
|
|
|
307
307
|
# Tag changes can flip automatic-collection matches, and `Tags::Bulk*`
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
module Sellers
|
|
6
|
+
module Invitations
|
|
7
|
+
# The acceptance link of a pending invitation onto a seller's team.
|
|
8
|
+
#
|
|
9
|
+
# The link carries the token that joins the team, so reading
|
|
10
|
+
# sellers is not enough: the caller must be able to change the
|
|
11
|
+
# seller, as when sending the invitation.
|
|
12
|
+
class AcceptanceLinksController < Admin::BaseController
|
|
13
|
+
scoped_resource :sellers
|
|
14
|
+
|
|
15
|
+
# GET /api/v3/admin/sellers/:seller_id/invitations/:invitation_id/acceptance_link
|
|
16
|
+
def show
|
|
17
|
+
seller = current_store.sellers.find_by_prefix_id!(params[:seller_id])
|
|
18
|
+
authorize!(:update, seller)
|
|
19
|
+
invitation = seller.invitations.pending.find_by_prefix_id!(params[:invitation_id])
|
|
20
|
+
|
|
21
|
+
render json: Spree.api.admin_invitation_acceptance_link_serializer.new(
|
|
22
|
+
invitation, params: { store: current_store }
|
|
23
|
+
).to_h
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def read_actions
|
|
29
|
+
[]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -41,7 +41,7 @@ module Spree
|
|
|
41
41
|
# POST /api/v3/admin/auth/setup
|
|
42
42
|
# Body: { setup_token, email, password, password_confirmation?,
|
|
43
43
|
# first_name?, last_name?, store_name, country_code,
|
|
44
|
-
# locale?, currency? }
|
|
44
|
+
# locale?, currency?, sample_data? }
|
|
45
45
|
# Currency defaults to the country's own; an unknown code is refused
|
|
46
46
|
# rather than ignored, since the token is spent in the same request.
|
|
47
47
|
def create
|
|
@@ -63,6 +63,10 @@ module Spree
|
|
|
63
63
|
adopt_default_store(user, store)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Outside the lock: the loader needs the admin committed, and it
|
|
67
|
+
# runs for minutes, so it is queued rather than awaited.
|
|
68
|
+
Spree::SampleData::LoadJob.perform_later(store.id) if sample_data_requested?
|
|
69
|
+
|
|
66
70
|
refresh_token = Spree::RefreshToken.create_for(user, audience: JWT_AUDIENCE_ADMIN, request_env: request_env_for_token)
|
|
67
71
|
set_refresh_cookie(refresh_token)
|
|
68
72
|
render json: auth_response(user)
|
|
@@ -167,6 +171,10 @@ module Spree
|
|
|
167
171
|
)
|
|
168
172
|
end
|
|
169
173
|
|
|
174
|
+
def sample_data_requested?
|
|
175
|
+
ActiveModel::Type::Boolean.new.cast(params[:sample_data]) == true
|
|
176
|
+
end
|
|
177
|
+
|
|
170
178
|
def admin_user_params
|
|
171
179
|
params.permit(:email, :password, :password_confirmation, :first_name, :last_name)
|
|
172
180
|
end
|
|
@@ -81,6 +81,7 @@ module Spree
|
|
|
81
81
|
:preferred_storefront_access,
|
|
82
82
|
:preferred_storefront_url,
|
|
83
83
|
:preferred_guest_checkout,
|
|
84
|
+
:preferred_always_include_confirm_step,
|
|
84
85
|
:preferred_company_field_enabled,
|
|
85
86
|
:preferred_address_requires_company,
|
|
86
87
|
:preferred_address_requires_phone,
|
|
@@ -104,6 +105,10 @@ module Spree
|
|
|
104
105
|
:preferred_payout_provider,
|
|
105
106
|
:preferred_default_payouts_schedule_interval,
|
|
106
107
|
:preferred_default_minimum_payout_amount,
|
|
108
|
+
:preferred_auto_approve_sellers,
|
|
109
|
+
:preferred_auto_approve_seller_products,
|
|
110
|
+
:preferred_send_seller_transactional_emails,
|
|
111
|
+
:preferred_default_commission_tax_rate,
|
|
107
112
|
:preferred_document_number_format,
|
|
108
113
|
:preferred_order_number_prefix,
|
|
109
114
|
:preferred_order_number_suffix,
|
|
@@ -39,7 +39,7 @@ module Spree
|
|
|
39
39
|
raw = params[:translations]
|
|
40
40
|
return render_empty_batch_error unless raw.is_a?(Array) && raw.any?
|
|
41
41
|
|
|
42
|
-
batch = Spree::Translations::Batch.new(batch_params)
|
|
42
|
+
batch = Spree::Translations::Batch.new(batch_params, store: current_store)
|
|
43
43
|
return unless require_batch_scopes!(batch)
|
|
44
44
|
|
|
45
45
|
records = batch.process! { |record| authorize!(:update, record) }
|
|
@@ -43,7 +43,7 @@ module Spree
|
|
|
43
43
|
# translation tables.
|
|
44
44
|
def parent_relation
|
|
45
45
|
klass = parent_lookup.klass
|
|
46
|
-
relation = klass.respond_to?(:
|
|
46
|
+
relation = klass.respond_to?(:translatable_scope) ? klass.translatable_scope(current_store) : klass.for_store(current_store)
|
|
47
47
|
|
|
48
48
|
children = klass.try(:translatable_children)
|
|
49
49
|
children ? relation.includes(children => :translations) : relation
|
|
@@ -42,6 +42,18 @@ module Spree
|
|
|
42
42
|
params[:webhook_endpoint_id]
|
|
43
43
|
)
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
# The payload is the record the event is about, so it is shown only
|
|
47
|
+
# to a caller who could read that record; the webhooks permission
|
|
48
|
+
# alone sees the delivery's status and response.
|
|
49
|
+
def serializer_params
|
|
50
|
+
super.merge(
|
|
51
|
+
payload_visible: lambda { |delivery|
|
|
52
|
+
key = delivery.payload_permission_key
|
|
53
|
+
key.nil? || holds_permission?(key)
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
end
|
|
45
57
|
end
|
|
46
58
|
end
|
|
47
59
|
end
|
|
@@ -8,6 +8,8 @@ module Spree
|
|
|
8
8
|
class WebhookEndpointsController < ResourceController
|
|
9
9
|
scoped_resource :webhooks
|
|
10
10
|
|
|
11
|
+
before_action :reject_unauthorized_credential_subscription!, only: [:create, :update]
|
|
12
|
+
|
|
11
13
|
# POST /api/v3/admin/webhook_endpoints/:id/send_test
|
|
12
14
|
#
|
|
13
15
|
# Fires a synthetic `webhook.test` delivery so admins can verify the
|
|
@@ -68,6 +70,31 @@ module Spree
|
|
|
68
70
|
def permitted_params
|
|
69
71
|
params.permit(*model_additional_permitted_attributes, :name, :url, :active, subscriptions: [])
|
|
70
72
|
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Customer password reset tokens are account credentials, so the
|
|
77
|
+
# webhooks permission alone cannot subscribe an endpoint to them, or
|
|
78
|
+
# repoint an endpoint that already receives them.
|
|
79
|
+
def reject_unauthorized_credential_subscription!
|
|
80
|
+
return if holds_permission?('write_customers')
|
|
81
|
+
|
|
82
|
+
requested = Array(permitted_params[:subscriptions]) & Spree::WebhookEndpoint::CREDENTIAL_EVENTS
|
|
83
|
+
return if requested.empty? && !repoints_credential_endpoint?
|
|
84
|
+
|
|
85
|
+
render_error(
|
|
86
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
87
|
+
message: "Receiving #{Spree::WebhookEndpoint::CREDENTIAL_EVENTS.to_sentence} requires permission to manage customers",
|
|
88
|
+
status: :forbidden
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def repoints_credential_endpoint?
|
|
93
|
+
return false unless action_name == 'update'
|
|
94
|
+
return false unless permitted_params.key?(:url) || permitted_params.key?(:subscriptions)
|
|
95
|
+
|
|
96
|
+
current_store.webhook_endpoints.find_by_prefix_id(params[:id])&.receives_credentials?
|
|
97
|
+
end
|
|
71
98
|
end
|
|
72
99
|
end
|
|
73
100
|
end
|
|
@@ -81,7 +81,10 @@ module Spree
|
|
|
81
81
|
if errors.is_a?(ActiveModel::Errors)
|
|
82
82
|
render_validation_error(errors)
|
|
83
83
|
else
|
|
84
|
-
|
|
84
|
+
# The unwrapped value, so a workflow's rejection symbol reaches
|
|
85
|
+
# the branch that translates it rather than the one that prints
|
|
86
|
+
# the wrapper.
|
|
87
|
+
render_service_error(errors)
|
|
85
88
|
end
|
|
86
89
|
end
|
|
87
90
|
|
|
@@ -93,10 +96,26 @@ module Spree
|
|
|
93
96
|
|
|
94
97
|
alias try_spree_current_user spree_current_user
|
|
95
98
|
|
|
96
|
-
#
|
|
99
|
+
# Who performed this request, for the associations that record it —
|
|
100
|
+
# `canceler`, `approver`, `created_by` and their siblings. Answers the
|
|
101
|
+
# signed-in user here; the Admin branch prefers the authenticating
|
|
102
|
+
# secret key, because a key-made write must record the key rather than
|
|
103
|
+
# nobody (see docs/plans/6.0-action-actors.md).
|
|
104
|
+
#
|
|
105
|
+
# Deliberately not `current_api_key || current_user` at this level: on
|
|
106
|
+
# a Store API request the key is the storefront's publishable one, and
|
|
107
|
+
# a customer's own action is not the storefront's.
|
|
108
|
+
#
|
|
109
|
+
# @return [Object, nil] a member of Spree.actor_classes
|
|
110
|
+
def current_actor
|
|
111
|
+
try_spree_current_user
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# CanCanCan ability, built from the swappable
|
|
115
|
+
# `Spree::Dependencies.ability_class` (Spree::Ability by default).
|
|
97
116
|
# @return [Spree::Ability]
|
|
98
117
|
def current_ability
|
|
99
|
-
@current_ability ||= Spree
|
|
118
|
+
@current_ability ||= Spree.ability_class.new(current_user, ability_options)
|
|
100
119
|
end
|
|
101
120
|
|
|
102
121
|
# Options passed to the CanCanCan ability
|
|
@@ -176,10 +176,25 @@ module Spree
|
|
|
176
176
|
def build_resource
|
|
177
177
|
resource = resource_scope.new
|
|
178
178
|
resource.assign_attributes(permitted_params) if create_workflow.nil?
|
|
179
|
-
resource.created_by =
|
|
179
|
+
resource.created_by = creator_for(resource) if resource.respond_to?(:created_by_id)
|
|
180
180
|
resource
|
|
181
181
|
end
|
|
182
182
|
|
|
183
|
+
# The actor to stamp on a new record, or nil when this model cannot
|
|
184
|
+
# hold it. `created_by` is polymorphic on the order operations and
|
|
185
|
+
# still admin-user-only elsewhere until 6.1, so a key-authenticated
|
|
186
|
+
# create of a gift card records nobody rather than raising — the same
|
|
187
|
+
# thing it recorded before keys became actors.
|
|
188
|
+
def creator_for(resource)
|
|
189
|
+
actor = current_actor
|
|
190
|
+
return actor if actor.nil?
|
|
191
|
+
|
|
192
|
+
association = resource.class.reflect_on_association(:created_by)
|
|
193
|
+
return actor if association.nil? || association.polymorphic?
|
|
194
|
+
|
|
195
|
+
actor.is_a?(association.klass) ? actor : nil
|
|
196
|
+
end
|
|
197
|
+
|
|
183
198
|
# The relation a new record is built on: a nested resource's parent
|
|
184
199
|
# association, the store's own association for this model, or the
|
|
185
200
|
# class itself for genuinely global data (countries, roles).
|
|
@@ -239,7 +254,7 @@ module Spree
|
|
|
239
254
|
|
|
240
255
|
@search = scope.includes(collection_includes).
|
|
241
256
|
preload_associations_lazily.
|
|
242
|
-
ransack(ransack_params)
|
|
257
|
+
ransack(ransack_params, auth_object: ransack_auth_object)
|
|
243
258
|
result = @search.result(distinct: collection_distinct?)
|
|
244
259
|
pagy_options = { limit: limit, page: page }
|
|
245
260
|
result = apply_collection_sort(result)
|
|
@@ -247,6 +262,16 @@ module Spree
|
|
|
247
262
|
@collection
|
|
248
263
|
end
|
|
249
264
|
|
|
265
|
+
# Who is filtering, as Ransack's auth object. Nil is the back office,
|
|
266
|
+
# which may use every allowlisted attribute; the Store and Seller
|
|
267
|
+
# branches name themselves so the models narrow what they may reach
|
|
268
|
+
# (see Spree::RansackableAttributes). Covers sorting too.
|
|
269
|
+
#
|
|
270
|
+
# @return [Symbol, nil]
|
|
271
|
+
def ransack_auth_object
|
|
272
|
+
nil
|
|
273
|
+
end
|
|
274
|
+
|
|
250
275
|
# Override in subclass to disable distinct (e.g., for custom sorting with computed columns)
|
|
251
276
|
# @return [Boolean] whether to apply distinct to the collection
|
|
252
277
|
def collection_distinct?
|
|
@@ -136,7 +136,7 @@ module Spree
|
|
|
136
136
|
def auth_response(user)
|
|
137
137
|
{
|
|
138
138
|
token: generate_jwt(user, audience: Spree::Api::V3::JwtAuthentication::JWT_AUDIENCE_SELLER),
|
|
139
|
-
user: Spree.api.
|
|
139
|
+
user: Spree.api.seller_account_serializer.new(user, params: { store: user.sellers.first&.store }).to_h,
|
|
140
140
|
sellers: serialized_sellers(user)
|
|
141
141
|
}
|
|
142
142
|
end
|
|
@@ -94,7 +94,10 @@ module Spree
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def authenticate_existing(user)
|
|
97
|
-
|
|
97
|
+
# Under the login lockout: otherwise this is an unthrottled way to
|
|
98
|
+
# guess an existing account's password with an invitation token.
|
|
99
|
+
verdict = Spree::Authentication::Lockout.check(user) { user.valid_password?(params[:password].to_s) }
|
|
100
|
+
return user if verdict == :valid
|
|
98
101
|
|
|
99
102
|
render_error(
|
|
100
103
|
code: ErrorHandler::ERROR_CODES[:authentication_failed],
|
|
@@ -127,7 +130,7 @@ module Spree
|
|
|
127
130
|
def auth_response(user)
|
|
128
131
|
{
|
|
129
132
|
token: generate_jwt(user, audience: Spree::Api::V3::JwtAuthentication::JWT_AUDIENCE_SELLER),
|
|
130
|
-
user: Spree.api.
|
|
133
|
+
user: Spree.api.seller_account_serializer.new(
|
|
131
134
|
user, params: { store: @invitation.store }
|
|
132
135
|
).to_h,
|
|
133
136
|
sellers: serialized_sellers(user)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Seller
|
|
5
|
+
module Invitations
|
|
6
|
+
# The acceptance link of a pending invitation onto this seller's team.
|
|
7
|
+
#
|
|
8
|
+
# The link carries the token that joins the team, so it is authorized
|
|
9
|
+
# like inviting someone: write access to the seller profile. Rooted at
|
|
10
|
+
# `current_seller.invitations`, so another seller's invitation reads
|
|
11
|
+
# as missing.
|
|
12
|
+
class AcceptanceLinksController < Seller::BaseController
|
|
13
|
+
scoped_resource :seller_profile
|
|
14
|
+
|
|
15
|
+
# GET /api/v3/seller/invitations/:invitation_id/acceptance_link
|
|
16
|
+
def show
|
|
17
|
+
invitation = current_seller.invitations.pending.find_by_prefix_id!(params[:invitation_id])
|
|
18
|
+
|
|
19
|
+
render json: Spree.api.seller_invitation_acceptance_link_serializer.new(
|
|
20
|
+
invitation, params: { store: current_store }
|
|
21
|
+
).to_h
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
def read_actions
|
|
27
|
+
[]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|