spree_api 6.0.0.beta1 → 6.0.0.beta2

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/stock_receipt_actions.rb +1 -1
  3. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +11 -0
  4. data/app/controllers/concerns/spree/api/v3/orders/claim_actions.rb +2 -2
  5. data/app/controllers/concerns/spree/api/v3/orders/exchange_actions.rb +3 -3
  6. data/app/controllers/concerns/spree/api/v3/orders/post_sale_actions.rb +1 -1
  7. data/app/controllers/concerns/spree/api/v3/orders/return_actions.rb +3 -3
  8. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +2 -2
  9. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +1 -1
  10. data/app/controllers/spree/api/v3/admin/orders_controller.rb +14 -8
  11. data/app/controllers/spree/api/v3/admin/setup_controller.rb +9 -1
  12. data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -0
  13. data/app/controllers/spree/api/v3/base_controller.rb +15 -0
  14. data/app/controllers/spree/api/v3/resource_controller.rb +16 -1
  15. data/app/controllers/spree/api/v3/seller/orders_controller.rb +1 -1
  16. data/app/serializers/spree/api/v3/admin/actor_serializer.rb +28 -0
  17. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +14 -1
  18. data/app/serializers/spree/api/v3/admin/claim_serializer.rb +3 -4
  19. data/app/serializers/spree/api/v3/admin/exchange_serializer.rb +3 -4
  20. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +28 -1
  21. data/app/serializers/spree/api/v3/admin/order_serializer.rb +7 -24
  22. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +6 -0
  23. data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -3
  24. data/app/serializers/spree/api/v3/admin/saved_report_serializer.rb +3 -4
  25. data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +2 -3
  26. data/app/serializers/spree/api/v3/admin/store_serializer.rb +2 -0
  27. data/app/serializers/spree/api/v3/base_serializer.rb +22 -0
  28. data/lib/spree/api/configuration.rb +19 -14
  29. data/lib/spree/api/dependencies.rb +1 -0
  30. data/lib/spree/api/engine.rb +6 -0
  31. metadata +7 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd13b5167236c50754b03e48d4e7ab4ff5f1c24cdfd13b5a1ea45ff7a2961dd7
4
- data.tar.gz: 77ab648d62775d22902a30ff8dbb4927fd0dab1b93668caad957c99a88bcd1fa
3
+ metadata.gz: 66c29980b5060309f11b480faec57266dc0bf8423b03e820a365e346a7f5f8a7
4
+ data.tar.gz: 0b3f632bfaa9cb6158694e492c3c8266a7b4e5c05c942abad15ce45a99662aea
5
5
  SHA512:
6
- metadata.gz: 36a716f02b07837cf6360d19cb914ef28a1de1e5cef02b0d6cb503bcc11d89afa6f00f36bd1613d0e9de6962197af6830c8068d03963ca257ac6f59c7ab2fa95
7
- data.tar.gz: 388abb4801575690aa02e55a907a1851a1d88570a55681c60dc870f693a784024ed7d352e8fcedf493666bd2999269822e9d7abb579d13f3b8345ea9bb340ae2
6
+ metadata.gz: bde5239802c9065c5b69fd3473b424dc2245c446035ff5ee1dc9cc475ea1de6cfcfbeffd65d11659427df2de94883bed3ae671916605e30155eb7ae9291e7ffe
7
+ data.tar.gz: ad098d05870290fae2ac19c12cf4ed92d91584466723f7992f8ac45beba9669ee1ad1c4f2a3a61c1a292e66b6b2568e6b027b42ba959efcb8f7662e4a5774c8f
@@ -50,7 +50,7 @@ module Spree
50
50
  received_at: params[:received_at],
51
51
  reference: params[:reference],
52
52
  notes: params[:notes],
53
- received_by: try_spree_current_user
53
+ received_by: current_actor
54
54
  }
55
55
  end
56
56
 
@@ -15,6 +15,17 @@ module Spree
15
15
  Spree::Api::V3::JwtAuthentication::JWT_AUDIENCE_ADMIN
16
16
  end
17
17
 
