spree_api 6.0.0.rc5 → 6.0.0.rc6
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/auth_cookies.rb +23 -0
- data/app/controllers/concerns/spree/api/v3/admin/refund_authorization.rb +26 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +1 -0
- data/app/controllers/concerns/spree/api/v3/api_key_authentication.rb +14 -2
- data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +15 -2
- data/app/controllers/concerns/spree/api/v3/digital_assets_resource.rb +68 -0
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +3 -0
- data/app/controllers/concerns/spree/api/v3/jwt_authentication.rb +27 -1
- data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +6 -1
- data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +4 -1
- data/app/controllers/spree/api/v3/admin/auth_controller.rb +32 -5
- data/app/controllers/spree/api/v3/admin/commission_lines_controller.rb +2 -2
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +4 -7
- data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +4 -0
- data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +13 -8
- data/app/controllers/spree/api/v3/admin/delivery_profiles/origin_groups_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/delivery_profiles_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/option_types_controller.rb +13 -0
- data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +9 -0
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +7 -4
- data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +10 -2
- data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +7 -1
- data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +18 -0
- data/app/controllers/spree/api/v3/admin/product_types_controller.rb +12 -1
- data/app/controllers/spree/api/v3/admin/products/digital_assets_controller.rb +5 -109
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +4 -1
- data/app/controllers/spree/api/v3/admin/products_controller.rb +28 -3
- data/app/controllers/spree/api/v3/admin/sellers/payouts_controller.rb +4 -3
- data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +3 -2
- data/app/controllers/spree/api/v3/admin/store_controller.rb +7 -0
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +11 -9
- data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +5 -2
- data/app/controllers/spree/api/v3/resource_controller.rb +15 -2
- data/app/controllers/spree/api/v3/seller/auth_controller.rb +10 -0
- data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +2 -0
- data/app/controllers/spree/api/v3/seller/me_controller.rb +4 -0
- data/app/controllers/spree/api/v3/seller/orders_controller.rb +11 -3
- data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +5 -1
- data/app/controllers/spree/api/v3/seller/products/digital_assets_controller.rb +67 -0
- data/app/controllers/spree/api/v3/seller/products_controller.rb +4 -2
- data/app/controllers/spree/api/v3/seller/team_controller.rb +1 -1
- data/app/controllers/spree/api/v3/store/carts/store_credits_controller.rb +9 -1
- data/app/controllers/spree/api/v3/store/carts_controller.rb +14 -0
- data/app/controllers/spree/api/v3/store/customers_controller.rb +13 -1
- data/app/controllers/spree/api/v3/store/delivery_methods_controller.rb +6 -1
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +19 -4
- data/app/request_schemas/spree/api/v3/digital_asset_request_schema.rb +25 -0
- data/app/serializers/concerns/spree/api/v3/digital_asset_details.rb +70 -0
- data/app/serializers/spree/api/v3/admin/delivery_origin_group_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/delivery_profile_serializer.rb +3 -5
- data/app/serializers/spree/api/v3/admin/digital_asset_serializer.rb +3 -47
- data/app/serializers/spree/api/v3/admin/newsletter_subscriber_serializer.rb +1 -0
- data/app/serializers/spree/api/v3/admin/price_history_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +8 -3
- data/app/serializers/spree/api/v3/admin/price_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_payout_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/seller_serializer.rb +1 -3
- data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +3 -5
- data/app/serializers/spree/api/v3/base_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/newsletter_subscriber_serializer.rb +5 -1
- data/app/serializers/spree/api/v3/seller/digital_asset_serializer.rb +16 -0
- data/app/serializers/spree/api/v3/seller/payout_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/store_serializer.rb +18 -0
- data/app/services/spree/api/v3/filters_aggregator.rb +63 -18
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +3 -0
- data/lib/spree/api/dependencies.rb +2 -2
- data/lib/spree/api/engine.rb +6 -0
- data/lib/spree/api/middleware/no_index.rb +33 -0
- data/lib/spree/api/openapi/schema_helper.rb +2 -1
- data/lib/spree/api/testing_support/v3/base.rb +18 -0
- metadata +14 -6
|
@@ -4,6 +4,7 @@ module Spree
|
|
|
4
4
|
module Admin
|
|
5
5
|
class OrdersController < ResourceController
|
|
6
6
|
include Spree::Api::V3::OrderLock
|
|
7
|
+
include Spree::Api::V3::Admin::RefundAuthorization
|
|
7
8
|
|
|
8
9
|
scoped_resource :orders
|
|
9
10
|
|
|
@@ -86,6 +87,7 @@ module Spree
|
|
|
86
87
|
|
|
87
88
|
skip_before_action :set_resource, only: [:index, :create]
|
|
88
89
|
before_action :set_resource, only: [:show, :update, :destroy, :complete, :cancel, :approve, :resend_confirmation, :resend_digital_links, :po_document]
|
|
90
|
+
before_action :require_refund_permission!, only: :cancel, if: -> { params[:refund_payments].to_b }
|
|
89
91
|
|
|
90
92
|
# POST /api/v3/admin/orders
|
|
91
93
|
def create
|
|
@@ -231,7 +233,9 @@ module Spree
|
|
|
231
233
|
# `fields` list names an order's attributes, and narrowing a group by
|
|
232
234
|
# it can drop the very keys that say it is one.
|
|
233
235
|
def serialize_completion(record)
|
|
234
|
-
|
|
236
|
+
# Read back lazily preloading, as #show is, so its fulfillments and
|
|
237
|
+
# their rows load once for all of them rather than per fulfillment.
|
|
238
|
+
return serialize_resource(Spree::Order.preload_associations_lazily.find(record.id)) unless record.is_a?(Spree::OrderGroup)
|
|
235
239
|
|
|
236
240
|
Spree.api.admin_order_group_serializer.new(completed_group(record), params: serializer_params).to_h
|
|
237
241
|
end
|
|
@@ -243,7 +247,7 @@ module Spree
|
|
|
243
247
|
:customer, :ship_address, :bill_address,
|
|
244
248
|
{ payments: %i[payment_method source] },
|
|
245
249
|
{ orders: [:seller, :market, :gift_card, :customer, { line_items: { variant: :prices } }] }
|
|
246
|
-
).find(group.id)
|
|
250
|
+
).preload_associations_lazily.find(group.id)
|
|
247
251
|
end
|
|
248
252
|
|
|
249
253
|
# Override scope — Order uses SingleStoreResource (for_store).
|
|
@@ -298,8 +302,12 @@ module Spree
|
|
|
298
302
|
# are loaded with their selected rate because the freight summary
|
|
299
303
|
# reads it — otherwise a page of orders costs a query per
|
|
300
304
|
# fulfillment for a field that is nil on every parcel order.
|
|
305
|
+
# Both addresses are named because they are two associations on
|
|
306
|
+
# one table, so a row that reads them separately costs a lookup
|
|
307
|
+
# each.
|
|
301
308
|
[:customer, :channel, :seller, :external_references, :cancel_reason,
|
|
302
309
|
:market, { fulfillments: %i[selected_delivery_rate external_references] },
|
|
310
|
+
:bill_address, :ship_address,
|
|
303
311
|
{ line_items: [:external_references, { variant: :prices }, { price_list: :catalog }] },
|
|
304
312
|
{ po_document_attachment: :blob }]
|
|
305
313
|
end
|
|
@@ -36,7 +36,9 @@ module Spree
|
|
|
36
36
|
def create
|
|
37
37
|
user = Spree.admin_user_class.find_by(email: params[:email])
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
# A deactivated account is treated as no account: no email is sent,
|
|
40
|
+
# and the response is the same 202.
|
|
41
|
+
if user && !account_deactivated?(user)
|
|
40
42
|
event_name = 'admin_user.password_reset_requested'
|
|
41
43
|
payload = user.event_payload_for(
|
|
42
44
|
event_name,
|
|
@@ -62,6 +64,10 @@ module Spree
|
|
|
62
64
|
)
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
# A link sent before the account was switched off must not change
|
|
68
|
+
# its password or sign it in.
|
|
69
|
+
return render_account_deactivated if account_deactivated?(user)
|
|
70
|
+
|
|
65
71
|
if user.update(password: params[:password], password_confirmation: params[:password_confirmation])
|
|
66
72
|
user.publish_event('admin_user.password_reset')
|
|
67
73
|
# A password reset must kill every existing session — a stolen
|
|
@@ -123,8 +123,26 @@ module Spree
|
|
|
123
123
|
[:price_adjustment_tiers]
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
def serializer_params
|
|
127
|
+
return super unless action_name == 'index'
|
|
128
|
+
|
|
129
|
+
super.merge(page_counts)
|
|
130
|
+
end
|
|
131
|
+
|
|
126
132
|
private
|
|
127
133
|
|
|
134
|
+
# One grouped count per figure for the whole page rather than two
|
|
135
|
+
# queries per list.
|
|
136
|
+
def page_counts
|
|
137
|
+
@page_counts ||= begin
|
|
138
|
+
price_list_ids = collection.map(&:id)
|
|
139
|
+
{
|
|
140
|
+
products_counts: Spree::PriceList.products_count_by_list(price_list_ids),
|
|
141
|
+
prices_counts: Spree::PriceList.prices_count_by_list(price_list_ids)
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
128
146
|
# Loads the record without the action-derived authorization
|
|
129
147
|
# `set_resource` runs (which would check `:activate` /
|
|
130
148
|
# `:deactivate` — actions that abilities don't grant). The
|
|
@@ -76,9 +76,21 @@ module Spree
|
|
|
76
76
|
def permitted_params
|
|
77
77
|
attrs = params.permit(*permitted_attributes)
|
|
78
78
|
attrs = scope_delivery_profile(attrs)
|
|
79
|
+
attrs = scope_option_type_ids(attrs)
|
|
79
80
|
scope_category_ids(attrs)
|
|
80
81
|
end
|
|
81
82
|
|
|
83
|
+
# `option_type_ids=` resolves prefixed ids with no store scoping, so a
|
|
84
|
+
# type could otherwise seed another store's option types onto every
|
|
85
|
+
# product it is applied to.
|
|
86
|
+
def scope_option_type_ids(attrs)
|
|
87
|
+
return attrs if attrs[:option_type_ids].blank?
|
|
88
|
+
|
|
89
|
+
option_type_ids = decode_ids(attrs[:option_type_ids], Spree::OptionType)
|
|
90
|
+
attrs[:option_type_ids] = Spree::OptionType.for_store(current_store).where(id: option_type_ids).pluck(:id)
|
|
91
|
+
attrs
|
|
92
|
+
end
|
|
93
|
+
|
|
82
94
|
# The template profile must belong to this store — a cross-store id
|
|
83
95
|
# 404s instead of silently linking.
|
|
84
96
|
def scope_delivery_profile(attrs)
|
|
@@ -93,7 +105,6 @@ module Spree
|
|
|
93
105
|
# `category_ids=` resolves prefixed ids with no store scoping, so a
|
|
94
106
|
# type in this store could otherwise be pointed at another store's
|
|
95
107
|
# categories. Mirrors ProductsController#apply_categories.
|
|
96
|
-
# (OptionType is global — nothing to scope there.)
|
|
97
108
|
def scope_category_ids(attrs)
|
|
98
109
|
return attrs if attrs[:category_ids].blank?
|
|
99
110
|
|
|
@@ -4,61 +4,19 @@ module Spree
|
|
|
4
4
|
module Admin
|
|
5
5
|
module Products
|
|
6
6
|
class DigitalAssetsController < ResourceController
|
|
7
|
+
include Spree::Api::V3::DigitalAssetsResource
|
|
8
|
+
|
|
7
9
|
scoped_resource :products
|
|
8
10
|
|
|
9
11
|
request_params :digital_asset do |body|
|
|
10
|
-
|
|
11
|
-
# separately (+attach_uploaded_file+).
|
|
12
|
-
body.attribute :signed_id, :string,
|
|
13
|
-
permit: false,
|
|
14
|
-
description: "A `signed_id` from `files.upload()` with `visibility: 'private'`. Required for an uploaded file, omitted for a provider-backed asset. On update it replaces the file for everyone who already bought it."
|
|
15
|
-
# Resolved through the parent rather than assigned, so an asset
|
|
16
|
-
# can't be pointed at a variant of another product or store.
|
|
17
|
-
body.attribute :variant_id, :id,
|
|
18
|
-
permit: false,
|
|
19
|
-
description: "Defaults to the product's default variant when omitted."
|
|
12
|
+
body.include_attributes Spree::Api::V3::DigitalAssetRequestSchema
|
|
20
13
|
# The model validates it names a registered provider.
|
|
21
14
|
body.attribute :provider_type, :string,
|
|
22
|
-
description: 'A provider shorthand from the providers endpoint. Omit for an uploaded file
|
|
15
|
+
description: 'A provider shorthand from the providers endpoint. Omit for an uploaded file; a provider-backed asset sends no `signed_id`.'
|
|
23
16
|
# Its keys are declared by the provider, so it lands verbatim
|
|
24
17
|
# under one key in the asset's metadata.
|
|
25
18
|
body.attribute :provider_settings, :hash,
|
|
26
19
|
description: "Values for the provider's settings schema, keyed by field name."
|
|
27
|
-
body.attribute :authorized_clicks, :integer,
|
|
28
|
-
nullable: true,
|
|
29
|
-
description: "Null means the store's download settings apply."
|
|
30
|
-
body.attribute :authorized_days, :integer,
|
|
31
|
-
nullable: true,
|
|
32
|
-
description: "Null means the store's download settings apply."
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# PATCH /api/v3/admin/products/:product_id/digital_assets/:id
|
|
36
|
-
#
|
|
37
|
-
# Replacing the file keeps every already-issued link working, since
|
|
38
|
-
# links resolve through the asset rather than the blob.
|
|
39
|
-
def update
|
|
40
|
-
return render_private_storage_error unless uploaded_blob_private?
|
|
41
|
-
|
|
42
|
-
# The asset already exists, so attaching a replacement blob writes
|
|
43
|
-
# to the database immediately, while the update below renders a 422
|
|
44
|
-
# without raising when it fails. Wrap both so a failed update rolls
|
|
45
|
-
# the new attachment back rather than leaving the file swapped but
|
|
46
|
-
# the record unchanged.
|
|
47
|
-
model_class.transaction do
|
|
48
|
-
@resource.variant = requested_variant if params[:variant_id].present?
|
|
49
|
-
attach_uploaded_file(@resource)
|
|
50
|
-
|
|
51
|
-
super
|
|
52
|
-
|
|
53
|
-
raise ActiveRecord::Rollback if @resource.errors.any?
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# POST /api/v3/admin/products/:product_id/digital_assets
|
|
58
|
-
def create
|
|
59
|
-
return render_private_storage_error unless uploaded_blob_private?
|
|
60
|
-
|
|
61
|
-
super
|
|
62
20
|
end
|
|
63
21
|
|
|
64
22
|
# GET /api/v3/admin/products/:product_id/digital_assets/providers
|
|
@@ -83,75 +41,13 @@ module Spree
|
|
|
83
41
|
|
|
84
42
|
protected
|
|
85
43
|
|
|
86
|
-
def model_class
|
|
87
|
-
Spree::DigitalAsset
|
|
88
|
-
end
|
|
89
|
-
|
|
90
44
|
def serializer_class
|
|
91
45
|
Spree.api.admin_digital_asset_serializer
|
|
92
46
|
end
|
|
93
47
|
|
|
94
48
|
def set_parent
|
|
95
49
|
@parent = current_store.products.find_by_prefix_id!(params[:product_id])
|
|
96
|
-
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# The parent association reaches assets through `variants`, which
|
|
100
|
-
# carries an ORDER BY on the variants table. Combined with the
|
|
101
|
-
# collection's DISTINCT, PostgreSQL rejects the query outright
|
|
102
|
-
# (ordering columns must appear in the select list), so order by
|
|
103
|
-
# the assets' own table instead. SQLite accepts either form, so
|
|
104
|
-
# this only fails on a real deployment.
|
|
105
|
-
def scope
|
|
106
|
-
super.reorder(created_at: :asc)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def scope_includes
|
|
110
|
-
[:variant, { attachment_attachment: :blob }]
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def permitted_params
|
|
114
|
-
params.permit(*declared_permitted_attributes)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
private
|
|
118
|
-
|
|
119
|
-
# A product without explicit variants keeps its file on the default
|
|
120
|
-
# variant, mirroring how the rest of the admin treats such products.
|
|
121
|
-
def build_resource
|
|
122
|
-
digital_asset = super
|
|
123
|
-
digital_asset.variant = requested_variant || @parent.default_variant
|
|
124
|
-
attach_uploaded_file(digital_asset)
|
|
125
|
-
digital_asset
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# Scoped to the parent, not merely to the store: an asset reached
|
|
129
|
-
# through a product's route has no business pointing at another
|
|
130
|
-
# product's variant. An id from anywhere else is a 404.
|
|
131
|
-
def requested_variant
|
|
132
|
-
return if params[:variant_id].blank?
|
|
133
|
-
|
|
134
|
-
@parent.variants.find_by_prefix_id!(params[:variant_id])
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def attach_uploaded_file(digital_asset)
|
|
138
|
-
return if params[:signed_id].blank?
|
|
139
|
-
|
|
140
|
-
digital_asset.attachment.attach(params[:signed_id])
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Attaching never moves a blob between storage services, so a file
|
|
144
|
-
# uploaded to the public bucket would stay publicly readable while
|
|
145
|
-
# looking attached. Refuse it rather than storing it wrongly.
|
|
146
|
-
def uploaded_blob_private?
|
|
147
|
-
return true if params[:signed_id].blank?
|
|
148
|
-
|
|
149
|
-
blob = ActiveStorage::Blob.find_signed(params[:signed_id])
|
|
150
|
-
blob.present? && blob.service_name.to_s == Spree.private_storage_service_name.to_s
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def render_private_storage_error
|
|
154
|
-
render_validation_error(I18n.t('spree.digital_assets_attachment_must_be_private'))
|
|
50
|
+
authorize_parent!(@parent)
|
|
155
51
|
end
|
|
156
52
|
end
|
|
157
53
|
end
|
|
@@ -28,8 +28,11 @@ module Spree
|
|
|
28
28
|
authorize!(:show, @parent)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# `active_stock_reservations` is what lets the quantifier count
|
|
32
|
+
# held stock in memory; without it the serializer's availability
|
|
33
|
+
# column sums reservations once per variant.
|
|
31
34
|
def scope_includes
|
|
32
|
-
[:prices, stock_levels: :stock_location]
|
|
35
|
+
[:prices, { stock_levels: [:stock_location, :active_stock_reservations] }]
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def create_workflow
|
|
@@ -219,7 +219,7 @@ module Spree
|
|
|
219
219
|
|
|
220
220
|
channels = scoped_channels
|
|
221
221
|
product_ids = bulk_collection.distinct.ids
|
|
222
|
-
channels.
|
|
222
|
+
channels.add_products(product_ids)
|
|
223
223
|
|
|
224
224
|
render json: { product_count: product_ids.size, channel_count: channels.size }
|
|
225
225
|
end
|
|
@@ -231,7 +231,7 @@ module Spree
|
|
|
231
231
|
|
|
232
232
|
channels = scoped_channels
|
|
233
233
|
product_ids = bulk_collection.distinct.ids
|
|
234
|
-
removed = channels.
|
|
234
|
+
removed = channels.remove_products(product_ids)
|
|
235
235
|
|
|
236
236
|
render json: { product_count: product_ids.size, channel_count: channels.size, removed: removed }
|
|
237
237
|
end
|
|
@@ -293,6 +293,14 @@ module Spree
|
|
|
293
293
|
{ media: Spree::Media, variants: Spree::Variant }
|
|
294
294
|
end
|
|
295
295
|
|
|
296
|
+
# A written product is serialized from a fresh read: lazy preloading
|
|
297
|
+
# does not reach records a write built, so the instance the workflow
|
|
298
|
+
# returned would query once per variant.
|
|
299
|
+
def serialize_resource(resource)
|
|
300
|
+
resource = scope.where(id: resource.id).preload_associations_lazily.take! if resource.lazy_preload_context.nil?
|
|
301
|
+
super
|
|
302
|
+
end
|
|
303
|
+
|
|
296
304
|
# Use SearchProvider::Database for collection to handle price/best_selling
|
|
297
305
|
# sorting correctly (counts before sorting, avoiding PG/Mobility issues).
|
|
298
306
|
def collection
|
|
@@ -323,7 +331,24 @@ module Spree
|
|
|
323
331
|
end
|
|
324
332
|
|
|
325
333
|
def permitted_params
|
|
326
|
-
params.permit(*permitted_attributes).tap
|
|
334
|
+
params.permit(*permitted_attributes).tap do |attrs|
|
|
335
|
+
attrs.delete(:status) if review_status?(attrs[:status])
|
|
336
|
+
scope_store_references(attrs)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Read through the store here because the model only checks that the
|
|
341
|
+
# record exists: another store's type or tax category would otherwise
|
|
342
|
+
# be accepted, and the type would seed its option types onto the
|
|
343
|
+
# product.
|
|
344
|
+
STORE_REFERENCES = { product_type_id: :product_types, tax_category_id: :tax_categories }.freeze
|
|
345
|
+
|
|
346
|
+
def scope_store_references(attrs)
|
|
347
|
+
STORE_REFERENCES.each do |key, association|
|
|
348
|
+
next if attrs[key].blank?
|
|
349
|
+
|
|
350
|
+
attrs[key] = current_store.public_send(association).find_by_param!(attrs[key]).id
|
|
351
|
+
end
|
|
327
352
|
end
|
|
328
353
|
|
|
329
354
|
# A review status is an outcome, not a value to assign: `proposed`
|
|
@@ -30,7 +30,8 @@ module Spree
|
|
|
30
30
|
# because another went through: the payout row exists and is
|
|
31
31
|
# failed or unresolved, and an operator reading "settled" would
|
|
32
32
|
# not know to go looking for it.
|
|
33
|
-
|
|
33
|
+
transfer_counts = Spree::SellerTransfer.where(payout_id: settled.map(&:id)).group(:payout_id).count
|
|
34
|
+
body = { data: settled.map { |payout| serialize_payout(payout, transfer_counts) } }
|
|
34
35
|
body[:meta] = { failures: failures } if failures.any?
|
|
35
36
|
|
|
36
37
|
render json: body, status: :created
|
|
@@ -96,8 +97,8 @@ module Spree
|
|
|
96
97
|
@seller.payable_currencies
|
|
97
98
|
end
|
|
98
99
|
|
|
99
|
-
def serialize_payout(payout)
|
|
100
|
-
Spree.api.admin_seller_payout_serializer.new(payout, params: { store: current_store }).to_h
|
|
100
|
+
def serialize_payout(payout, transfer_counts)
|
|
101
|
+
Spree.api.admin_seller_payout_serializer.new(payout, params: { store: current_store, transfer_counts: transfer_counts }).to_h
|
|
101
102
|
end
|
|
102
103
|
end
|
|
103
104
|
end
|
|
@@ -202,9 +202,10 @@ module Spree
|
|
|
202
202
|
end
|
|
203
203
|
|
|
204
204
|
# The listing and the detail page both read a transfer's totals off
|
|
205
|
-
# its lines.
|
|
205
|
+
# its lines. Both warehouses come from one table, so their external
|
|
206
|
+
# references are loaded here rather than once per warehouse.
|
|
206
207
|
def scope_includes
|
|
207
|
-
[line_includes]
|
|
208
|
+
[{ source_location: :external_references, destination_location: :external_references }, line_includes]
|
|
208
209
|
end
|
|
209
210
|
end
|
|
210
211
|
end
|
|
@@ -65,6 +65,13 @@ module Spree
|
|
|
65
65
|
description: 'Admits a seller as soon as they finish onboarding, with no operator review.'
|
|
66
66
|
body.attribute :auto_approve_seller_products, :boolean,
|
|
67
67
|
description: 'Puts a seller product on sale the moment it is submitted, with no review.'
|
|
68
|
+
body.attribute :seller_digital_assets_enabled, :boolean,
|
|
69
|
+
description: 'Lets sellers attach downloadable files to their own products.'
|
|
70
|
+
body.attribute :seller_digital_asset_content_types, :array, of: :string,
|
|
71
|
+
example: %w[application/pdf application/zip],
|
|
72
|
+
description: "The content types a seller's file may have, checked against the file's own signature where it has one. An entry names one type or, with `*` as the subtype, a whole family. The operator's own uploads are not restricted."
|
|
73
|
+
body.attribute :review_seller_digital_asset_changes, :boolean,
|
|
74
|
+
description: 'Sends a seller product already on sale back to review when the seller adds or replaces a file on it.'
|
|
68
75
|
body.attribute :send_seller_transactional_emails, :boolean,
|
|
69
76
|
description: 'When false, transactional seller emails (approval, payouts and so on) are not sent.'
|
|
70
77
|
body.attribute :default_commission_tax_rate, :decimal,
|
|
@@ -63,22 +63,24 @@ module Spree
|
|
|
63
63
|
false
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
# Tagging filter.
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
# taggables like the customer/user type carry no store, so their tags
|
|
71
|
-
# are not store-scoped here.
|
|
66
|
+
# Tagging filter. A taggable declaring `acts_as_taggable_tenant :store_id`
|
|
67
|
+
# stores the owning store on each tagging, so its vocabulary is bounded
|
|
68
|
+
# to the current store with a single indexed `tenant` filter. Taggables
|
|
69
|
+
# without a store tenant are not store-scoped here.
|
|
72
70
|
def taggings_conditions
|
|
73
71
|
conditions = { taggable_type: taggable_type, context: 'tags' }
|
|
74
72
|
conditions[:tenant] = current_store.id.to_s if store_tenanted_taggable?
|
|
75
73
|
conditions
|
|
76
74
|
end
|
|
77
75
|
|
|
78
|
-
# True for taggables whose taggings carry a store `tenant`
|
|
79
|
-
# (`acts_as_taggable_tenant :store_id`), i.e. store-owned records.
|
|
80
76
|
def store_tenanted_taggable?
|
|
81
|
-
|
|
77
|
+
taggable_class.try(:tenant_column).to_s == 'store_id'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# `taggable_type` is already matched against `Spree.taggable_types`,
|
|
81
|
+
# so only a registered class name is ever constantized.
|
|
82
|
+
def taggable_class
|
|
83
|
+
taggable_type.constantize
|
|
82
84
|
end
|
|
83
85
|
|
|
84
86
|
# Per-type scope check for API-key principals: a key listing product
|
|
@@ -21,7 +21,7 @@ module Spree
|
|
|
21
21
|
description: 'Defaults to true on create. Prefer the disable and enable endpoints to pause one.'
|
|
22
22
|
body.attribute :subscriptions, :array, of: :string,
|
|
23
23
|
example: %w[order.placed order.canceled],
|
|
24
|
-
description: 'Event names this endpoint receives. An empty list or `["*"]` subscribes to every event; otherwise list exact names (`order.completed`) or patterns (`order
|
|
24
|
+
description: 'Event names this endpoint receives. An empty list or `["*"]` subscribes to every event; otherwise list exact names (`order.completed`) or patterns with `*` in place of a segment (`order.*`, `*.created`). At most 200 entries of up to 100 characters.'
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
request_params 'WebhookEndpointDisableParams', action: :disable do |body|
|
|
@@ -112,8 +112,11 @@ module Spree
|
|
|
112
112
|
)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# Checked after the model's normalization, so what the gate sees is
|
|
116
|
+
# exactly what would be stored.
|
|
115
117
|
def requested_credential_events
|
|
116
|
-
Spree::
|
|
118
|
+
subscriptions = Spree::WebhookEndpoint.normalize_value_for(:subscriptions, permitted_params[:subscriptions])
|
|
119
|
+
Spree::Events.catalog.credential_events(subscriptions)
|
|
117
120
|
end
|
|
118
121
|
|
|
119
122
|
def repointed_credential_events
|
|
@@ -248,8 +248,11 @@ module Spree
|
|
|
248
248
|
end
|
|
249
249
|
|
|
250
250
|
# Finds a single resource within scope using prefixed ID
|
|
251
|
+
# Loaded through the lazy-preload relation, as the collection is, so
|
|
252
|
+
# the serializer's walk over the record's children batches their
|
|
253
|
+
# associations instead of querying once per child.
|
|
251
254
|
def find_resource
|
|
252
|
-
scope.find_by_prefix_id!(params[:id])
|
|
255
|
+
scope.preload_associations_lazily.find_by_prefix_id!(params[:id])
|
|
253
256
|
end
|
|
254
257
|
|
|
255
258
|
# Per-record authorization hook. A no-op in the shared base: the Store
|
|
@@ -481,13 +484,23 @@ module Spree
|
|
|
481
484
|
base_scope = if @parent.present?
|
|
482
485
|
@parent.send(parent_association)
|
|
483
486
|
else
|
|
484
|
-
|
|
487
|
+
store_scope
|
|
485
488
|
end
|
|
486
489
|
base_scope = base_scope.includes(scope_includes) if scope_includes.any?
|
|
487
490
|
base_scope = base_scope.preload_associations_lazily
|
|
488
491
|
model_class.include?(Spree::TranslatableResource) ? base_scope.i18n : base_scope
|
|
489
492
|
end
|
|
490
493
|
|
|
494
|
+
# The store's rows of this model when the resource has no parent. A
|
|
495
|
+
# model the store reaches through another record rather than an
|
|
496
|
+
# association of its own — a commission line through its order —
|
|
497
|
+
# names that path here, since `for_store` answers nothing for it.
|
|
498
|
+
#
|
|
499
|
+
# @return [ActiveRecord::Relation]
|
|
500
|
+
def store_scope
|
|
501
|
+
model_class.for_store(current_store)
|
|
502
|
+
end
|
|
503
|
+
|
|
491
504
|
# Action names treated as reads. Override in subclasses with custom
|
|
492
505
|
# read-only member/collection actions (e.g. add `analytics`, `types`)
|
|
493
506
|
# so they map to the `:show` ability instead of a write.
|
|
@@ -52,6 +52,8 @@ module Spree
|
|
|
52
52
|
return render_authentication_failed(result.error) unless result.success?
|
|
53
53
|
|
|
54
54
|
user = result.value
|
|
55
|
+
return render_account_deactivated if account_deactivated?(user)
|
|
56
|
+
|
|
55
57
|
# A store's own staff share this user class, so authenticating is
|
|
56
58
|
# not enough: without a role on some seller there is no panel to
|
|
57
59
|
# sign in to, and minting a token would hand out an audience the
|
|
@@ -101,6 +103,14 @@ module Spree
|
|
|
101
103
|
end
|
|
102
104
|
|
|
103
105
|
user = refresh_token.user
|
|
106
|
+
# Deactivating deletes the account's refresh tokens; this catches a
|
|
107
|
+
# refresh already under way when that happened.
|
|
108
|
+
if account_deactivated?(user)
|
|
109
|
+
refresh_token.destroy
|
|
110
|
+
clear_refresh_cookie
|
|
111
|
+
return render_account_deactivated
|
|
112
|
+
end
|
|
113
|
+
|
|
104
114
|
# Membership can be revoked while a session is live; the refresh is
|
|
105
115
|
# where that takes effect.
|
|
106
116
|
unless user.seller_member?
|
|
@@ -46,6 +46,8 @@ module Spree
|
|
|
46
46
|
|
|
47
47
|
user = resolve_or_create_invitee(@invitation)
|
|
48
48
|
return if performed?
|
|
49
|
+
# Checked before accepting: a deactivated account gains no role.
|
|
50
|
+
return render_account_deactivated if account_deactivated?(user)
|
|
49
51
|
|
|
50
52
|
@invitation.invitee = user
|
|
51
53
|
result = Spree.invitation_accept_workflow.call(invitation: @invitation)
|
|
@@ -58,6 +58,10 @@ module Spree
|
|
|
58
58
|
{
|
|
59
59
|
user: Spree.api.seller_account_serializer.new(current_user, params: serializer_params).to_h,
|
|
60
60
|
sellers: serialized_sellers,
|
|
61
|
+
# The marketplace's own settings that shape what the panel lets a
|
|
62
|
+
# seller do, read before any seller is chosen since there is one
|
|
63
|
+
# store per installation.
|
|
64
|
+
store: Spree.api.seller_store_serializer.new(serializer_params[:store], params: serializer_params).to_h,
|
|
61
65
|
# Both shapes, exactly as the admin `/me`: the panel's `<Can>`
|
|
62
66
|
# reads CanCanCan rules, the key gate reads keys. Sending only
|
|
63
67
|
# keys left `<Can>` answering false for everything on the seller
|
|
@@ -84,7 +84,7 @@ module Spree
|
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
if result.success?
|
|
87
|
-
render json: serialize_resource(
|
|
87
|
+
render json: serialize_resource(reloaded_resource)
|
|
88
88
|
else
|
|
89
89
|
render_service_error(@resource.errors.presence || result.error)
|
|
90
90
|
end
|
|
@@ -115,7 +115,7 @@ module Spree
|
|
|
115
115
|
next render_validation_error(invalid.errors) if invalid
|
|
116
116
|
|
|
117
117
|
if @resource.update(addresses)
|
|
118
|
-
render json: serialize_resource(
|
|
118
|
+
render json: serialize_resource(reloaded_resource)
|
|
119
119
|
else
|
|
120
120
|
render_validation_error(@resource.errors)
|
|
121
121
|
end
|
|
@@ -143,12 +143,20 @@ module Spree
|
|
|
143
143
|
super.not_drafts
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
+
# Both addresses are named because they are two associations on one
|
|
147
|
+
# table, so a row that reads them separately costs a lookup each.
|
|
146
148
|
def collection_includes
|
|
147
|
-
[:line_items, :customer, :channel]
|
|
149
|
+
[:line_items, :customer, :channel, :bill_address, :ship_address]
|
|
148
150
|
end
|
|
149
151
|
|
|
150
152
|
private
|
|
151
153
|
|
|
154
|
+
# Read back as #show reads it, lazily preloading, so its
|
|
155
|
+
# fulfillments' rates and methods load once rather than per row.
|
|
156
|
+
def reloaded_resource
|
|
157
|
+
scope.preload_associations_lazily.find(@resource.id)
|
|
158
|
+
end
|
|
159
|
+
|
|
152
160
|
# Either address, as a correction rather than a replacement: the
|
|
153
161
|
# sent fields are laid over what the order already holds, so a
|
|
154
162
|
# request naming one line does not blank out the country and
|
|
@@ -43,7 +43,8 @@ module Spree
|
|
|
43
43
|
# admin who runs no seller has no panel to be sent to, and mailing
|
|
44
44
|
# them a seller reset link would be an invitation to a session they
|
|
45
45
|
# cannot hold.
|
|
46
|
-
|
|
46
|
+
# A deactivated account is treated as no account: no email is sent.
|
|
47
|
+
if user&.seller_member? && !account_deactivated?(user)
|
|
47
48
|
user.publish_event(RESET_REQUESTED_EVENT, event_payload(user))
|
|
48
49
|
end
|
|
49
50
|
|
|
@@ -60,6 +61,9 @@ module Spree
|
|
|
60
61
|
# it can be revoked between the email being sent and the link being
|
|
61
62
|
# opened, and signing in is what the success path does.
|
|
62
63
|
return render_token_invalid unless user&.seller_member?
|
|
64
|
+
# A link sent before the account was switched off must not change
|
|
65
|
+
# its password or sign it in.
|
|
66
|
+
return render_account_deactivated if account_deactivated?(user)
|
|
63
67
|
|
|
64
68
|
unless user.update(password: params[:password], password_confirmation: params[:password_confirmation])
|
|
65
69
|
return render_errors(user.errors)
|