spree_api 6.0.2 → 6.0.4
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 +5 -2
- data/app/controllers/concerns/spree/api/v3/idempotent.rb +10 -1
- data/app/controllers/concerns/spree/api/v3/security_headers.rb +3 -0
- data/app/controllers/spree/api/v3/admin/base_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/companies/memberships_controller.rb +21 -4
- data/app/controllers/spree/api/v3/admin/diagnostics_controller.rb +46 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +0 -13
- data/app/controllers/spree/api/v3/admin/media_controller.rb +10 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +6 -2
- data/app/controllers/spree/api/v3/admin/products_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/sellers/invitations_controller.rb +39 -3
- data/app/controllers/spree/api/v3/admin/sellers/team_controller.rb +1 -3
- data/app/controllers/spree/api/v3/admin/sellers_controller.rb +24 -1
- data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +15 -1
- data/app/controllers/spree/api/v3/admin/updates_controller.rb +4 -15
- data/app/controllers/spree/api/v3/seller/products/media_controller.rb +46 -0
- data/app/controllers/spree/api/v3/seller/products_controller.rb +4 -2
- data/app/controllers/spree/api/v3/seller/profile_controller.rb +13 -5
- data/app/controllers/spree/api/v3/store/carts_controller.rb +10 -7
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/company_membership_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/delivery_profile_serializer.rb +2 -1
- data/app/serializers/spree/api/v3/admin/diagnostics_serializer.rb +30 -0
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/import_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +3 -1
- data/app/serializers/spree/api/v3/admin/product_serializer.rb +8 -0
- data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +13 -0
- data/app/serializers/spree/api/v3/admin/return_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +3 -2
- data/app/serializers/spree/api/v3/admin/seller_serializer.rb +12 -2
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_category_serializer.rb +26 -0
- data/app/serializers/spree/api/v3/product_collection_serializer.rb +26 -0
- data/app/serializers/spree/api/v3/seller/import_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/order_line_item_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/product_serializer.rb +8 -0
- data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +3 -2
- data/config/initializers/typelizer.rb +8 -19
- data/config/routes.rb +8 -1
- data/lib/spree/api/admin_document.rb +284 -0
- data/lib/spree/api/agent_operations.rb +300 -0
- data/lib/spree/api/agent_resource_map.rb +74 -14
- data/lib/spree/api/agent_write_schemas.rb +15 -51
- data/lib/spree/api/dependencies.rb +1 -1
- data/lib/spree/api/engine.rb +14 -1
- data/lib/spree/api/enum_types.rb +4 -1
- data/lib/spree/api/openapi/schema_helper.rb +3 -3
- data/lib/spree/api/sdk_types.rb +190 -0
- data/lib/spree/api/sdk_types_task.rb +68 -0
- data/lib/spree/api/testing_support/agent_tools.rb +35 -0
- data/lib/spree/api/testing_support/agent_tools_contract.rb +110 -0
- data/lib/spree/api/testing_support/deterministic_openapi.rb +147 -0
- data/lib/spree/api/testing_support/filter_parameters.rb +99 -0
- data/lib/spree/api/testing_support/openapi.rb +233 -0
- data/lib/spree/api/testing_support/openapi_tasks.rb +21 -0
- data/lib/spree/api/testing_support/sdk_method.rb +34 -0
- data/lib/spree/api/typelizer_generation.rb +39 -11
- data/lib/tasks/openapi.rake +5 -0
- data/lib/tasks/sdk_types.rake +6 -0
- data/lib/typelizer/open_enums.rb +13 -1
- data/openapi/admin.yaml +2241 -318
- data/openapi/store.yaml +125 -125
- metadata +26 -9
- data/lib/spree/api/agent_workflow_endpoints.rb +0 -113
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5eaa75827fbded08b54001d86e1f068f30f011414f6b4530aab8db17830f5779
|
|
4
|
+
data.tar.gz: f50e306fd54087d3e3a05a97d599e63805fc56e4329dcb77839f940d0d127341
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e50bf307deb4e72c30d4f31ef0b70a65cfd225f799d2ffedc05e2e588a168869d81e6c60128353e9add061a4774da56da6a482a98ac32dd0456f32aa0f9aabd0
|
|
7
|
+
data.tar.gz: 9bce5f92eed950d2586ea278cfce9bd5797a67b13bffc7de5dc20811ca3ad219364c667cc67d384f034c3d98df4a5c576d3dff283f25ff71322aa7001d1456cd
|
|
@@ -13,7 +13,9 @@ module Spree
|
|
|
13
13
|
#
|
|
14
14
|
# Gift card codes are bearer credentials that ride along on every
|
|
15
15
|
# order, so serializers show them in full only to a caller who may read
|
|
16
|
-
# gift cards and masked to everyone else.
|
|
16
|
+
# gift cards and masked to everyone else. Customer records embedded in
|
|
17
|
+
# a rule (a promotion's or a price list's customer rule) are shown only
|
|
18
|
+
# to a caller who may read customers; the rule keeps its `customer_ids`.
|
|
17
19
|
module EmbedPermissions
|
|
18
20
|
extend ActiveSupport::Concern
|
|
19
21
|
|
|
@@ -44,7 +46,8 @@ module Spree
|
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
def serializer_params
|
|
47
|
-
super.merge(gift_card_codes: holds_permission?('read_gift_cards')
|
|
49
|
+
super.merge(gift_card_codes: holds_permission?('read_gift_cards'),
|
|
50
|
+
customer_records: holds_permission?('read_customers'))
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
private
|
|
@@ -94,8 +94,17 @@ module Spree
|
|
|
94
94
|
"spree:idempotency:#{digest}"
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# Binds replay to the method, path, body and exact query bytes.
|
|
98
|
+
# Separate fixed-length digests prevent query/body delimiter collisions;
|
|
99
|
+
# requests without a query retain their existing cached fingerprint.
|
|
100
|
+
#
|
|
101
|
+
# @return [String] SHA-256 request fingerprint
|
|
97
102
|
def request_fingerprint
|
|
98
|
-
Digest::SHA256.hexdigest("#{request.method}:#{request.path}:#{request.raw_post}")
|
|
103
|
+
fingerprint = Digest::SHA256.hexdigest("#{request.method}:#{request.path}:#{request.raw_post}")
|
|
104
|
+
return fingerprint if request.query_string.blank?
|
|
105
|
+
|
|
106
|
+
query_fingerprint = Digest::SHA256.hexdigest(request.query_string)
|
|
107
|
+
Digest::SHA256.hexdigest("#{fingerprint}:#{query_fingerprint}")
|
|
99
108
|
end
|
|
100
109
|
end
|
|
101
110
|
end
|
|
@@ -14,6 +14,9 @@ module Spree
|
|
|
14
14
|
response.headers['X-Content-Type-Options'] = 'nosniff'
|
|
15
15
|
response.headers['X-Frame-Options'] = 'DENY'
|
|
16
16
|
response.headers['X-Request-Id'] = request.request_id
|
|
17
|
+
# A dashboard on another origin reads the request ID for its
|
|
18
|
+
# developer inspector; browsers hide unlisted headers from scripts.
|
|
19
|
+
response.headers['Access-Control-Expose-Headers'] = 'X-Request-Id'
|
|
17
20
|
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
|
18
21
|
response.headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()'
|
|
19
22
|
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' if request.ssl?
|
|
@@ -15,6 +15,37 @@ module Spree
|
|
|
15
15
|
include Spree::Api::V3::StrictMoneyParams
|
|
16
16
|
include Spree::Api::V3::Admin::ValidationDetails
|
|
17
17
|
include Spree::Api::V3::Admin::EmbedPermissions
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# For endpoints about the signed-in staffer rather than the store
|
|
22
|
+
# (/me, /updates, /diagnostics): a secret API key has no user, so it
|
|
23
|
+
# gets a 404 rather than a 500 from a nil user.
|
|
24
|
+
def require_current_user!
|
|
25
|
+
return if current_user
|
|
26
|
+
|
|
27
|
+
render_error(
|
|
28
|
+
code: ERROR_CODES[:record_not_found],
|
|
29
|
+
message: I18n.t('spree.me_no_current_user'),
|
|
30
|
+
status: :not_found
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# For endpoints that reveal the running version and stack (/updates,
|
|
35
|
+
# /diagnostics): a staff member signed in to the dashboard and nobody
|
|
36
|
+
# else. A connected app acts as the admin who approved it, so it
|
|
37
|
+
# would pass every check written for that admin, whatever it was
|
|
38
|
+
# granted, and no scope stands for "fingerprint the deployment".
|
|
39
|
+
def require_staff_session!
|
|
40
|
+
return require_current_user! unless current_user
|
|
41
|
+
return if current_oauth_token.blank?
|
|
42
|
+
|
|
43
|
+
render_error(
|
|
44
|
+
code: ERROR_CODES[:access_denied],
|
|
45
|
+
message: 'This endpoint is for staff signed in to the dashboard, not for connected apps.',
|
|
46
|
+
status: :forbidden
|
|
47
|
+
)
|
|
48
|
+
end
|
|
18
49
|
end
|
|
19
50
|
end
|
|
20
51
|
end
|
|
@@ -14,18 +14,25 @@ module Spree
|
|
|
14
14
|
before_action :authorize_parent_access!
|
|
15
15
|
|
|
16
16
|
request_params :company_membership do |body|
|
|
17
|
+
body.attribute :customer_id, :string,
|
|
18
|
+
example: 'cus_UkLWZg9DAJ',
|
|
19
|
+
description: 'An existing customer, who becomes a member immediately. ' \
|
|
20
|
+
'Send this or `customer_email`.'
|
|
17
21
|
body.attribute :customer_email, :string,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
example: 'buyer@example.com',
|
|
23
|
+
description: 'An existing customer becomes a member immediately; an unknown ' \
|
|
24
|
+
'email becomes an invitation, and the response is an invitation ' \
|
|
25
|
+
'instead of a membership. Send this or `customer_id`.'
|
|
21
26
|
body.attribute :metadata, :hash
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
# POST /api/v3/admin/companies/:company_id/memberships
|
|
25
30
|
def create
|
|
31
|
+
customer = member_customer
|
|
26
32
|
result = Spree.company_add_member_service.call(
|
|
27
33
|
company: @parent,
|
|
28
|
-
|
|
34
|
+
customer: customer,
|
|
35
|
+
email: customer ? nil : params.require(:customer_email),
|
|
29
36
|
metadata: permitted_params[:metadata]
|
|
30
37
|
)
|
|
31
38
|
|
|
@@ -43,6 +50,16 @@ module Spree
|
|
|
43
50
|
|
|
44
51
|
protected
|
|
45
52
|
|
|
53
|
+
# Through the ability, like every customer lookup here: a customer
|
|
54
|
+
# this role cannot see 404s rather than joining the company.
|
|
55
|
+
def member_customer
|
|
56
|
+
return if params[:customer_id].blank?
|
|
57
|
+
|
|
58
|
+
Spree.customer_class.
|
|
59
|
+
accessible_by(current_ability, :show).
|
|
60
|
+
find_by_prefix_id!(params[:customer_id])
|
|
61
|
+
end
|
|
62
|
+
|
|
46
63
|
def model_class
|
|
47
64
|
Spree::CompanyMembership
|
|
48
65
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# The anonymised diagnostics report (Spree::Diagnostics) for bug
|
|
6
|
+
# reports. It reveals the running version and stack, so it answers
|
|
7
|
+
# signed-in staff who can manage the store and nobody else, like
|
|
8
|
+
# GET /updates: a secret API key gets a 404, a connected app a 403.
|
|
9
|
+
class DiagnosticsController < Admin::BaseController
|
|
10
|
+
skip_scope_check!
|
|
11
|
+
|
|
12
|
+
before_action :require_staff_session!
|
|
13
|
+
|
|
14
|
+
# Only a published @spree/* name and a version: anything else in
|
|
15
|
+
# the list is dropped rather than echoed into a public report.
|
|
16
|
+
PACKAGE = %r{\A(@spree/[a-z0-9-]{1,50})@(\d+\.\d+\.\d+[0-9A-Za-z.-]{0,20})\z}
|
|
17
|
+
PACKAGE_LIMIT = 20
|
|
18
|
+
|
|
19
|
+
# GET /api/v3/admin/diagnostics
|
|
20
|
+
def show
|
|
21
|
+
authorize! :manage, current_store
|
|
22
|
+
|
|
23
|
+
report = Spree::Diagnostics.report(store: current_store, extra_sections: dashboard_section + client_section)
|
|
24
|
+
render json: Spree.api.admin_diagnostics_serializer.new(report).to_h
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# The caller's @spree/* packages, which the server can't see: the
|
|
30
|
+
# dashboard may be built and deployed apart from it.
|
|
31
|
+
def dashboard_section
|
|
32
|
+
rows = params[:packages].to_s.split(',').first(PACKAGE_LIMIT).filter_map { |package| PACKAGE.match(package)&.captures }
|
|
33
|
+
rows.any? ? [{ title: 'Dashboard', rows: rows.to_h }] : []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The browser the report was opened in, which only this request knows.
|
|
37
|
+
def client_section
|
|
38
|
+
return [] if request.user_agent.blank?
|
|
39
|
+
|
|
40
|
+
[{ title: 'Client', rows: { 'User agent' => request.user_agent } }]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -70,19 +70,6 @@ module Spree
|
|
|
70
70
|
)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
# A request authenticated by a secret API key has no Spree user to
|
|
74
|
-
# describe/update, so it gets a 404 pointing at the key endpoint
|
|
75
|
-
# rather than a 500 from a nil user — mirroring ApiKeysController#current.
|
|
76
|
-
def require_current_user!
|
|
77
|
-
return if current_user
|
|
78
|
-
|
|
79
|
-
render_error(
|
|
80
|
-
code: ERROR_CODES[:record_not_found],
|
|
81
|
-
message: I18n.t('spree.me_no_current_user'),
|
|
82
|
-
status: :not_found
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
73
|
# /me describes the signed-in user rather than a store, and the
|
|
87
74
|
# dashboard reads it before any store is selected. A user who holds
|
|
88
75
|
# no role on any store (not yet given one, or removed from all of
|
|
@@ -55,6 +55,16 @@ module Spree
|
|
|
55
55
|
Spree::Media
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# Sorting by file name or size orders by a subquery on the blob,
|
|
59
|
+
# which PostgreSQL rejects under SELECT DISTINCT. Nothing here can
|
|
60
|
+
# repeat a row: the filename filter joins each media row to its one
|
|
61
|
+
# attachment and that attachment's one blob, and every other filter,
|
|
62
|
+
# like the product scope's legacy default-variant branch, reads this
|
|
63
|
+
# table's own columns. The library inherits this.
|
|
64
|
+
def collection_distinct?
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
58
68
|
def serializer_class
|
|
59
69
|
Spree.api.admin_media_serializer
|
|
60
70
|
end
|
|
@@ -56,12 +56,16 @@ module Spree
|
|
|
56
56
|
# DELETE /api/v3/admin/orders/:order_id/items/:id
|
|
57
57
|
def destroy
|
|
58
58
|
with_order_lock do
|
|
59
|
-
Spree.order_remove_line_item_service.call(
|
|
59
|
+
result = Spree.order_remove_line_item_service.call(
|
|
60
60
|
order: @parent,
|
|
61
61
|
line_item: @resource
|
|
62
62
|
)
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
if result.success?
|
|
65
|
+
head :no_content
|
|
66
|
+
else
|
|
67
|
+
render_order_item_error(result, default_code: ERROR_CODES[:processing_error])
|
|
68
|
+
end
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
71
|
|
|
@@ -156,6 +156,28 @@ module Spree
|
|
|
156
156
|
run_review_workflow(Spree.product_reject_workflow, reason: params[:reason])
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
+
# PATCH /api/v3/admin/products/:id/activate
|
|
160
|
+
#
|
|
161
|
+
# Puts one product on sale.
|
|
162
|
+
def activate
|
|
163
|
+
run_status_workflow(Spree.product_activate_workflow, 'active')
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# PATCH /api/v3/admin/products/:id/archive
|
|
167
|
+
#
|
|
168
|
+
# Takes one product off sale, archived; activating it again puts it
|
|
169
|
+
# back on sale.
|
|
170
|
+
def archive
|
|
171
|
+
run_status_workflow(Spree.product_archive_workflow, 'archived')
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# PATCH /api/v3/admin/products/:id/draft
|
|
175
|
+
#
|
|
176
|
+
# Takes one product off sale, back to a draft.
|
|
177
|
+
def draft
|
|
178
|
+
run_status_workflow(Spree.product_draft_workflow, 'draft')
|
|
179
|
+
end
|
|
180
|
+
|
|
159
181
|
# POST /api/v3/admin/products/bulk_status_update
|
|
160
182
|
# Body: { ids: [...], status: 'draft' | 'active' | 'archived' }
|
|
161
183
|
def bulk_status_update
|
|
@@ -363,6 +385,40 @@ module Spree
|
|
|
363
385
|
|
|
364
386
|
private
|
|
365
387
|
|
|
388
|
+
# Unlike bulk_status_update, which writes the column, these run the
|
|
389
|
+
# status workflow, so its hooks and its event fire. A product in
|
|
390
|
+
# review is refused: its next status is a decision, made through
|
|
391
|
+
# approve or reject (docs/plans/6.0-seller-product-submission.md).
|
|
392
|
+
#
|
|
393
|
+
# A product already in that status is answered as it is: running the
|
|
394
|
+
# workflow again would publish its event again, and every webhook
|
|
395
|
+
# subscriber would see the move twice.
|
|
396
|
+
def run_status_workflow(workflow, status)
|
|
397
|
+
@resource = find_resource
|
|
398
|
+
authorize!(:update, @resource)
|
|
399
|
+
|
|
400
|
+
return render json: serialize_resource(@resource) if @resource.status == status
|
|
401
|
+
|
|
402
|
+
if @resource.in_review?
|
|
403
|
+
return render_error(
|
|
404
|
+
code: 'product_in_review',
|
|
405
|
+
message: I18n.t('spree.errors.messages.product_in_review'),
|
|
406
|
+
status: :unprocessable_content
|
|
407
|
+
)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Not under a row lock taken here: that would wrap the workflow in
|
|
411
|
+
# a transaction, and it publishes its event once its own
|
|
412
|
+
# transaction is done, which an outer one would no longer be.
|
|
413
|
+
result = workflow.call(product: @resource)
|
|
414
|
+
|
|
415
|
+
if result.success?
|
|
416
|
+
render json: serialize_resource(@resource.reload)
|
|
417
|
+
else
|
|
418
|
+
render_service_error(@resource.errors.presence || result.error)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
366
422
|
# Approving and rejecting are edits to the catalog, so they answer to
|
|
367
423
|
# the same key as any other product write.
|
|
368
424
|
def run_review_workflow(workflow, **arguments)
|
|
@@ -10,15 +10,23 @@ module Spree
|
|
|
10
10
|
# endpoint, and rooted the same way — at the seller's invitations, so
|
|
11
11
|
# an id from another seller reads as missing rather than as denied.
|
|
12
12
|
#
|
|
13
|
-
# Creating one
|
|
14
|
-
#
|
|
15
|
-
# the
|
|
13
|
+
# Creating one here adds someone to the team and leaves the seller's
|
|
14
|
+
# status alone, so it works for a seller already trading. The
|
|
15
|
+
# `invite` action on the seller is the lifecycle step that opens a
|
|
16
|
+
# pending seller for its first owner.
|
|
16
17
|
class InvitationsController < Admin::BaseController
|
|
17
18
|
scoped_resource :sellers
|
|
18
19
|
|
|
19
20
|
before_action :set_seller
|
|
21
|
+
# Every write hands out or withdraws a way onto the team, so it
|
|
22
|
+
# needs the same right as changing the seller.
|
|
23
|
+
before_action :authorize_seller_update, only: [:create, :destroy, :resend]
|
|
20
24
|
before_action :set_invitation, only: [:destroy, :resend]
|
|
21
25
|
|
|
26
|
+
request_params 'SellerInvitationCreateParams', action: :create do |body|
|
|
27
|
+
body.attribute :email, :string, required: true, example: 'teammate@example.com'
|
|
28
|
+
end
|
|
29
|
+
|
|
22
30
|
# Pending only: an accepted invitation is a team member, which the
|
|
23
31
|
# team endpoint lists.
|
|
24
32
|
def index
|
|
@@ -27,6 +35,30 @@ module Spree
|
|
|
27
35
|
render json: { data: invitations.map { |invitation| serialize(invitation) } }
|
|
28
36
|
end
|
|
29
37
|
|
|
38
|
+
# A seller the marketplace turned away has no team to join, the
|
|
39
|
+
# same line the seller's own `invite` action draws.
|
|
40
|
+
def create
|
|
41
|
+
if @seller.rejected?
|
|
42
|
+
return render_error(
|
|
43
|
+
code: ErrorHandler::ERROR_CODES[:processing_error],
|
|
44
|
+
message: I18n.t('spree.invitation_seller_rejected'),
|
|
45
|
+
status: :unprocessable_content
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
invitation = @seller.invitations.new(
|
|
50
|
+
email: params[:email],
|
|
51
|
+
role: @seller.default_user_role,
|
|
52
|
+
inviter: try_spree_current_user
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if invitation.save
|
|
56
|
+
render json: serialize(invitation), status: :created
|
|
57
|
+
else
|
|
58
|
+
render_validation_error(invitation.errors)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
30
62
|
def destroy
|
|
31
63
|
@invitation.destroy
|
|
32
64
|
head :no_content
|
|
@@ -60,6 +92,10 @@ module Spree
|
|
|
60
92
|
authorize! :show, @seller
|
|
61
93
|
end
|
|
62
94
|
|
|
95
|
+
def authorize_seller_update
|
|
96
|
+
authorize! :update, @seller
|
|
97
|
+
end
|
|
98
|
+
|
|
63
99
|
def set_invitation
|
|
64
100
|
@invitation = @seller.invitations.pending.find_by_prefix_id!(params[:id])
|
|
65
101
|
end
|
|
@@ -11,9 +11,7 @@ module Spree
|
|
|
11
11
|
# person was invited — which a seller by definition cannot do from
|
|
12
12
|
# inside their own panel.
|
|
13
13
|
#
|
|
14
|
-
# Inviting
|
|
15
|
-
# itself, which is a lifecycle workflow rather than bookkeeping on a
|
|
16
|
-
# team.
|
|
14
|
+
# Inviting someone is the invitations endpoint next to this one.
|
|
17
15
|
class TeamController < Admin::BaseController
|
|
18
16
|
scoped_resource :sellers
|
|
19
17
|
|
|
@@ -18,6 +18,11 @@ module Spree
|
|
|
18
18
|
|
|
19
19
|
before_action :set_resource, only: [:show, :update, :destroy, :invite, :approve, :suspend, :reject,
|
|
20
20
|
:onboarding, :reopen_onboarding]
|
|
21
|
+
before_action :authorize_returns_address!, only: [:create, :update]
|
|
22
|
+
|
|
23
|
+
RETURNS_ADDRESS_DESCRIPTION = "Where customers send returns, written inline. It is the address of the seller's " \
|
|
24
|
+
'returns stock location, so it updates that location, or creates one for a seller ' \
|
|
25
|
+
'who has none. Names and `label` are ignored: a location has none.'.freeze
|
|
21
26
|
|
|
22
27
|
# Enumerated rather than borrowing the legacy global list, which
|
|
23
28
|
# permits :id, :user_id and :deleted_at — the first of which would
|
|
@@ -64,6 +69,11 @@ module Spree
|
|
|
64
69
|
description: "The seller's own billing address, written inline. There is no `billing_address_id`: an address belongs to whoever it was created for, so it is never referenced by id." do |billing_address|
|
|
65
70
|
billing_address.include_attributes Spree::Api::V3::AddressRequestSchema
|
|
66
71
|
end
|
|
72
|
+
body.attribute :returns_address, :object,
|
|
73
|
+
as: 'SellerAddressParams',
|
|
74
|
+
description: RETURNS_ADDRESS_DESCRIPTION do |returns_address|
|
|
75
|
+
returns_address.include_attributes Spree::Api::V3::AddressRequestSchema
|
|
76
|
+
end
|
|
67
77
|
body.attribute :custom_fields, :array, of: Spree::Api::V3::CustomFieldValueRequestSchema,
|
|
68
78
|
description: 'Custom field values, each validated against its definition.'
|
|
69
79
|
end
|
|
@@ -78,7 +88,10 @@ module Spree
|
|
|
78
88
|
|
|
79
89
|
request_params 'SellerApproveParams', action: :approve do |body|
|
|
80
90
|
body.attribute :override_requirements, :boolean,
|
|
81
|
-
|
|
91
|
+
description: "Admits a seller whose onboarding checklist is unfinished: the operator's " \
|
|
92
|
+
'deliberate exception, which the `seller.approved` event records along with ' \
|
|
93
|
+
'what was outstanding. Also admits a `pending` or `invited` seller who never ' \
|
|
94
|
+
'started onboarding, which is refused without it.'
|
|
82
95
|
end
|
|
83
96
|
|
|
84
97
|
request_params 'SellerReopenOnboardingParams', action: :reopen_onboarding do |body|
|
|
@@ -235,6 +248,16 @@ module Spree
|
|
|
235
248
|
|
|
236
249
|
@resource.roles.find_by_prefix_id!(params[:role_id])
|
|
237
250
|
end
|
|
251
|
+
|
|
252
|
+
# The returns address is a stock location's, and writing a location
|
|
253
|
+
# needs `write_settings` (see StockLocationsController), which the
|
|
254
|
+
# `sellers` scope does not grant.
|
|
255
|
+
def authorize_returns_address!
|
|
256
|
+
return unless params.key?(:returns_address)
|
|
257
|
+
return if holds_permission?('write_settings')
|
|
258
|
+
|
|
259
|
+
render_missing_permission('write_settings')
|
|
260
|
+
end
|
|
238
261
|
end
|
|
239
262
|
end
|
|
240
263
|
end
|
|
@@ -44,6 +44,10 @@ module Spree
|
|
|
44
44
|
example: -3,
|
|
45
45
|
description: 'A relative change, for feeds that report movements rather than levels. Each row needs `count_on_hand` or `adjustment`.'
|
|
46
46
|
stock_level.attribute :backorderable, :boolean, nullable: true
|
|
47
|
+
stock_level.attribute :reason, :string,
|
|
48
|
+
nullable: true,
|
|
49
|
+
example: 'received',
|
|
50
|
+
description: 'Labels the movement in the stock history. Defaults to "Inventory feed".'
|
|
47
51
|
end
|
|
48
52
|
end
|
|
49
53
|
|
|
@@ -73,6 +77,15 @@ module Spree
|
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
result = Spree.stock_level_bulk_upsert_workflow.call(store: current_store, rows: rows)
|
|
80
|
+
if result.failure?
|
|
81
|
+
return render_error(
|
|
82
|
+
code: 'invalid_stock_levels',
|
|
83
|
+
message: result.error.to_s,
|
|
84
|
+
status: :unprocessable_content,
|
|
85
|
+
details: { rows: result.error.value[:rows] }
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
76
89
|
render json: result.value
|
|
77
90
|
end
|
|
78
91
|
|
|
@@ -173,7 +186,8 @@ module Spree
|
|
|
173
186
|
stock_location_id: location_ids[row[:stock_location_id]],
|
|
174
187
|
count_on_hand: row[:count_on_hand],
|
|
175
188
|
adjustment: row[:adjustment],
|
|
176
|
-
backorderable: row[:backorderable]
|
|
189
|
+
backorderable: row[:backorderable],
|
|
190
|
+
reason: row[:reason].presence
|
|
177
191
|
}.compact
|
|
178
192
|
end
|
|
179
193
|
end
|
|
@@ -7,12 +7,13 @@ module Spree
|
|
|
7
7
|
#
|
|
8
8
|
# The only place the Admin API reveals the running Spree version, so it
|
|
9
9
|
# answers signed-in staff who can manage the store and nobody else. A
|
|
10
|
-
# secret API key gets a 404, like GET /me
|
|
11
|
-
# otherwise fingerprint the deployment for
|
|
10
|
+
# secret API key gets a 404, like GET /me, and a connected app a 403: an
|
|
11
|
+
# integration credential would otherwise fingerprint the deployment for
|
|
12
|
+
# anyone matching CVEs.
|
|
12
13
|
class UpdatesController < Admin::BaseController
|
|
13
14
|
skip_scope_check!
|
|
14
15
|
|
|
15
|
-
before_action :
|
|
16
|
+
before_action :require_staff_session!
|
|
16
17
|
|
|
17
18
|
# GET /api/v3/admin/updates
|
|
18
19
|
def show
|
|
@@ -20,18 +21,6 @@ module Spree
|
|
|
20
21
|
|
|
21
22
|
render json: Spree.api.admin_update_status_serializer.new(Spree::UpdateCheck.status).to_h
|
|
22
23
|
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def require_current_user!
|
|
27
|
-
return if current_user
|
|
28
|
-
|
|
29
|
-
render_error(
|
|
30
|
-
code: ERROR_CODES[:record_not_found],
|
|
31
|
-
message: I18n.t('spree.me_no_current_user'),
|
|
32
|
-
status: :not_found
|
|
33
|
-
)
|
|
34
|
-
end
|
|
35
24
|
end
|
|
36
25
|
end
|
|
37
26
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Seller
|
|
5
|
+
module Products
|
|
6
|
+
# Removing a file from one of this seller's own products.
|
|
7
|
+
#
|
|
8
|
+
# Delete only: the gallery's other writes ride the product update,
|
|
9
|
+
# which never removes a file it was not sent — so a form holding
|
|
10
|
+
# stale state cannot wipe a seller's pictures. Like the rest of a
|
|
11
|
+
# gallery edit, removal does not send an approved listing back to
|
|
12
|
+
# review.
|
|
13
|
+
class MediaController < Seller::ResourceController
|
|
14
|
+
scoped_resource :products
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
def model_class
|
|
19
|
+
Spree::Media
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Rooted in the seller's own catalog, so another seller's product
|
|
23
|
+
# is a 404. Changing a product's gallery is a change to the
|
|
24
|
+
# product, so it needs `:update` on it.
|
|
25
|
+
def set_parent
|
|
26
|
+
@parent = current_seller.products.find_by_prefix_id!(params[:product_id])
|
|
27
|
+
authorize_parent!(@parent)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The product's gallery, plus any file still pinned to its default
|
|
31
|
+
# variant from before galleries moved to the product — the same
|
|
32
|
+
# rows the product shows.
|
|
33
|
+
def scope
|
|
34
|
+
Spree::Media.where(viewable: @parent).or(
|
|
35
|
+
Spree::Media.where(viewable_type: 'Spree::Variant', viewable_id: @parent.default_variant&.id)
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The product's own authorization above is the check.
|
|
40
|
+
def authorize_resource!(resource = @resource, action = action_name.to_sym); end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -62,8 +62,10 @@ module Spree
|
|
|
62
62
|
# operator's endpoint: it would make the server fetch a URL the
|
|
63
63
|
# caller chose.
|
|
64
64
|
body.attribute :media, :array,
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
as: 'SellerProductMediaParams',
|
|
66
|
+
description: 'Files to add or update: `signed_id` attaches a fresh upload; ' \
|
|
67
|
+
'`id` updates one already there. A file left out stays; remove ' \
|
|
68
|
+
'one with `DELETE /products/{product_id}/media/{id}`.' do |media_item|
|
|
67
69
|
media_item.attribute :id, :id
|
|
68
70
|
media_item.attribute :signed_id, :string, description: 'A `signed_id` from `POST /files`.'
|
|
69
71
|
media_item.include_attributes Spree::Api::V3::MediaRequestSchema
|
|
@@ -8,10 +8,10 @@ module Spree
|
|
|
8
8
|
# `current_seller` — never an id from the request. A seller cannot
|
|
9
9
|
# address another seller here even by guessing one.
|
|
10
10
|
#
|
|
11
|
-
# What they may change is presentation, contact and
|
|
12
|
-
# by design: `status` (the lifecycle belongs to the
|
|
13
|
-
# workflows), the settlement and tax configuration, and
|
|
14
|
-
# seller renaming their own storefront address breaks every link
|
|
11
|
+
# What they may change is presentation, contact, addresses and holiday
|
|
12
|
+
# mode. Absent by design: `status` (the lifecycle belongs to the
|
|
13
|
+
# operator's workflows), the settlement and tax configuration, and
|
|
14
|
+
# `slug` — a seller renaming their own storefront address breaks every link
|
|
15
15
|
# pointing at it.
|
|
16
16
|
class ProfileController < Seller::BaseController
|
|
17
17
|
# A singular resource, so this extends the plain base rather than
|
|
@@ -42,6 +42,9 @@ module Spree
|
|
|
42
42
|
body.attribute :cover_photo_signed_id, :string,
|
|
43
43
|
nullable: true,
|
|
44
44
|
description: 'A `signed_id` from `POST /files`; `null` removes the image.'
|
|
45
|
+
body.attribute :holiday_mode_until, :datetime,
|
|
46
|
+
nullable: true,
|
|
47
|
+
description: 'Set while the seller is away: the catalog stays but stops selling. `null` ends holiday mode.'
|
|
45
48
|
body.attribute :billing_address, :object, as: 'SellerAddressParams' do |billing_address|
|
|
46
49
|
billing_address.include_attributes Spree::Api::V3::AddressRequestSchema
|
|
47
50
|
end
|
|
@@ -133,8 +136,13 @@ module Spree
|
|
|
133
136
|
end
|
|
134
137
|
end
|
|
135
138
|
|
|
139
|
+
# Plus what extensions opened to sellers — never the operator-level
|
|
140
|
+
# list — so fields a plugin adds to the panel's profile form save.
|
|
136
141
|
def permitted_params
|
|
137
|
-
normalize_params(
|
|
142
|
+
normalize_params(
|
|
143
|
+
params.permit(*declared_permitted_attributes,
|
|
144
|
+
*Array(Spree::Seller.try(:additional_seller_permitted_attributes)))
|
|
145
|
+
)
|
|
138
146
|
end
|
|
139
147
|
|
|
140
148
|
private
|