18
+ # A key-authenticated write records the key: it is the thing that gets
19
+ # named, scoped and revoked, so it is what an order's `canceler` or a
20
+ # receipt's `received_by` should point at. A JWT request records the
21
+ # admin. When both credentials are present the JWT user wins here too,
22
+ # matching how permissions resolve below.
23
+ #
24
+ # @return [Object, nil]
25
+ def current_actor
26
+ try_spree_current_user || @current_api_key
27
+ end
28
+
18
29
  # API-key-only requests bypass CanCanCan: the ScopedAuthorization
19
30
  # concern is the authoritative gate (read_/write_ scopes per resource).
20
31
  # JWT admin users keep CanCanCan abilities; if both credentials are
@@ -19,7 +19,7 @@ module Spree
19
19
 
20
20
  # PATCH .../claims/:id/approve
21
21
  def approve
22
- run_workflow(Spree.claim_approve_workflow, approver: try_spree_current_user)
22
+ run_workflow(Spree.claim_approve_workflow, approver: current_actor)
23
23
  end
24
24
 
25
25
  # PATCH .../claims/:id/resolve — a refund, a replacement, or both.
@@ -29,7 +29,7 @@ module Spree
29
29
  refund_method: params[:refund_method] || 'store_credit',
30
30
  amount: params[:amount],
31
31
  replacement_line_item_ids: replacement_line_item_ids,
32
- resolver: try_spree_current_user)
32
+ resolver: current_actor)
33
33
  end
34
34
 
35
35
  # PATCH .../claims/:id/deny
@@ -21,14 +21,14 @@ module Spree
21
21
 
22
22
  # PATCH .../exchanges/:id/approve
23
23
  def approve
24
- run_workflow(Spree.exchange_approve_workflow, approver: try_spree_current_user)
24
+ run_workflow(Spree.exchange_approve_workflow, approver: current_actor)
25
25
  end
26
26
 
27
27
  # PATCH .../exchanges/:id/receive
28
28
  def receive
29
29
  run_workflow(Spree.exchange_receive_workflow,
30
30
  items: items_for_receive,
31
- received_by: try_spree_current_user)
31
+ received_by: current_actor)
32
32
  end
33
33
 
34
34
  # PATCH .../exchanges/:id/fulfill — sends the replacement, settling
@@ -36,7 +36,7 @@ module Spree
36
36
  def fulfill
37
37
  run_workflow(Spree.exchange_fulfill_workflow,
38
38
  refund_method: params[:refund_method] || 'store_credit',
39
- refunder: try_spree_current_user)
39
+ refunder: current_actor)
40
40
  end
41
41
 
42
42
  # PATCH .../exchanges/:id/cancel
@@ -59,7 +59,7 @@ module Spree
59
59
  items: items_for_create,
60
60
  reason: reason_for_create,
61
61
  memo: create_params[:memo],
62
- created_by: try_spree_current_user,
62
+ created_by: current_actor,
63
63
  **arguments
64
64
  )
65
65
 
@@ -21,14 +21,14 @@ module Spree
21
21
 
22
22
  # PATCH .../returns/:id/approve
23
23
  def approve
24
- run_workflow(Spree.return_approve_workflow, approver: try_spree_current_user)
24
+ run_workflow(Spree.return_approve_workflow, approver: current_actor)
25
25
  end
26
26
 
27
27
  # PATCH .../returns/:id/receive — what the warehouse actually counted.
28
28
  def receive
29
29
  run_workflow(Spree.return_receive_workflow,
30
30
  items: items_for_receive,
31
- received_by: try_spree_current_user)
31
+ received_by: current_actor)
32
32
  end
33
33
 
34
34
  # PATCH .../returns/:id/refund
@@ -36,7 +36,7 @@ module Spree
36
36
  run_workflow(Spree.return_refund_workflow,
37
37
  amount: params[:amount],
38
38
  refund_method: params[:refund_method] || 'original_payment',
39
- refunder: try_spree_current_user)
39
+ refunder: current_actor)
40
40
  end
41
41
 
42
42
  # PATCH .../returns/:id/cancel
@@ -47,7 +47,7 @@ module Spree
47
47
  @resource = find_resource
48
48
  authorize!(:update, @resource)
49
49
 
