spree_api 6.0.0.beta2 → 6.0.0.beta3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/embed_permissions.rb +55 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +34 -5
  4. data/app/controllers/concerns/spree/api/v3/current_password_confirmation.rb +8 -0
  5. data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +15 -2
  6. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +14 -0
  7. data/app/controllers/concerns/spree/api/v3/store/storefront_products.rb +35 -0
  8. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +9 -0
  9. data/app/controllers/spree/api/v3/admin/base_controller.rb +1 -0
  10. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +8 -3
  11. data/app/controllers/spree/api/v3/admin/customers_controller.rb +0 -14
  12. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +29 -2
  13. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +4 -1
  14. data/app/controllers/spree/api/v3/admin/invitations/acceptance_links_controller.rb +41 -0
  15. data/app/controllers/spree/api/v3/admin/me_controller.rb +11 -0
  16. data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +14 -1
  17. data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +22 -1
  18. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
  19. data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +22 -1
  20. data/app/controllers/spree/api/v3/admin/orders_controller.rb +27 -1
  21. data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +31 -11
  22. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +1 -19
  23. data/app/controllers/spree/api/v3/admin/prices_controller.rb +33 -2
  24. data/app/controllers/spree/api/v3/admin/products_controller.rb +1 -1
  25. data/app/controllers/spree/api/v3/admin/resource_controller.rb +1 -0
  26. data/app/controllers/spree/api/v3/admin/sellers/invitations/acceptance_links_controller.rb +37 -0
  27. data/app/controllers/spree/api/v3/admin/setup_controller.rb +1 -1
  28. data/app/controllers/spree/api/v3/admin/store_controller.rb +4 -0
  29. data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +1 -1
  30. data/app/controllers/spree/api/v3/admin/translations_controller.rb +1 -1
  31. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +12 -0
  32. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +27 -0
  33. data/app/controllers/spree/api/v3/base_controller.rb +7 -3
  34. data/app/controllers/spree/api/v3/resource_controller.rb +11 -1
  35. data/app/controllers/spree/api/v3/seller/auth_controller.rb +1 -1
  36. data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +5 -2
  37. data/app/controllers/spree/api/v3/seller/invitations/acceptance_links_controller.rb +34 -0
  38. data/app/controllers/spree/api/v3/seller/me_controller.rb +25 -2
  39. data/app/controllers/spree/api/v3/seller/orders_controller.rb +19 -24
  40. data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +1 -1
  41. data/app/controllers/spree/api/v3/seller/resource_controller.rb +4 -0
  42. data/app/controllers/spree/api/v3/store/carts/items_controller.rb +7 -4
  43. data/app/controllers/spree/api/v3/store/carts/payment_sessions_controller.rb +10 -1
  44. data/app/controllers/spree/api/v3/store/carts/payments_controller.rb +5 -1
  45. data/app/controllers/spree/api/v3/store/companies/members_controller.rb +10 -11
  46. data/app/controllers/spree/api/v3/store/companies/orders_controller.rb +6 -0
  47. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +1 -0
  48. data/app/controllers/spree/api/v3/store/products_controller.rb +6 -12
  49. data/app/controllers/spree/api/v3/store/resource_controller.rb +4 -0
  50. data/app/controllers/spree/api/v3/store/wishlist_items_controller.rb +11 -0
  51. data/app/controllers/spree/api/v3/webhooks/fulfillments_controller.rb +10 -6
  52. data/app/controllers/spree/api/v3/webhooks/payments_controller.rb +6 -2
  53. data/app/controllers/spree/api/v3/webhooks/payouts_controller.rb +6 -2
  54. data/app/jobs/spree/webhook_delivery_job.rb +18 -4
  55. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +6 -0
  56. data/app/serializers/spree/api/v3/admin/invitation_acceptance_link_serializer.rb +15 -0
  57. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +0 -11
  58. data/app/serializers/spree/api/v3/admin/order_group_serializer.rb +7 -0
  59. data/app/serializers/spree/api/v3/admin/store_serializer.rb +8 -0
  60. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +6 -2
  61. data/app/serializers/spree/api/v3/gift_card_serializer.rb +3 -1
  62. data/app/serializers/spree/api/v3/line_item_serializer.rb +2 -1
  63. data/app/serializers/spree/api/v3/market_serializer.rb +5 -0
  64. data/app/serializers/spree/api/v3/product_filters_serializer.rb +3 -0
  65. data/app/serializers/spree/api/v3/seller/account_serializer.rb +19 -0
  66. data/app/serializers/spree/api/v3/seller/invitation_acceptance_link_serializer.rb +15 -0
  67. data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +1 -10
  68. data/app/services/spree/api/v3/filters_aggregator.rb +5 -2
  69. data/config/routes.rb +7 -0
  70. data/lib/spree/api/dependencies.rb +3 -0
  71. data/lib/spree/api/openapi/schema_helper.rb +10 -4
  72. metadata +14 -6
