spree_api 6.0.0.beta1 → 6.0.0.beta3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) 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/admin/stock_receipt_actions.rb +1 -1
  5. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +11 -0
  6. data/app/controllers/concerns/spree/api/v3/current_password_confirmation.rb +8 -0
  7. data/app/controllers/concerns/spree/api/v3/orders/claim_actions.rb +2 -2
  8. data/app/controllers/concerns/spree/api/v3/orders/exchange_actions.rb +3 -3
  9. data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +15 -2
  10. data/app/controllers/concerns/spree/api/v3/orders/post_sale_actions.rb +1 -1
  11. data/app/controllers/concerns/spree/api/v3/orders/return_actions.rb +3 -3
  12. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +14 -0
  13. data/app/controllers/concerns/spree/api/v3/store/storefront_products.rb +35 -0
  14. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +9 -0
  15. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +2 -2
  16. data/app/controllers/spree/api/v3/admin/base_controller.rb +1 -0
  17. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +8 -3
  18. data/app/controllers/spree/api/v3/admin/customers_controller.rb +0 -14
  19. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +29 -2
  20. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +4 -1
  21. data/app/controllers/spree/api/v3/admin/invitations/acceptance_links_controller.rb +41 -0
  22. data/app/controllers/spree/api/v3/admin/me_controller.rb +11 -0
  23. data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +14 -1
  24. data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +22 -1
  25. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
  26. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +1 -1
  27. data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +22 -1
  28. data/app/controllers/spree/api/v3/admin/orders_controller.rb +41 -9
  29. data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +31 -11
  30. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +1 -19
  31. data/app/controllers/spree/api/v3/admin/prices_controller.rb +33 -2
  32. data/app/controllers/spree/api/v3/admin/products_controller.rb +1 -1
  33. data/app/controllers/spree/api/v3/admin/resource_controller.rb +1 -0
  34. data/app/controllers/spree/api/v3/admin/sellers/invitations/acceptance_links_controller.rb +37 -0
  35. data/app/controllers/spree/api/v3/admin/setup_controller.rb +9 -1
  36. data/app/controllers/spree/api/v3/admin/store_controller.rb +5 -0
  37. data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +1 -1
  38. data/app/controllers/spree/api/v3/admin/translations_controller.rb +1 -1
  39. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +12 -0
  40. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +27 -0
  41. data/app/controllers/spree/api/v3/base_controller.rb +22 -3
  42. data/app/controllers/spree/api/v3/resource_controller.rb +27 -2
  43. data/app/controllers/spree/api/v3/seller/auth_controller.rb +1 -1
  44. data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +5 -2
  45. data/app/controllers/spree/api/v3/seller/invitations/acceptance_links_controller.rb +34 -0
  46. data/app/controllers/spree/api/v3/seller/me_controller.rb +25 -2
  47. data/app/controllers/spree/api/v3/seller/orders_controller.rb +20 -25
  48. data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +1 -1
  49. data/app/controllers/spree/api/v3/seller/resource_controller.rb +4 -0
  50. data/app/controllers/spree/api/v3/store/carts/items_controller.rb +7 -4
  51. data/app/controllers/spree/api/v3/store/carts/payment_sessions_controller.rb +10 -1
  52. data/app/controllers/spree/api/v3/store/carts/payments_controller.rb +5 -1
  53. data/app/controllers/spree/api/v3/store/companies/members_controller.rb +10 -11
  54. data/app/controllers/spree/api/v3/store/companies/orders_controller.rb +6 -0
  55. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +1 -0
  56. data/app/controllers/spree/api/v3/store/products_controller.rb +6 -12
  57. data/app/controllers/spree/api/v3/store/resource_controller.rb +4 -0
  58. data/app/controllers/spree/api/v3/store/wishlist_items_controller.rb +11 -0
  59. data/app/controllers/spree/api/v3/webhooks/fulfillments_controller.rb +10 -6
  60. data/app/controllers/spree/api/v3/webhooks/payments_controller.rb +6 -2
  61. data/app/controllers/spree/api/v3/webhooks/payouts_controller.rb +6 -2
  62. data/app/jobs/spree/webhook_delivery_job.rb +18 -4
  63. data/app/serializers/spree/api/v3/admin/actor_serializer.rb +28 -0
  64. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +14 -1
  65. data/app/serializers/spree/api/v3/admin/claim_serializer.rb +3 -4
  66. data/app/serializers/spree/api/v3/admin/exchange_serializer.rb +3 -4
  67. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +6 -0
  68. data/app/serializers/spree/api/v3/admin/invitation_acceptance_link_serializer.rb +15 -0
  69. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +0 -11
  70. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +28 -1
  71. data/app/serializers/spree/api/v3/admin/order_group_serializer.rb +7 -0
  72. data/app/serializers/spree/api/v3/admin/order_serializer.rb +7 -24
  73. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +6 -0
  74. data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -3
  75. data/app/serializers/spree/api/v3/admin/saved_report_serializer.rb +3 -4
  76. data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +2 -3
  77. data/app/serializers/spree/api/v3/admin/store_serializer.rb +10 -0
  78. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +6 -2
  79. data/app/serializers/spree/api/v3/base_serializer.rb +22 -0
  80. data/app/serializers/spree/api/v3/gift_card_serializer.rb +3 -1
  81. data/app/serializers/spree/api/v3/line_item_serializer.rb +2 -1
  82. data/app/serializers/spree/api/v3/market_serializer.rb +5 -0
  83. data/app/serializers/spree/api/v3/product_filters_serializer.rb +3 -0
  84. data/app/serializers/spree/api/v3/seller/account_serializer.rb +19 -0
  85. data/app/serializers/spree/api/v3/seller/invitation_acceptance_link_serializer.rb +15 -0
  86. data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +1 -10
  87. data/app/services/spree/api/v3/filters_aggregator.rb +5 -2
  88. data/config/routes.rb +7 -0
  89. data/lib/spree/api/configuration.rb +19 -14
  90. data/lib/spree/api/dependencies.rb +4 -0
  91. data/lib/spree/api/engine.rb +6 -0
  92. data/lib/spree/api/openapi/schema_helper.rb +10 -4
  93. metadata +15 -6