50
- @resource.revoke!(try_spree_current_user)
50
+ @resource.revoke!(current_actor)
51
51
  render json: serialize_resource(@resource)
52
52
  end
53
53
 
@@ -90,7 +90,7 @@ module Spree
90
90
  # happens in `before_validation :generate_token` on the model.
91
91
  def build_resource
92
92
  scope.new(permitted_params).tap do |key|
93
- key.created_by = try_spree_current_user
93
+ key.created_by = current_actor
94
94
  end
95
95
  end
96
96
 
@@ -26,7 +26,7 @@ module Spree
26
26
  payment: payment,
27
27
  amount: params[:amount],
28
28
  reason: reason,
29
- refunder: try_spree_current_user,
29
+ refunder: current_actor,
30
30
  # Names which order is being refunded. Only matters when the
31
31
  # payment is shared by a split checkout, where it covers several
32
32
  # and the payment cannot say which one this is for.
@@ -23,7 +23,7 @@ module Spree
23
23
  result = Spree.order_create_service.call(
24
24
  store: current_store,
25
25
  customer: resolve_customer,
26
- created_by: try_spree_current_user,
26
+ created_by: current_actor,
27
27
  params: order_create_params
28
28
  )
29
29
 
@@ -73,7 +73,7 @@ module Spree
73
73
  with_order_lock do
74
74
  result = Spree.order_cancel_workflow.call(
75
75
  order: @resource,
76
- canceler: try_spree_current_user,
76
+ canceler: current_actor,
77
77
  reason: cancel_reason,
78
78
  note: params[:cancel_note].presence,
79
79
  refund_payments: params[:refund_payments].to_b,
@@ -92,7 +92,7 @@ module Spree
92
92
  # PATCH /api/v3/admin/orders/:id/approve
93
93
  def approve
94
94
  with_order_lock do
95
- @resource.approved_by(try_spree_current_user)
95
+ @resource.approved_by(current_actor)
96
96
  render json: serialize_resource(@resource.reload)
97
97
  end
98
98
  end
@@ -150,11 +150,13 @@ module Spree
150
150
  end
151
151
 
152
152
  # Override scope — Order uses SingleStoreResource (for_store).
153
- # Variant prices are preloaded here rather than via scope_includes,
154
- # which this override bypasses; the serializer reads them per row.
153
+ # Variant prices and each line's price list are named here rather
154
+ # than via scope_includes, which this override bypasses; the
155
+ # serializer reads both per row.
155
156
  def scope
156
157
  base = current_store.orders.accessible_by(current_ability, :show).
157
- includes(line_items: { variant: :prices }).preload_associations_lazily
158
+ includes(line_items: [{ variant: :prices }, { price_list: :catalog }]).
159
+ preload_associations_lazily
158
160
 
159
161
  # Transient completion drafts (status draft + cart_id set) belong
160
162
  # to in-flight checkouts, never to the admin. Admin drafts are the
@@ -190,7 +192,10 @@ module Spree
190
192
  # only its id is reported and that comes off the order's own column.
191
193
  # Variant prices ride along because the admin line-item serializer
192
194
  # reads the base catalog price for every row (the negotiated-price
193
- # comparison); without it each line costs its own price query.
195
+ # comparison); without it each line costs its own price query. Each
196
+ # line's price list and its catalog are named for the same reason —
197
+ # stated rather than left to ar_lazy_preload, which happens to cover
198
+ # them today.
194
199
  def collection_includes
195
200
  # `market` is the withdrawal deadline's other input. Fulfillments
196
201
  # are loaded with their selected rate because the freight summary
@@ -198,7 +203,8 @@ module Spree
198
203
  # fulfillment for a field that is nil on every parcel order.
199
204
  [:customer, :channel, :seller, :external_references, :cancel_reason,
200
205
  :market, { fulfillments: :selected_delivery_rate },
201
- { line_items: { variant: :prices } }, { po_document_attachment: :blob }]
206
+ { line_items: [{ variant: :prices }, { price_list: :catalog }] },
207
+ { po_document_attachment: :blob }]
202
208
  end
203
209
 
204
210
  # Read through the store's own vocabulary, so a reason belonging to
@@ -41,7 +41,7 @@ module Spree
41
41
  # POST /api/v3/admin/auth/setup
