spree_api 6.0.3 → 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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/idempotent.rb +10 -1
  3. data/app/controllers/concerns/spree/api/v3/security_headers.rb +3 -0
  4. data/app/controllers/spree/api/v3/admin/base_controller.rb +31 -0
  5. data/app/controllers/spree/api/v3/admin/companies/memberships_controller.rb +21 -4
  6. data/app/controllers/spree/api/v3/admin/diagnostics_controller.rb +46 -0
  7. data/app/controllers/spree/api/v3/admin/me_controller.rb +0 -13
  8. data/app/controllers/spree/api/v3/admin/media_controller.rb +10 -0
  9. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +6 -2
  10. data/app/controllers/spree/api/v3/admin/sellers/invitations_controller.rb +39 -3
  11. data/app/controllers/spree/api/v3/admin/sellers/team_controller.rb +1 -3
  12. data/app/controllers/spree/api/v3/admin/sellers_controller.rb +24 -1
  13. data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +15 -1
  14. data/app/controllers/spree/api/v3/admin/updates_controller.rb +4 -15
  15. data/app/controllers/spree/api/v3/seller/products/media_controller.rb +46 -0
  16. data/app/controllers/spree/api/v3/seller/products_controller.rb +4 -2
  17. data/app/controllers/spree/api/v3/seller/profile_controller.rb +13 -5
  18. data/app/controllers/spree/api/v3/store/carts_controller.rb +10 -7
  19. data/app/serializers/spree/api/v3/admin/company_membership_serializer.rb +4 -0
  20. data/app/serializers/spree/api/v3/admin/diagnostics_serializer.rb +30 -0
  21. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +6 -1
  22. data/app/serializers/spree/api/v3/admin/seller_serializer.rb +10 -0
  23. data/app/serializers/spree/api/v3/product_category_serializer.rb +26 -0
  24. data/app/serializers/spree/api/v3/product_collection_serializer.rb +26 -0
  25. data/app/serializers/spree/api/v3/seller/order_line_item_serializer.rb +2 -2
  26. data/config/routes.rb +3 -1
  27. data/lib/spree/api/agent_resource_map.rb +42 -7
  28. data/lib/spree/api/dependencies.rb +1 -1
  29. data/lib/spree/api/testing_support/openapi.rb +2 -1
  30. data/lib/spree/api/testing_support/sdk_method.rb +34 -0
  31. data/openapi/admin.yaml +695 -279
  32. data/openapi/store.yaml +125 -125
  33. metadata +12 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f1aae2e30afb2db5577e3315cd2779ab8f273bfa114b7e369f7c0c8a519214d
4
- data.tar.gz: 15e89346921cf2a0f21bdccd6050e7f4854d0aea5d17d0ef666d853ea475f769
3
+ metadata.gz: 5eaa75827fbded08b54001d86e1f068f30f011414f6b4530aab8db17830f5779
4
+ data.tar.gz: f50e306fd54087d3e3a05a97d599e63805fc56e4329dcb77839f940d0d127341
5
5
  SHA512:
6
- metadata.gz: 555454b4f93a2ec776ebd7a1c6ea407bbc359b10493768c838d29617b0d58011a024168a2f384c4e2ad21a983a2f31e3989afce90d864c6f272f5bc13b7253ae
7
- data.tar.gz: 0f00cda28bba945fc65e7509cfd61bd5626af3c91957e2f6dea5819e078593a7aad38152e5439495019bfdaece73cfaf65751313a10604f6af4f864176f84cb1
6
+ metadata.gz: e50bf307deb4e72c30d4f31ef0b70a65cfd225f799d2ffedc05e2e588a168869d81e6c60128353e9add061a4774da56da6a482a98ac32dd0456f32aa0f9aabd0
7
+ data.tar.gz: 9bce5f92eed950d2586ea278cfce9bd5797a67b13bffc7de5dc20811ca3ad219364c667cc67d384f034c3d98df4a5c576d3dff283f25ff71322aa7001d1456cd
@@ -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
- required: true, on: :create,
19
- example: 'buyer@example.com',
20
- description: 'An existing customer becomes a member immediately; an unknown email becomes an invitation, and the response is an invitation instead of a membership.'
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
- email: params.require(:customer_email),
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
- head :no_content
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
 