@@ -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
 
@@ -43,7 +43,7 @@ module Spree
43
43
  with_order_lock do
44
44
  result = Spree.order_cancel_workflow.call(
45
45
  order: @resource,
46
- canceler: try_spree_current_user,
46
+ canceler: current_actor,
47
47
  reason: cancel_reason,
48
48
  note: params[:cancel_note].presence,
49
49
  refund_payments: params[:refund_payments].to_b,
@@ -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,
@@ -5,6 +5,7 @@ module Spree
5
5
  class ProductsController < ResourceController
6
6
  include Spree::Api::V3::HttpCaching
7
7
  include Spree::Api::V3::Store::SearchProviderSupport
8
+ include Spree::Api::V3::Store::StorefrontProducts
8
9
 
9
10
  protected
10
11
 
@@ -28,19 +29,12 @@ module Spree
28
29
  end
29
30
  end
30
31
 
32
+ # Catalog narrowing for the buyer: their company's effective
33
+ # catalogs, their group's, or the channel default — union of
34
+ # assortments, resolved in one place
35
+ # (docs/plans/6.0-b2b-companies-and-catalogs.md).
31
36
  def scope
32
- base = super.available(Time.current, Spree::Current.currency, include_preorderable: true)
33
-
34
- # Catalog narrowing for the buyer: their company's effective
35
- # catalogs, their group's, or the channel default — union of
36
- # assortments, resolved in one place
37
- # (docs/plans/6.0-b2b-companies-and-catalogs.md).
38
- Spree.products_for_context_service.call(
39
- store: current_store,
40
- channel: current_channel,
41
- customer: current_user,
42
- base: base
43
- ).value
37
+ storefront_products(super)
44
38
  end
45
39
 
46
40
  # these scopes are not automatically picked by ar_lazy_preload gem and we need to explicitly include them
@@ -28,6 +28,10 @@ module Spree
28
28
  def authenticate_request!
29
29
  authenticate_api_key!
30
30
  end
31
+
32
+ def ransack_auth_object
33
+ :store
34
+ end
31
35
  end
32
36
  end
33
37
  end
@@ -3,6 +3,8 @@ module Spree
3
3
  module V3
4
4
  module Store
5
5
  class WishlistItemsController < ResourceController
6
+ include Spree::Api::V3::Store::StorefrontProducts
7
+
6
8
  prepend_before_action :require_authentication!
7
9
 
8
10
  protected
@@ -29,6 +31,15 @@ module Spree
29
31
  def resource_permitted_attributes
30
32
  [:variant_id, :quantity]
31
33
  end
34
+
35
+ # The item renders its variant and product, so the variant must be
36
+ # one the buyer could find in the listing — not a draft, another
37
+ # catalog's product, or an id guessed from the sequence.
38
+ def permitted_params
39
+ @permitted_params ||= super.tap do |attributes|
40
+ attributes[:variant_id] = storefront_variants.find_by_prefix_id!(params[:variant_id]).id if params.key?(:variant_id)
41
+ end
42
+ end
32
43
  end
33
44
  end
34
45
  end
@@ -15,7 +15,6 @@ module Spree
15
15
  # sender's timeout.
16
16
  class FulfillmentsController < ActionController::API
17
17
  include ActionController::RateLimiting
18
- include Spree::Core::ControllerHelpers::Store
19
18
 
20
19
  # Must render — instance_exec'd in a before_action, where only
21
20
  # render/redirect halts the chain.
@@ -31,12 +30,17 @@ module Spree
31
30
  with: RATE_LIMIT_RESPONSE
32
31
 
33
32
  def create
34
- integration = current_store.integrations.active.find_by_prefix_id!(params[:integration_id])
33
+ # By id alone: providers call back without an API key or store
34
+ # header, so the request cannot name the store — the record does.
35
+ # The signature check below, against the record's own secret, is
36
+ # what authenticates the request.
37
+ integration = Spree::Integration.active.find_by_prefix_id!(params[:integration_id])
38
+ Spree::Current.store = integration.store
35
39
 
36
40
  event = integration.parse_webhook_event(request.raw_post, request.headers)
37
41
  return head :ok if event.nil?
38
42
 
39
- delivery = find_delivery(event[:tracking_code])
43
+ delivery = find_delivery(integration.store, event[:tracking_code])
40
44
  return head :ok if delivery.nil?
41
45
 
42
46
  Spree.delivery_update_tracking_workflow.call(
@@ -57,7 +61,7 @@ module Spree
57
61
  private
58
62
 
59
63
  # Scoped to the store the verified integration belongs to — a
60
- # tracking code can never address another tenant's parcel — and to
64
+ # tracking code can never address another store's parcel — and to
61
65
  # consignments whose parcel was not canceled, so a recycled code
62
66
  # cannot reopen an old one. The newest wins when a code was reused.
63
67
  # Carriers reuse tracking numbers across years, so a code can match
@@ -67,14 +71,14 @@ module Spree
67
71
  # database. The newest survivor wins.
68
72
  CANDIDATE_LIMIT = 20
69
73
 
70
- def find_delivery(tracking_code)
74
+ def find_delivery(store, tracking_code)
71
75
  return if tracking_code.blank?
72
76
 
73
77
  # Paged rather than capped: a tracking number reused across
74
78
  # canceled owners would otherwise fill the window and hide the
75
79
  # live delivery behind them, and the webhook would be
76
80
  # acknowledged without applying the carrier's update.
77
- current_store.deliveries.
81
+ store.deliveries.
78
82
  where(tracking_number: tracking_code).
79
83
  order(created_at: :desc, id: :desc).
80
84
  includes(:owner).
@@ -4,7 +4,6 @@ module Spree
4
4
  module Webhooks
5
5
  class PaymentsController < ActionController::API
6
6
  include ActionController::RateLimiting
7
- include Spree::Core::ControllerHelpers::Store
8
7
 
9
8
  # Must render — instance_exec'd in a before_action, where only
10
9
  # render/redirect halts the chain.
@@ -24,7 +23,12 @@ module Spree
24
23
  # Verifies the webhook signature synchronously (returns 401 if invalid),
25
24
  # then enqueues async processing and returns 200 immediately.
26
25
  def create
27
- payment_method = current_store.payment_methods.find_by_prefix_id!(params[:payment_method_id])
26
+ # By id alone: providers call back without an API key or store
27
+ # header, so the request cannot name the store — the record does.
28
+ # The signature check below, against the record's own secret, is
29
+ # what authenticates the request.
30
+ payment_method = Spree::PaymentMethod.find_by_prefix_id!(params[:payment_method_id])
31
+ Spree::Current.store = payment_method.store
28
32
 
29
33
  # Signature verification must be synchronous — invalid = 401
30
34
  result = payment_method.parse_webhook_event(request.raw_post, request.headers)
@@ -19,7 +19,6 @@ module Spree
19
19
  # database writes, well inside any sender's timeout.
20
20
  class PayoutsController < ActionController::API
21
21
  include ActionController::RateLimiting
22
- include Spree::Core::ControllerHelpers::Store
23
22
 
24
23
  # Must render — instance_exec'd in a before_action, where only
25
24
  # render/redirect halts the chain.
@@ -35,7 +34,12 @@ module Spree
35
34
  with: RATE_LIMIT_RESPONSE
36
35
 
37
36
  def create
38
- payment_method = current_store.payment_methods.find_by_prefix_id!(params[:payment_method_id])
37
+ # By id alone: providers call back without an API key or store
38
+ # header, so the request cannot name the store — the record does.
39
+ # The signature check below, against the record's own secret, is
40
+ # what authenticates the request.
41
+ payment_method = Spree::PaymentMethod.find_by_prefix_id!(params[:payment_method_id])
42
+ Spree::Current.store = payment_method.store
39
43
  return head :not_found unless payment_method.respond_to?(:handle_payout_webhook)
40
44
 
41
45
  payment_method.handle_payout_webhook(request.raw_post, request.headers)
@@ -2,11 +2,19 @@
2
2
 
3
3
  module Spree
4
4
  class WebhookDeliveryJob < Spree::BaseJob
5
+ # Raised when the endpoint answered with anything other than 2xx, timed
6
+ # out or could not be reached, so the delivery is sent again with backoff.
7
+ class DeliveryFailed < StandardError; end
8
+
5
9
  queue_as Spree.queues.webhooks
6
10
 
7
- # Webhook delivery hits external endpoints; broad retry covers network timeouts,
8
- # 5xx, DNS failures, etc.
11
+ # Delivery is retry-safe: each attempt overwrites the outcome on the same
12
+ # delivery record, and receivers deduplicate on the event id.
9
13
  retry_on StandardError, wait: :polynomially_longer, attempts: 5
14
+ # The outcome of the last attempt is already on the delivery record, so an
15
+ # exhausted retry is not re-raised into the queue backend.
16
+ retry_on DeliveryFailed, wait: :polynomially_longer, attempts: 5 do |_job, _error|
17
+ end
10
18
  # Must come after `retry_on StandardError` so DeserializationError lands in discard
11
19
  # (ActiveJob handler lookup is reverse-declaration-order).
12
20
  discard_on ActiveJob::DeserializationError
@@ -21,9 +29,15 @@ module Spree
21
29
  delivery = Spree::WebhookDelivery.find_by(id: delivery_id)
22
30
  return if delivery.nil?
23
31
 
32
+ endpoint = delivery.webhook_endpoint
33
+ return if endpoint.nil?
34
+ # A retry stops once the endpoint was switched off, by hand or by auto-disable.
35
+ return if executions > 1 && !endpoint.active?
36
+
24
37
  delivery.payload_secrets = payload_secrets
25
- secret_key = delivery.webhook_endpoint.secret_key
26
- Spree::Webhooks::DeliverWebhook.call(delivery: delivery, secret_key: secret_key)
38
+ Spree::Webhooks::DeliverWebhook.call(delivery: delivery, secret_key: endpoint.secret_key)
39
+
40
+ raise DeliveryFailed, "Webhook delivery #{delivery.prefixed_id} failed" if delivery.failed?
27
41
  end
28
42
  end
29
43
  end
@@ -0,0 +1,28 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Whoever performed an action — an admin user, an API key, or a class
6
+ # an extension registered in `Spree.actor_classes`.
7
+ #
8
+ # One shape for every kind, so a client renders "cancelled by" the same
9
+ # way whether a person clicked the button or a warehouse connector
10
+ # called the endpoint. See docs/plans/6.0-action-actors.md.
11
+ class ActorSerializer < V3::BaseSerializer
12
+ typelize type: [:string, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind'],
13
+ label: [:string, nullable: true]
14
+
15
+ # `admin_user` / `api_key`, never the Ruby class name.
16
+ attribute :type do |actor|
17
+ actor.actor_kind
18
+ end
19
+
20
+ # What a timeline shows: a person's name or email, a key's name.
21
+ attribute :label do |actor|
22
+ actor.actor_label
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -19,6 +19,8 @@ module Spree
19
19
  revoked_at: [:string, nullable: true],
20
20
  last_used_at: [:string, nullable: true],
21
21
  created_by_email: [:string, nullable: true],
22
+ created_by_type: [:string, nullable: true, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind'],
23
+ created_by_label: [:string, nullable: true],
22
24
  channel_id: [:string, nullable: true]
23
25
 
24
26
  attributes :name, :key_type, :token_prefix, :scopes,
@@ -37,8 +39,19 @@ module Spree
37
39
  key.plaintext_token
38
40
  end
39
41
 
42
+ # A key can be minted by another key, which has no email — so the
43
+ # address is answered only for the actors that have one, and
44
+ # `created_by_label` is what a list column should render.
40
45
  attribute :created_by_email do |key|
41
- key.created_by&.email
46
+ key.created_by.try(:email)
47
+ end
48
+
49
+ attribute :created_by_type do |key|
50
+ Spree::Base.polymorphic_api_type(key.created_by_type)
51
+ end
52
+
53
+ attribute :created_by_label do |key|
54
+ key.created_by.try(:actor_label)
42
55
  end
43
56
  end
44
57
  end
@@ -7,13 +7,12 @@ module Spree
7
7
  class ClaimSerializer < V3::ClaimSerializer
8
8
  typelize memo: [:string, nullable: true],
9
9
  metadata: 'Record<string, unknown>',
10
- created_by_id: [:string, nullable: true]
10
+ created_by_id: [:string, nullable: true],
11
+ created_by_type: [:string, nullable: true, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind']
11
12
 
12
13
  attributes :memo, :metadata, created_at: :iso8601, updated_at: :iso8601
13
14
 
14
- attribute :created_by_id do |claim|
15
- claim.created_by&.prefixed_id
16
- end
15
+ actor_attributes :created_by
17
16
 
18
17
  many :claim_line_items,
19
18
  resource: proc { Spree.api.admin_claim_line_item_serializer },
@@ -8,7 +8,8 @@ module Spree
8
8
  typelize memo: [:string, nullable: true],
9
9
  metadata: 'Record<string, unknown>',
10
10
  stock_location_id: [:string, nullable: true],
11
- created_by_id: [:string, nullable: true]
11
+ created_by_id: [:string, nullable: true],
12
+ created_by_type: [:string, nullable: true, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind']
12
13
 
13
14
  attributes :memo, :metadata, created_at: :iso8601, updated_at: :iso8601
14
15
 
@@ -16,9 +17,7 @@ module Spree
16
17
  exchange.stock_location&.prefixed_id
17
18
  end
18
19
 
19
- attribute :created_by_id do |exchange|
20
- exchange.created_by&.prefixed_id
21
- end
20
+ actor_attributes :created_by
22
21
 
23
22
  many :exchange_line_items,
24
23
  resource: proc { Spree.api.admin_exchange_line_item_serializer },
@@ -18,6 +18,12 @@ module Spree
18
18
 
19
19
  attributes created_at: :iso8601, updated_at: :iso8601
20
20
 
21
+ # A bearer credential: in full only to a caller who may read gift
22
+ # cards, masked when the card rides along on an order.
23
+ attribute :code do |gift_card|
24
+ params[:gift_card_codes] ? gift_card.display_code : gift_card.masked_code
25
+ end
26
+
21
27
  attribute :customer_id do |gift_card|
22
28
  gift_card.customer&.prefixed_id
23
29
  end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # The acceptance link of one staff invitation, served on its own so the
6
+ # token never travels with the invitation listing.
7
+ class InvitationAcceptanceLinkSerializer < V3::BaseSerializer
8
+ typelize acceptance_url: :string
9
+
10
+ attributes :acceptance_url
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -13,7 +13,6 @@ module Spree
13
13
  role_name: :string,
14
14
  inviter_email: :string,
15
15
  expires_at: :string,
16
- acceptance_url: :string,
17
16
  invitee_exists: :boolean,
18
17
  store: '{ id: string; name: string }'
19
18
 
@@ -34,16 +33,6 @@ module Spree
34
33
  invitation.inviter.email
35
34
  end
36
35
 
37
- # Absolute URL when a dashboard origin is configured, otherwise the
38
- # path so the SPA can prepend `window.location.origin`.
39
- attribute :acceptance_url do |invitation|
40
- if Spree::Config[:dashboard_url].present?
41
- Rails.application.routes.url_helpers.admin_invitation_acceptance_url(invitation)
42
- else
43
- "/accept-invitation/#{invitation.prefixed_id}?token=#{invitation.token}"
44
- end
45
- end
46
-
47
36
  # Drives the SPA's sign-in vs sign-up branch on the acceptance page.
48
37
  # Looked up by email so an admin who's already on another store sees
49
38
  # the password prompt, not a fresh account form.