42
42
  # Body: { setup_token, email, password, password_confirmation?,
43
43
  # first_name?, last_name?, store_name, country_code,
44
- # locale?, currency? }
44
+ # locale?, currency?, sample_data? }
45
45
  # Currency defaults to the country's own; an unknown code is refused
46
46
  # rather than ignored, since the token is spent in the same request.
47
47
  def create
@@ -63,6 +63,10 @@ module Spree
63
63
  adopt_default_store(user, store)
64
64
  end
65
65
 
66
+ # Outside the lock: the loader needs the admin committed, and it
67
+ # runs for minutes, so it is queued rather than awaited.
68
+ Spree::SampleData::LoadJob.perform_later if sample_data_requested?
69
+
66
70
  refresh_token = Spree::RefreshToken.create_for(user, audience: JWT_AUDIENCE_ADMIN, request_env: request_env_for_token)
67
71
  set_refresh_cookie(refresh_token)
68
72
  render json: auth_response(user)
@@ -167,6 +171,10 @@ module Spree
167
171
  )
168
172
  end
169
173
 
174
+ def sample_data_requested?
175
+ ActiveModel::Type::Boolean.new.cast(params[:sample_data]) == true
176
+ end
177
+
170
178
  def admin_user_params
171
179
  params.permit(:email, :password, :password_confirmation, :first_name, :last_name)
172
180
  end
@@ -81,6 +81,7 @@ module Spree
81
81
  :preferred_storefront_access,
82
82
  :preferred_storefront_url,
83
83
  :preferred_guest_checkout,
84
+ :preferred_always_include_confirm_step,
84
85
  :preferred_company_field_enabled,
85
86
  :preferred_address_requires_company,
86
87
  :preferred_address_requires_phone,
@@ -93,6 +93,21 @@ module Spree
93
93
 
94
94
  alias try_spree_current_user spree_current_user
95
95
 
96
+ # Who performed this request, for the associations that record it —
97
+ # `canceler`, `approver`, `created_by` and their siblings. Answers the
98
+ # signed-in user here; the Admin branch prefers the authenticating
99
+ # secret key, because a key-made write must record the key rather than
100
+ # nobody (see docs/plans/6.0-action-actors.md).
101
+ #
102
+ # Deliberately not `current_api_key || current_user` at this level: on
103
+ # a Store API request the key is the storefront's publishable one, and
104
+ # a customer's own action is not the storefront's.
105
+ #
106
+ # @return [Object, nil] a member of Spree.actor_classes
107
+ def current_actor
108
+ try_spree_current_user
109
+ end
110
+
96
111
  # CanCanCan ability
97
112
  # @return [Spree::Ability]
98
113
  def current_ability
@@ -176,10 +176,25 @@ module Spree
176
176
  def build_resource
177
177
  resource = resource_scope.new
178
178
  resource.assign_attributes(permitted_params) if create_workflow.nil?
179
- resource.created_by = try_spree_current_user if resource.respond_to?(:created_by_id)
179
+ resource.created_by = creator_for(resource) if resource.respond_to?(:created_by_id)
180
180
  resource
181
181
  end
182
182
 
183
+ # The actor to stamp on a new record, or nil when this model cannot
184
+ # hold it. `created_by` is polymorphic on the order operations and
185
+ # still admin-user-only elsewhere until 6.1, so a key-authenticated
186
+ # create of a gift card records nobody rather than raising — the same
187
+ # thing it recorded before keys became actors.
188
+ def creator_for(resource)
189
+ actor = current_actor
190
+ return actor if actor.nil?
191
+
192
+ association = resource.class.reflect_on_association(:created_by)
193
+ return actor if association.nil? || association.polymorphic?
194
+
195
+ actor.is_a?(association.klass) ? actor : nil
196
+ end
197
+
183
198
  # The relation a new record is built on: a nested resource's parent
184
199
  # association, the store's own association for this model, or the
185
200
  # class itself for genuinely global data (countries, roles).
@@ -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,
@@ -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 },
@@ -20,7 +20,11 @@ module Spree
20
20
  cost_price: [:string, nullable: true],