@@ -122,7 +122,7 @@ module Spree
122
122
  end
123
123
 
124
124
  def permitted_params
125
- attrs = normalize_params(
125
+ normalize_params(
126
126
  params.permit(
127
127
  :name, :description, :position,
128
128
  :starts_at, :ends_at, :match_policy,
@@ -132,24 +132,6 @@ module Spree
132
132
  prices: [:id, :variant_id, :currency, :min_quantity, :amount, :compare_at_amount]
133
133
  )
134
134
  )
135
- reject_foreign_variants(attrs)
136
- end
137
-
138
- # The price rows resolve variant ids with no store scoping, so a
139
- # list in this store could otherwise be populated with another
140
- # store's variants. Drop any id that isn't in the current store
141
- # before assignment.
142
- def reject_foreign_variants(attrs)
143
- if attrs[:prices].present?
144
- incoming = Array(attrs[:prices])
145
- store_variant_ids = current_store.variants.where(id: incoming.map { |r| r[:variant_id] }.compact).
146
- pluck(:id).map(&:to_s).to_set
147
- attrs[:prices] = incoming.select do |row|
148
- row[:variant_id].blank? || store_variant_ids.include?(row[:variant_id].to_s)
149
- end
150
- end
151
-
152
- attrs
153
135
  end
154
136
 
155
137
  private
@@ -58,6 +58,10 @@ module Spree
58
58
  # validations (docs/plans/6.0-volume-pricing.md). Each branch is
59
59
  # guarded on the key its own failure carries, so a failure mode the
60
60
  # service grows later is not reported under one of these names.
61
+ #
62
+ # `I18n.t` rather than `Spree.t` for these: the models raise them
63
+ # under `activerecord.errors`, and `Spree.t` would scope the lookup
64
+ # to `spree.` and miss the key.
61
65
  if (invalid = result.error&.value.try(:[], :invalid_quantities))
62
66
  return render_error(
63
67
  code: 'invalid_min_quantity',
@@ -70,16 +74,34 @@ module Spree
70
74
  if (negative = result.error&.value.try(:[], :invalid_amounts))
71
75
  return render_error(
72
76
  code: 'invalid_amount',
73
- message: Spree.t('activerecord.errors.models.spree/price_list.attributes.base.negative_price').upcase_first,
77
+ message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.negative_price').upcase_first,
74
78
  status: :unprocessable_content,
75
79
  details: { rows: negative }
76
80
  )
77
81
  end
78
82
 
83
+ if (misplaced = result.error&.value.try(:[], :quantities_on_base_prices))
84
+ return render_error(
85
+ code: 'quantity_on_base_price',
86
+ message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.quantity_on_base_price').upcase_first,
87
+ status: :unprocessable_content,
88
+ details: { rows: misplaced }
89
+ )
90
+ end
91
+
92
+ if (duplicates = result.error&.value.try(:[], :duplicate_quantities))
93
+ return render_error(
94
+ code: 'duplicate_min_quantity',
95
+ message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.duplicate_quantity').upcase_first,
96
+ status: :unprocessable_content,
97
+ details: { rows: duplicates }
98
+ )
99
+ end
100
+
79
101
  if (over_cap = result.error&.value.try(:[], :over_cap))
80
102
  return render_error(
81
103
  code: 'too_many_breaks',
82
- message: Spree.t(
104
+ message: I18n.t(
83
105
  'activerecord.errors.models.spree/price.attributes.min_quantity.too_many_breaks',
84
106
  count: Spree::Price::MAXIMUM_BREAKS_PER_VARIANT
85
107
  ).upcase_first,
@@ -88,6 +110,15 @@ module Spree
88
110
  )
89
111
  end
90
112
 
113
+ if (rising = result.error&.value.try(:[], :rising_ladders))
114
+ return render_error(
115
+ code: 'price_rises_with_quantity',
116
+ message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.price_rises_with_quantity').upcase_first,
117
+ status: :unprocessable_content,
118
+ details: { ladders: rising }
119
+ )
120
+ end
121
+
91
122
  render json: result.value
92
123
  end
93
124
 
@@ -301,7 +301,7 @@ module Spree
301
301
  end
302
302
 
303
303
  def search_provider
304
- @search_provider ||= Spree::SearchProvider::Database.new(current_store)
304
+ @search_provider ||= Spree::SearchProvider::Database.new(current_store, audience: nil)
305
305
  end
306
306
 
307
307
  # Tag changes can flip automatic-collection matches, and `Tags::Bulk*`
@@ -17,6 +17,7 @@ module Spree
17
17
  # include.
18
18
  include Spree::Api::V3::Admin::Concerns::ExternalReferences
19
19
  include Spree::Api::V3::Admin::ValidationDetails
20
+ include Spree::Api::V3::Admin::EmbedPermissions
20
21
 
21
22
  protected
22
23
 
@@ -0,0 +1,37 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Sellers
6
+ module Invitations
7
+ # The acceptance link of a pending invitation onto a seller's team.
8
+ #
9
+ # The link carries the token that joins the team, so reading
10
+ # sellers is not enough: the caller must be able to change the
11
+ # seller, as when sending the invitation.
12
+ class AcceptanceLinksController < Admin::BaseController
13
+ scoped_resource :sellers
14
+
15
+ # GET /api/v3/admin/sellers/:seller_id/invitations/:invitation_id/acceptance_link
16
+ def show
17
+ seller = current_store.sellers.find_by_prefix_id!(params[:seller_id])
18
+ authorize!(:update, seller)
19
+ invitation = seller.invitations.pending.find_by_prefix_id!(params[:invitation_id])
20
+
21
+ render json: Spree.api.admin_invitation_acceptance_link_serializer.new(
22
+ invitation, params: { store: current_store }
23
+ ).to_h
24
+ end
25
+
26
+ protected
27
+
28
+ def read_actions
29
+ []
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -65,7 +65,7 @@ module Spree
65
65
 
66
66
  # Outside the lock: the loader needs the admin committed, and it
67
67
  # runs for minutes, so it is queued rather than awaited.
68
- Spree::SampleData::LoadJob.perform_later if sample_data_requested?
68
+ Spree::SampleData::LoadJob.perform_later(store.id) if sample_data_requested?
69
69
 
70
70
  refresh_token = Spree::RefreshToken.create_for(user, audience: JWT_AUDIENCE_ADMIN, request_env: request_env_for_token)
71
71
  set_refresh_cookie(refresh_token)
@@ -105,6 +105,10 @@ module Spree
105
105
  :preferred_payout_provider,
106
106
  :preferred_default_payouts_schedule_interval,
107
107
  :preferred_default_minimum_payout_amount,
108
+ :preferred_auto_approve_sellers,
109
+ :preferred_auto_approve_seller_products,
110
+ :preferred_send_seller_transactional_emails,
111
+ :preferred_default_commission_tax_rate,
108
112
  :preferred_document_number_format,
109
113
  :preferred_order_number_prefix,
110
114
  :preferred_order_number_suffix,
@@ -39,7 +39,7 @@ module Spree
39
39
  raw = params[:translations]
40
40
  return render_empty_batch_error unless raw.is_a?(Array) && raw.any?
41
41
 
42
- batch = Spree::Translations::Batch.new(batch_params)
42
+ batch = Spree::Translations::Batch.new(batch_params, store: current_store)
43
43
  return unless require_batch_scopes!(batch)
44
44
 
45
45
  records = batch.process! { |record| authorize!(:update, record) }
@@ -43,7 +43,7 @@ module Spree
43
43
  # translation tables.
44
44
  def parent_relation
45
45
  klass = parent_lookup.klass
46
- relation = klass.respond_to?(:for_store) ? klass.for_store(current_store) : klass
46
+ relation = klass.respond_to?(:translatable_scope) ? klass.translatable_scope(current_store) : klass.for_store(current_store)
47
47
 
48
48
  children = klass.try(:translatable_children)
49
49
  children ? relation.includes(children => :translations) : relation
@@ -42,6 +42,18 @@ module Spree
42
42
  params[:webhook_endpoint_id]
43
43
  )
44
44
  end
45
+
46
+ # The payload is the record the event is about, so it is shown only
47
+ # to a caller who could read that record; the webhooks permission
48
+ # alone sees the delivery's status and response.
49
+ def serializer_params
50
+ super.merge(
51
+ payload_visible: lambda { |delivery|
52
+ key = delivery.payload_permission_key
53
+ key.nil? || holds_permission?(key)
54
+ }
55
+ )
56
+ end
45
57
  end
46
58
  end
47
59
  end
@@ -8,6 +8,8 @@ module Spree
8
8
  class WebhookEndpointsController < ResourceController
9
9
  scoped_resource :webhooks
10
10
 
11
+ before_action :reject_unauthorized_credential_subscription!, only: [:create, :update]
12
+
11
13
  # POST /api/v3/admin/webhook_endpoints/:id/send_test
12
14
  #
13
15
  # Fires a synthetic `webhook.test` delivery so admins can verify the
@@ -68,6 +70,31 @@ module Spree
68
70
  def permitted_params
69
71
  params.permit(*model_additional_permitted_attributes, :name, :url, :active, subscriptions: [])
70
72
  end
73
+
74
+ private
75
+
76
+ # Customer password reset tokens are account credentials, so the
77
+ # webhooks permission alone cannot subscribe an endpoint to them, or
78
+ # repoint an endpoint that already receives them.
79
+ def reject_unauthorized_credential_subscription!
80
+ return if holds_permission?('write_customers')
81
+
82
+ requested = Array(permitted_params[:subscriptions]) & Spree::WebhookEndpoint::CREDENTIAL_EVENTS
83
+ return if requested.empty? && !repoints_credential_endpoint?
84
+
85
+ render_error(
86
+ code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
87
+ message: "Receiving #{Spree::WebhookEndpoint::CREDENTIAL_EVENTS.to_sentence} requires permission to manage customers",
88
+ status: :forbidden
89
+ )
90
+ end
91
+
92
+ def repoints_credential_endpoint?
93
+ return false unless action_name == 'update'
94
+ return false unless permitted_params.key?(:url) || permitted_params.key?(:subscriptions)
95
+
96
+ current_store.webhook_endpoints.find_by_prefix_id(params[:id])&.receives_credentials?
97
+ end
71
98
  end
72
99
  end
73
100
  end
@@ -81,7 +81,10 @@ module Spree
81
81
  if errors.is_a?(ActiveModel::Errors)
82
82
  render_validation_error(errors)
83
83
  else
84
- render_service_error(error)
84
+ # The unwrapped value, so a workflow's rejection symbol reaches
85
+ # the branch that translates it rather than the one that prints
86
+ # the wrapper.
87
+ render_service_error(errors)
85
88
  end
86
89
  end
87
90
 
@@ -108,10 +111,11 @@ module Spree
108
111
  try_spree_current_user
109
112
  end
110
113
 
111
- # CanCanCan ability
114
+ # CanCanCan ability, built from the swappable
115
+ # `Spree::Dependencies.ability_class` (Spree::Ability by default).
112
116
  # @return [Spree::Ability]
113
117
  def current_ability
114
- @current_ability ||= Spree::Ability.new(current_user, ability_options)
118
+ @current_ability ||= Spree.ability_class.new(current_user, ability_options)
115
119
  end
116
120
 
117
121
  # Options passed to the CanCanCan ability
@@ -254,7 +254,7 @@ module Spree
254
254
 
255
255
  @search = scope.includes(collection_includes).
256
256
  preload_associations_lazily.
257
- ransack(ransack_params)
257
+ ransack(ransack_params, auth_object: ransack_auth_object)
258
258
  result = @search.result(distinct: collection_distinct?)
259
259
  pagy_options = { limit: limit, page: page }
260
260
  result = apply_collection_sort(result)
@@ -262,6 +262,16 @@ module Spree
262
262
  @collection
263
263
  end
264
264
 
265
+ # Who is filtering, as Ransack's auth object. Nil is the back office,
266
+ # which may use every allowlisted attribute; the Store and Seller
267
+ # branches name themselves so the models narrow what they may reach
268
+ # (see Spree::RansackableAttributes). Covers sorting too.
269
+ #
270
+ # @return [Symbol, nil]
271
+ def ransack_auth_object
272
+ nil
273
+ end
274
+
265
275
  # Override in subclass to disable distinct (e.g., for custom sorting with computed columns)
266
276
  # @return [Boolean] whether to apply distinct to the collection
267
277
  def collection_distinct?
@@ -136,7 +136,7 @@ module Spree
136
136
  def auth_response(user)
137
137
  {
138
138
  token: generate_jwt(user, audience: Spree::Api::V3::JwtAuthentication::JWT_AUDIENCE_SELLER),
139
- user: Spree.api.seller_team_member_serializer.new(user, params: { store: user.sellers.first&.store }).to_h,
139
+ user: Spree.api.seller_account_serializer.new(user, params: { store: user.sellers.first&.store }).to_h,
140
140
  sellers: serialized_sellers(user)
141
141
  }
142
142
  end
@@ -94,7 +94,10 @@ module Spree
94
94
  end
95
95
 
96
96
  def authenticate_existing(user)
97
- return user if user.valid_password?(params[:password].to_s)
97
+ # Under the login lockout: otherwise this is an unthrottled way to
98
+ # guess an existing account's password with an invitation token.
99
+ verdict = Spree::Authentication::Lockout.check(user) { user.valid_password?(params[:password].to_s) }
100
+ return user if verdict == :valid
98
101
 
99
102
  render_error(
100
103
  code: ErrorHandler::ERROR_CODES[:authentication_failed],
@@ -127,7 +130,7 @@ module Spree
127
130
  def auth_response(user)
128
131
  {
129
132
  token: generate_jwt(user, audience: Spree::Api::V3::JwtAuthentication::JWT_AUDIENCE_SELLER),
130
- user: Spree.api.seller_team_member_serializer.new(
133
+ user: Spree.api.seller_account_serializer.new(
131
134
  user, params: { store: @invitation.store }
132
135
  ).to_h,
133
136
  sellers: serialized_sellers(user)
@@ -0,0 +1,34 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Seller
5
+ module Invitations
6
+ # The acceptance link of a pending invitation onto this seller's team.
7
+ #
8
+ # The link carries the token that joins the team, so it is authorized
9
+ # like inviting someone: write access to the seller profile. Rooted at
10
+ # `current_seller.invitations`, so another seller's invitation reads
11
+ # as missing.
12
+ class AcceptanceLinksController < Seller::BaseController
13
+ scoped_resource :seller_profile
14
+
15
+ # GET /api/v3/seller/invitations/:invitation_id/acceptance_link
16
+ def show
17
+ invitation = current_seller.invitations.pending.find_by_prefix_id!(params[:invitation_id])
18
+
19
+ render json: Spree.api.seller_invitation_acceptance_link_serializer.new(
20
+ invitation, params: { store: current_store }
21
+ ).to_h
22
+ end
23
+
24
+ protected
25
+
26
+ def read_actions
27
+ []
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -20,11 +20,32 @@ module Spree
20
20
  render json: me_response
21
21
  end
22
22
 
23
+ # Self-service update of the signed-in person's own account — the
24
+ # name and photo their team sees, and the panel's language. It
25
+ # operates on `current_user` directly, so it needs no per-record
26
+ # authorization, and no seller role either: this is theirs whichever
27
+ # seller they are acting for.
28
+ #
29
+ # Distinct from PATCH /profile, which writes the seller business.
30
+ # `avatar` accepts an ActiveStorage direct-upload signed id to set
31
+ # the photo, or `null` to remove it.
32
+ def update
33
+ if current_user.update(permitted_params)
34
+ render json: me_response
35
+ else
36
+ render_validation_error(current_user.errors)
37
+ end
38
+ end
39
+
23
40
  private
24
41
 
42
+ def permitted_params
43
+ params.permit(:selected_locale, :first_name, :last_name, :avatar)
44
+ end
45
+
25
46
  def me_response
26
47
  {
27
- user: Spree.api.seller_team_member_serializer.new(current_user, params: serializer_params).to_h,
48
+ user: Spree.api.seller_account_serializer.new(current_user, params: serializer_params).to_h,
28
49
  sellers: serialized_sellers,
29
50
  # Both shapes, exactly as the admin `/me`: the panel's `<Can>`
30
51
  # reads CanCanCan rules, the key gate reads keys. Sending only
@@ -44,7 +65,9 @@ module Spree
44
65
  # Empty until a seller is named: capability is per seller, so there is
45
66
  # no meaningful answer spanning all of them. A bare ability (no
46
67
  # resource) serializes to nothing rather than raising, so the panel
47
- # can call `/me` before choosing a seller.
68
+ # can call `/me` before choosing a seller. Deliberately the core
69
+ # class, not `Spree.ability_class`: a custom ability may add rules
70
+ # after `super`, and those must not leak into the no-seller answer.
48
71
  def seller_ability
49
72
  return Spree::Ability.new(nil) if current_seller.nil?
50
73
 
@@ -77,7 +77,11 @@ module Spree
77
77
  # each lay their fields over the same snapshot, and the later one
78
78
  # would undo the earlier one's lines.
79
79
  with_order_lock do
80
- if @resource.update(address_params)
80
+ addresses = corrected_addresses
81
+ invalid = addresses.values.find(&:invalid?)
82
+ next render_validation_error(invalid.errors) if invalid
83
+
84
+ if @resource.update(addresses)
81
85
  render json: serialize_resource(@resource.reload)
82
86
  else
83
87
  render_validation_error(@resource.errors)
@@ -115,9 +119,10 @@ module Spree
115
119
  # Either address, as a correction rather than a replacement: the
116
120
  # sent fields are laid over what the order already holds, so a
117
121
  # request naming one line does not blank out the country and
118
- # postcode beside it. The nested writer builds a fresh address row
119
- # either way, which is what keeps a shared customer address book
120
- # entry from being rewritten by an order-level fix.
122
+ # postcode beside it. Written as a fresh ownerless snapshot straight
123
+ # onto the order — never through the buyer's nested writer, which
124
+ # files the address in their book and makes it their default for
125
+ # every later checkout on the marketplace.
121
126
  def sent_addresses
122
127
  @sent_addresses ||= params.permit(
123
128
  shipping_address: address_permitted_keys,
@@ -125,32 +130,22 @@ module Spree
125
130
  ).slice(:shipping_address, :billing_address).reject { |_key, value| value.blank? }
126
131
  end
127
132
 
128
- def address_params
129
- permitted = sent_addresses
130
-
131
- {}.tap do |attributes|
132
- if permitted[:shipping_address].present?
133
- attributes[:ship_address_attributes] =
134
- merged_address(@resource.ship_address, permitted[:shipping_address])
133
+ def corrected_addresses
134
+ {}.tap do |addresses|
135
+ if sent_addresses[:shipping_address].present?
136
+ addresses[:ship_address] = corrected_address(@resource.ship_address, sent_addresses[:shipping_address])
135
137
  end
136
138
 
137
- if permitted[:billing_address].present?
138
- attributes[:bill_address_attributes] =
139
- merged_address(@resource.bill_address, permitted[:billing_address])
139
+ if sent_addresses[:billing_address].present?
140
+ addresses[:bill_address] = corrected_address(@resource.bill_address, sent_addresses[:billing_address])
140
141
  end
141
142
  end
142
143
  end
143
144
 
144
- def merged_address(address, sent)
145
- current = address ? address.attributes.slice(*address_attribute_names) : {}
146
- current.merge(sent.to_h)
147
- end
148
-
149
- # The permitted keys that are real columns; the rest are the writer
150
- # aliases a client may send, which have no value to carry over.
151
- def address_attribute_names
152
- @address_attribute_names ||=
153
- address_permitted_keys.map(&:to_s) & Spree::Address.column_names
145
+ def corrected_address(address, sent)
146
+ # No label: an address-book label means nothing on an order's copy,
147
+ # and labels are unique per owner, which an ownerless copy lacks.
148
+ (address&.snapshot || Spree::Address.new).tap { |copy| copy.assign_attributes(sent.to_h.except('label', :label)) }
154
149
  end
155
150
 
156
151
  def address_permitted_keys
@@ -128,7 +128,7 @@ module Spree
128
128
  def auth_response(user)
129
129
  {
130
130
  token: generate_jwt(user, audience: Spree::Api::V3::JwtAuthentication::JWT_AUDIENCE_SELLER),
131
- user: Spree.api.seller_team_member_serializer.new(
131
+ user: Spree.api.seller_account_serializer.new(
132
132
  user, params: { store: seller_store(user) }
133
133
  ).to_h,
134
134
  sellers: serialized_sellers(user)
@@ -18,6 +18,10 @@ module Spree
18
18
  authenticate_seller!
19
19
  end
20
20
 
21
+ def ransack_auth_object
22
+ :seller
23
+ end
24
+
21
25
  # Every collection is rooted in the seller's own records.
22
26
  #
23
27
  # The inherited implementation roots at `for_store(current_store)`,
@@ -97,12 +97,15 @@ module Spree
97
97
  end
98
98
 
99
99
  # Extension attributes ride in `options`, which is how AddItem
100
- # forwards per-line-item values onto the record.
100
+ # forwards per-line-item values onto the record. Only those reach
101
+ # it: anything else in `options` is internal to AddItem (the
102
+ # fulfillment an admin pins a line to) and not the shopper's to set.
101
103
  def item_options
102
- keys = Spree::LineItem.additional_permitted_attributes.flat_map { |a| a.is_a?(Hash) ? a.keys : a }
103
- extras = permitted_params.to_h.slice(*keys.map(&:to_s)).symbolize_keys
104
+ keys = Spree::LineItem.additional_permitted_attributes.flat_map { |a| a.is_a?(Hash) ? a.keys : a }.map(&:to_s)
104
105
 
105
- (permitted_params[:options] || {}).to_h.symbolize_keys.merge(extras)
106
+ (permitted_params[:options] || {}).to_h.slice(*keys).
107
+ merge(permitted_params.to_h.slice(*keys)).
108
+ symbolize_keys
106
109
  end
107
110
 
108
111
  def permitted_params
@@ -13,7 +13,16 @@ module Spree
13
13
  # POST /api/v3/store/carts/:cart_id/payment_sessions
14
14
  def create
15
15
  with_order_lock do
16
- payment_method = current_store.payment_methods.find_by_prefix_id!(permitted_params[:payment_method_id])
16
+ payment_method = current_store.payment_methods.active.storefront_visible.
17
+ find_by_prefix_id!(permitted_params[:payment_method_id])
18
+
19
+ unless payment_method.available_for_order?(@cart)
20
+ return render_error(
21
+ code: 'payment_method_unavailable',
22
+ message: Spree.t('api.v3.payments.method_unavailable'),
23
+ status: :unprocessable_content
24
+ )
25
+ end
17
26
 
18
27
  @payment_session = payment_method.create_payment_session(
19
28
  order: @cart,
@@ -11,7 +11,11 @@ module Spree
11
11
  # POST /api/v3/store/carts/:cart_id/payments
12
12
  # Creates a payment for non-session payment methods (e.g. Check, Cash on Delivery, Bank Transfer)
13
13
  def create
14
- payment_method = current_store.payment_methods.find_by_prefix_id!(params[:payment_method_id])
14
+ # The same methods the cart offers: a disabled or staff-only
15
+ # method (an offline one marks the order paid) must not be
16
+ # reachable by naming its id.
17
+ payment_method = current_store.payment_methods.active.storefront_visible.
18
+ find_by_prefix_id!(params[:payment_method_id])
15
19
 
16
20
  if payment_method.session_required?
17
21
  return render_error(
@@ -4,26 +4,25 @@ module Spree
4
4
  module Store
5
5
  module Companies
6
6
  # The people with standing over one node, listed and added by its
7
- # members. Adding takes an email and does the right thing: a
8
- # membership for an existing customer, an invitation otherwise —
9
- # the same convergent behavior as the dashboard endpoint. Any member
10
- # can invite; restraint is Enterprise's policy layer.
7
+ # members. Adding takes an email and always sends an invitation,
8
+ # even to an existing customer: a member typing an address cannot
9
+ # vouch that it is the person they mean, so the invitee accepts with
10
+ # the emailed token. The response is the same whether or not the
11
+ # email has an account. Any member can invite; restraint is
12
+ # Enterprise's policy layer.
11
13
  class MembersController < BaseController
12
14
  # POST /api/v3/store/companies/:company_id/members
13
15
  def create
14
16
  result = Spree.company_add_member_service.call(
15
17
  company: @parent,
16
18
  email: params.require(:customer_email),
17
- inviter: current_user
19
+ inviter: current_user,
20
+ require_acceptance: true
18
21
  )
19
22
 
20
23
  if result.success?
21
- serializer = if result.value.is_a?(Spree::CompanyInvitation)
22
- Spree.api.company_invitation_serializer
23
- else
24
- serializer_class
25
- end
26
- render json: serializer.new(result.value, params: serializer_params).to_h, status: :created
24
+ render json: Spree.api.company_invitation_serializer.new(result.value, params: serializer_params).to_h,
25
+ status: :created
27
26
  else
28
27
  render_validation_error(result.value.errors)
29
28
  end
@@ -20,6 +20,12 @@ module Spree
20
20
  current_store.orders.complete.
21
21
  where(company_id: @parent.self_and_descendants.select(:id))
22
22
  end
23
+
24
+ # These are other members' orders: gift card codes and download
25
+ # links are the buyer's credentials, not the company's.
26
+ def serializer_params
27
+ super.merge(hide_credentials: true)
28
+ end
23
29
  end
24
30
  end
25
31
  end
@@ -40,6 +40,7 @@ module Spree
40
40
  current_store.id,
41
41
  current_currency,
42
42
  current_locale,
43
+ (hide_prices? ? 'prices_hidden' : nil),
43
44
  category&.cache_key_with_version,
44
45
  collection&.cache_key_with_version,
45
46
  search_query,