@@ -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 is the `invite` action on the seller itself; what is
14
- # here is the bookkeeping the operator needs when an offer went to
15
- # the wrong address or never arrived.
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 is not here: it is the `invite` action on the seller
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
- description: 'Admits a seller whose onboarding checklist is unfinished: the operator\'s deliberate exception, which the `seller.approved` event records along with what was outstanding.'
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: an integration key would
11
- # otherwise fingerprint the deployment for anyone matching CVEs.
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 :require_current_user!
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
- as: 'SellerProductMediaParams',
66
- description: 'The gallery, as a whole: a file absent from the list is removed. `signed_id` attaches a fresh upload; `id` updates one already there.' do |media_item|
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 addresses. Absent
12
- # by design: `status` (the lifecycle belongs to the operator's
13
- # workflows), the settlement and tax configuration, and `slug` — a
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(params.permit(*declared_permitted_attributes))
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
@@ -129,17 +129,20 @@ module Spree
129
129
  end
130
130
  end
131
131
 
132
- # DELETE /api/v3/store/carts/:id
133
- # Deletes/abandons the cart
132
+ # Abandons a cart after checking its completion claim under the row lock.
133
+ #
134
+ # @return [void]
134
135
  def destroy
135
136
  find_cart!
136
137
 
137
- result = Spree.cart_destroy_service.call(cart: @cart)
138
+ with_order_lock do
139
+ result = Spree.cart_destroy_service.call(cart: @cart)
138
140
 
139
- if result.success?
140
- head :no_content
141
- else
142
- render_service_error(result.error.to_s)
141
+ if result.success?
142
+ head :no_content
143
+ else
144
+ render_service_error(result.error.to_s)
145
+ end
143
146
  end
144
147
  end
145
148
 
@@ -4,6 +4,10 @@ module Spree
4
4
  module Admin
5
5
  class CompanyMembershipSerializer < V3::CompanyMembershipSerializer
6
6
  attributes created_at: :iso8601, updated_at: :iso8601
7
+
8
+ # The member as a customer, so a list can show their name, not only
9
+ # the email.
10
+ one :customer, resource: proc { Spree.api.admin_customer_serializer }, if: proc { expand?('customer') }
7
11
  end
8
12
  end
9
13
  end
@@ -0,0 +1,30 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # The diagnostics report (Spree::Diagnostics::Report). A computed
6
+ # value rather than a record, so it carries no id. `errors[].error_class`
7
+ # is an exception's class name on purpose: it is what a maintainer
8
+ # reads in a bug report, not a value any client branches on.
9
+ class DiagnosticsSerializer < V3::BaseSerializer
10
+ typelize generated_at: :string,
11
+ spree_version: :string,
12
+ markdown: :string,
13
+ issue_url: :string,
14
+ issue_prefilled: :boolean,
15
+ sections: 'Array<{ key: string; title: string; rows: Array<{ label: string; value: string }> }>',
16
+ errors: 'Array<{ error_class: string; source: string | null; frames: string[]; ' \
17
+ 'request_id: string | null; count: number; first_seen_at: string; last_seen_at: string }>'
18
+
19
+ _attributes.delete(:id)
20
+
21
+ attributes :spree_version, :sections, :errors, generated_at: :iso8601
22
+
23
+ attribute(:markdown, &:to_markdown)
24
+ attribute(:issue_url, &:issue_url)
25
+ attribute(:issue_prefilled, &:issue_prefilled?)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -28,7 +28,8 @@ module Spree
28
28
  price_list_id: [:string, nullable: true],
29
29
  price_list_name: [:string, nullable: true],
30
30
  catalog_id: [:string, nullable: true],
31
- catalog_name: [:string, nullable: true]
31
+ catalog_name: [:string, nullable: true],
32
+ product_id: [:string, nullable: true]
32
33
 
33
34
  # price_source is operational provenance — admin-only, never on the
34
35
  # store serializer.
@@ -66,6 +67,10 @@ module Spree
66
67
 
67
68
  prefixed_id_attributes :tax_category
68
69
 
70
+ # So an order page can link each line to its product without
71
+ # expanding the variant.
72
+ prefixed_id_attributes :product
73
+
69
74
  # Override inherited associations to use admin serializers
70
75
  many :option_values, resource: proc { Spree.api.admin_option_value_serializer }
71
76
  many :digital_links, resource: proc { Spree.api.admin_digital_link_serializer }