21
21
  tax_category_id: [:string, nullable: true],
22
22
  price_source: [:string, nullable: true],
23
- catalog_price: [:string, nullable: true]
23
+ catalog_price: [:string, nullable: true],
24
+ price_list_id: [:string, nullable: true],
25
+ price_list_name: [:string, nullable: true],
26
+ catalog_id: [:string, nullable: true],
27
+ catalog_name: [:string, nullable: true]
24
28
 
25
29
  # price_source is operational provenance — admin-only, never on the
26
30
  # store serializer.
@@ -33,6 +37,29 @@ module Spree
33
37
  line_item.variant&.amount_in(line_item.currency)&.to_s
34
38
  end
35
39
 
40
+ # Which agreement priced this line. The list id is stamped on the
41
+ # row and never re-resolved (the same reason `catalog_price` above
42
+ # is a base price); the catalog is whichever one owns that list
43
+ # now, so moving a list between catalogs re-labels past orders.
44
+ # Empty on a shop-price or hand-negotiated line. Names ride along
45
+ # beside the ids so a page of lines does not cost a request each to
46
+ # label, as `company_name` already does.
47
+ attribute :price_list_id do |line_item|
48
+ line_item.price_list&.prefixed_id
49
+ end
50
+
51
+ attribute :price_list_name do |line_item|
52
+ line_item.price_list&.name
53
+ end
54
+
55
+ attribute :catalog_id do |line_item|
56
+ line_item.price_list&.catalog&.prefixed_id
57
+ end
58
+
59
+ attribute :catalog_name do |line_item|
60
+ line_item.price_list&.catalog&.name
61
+ end
62
+
36
63
  attribute :cost_price do |line_item|
37
64
  line_item.cost_price&.to_s
38
65
  end
@@ -49,7 +49,10 @@ module Spree
49
49
  store_owner_notification_delivered: :boolean,
50
50
  internal_note: [:string, nullable: true], internal_note_html: [:string, nullable: true],
51
51
  approver_id: [:string, nullable: true],
52
+ approver_type: [:string, nullable: true, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind'],
52
53
  canceler_id: [:string, nullable: true], created_by_id: [:string, nullable: true],
54
+ canceler_type: [:string, nullable: true, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind'],
55
+ created_by_type: [:string, nullable: true, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind'],
53
56
  customer_id: [:string, nullable: true],
54
57
  preferred_stock_location_id: [:string, nullable: true],
55
58
  canceled_at: [:string, nullable: true], approved_at: [:string, nullable: true],
@@ -128,13 +131,10 @@ module Spree
128
131
  order.internal_note.presence
129
132
  end
130
133
 
131
- attribute :approver_id do |order|
132
- order.approver&.prefixed_id
133
- end
134
-
135
- attribute :canceler_id do |order|
136
- order.canceler&.prefixed_id
137
- end
134
+ # Who approved, cancelled and opened it — a member of staff, or the
135
+ # API key an integration called with. An order cancelled through a
136
+ # secret key names the key.
137
+ actor_attributes :approver, :canceler, :created_by
138
138
 
139
139
  attribute :cancel_reason_id do |order|
140
140
  order.cancel_reason&.prefixed_id
@@ -147,10 +147,6 @@ module Spree
147
147
  order.cancel_reason&.name
148
148
  end
149
149
 
150
- attribute :created_by_id do |order|
151
- order.created_by&.prefixed_id
152
- end
153
-
154
150
  attribute :customer_id do |order|
155
151
  order.customer&.prefixed_id
156
152
  end
@@ -189,23 +185,10 @@ module Spree
189
185
  resource: proc { Spree.api.admin_customer_serializer },
190
186
  if: proc { expand?('customer') }
191
187
 
192
- # Staff actors, not customers — these point at Spree.admin_user_class.
193
- one :approver,
194
- resource: proc { Spree.api.admin_admin_user_serializer },
195
- if: proc { expand?('approver') }
196
-
197
- one :canceler,
198
- resource: proc { Spree.api.admin_admin_user_serializer },
199
- if: proc { expand?('canceler') }
200
-
201
188
  one :cancel_reason,
202
189
  resource: proc { Spree.api.admin_order_cancellation_reason_serializer },
203
190
  if: proc { expand?('cancel_reason') }
204
191
 
205
- one :created_by,
206
- resource: proc { Spree.api.admin_admin_user_serializer },
207
- if: proc { expand?('created_by') }
208
-
209
192
 
210
193
  many :returns,
211
194
  resource: proc { Spree.api.admin_return_serializer },
@@ -5,11 +5,17 @@ module Spree
5
5
  class RefundSerializer < V3::RefundSerializer
6
6
  typelize payment_id: [:string, nullable: true],
7
7
  refund_reason_id: [:string, nullable: true],
8
+ refunder_id: [:string, nullable: true],
9
+ refunder_type: [:string, nullable: true, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind'],
8
10
  metadata: 'Record<string, unknown>'
9
11
 
10
12
  attributes :metadata,
11
13
  created_at: :iso8601, updated_at: :iso8601
12
14
 
15
+ # Who issued it — an admin user, or the API key an integration
16
+ # refunded through.
17
+ actor_attributes :refunder
18
+
13
19
  one :payment,
14
20
  resource: proc { Spree.api.admin_payment_serializer },
15
21
  if: proc { expand?('payment') }
@@ -10,6 +10,7 @@ module Spree
10
10
  documents: "Array<{ kind: string; url: string }>",
11
11
  stock_location_id: [:string, nullable: true],
12
12
  created_by_id: [:string, nullable: true],
13
+ created_by_type: [:string, nullable: true, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind'],
13
14
  refunded_total: :string,
14
15
  refundable_total: :string
15
16
 
@@ -19,9 +20,7 @@ module Spree
19
20
  return_record.stock_location&.prefixed_id
20
21
  end
21
22
 
22
- attribute :created_by_id do |return_record|
23
- return_record.created_by&.prefixed_id
24
- end
23
+ actor_attributes :created_by
25
24
 
26
25
  attribute :refunded_total do |return_record|
27
26
  return_record.refunded_total.to_s
@@ -20,11 +20,10 @@ module Spree
20
20
  report.user&.prefixed_id
21
21
  end
22
22
 
23
+ # The same "name, else email" rule every actor answers, so a report's
24
+ # author and an order's canceler read alike.
23
25
  attribute :author_name do |report|
24
- user = report.user
25
- next nil unless user
26
-
27
- user.full_name.presence || user.email
26
+ report.user&.actor_label
28
27
  end
29
28
  end
30
29
  end
@@ -12,6 +12,7 @@ module Spree
12
12
  receivable_type: [:string, enum: %w[purchase_order stock_transfer]],
13
13
  receivable_id: :string,
14
14
  received_by_id: 'string | null',
15
+ received_by_type: [:string, nullable: true, enum: Spree::Actor::BUILT_IN_KINDS, enum_type_name: 'ActorKind'],
15
16
  items_count: :number,
16
17
  quantity_accepted_total: :number,
17
18
  quantity_rejected_total: :number,
@@ -31,9 +32,7 @@ module Spree
31
32
  receipt.receivable&.prefixed_id
32
33
  end
33
34
 
34
- attribute :received_by_id do |receipt|
35
- receipt.received_by.try(:prefixed_id)
36
- end
35
+ actor_attributes :received_by
37
36
 
38
37
  attribute :items_count do |receipt|
39
38
  receipt.items.size
@@ -22,6 +22,7 @@ module Spree
22
22
  preferred_unit_system: :string,
23
23
  preferred_storefront_access: :string,
24
24
  preferred_guest_checkout: :boolean,
25
+ preferred_always_include_confirm_step: :boolean,
25
26
  preferred_company_field_enabled: :boolean,
26
27
  preferred_address_requires_company: :boolean,
27
28
  preferred_address_requires_phone: :boolean,
@@ -69,6 +70,7 @@ module Spree
69
70
  :preferred_unit_system,
70
71
  :preferred_storefront_access,
71
72
  :preferred_guest_checkout,
73
+ :preferred_always_include_confirm_step,
72
74
  :preferred_company_field_enabled,
73
75
  :preferred_address_requires_company,
74
76
  :preferred_address_requires_phone,
@@ -27,6 +27,28 @@ module Spree
27
27
  end
28
28
  end
29
29
 
30
+ # Declares the wire form of an `acted_by` association: the actor's
31
+ # prefixed id, the kind of actor it is, and the expansion. Both
32
+ # halves read off the columns, so naming an actor costs no query and
33
+ # the id and the kind always describe the same row (see
34
+ # docs/plans/6.0-action-actors.md).
35
+ #
36
+ # Declare the matching `typelize` entries alongside, as with any
37
+ # other attribute.
38
+ def self.actor_attributes(*names)
39
+ names.each do |name|
40
+ attribute(:"#{name}_id") { |object| object.acted_by_prefixed_id(name) }
41
+
42
+ attribute(:"#{name}_type") do |object|
43
+ Spree::Base.polymorphic_api_type(object.acted_by_type(name))
44
+ end
45
+
46
+ one name,
47
+ resource: proc { Spree.api.admin_actor_serializer },
48
+ if: proc { expand?(name.to_s) }
49
+ end
50
+ end
51
+
30
52
  # Plain decimal notation. BigDecimal renders 0.06 as "0.6e-1", which
31
53
  # is what a client would otherwise print beside a unit.
32
54
  #
@@ -3,25 +3,30 @@ require 'spree/core/preferences/runtime_configuration'
3
3
  module Spree
4
4
  module Api
5
5
  class Configuration < Spree::Preferences::RuntimeConfiguration
6
- preference :jwt_expiration, :integer, default: 3600 # 1 hour in seconds (customer/store JWT default)
7
- preference :admin_jwt_expiration, :integer, default: 300 # 5 minutes — admin tokens have higher blast radius
8
- preference :jwt_secret_key, :string, default: nil
9
- preference :refresh_token_expiry, :integer, default: 2_592_000 # 30 days in seconds
6
+ preference :jwt_expiration, :integer, default: 3600, env: 'SPREE_JWT_EXPIRATION' # 1 hour in seconds (customer/store JWT default)
7
+ preference :admin_jwt_expiration, :integer, default: 300, env: 'SPREE_ADMIN_JWT_EXPIRATION' # 5 minutes — admin tokens have higher blast radius
8
+ # SPREE_JWT_SECRET_KEY resolves inside the preference, so it sits ahead of
9
+ # the Rails credentials fallback in Spree::Api::V3::JwtAuthentication
10
+ # an operator who sets it means it. The older, unprefixed JWT_SECRET_KEY
11
+ # keeps its place behind credentials so existing deployments are
12
+ # unaffected.
13
+ preference :jwt_secret_key, :string, default: nil, env: 'SPREE_JWT_SECRET_KEY'
14
+ preference :refresh_token_expiry, :integer, default: 2_592_000, env: 'SPREE_REFRESH_TOKEN_EXPIRY' # 30 days in seconds
10
15
 
11
16
  # Rate limiting
12
- preference :rate_limit_per_key, :integer, default: 300 # per publishable API key + client IP (per visitor)
13
- preference :rate_limit_per_secret_key, :integer, default: 600 # per secret API key, across the whole API
14
- preference :rate_limit_window, :integer, default: 60 # window in seconds
15
- preference :rate_limit_login, :integer, default: 5 # per IP
16
- preference :rate_limit_register, :integer, default: 3 # per IP
17
- preference :rate_limit_refresh, :integer, default: 10 # per IP
18
- preference :rate_limit_password_reset, :integer, default: 3 # per IP
17
+ preference :rate_limit_per_key, :integer, default: 300, env: 'SPREE_RATE_LIMIT_PER_KEY' # per publishable API key + client IP (per visitor)
18
+ preference :rate_limit_per_secret_key, :integer, default: 600, env: 'SPREE_RATE_LIMIT_PER_SECRET_KEY' # per secret API key, across the whole API
19
+ preference :rate_limit_window, :integer, default: 60, env: 'SPREE_RATE_LIMIT_WINDOW' # window in seconds
20
+ preference :rate_limit_login, :integer, default: 5, env: 'SPREE_RATE_LIMIT_LOGIN' # per IP
21
+ preference :rate_limit_register, :integer, default: 3, env: 'SPREE_RATE_LIMIT_REGISTER' # per IP
22
+ preference :rate_limit_refresh, :integer, default: 10, env: 'SPREE_RATE_LIMIT_REFRESH' # per IP
23
+ preference :rate_limit_password_reset, :integer, default: 3, env: 'SPREE_RATE_LIMIT_PASSWORD_RESET' # per IP
19
24
 
20
25
  # Request body size limit in bytes
21
- preference :max_request_body_size, :integer, default: 102_400 # 100KB
26
+ preference :max_request_body_size, :integer, default: 102_400, env: 'SPREE_MAX_REQUEST_BODY_SIZE' # 100KB
22
27
 
23
- preference :webhooks_enabled, :boolean, default: true
24
- preference :webhooks_verify_ssl, :boolean, default: !Rails.env.development?
28
+ preference :webhooks_enabled, :boolean, default: true, env: 'SPREE_WEBHOOKS_ENABLED'
29
+ preference :webhooks_verify_ssl, :boolean, default: !Rails.env.development?, env: 'SPREE_WEBHOOKS_VERIFY_SSL'
25
30
  end
26
31
  end
27
32
  end
@@ -181,6 +181,7 @@ module Spree
181
181
  admin_catalog_order_minimum_serializer: 'Spree::Api::V3::Admin::CatalogOrderMinimumSerializer',
182
182
  admin_tax_exemption_certificate_serializer: 'Spree::Api::V3::Admin::TaxExemptionCertificateSerializer',
183
183
  admin_admin_user_serializer: 'Spree::Api::V3::Admin::AdminUserSerializer',
184
+ admin_actor_serializer: 'Spree::Api::V3::Admin::ActorSerializer',
184
185
  admin_seller_team_member_serializer: 'Spree::Api::V3::Admin::SellerTeamMemberSerializer',
185
186
  seller_serializer: 'Spree::Api::V3::SellerSerializer',
186
187
  seller_profile_serializer: 'Spree::Api::V3::Seller::ProfileSerializer',
@@ -24,6 +24,12 @@ module Spree
24
24
  Spree.subscribers << Spree::WebhookEventSubscriber
25
25
  end
26
26
 
27
+ # Runs after initializers so an explicitly assigned preference — which
28
+ # wins over the environment — is never rejected for a stale env var.
29
+ config.after_initialize do
30
+ Spree::Api::Configuration.validate_env!(Spree::Api::Config)
31
+ end
32
+
27
33
  # Warn in production if no dedicated JWT secret key is configured
28
34
  config.after_initialize do
29
35
  next unless Rails.env.production?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0.beta1
4
+ version: 6.0.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-15 00:00:00.000000000 Z
11
+ date: 2026-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 6.0.0.beta1
75
+ version: 6.0.0.beta2
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 6.0.0.beta1
82
+ version: 6.0.0.beta2
83
83
  description: Spree Commerce Store / Admin API and HTTP webhooks
84
84
  email:
85
85
  - hello@spreecommerce.org
@@ -393,6 +393,7 @@ files:
393
393
  - app/models/spree/api_key_ability.rb
394
394
  - app/serializers/concerns/spree/api/v3/admin/translatable.rb
395
395
  - app/serializers/spree/api/v3/address_serializer.rb
396
+ - app/serializers/spree/api/v3/admin/actor_serializer.rb
396
397
  - app/serializers/spree/api/v3/admin/address_serializer.rb
397
398
  - app/serializers/spree/api/v3/admin/admin_user_serializer.rb
398
399
  - app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb
@@ -698,9 +699,9 @@ licenses:
698
699
  - BSD-3-Clause
699
700
  metadata:
700
701
  bug_tracker_uri: https://github.com/spree/spree/issues
701
- changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.0.beta1
702
+ changelog_uri: https://github.com/spree/spree/releases/tag/v6.0.0.beta2
702
703
  documentation_uri: https://docs.spreecommerce.org/
703
- source_code_uri: https://github.com/spree/spree/tree/v6.0.0.beta1
704
+ source_code_uri: https://github.com/spree/spree/tree/v6.0.0.beta2
704
705
  post_install_message:
705
706
  rdoc_options: []
706
707
  require_paths: