spree_api 6.0.0.rc3 → 6.0.0.rc4

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/email_template_lookup.rb +6 -1
  3. data/app/controllers/concerns/spree/api/v3/admin/reporting_authorization.rb +17 -8
  4. data/app/controllers/concerns/spree/api/v3/admin/store_context.rb +7 -0
  5. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +61 -0
  6. data/app/controllers/concerns/spree/api/v3/error_handler.rb +49 -0
  7. data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +76 -7
  8. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +60 -3
  9. data/app/controllers/spree/api/v3/admin/companies/tax_exemption_certificates_controller.rb +11 -5
  10. data/app/controllers/spree/api/v3/admin/concerns/external_references.rb +3 -0
  11. data/app/controllers/spree/api/v3/admin/exports_controller.rb +15 -3
  12. data/app/controllers/spree/api/v3/admin/imports_controller.rb +12 -3
  13. data/app/controllers/spree/api/v3/admin/markets_controller.rb +3 -1
  14. data/app/controllers/spree/api/v3/admin/me_controller.rb +21 -1
  15. data/app/controllers/spree/api/v3/admin/oauth/application_tokens_controller.rb +56 -0
  16. data/app/controllers/spree/api/v3/admin/oauth/applications_controller.rb +55 -0
  17. data/app/controllers/spree/api/v3/admin/oauth/authorizations_controller.rb +294 -0
  18. data/app/controllers/spree/api/v3/admin/order_groups_controller.rb +1 -1
  19. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +8 -0
  20. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +1 -0
  21. data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +7 -0
  22. data/app/controllers/spree/api/v3/admin/orders_controller.rb +9 -2
  23. data/app/controllers/spree/api/v3/admin/purchase_orders_controller.rb +1 -0
  24. data/app/controllers/spree/api/v3/admin/reporting_controller.rb +20 -14
  25. data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +1 -1
  26. data/app/controllers/spree/api/v3/admin/tags_controller.rb +8 -5
  27. data/app/controllers/spree/api/v3/admin/tax_exemption_reasons_controller.rb +58 -0
  28. data/app/controllers/spree/api/v3/admin/tax_rates_controller.rb +0 -1
  29. data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +17 -11
  30. data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +61 -0
  31. data/app/controllers/spree/api/v3/oauth/metadata_controller.rb +57 -0
  32. data/app/controllers/spree/api/v3/oauth/server_metadata_controller.rb +57 -0
  33. data/app/controllers/spree/api/v3/resource_controller.rb +1 -0
  34. data/app/controllers/spree/api/v3/store/carts_controller.rb +4 -5
  35. data/app/request_schemas/spree/api/v3/admin/email_template_preview_request_schema.rb +4 -0
  36. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -0
  37. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +2 -0
  38. data/app/serializers/spree/api/v3/admin/oauth_access_token_serializer.rb +62 -0
  39. data/app/serializers/spree/api/v3/admin/oauth_application_serializer.rb +56 -0
  40. data/app/serializers/spree/api/v3/admin/oauth_consent_request_serializer.rb +25 -0
  41. data/app/serializers/spree/api/v3/admin/oauth_protected_resource_serializer.rb +27 -0
  42. data/app/serializers/spree/api/v3/admin/oauth_redirect_serializer.rb +17 -0
  43. data/app/serializers/spree/api/v3/admin/payment_capture_event_serializer.rb +16 -0
  44. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +12 -1
  45. data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -0
  46. data/app/serializers/spree/api/v3/admin/tax_exemption_certificate_serializer.rb +7 -3
  47. data/app/serializers/spree/api/v3/admin/tax_exemption_reason_serializer.rb +20 -0
  48. data/app/serializers/spree/api/v3/admin/tax_rate_serializer.rb +1 -2
  49. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +4 -1
  50. data/app/serializers/spree/api/v3/base_serializer.rb +14 -0
  51. data/config/routes.rb +61 -1
  52. data/lib/spree/api/agent_membership_map.rb +73 -0
  53. data/lib/spree/api/agent_resource_map.rb +350 -0
  54. data/lib/spree/api/agent_workflow_endpoints.rb +113 -0
  55. data/lib/spree/api/agent_write_schemas.rb +119 -0
  56. data/lib/spree/api/dependencies.rb +4 -3
  57. data/lib/spree/api/engine.rb +40 -0
  58. data/lib/spree/api/extension_fields.rb +167 -0
  59. data/lib/spree/api/oauth.rb +243 -0
  60. data/lib/spree/api.rb +8 -0
  61. data/lib/typelizer/open_enums.rb +17 -3
  62. metadata +26 -6
@@ -20,9 +20,10 @@ module Spree
20
20
  # authorization, and rendering.
21
21
  class BatchesController < Admin::BaseController
22
22
  # The batch spans heterogeneous resource types, so a single static
23
- # scope can't gate it. We verify the API key holds the matching
24
- # write_<resource> scope for EVERY entry (see require_batch_scopes!),
25
- # which is stricter than the static check and keeps the audit model.
23
+ # scope can't gate it. We verify the caller holds the matching
24
+ # write_<resource> permission for EVERY entry (see
25
+ # require_batch_scopes!), which is stricter than the static check
26
+ # and keeps the audit model.
26
27
  skip_scope_check!
27
28
 
28
29
  request_params 'TranslationBatchParams' do |body|
@@ -63,19 +64,24 @@ module Spree
63
64
 
64
65
  private
65
66
 
66
- # For API-key callers, require write_<resource> for every distinct
67
- # resource type in the batch. JWT callers bypass (current_api_key is
68
- # nil) and rely on the per-record authorize!(:update, record) above.
69
- # Returns false (and renders 403) when a scope is missing.
67
+ # Require write_<resource> for every distinct resource type in the
68
+ # batch.
69
+ #
70
+ # Asked of the caller rather than of a secret key, because there
71
+ # are three principals and only one of them carries scopes on a
72
+ # key. An OAuth token resolves as a staff user with no key, so
73
+ # reading `current_api_key` here waved it through and the ceiling
74
+ # the merchant consented to went unenforced on the one write this
75
+ # controller performs — `holds_permission?` answers for all three.
76
+ #
77
+ # Returns false (and renders 403) when a permission is missing.
70
78
  def require_batch_scopes!(batch)
71
- return true unless current_api_key
72
-
73
- missing = batch.required_scopes.reject { |scope| current_api_key.has_scope?(scope) }
79
+ missing = batch.required_scopes.reject { |scope| holds_permission?(scope) }
74
80
  return true if missing.empty?
75
81
 
76
82
  render_error(
77
83
  code: ERROR_CODES[:access_denied],
78
- message: "API key lacks scope(s): #{missing.join(', ')}",
84
+ message: "You do not have permission to #{missing.map { |key| key.tr('_', ' ') }.to_sentence}.",
79
85
  status: :forbidden,
80
86
  details: { required_scopes: missing }
81
87
  )
@@ -0,0 +1,61 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Oauth
5
+ # The authorization endpoint a client sends the browser to.
6
+ #
7
+ # It renders nothing itself: consent belongs in the dashboard, where
8
+ # the merchant is already signed in and the screen matches the rest of
9
+ # the admin. This hands the request over with its parameters intact,
10
+ # and the dashboard reads them back through the JSON endpoints under
11
+ # /api/v3/admin/oauth.
12
+ #
13
+ # It is a real route rather than a skipped one because RFC 8414
14
+ # requires the metadata document to name an `authorization_endpoint`,
15
+ # and a consumer client refuses a document that advertises the
16
+ # authorization-code flow without one.
17
+ class AuthorizationsController < ActionController::Base
18
+ include Spree::Core::ControllerHelpers::Store
19
+
20
+ # The authorization endpoint is reached by a cross-site GET on
21
+ # purpose — a client sends the browser here — and forgery
22
+ # protection never applies to a GET, so `:exception` costs nothing
23
+ # while leaving the endpoint reachable. Nothing is granted here
24
+ # either way: the action only redirects, and consent is submitted
25
+ # to the Admin API under the merchant's own session.
26
+ protect_from_forgery with: :exception
27
+
28
+ # Carried across to the dashboard. Everything else is dropped, so a
29
+ # crafted link cannot smuggle extra parameters into the consent page.
30
+ FORWARDED_PARAMETERS = %w[
31
+ client_id redirect_uri response_type scope state
32
+ code_challenge code_challenge_method resource
33
+ ].freeze
34
+
35
+ def new
36
+ redirect_to consent_url, allow_other_host: true
37
+ end
38
+
39
+ private
40
+
41
+ # The client id names the store, not the hostname. An installation
42
+ # serving several stores from one API origin would otherwise send
43
+ # every consent to whichever store that host happens to resolve as,
44
+ # and the merchant would approve on the wrong one.
45
+ def consent_url
46
+ query = params.permit(*FORWARDED_PARAMETERS).to_h.compact_blank.to_query
47
+ base = Spree::Stores::DashboardUrl.call(store: consent_store)
48
+
49
+ "#{base}/#{consent_store.prefixed_id}/oauth/authorize?#{query}"
50
+ end
51
+
52
+ # @return [Spree::Store]
53
+ def consent_store
54
+ @consent_store ||=
55
+ Spree::OauthApplication.find_by(uid: params[:client_id])&.store || current_store
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,57 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Oauth
5
+ # RFC 9728 Protected Resource Metadata — the one discovery document
6
+ # Doorkeeper does not ship (it serves RFC 8414 authorization-server
7
+ # metadata itself).
8
+ #
9
+ # A client reads this to learn which authorization server guards a
10
+ # resource. It is deliberately unauthenticated: a client has no
11
+ # credential at the point it asks.
12
+ class MetadataController < ActionController::API
13
+
14
+ # A public discovery document, read before any credential exists.
15
+ # The module has to be included because `ActionController::API`
16
+ # carries none of it; `:exception` is free here because the only
17
+ # action is a GET, which forgery protection never applies to.
18
+ include ActionController::RequestForgeryProtection
19
+ protect_from_forgery with: :exception
20
+
21
+ # The `resource` value must equal the URL the client was pointed at,
22
+ # path and all, or a consumer client refuses to continue — so it is
23
+ # built from this request's own origin rather than from anything
24
+ # configured elsewhere.
25
+ def show
26
+ resource = Spree::Api::Oauth.resource_identifier(params[:resource_key], request.base_url)
27
+ return head :not_found if resource.blank?
28
+
29
+ document = Spree::OauthProtectedResource.new(
30
+ resource: resource,
31
+ authorization_servers: [issuer],
32
+ # Exactly what the authorization server will accept.
33
+ #
34
+ # The spec tells a client with no scope challenge to request
35
+ # everything advertised here, and Doorkeeper refuses a request
36
+ # naming a scope it does not know — so advertising one more
37
+ # than it accepts turns every connection into "The requested
38
+ # scope is invalid".
39
+ scopes_supported: Spree::Api::Oauth.staff_scope_keys,
40
+ resource_documentation: 'https://spreecommerce.org/docs/developer/mcp/overview'
41
+ )
42
+
43
+ render json: Spree.api.admin_oauth_protected_resource_serializer.new(document).to_h
44
+ end
45
+
46
+ private
47
+
48
+ # The authorization server's own origin. This request reached it, so
49
+ # the origin it arrived on is the one a client can use.
50
+ def issuer
51
+ ::Doorkeeper.config.issuer.presence || request.base_url
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,57 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Oauth
5
+ # RFC 8414 Authorization Server Metadata.
6
+ #
7
+ # The document itself is entirely Doorkeeper's. Only URL generation is
8
+ # Spree's problem: Doorkeeper asks for each endpoint by controller name
9
+ # anchored at the application root, and these routes are drawn inside
10
+ # an isolated engine, which cannot generate from such a name. The
11
+ # injected resolver answers from the engine's own router instead.
12
+ class ServerMetadataController < ::Doorkeeper::MetadataController
13
+ private
14
+
15
+ def metadata_response
16
+ @metadata_response ||= ::Doorkeeper::OAuth::MetadataResponse.new(
17
+ request.base_url,
18
+ ->(**arguments) { resolve_endpoint(**arguments) }
19
+ )
20
+ end
21
+
22
+ # Doorkeeper hands over a controller name anchored at the
23
+ # application root (`/api/v3/…`), while the engine's router knows
24
+ # its own controllers by their engine-qualified names
25
+ # (`spree/api/v3/…`). Doorkeeper's own controllers are already
26
+ # absolute and generate from the application router unchanged.
27
+ def resolve_endpoint(**arguments)
28
+ # Doorkeeper prepends a slash to whatever the route mapping
29
+ # held, so a name configured as "/doorkeeper/tokens" arrives
30
+ # doubled.
31
+ name = arguments[:controller].to_s.sub(%r{\A/+}, '')
32
+
33
+ if name.start_with?('doorkeeper/')
34
+ return Spree::Core::Engine.routes.url_for(
35
+ **arguments.except(:controller),
36
+ controller: "/#{name}",
37
+ only_path: false,
38
+ host: request.host,
39
+ protocol: request.protocol,
40
+ port: request.optional_port
41
+ )
42
+ end
43
+
44
+ Spree::Core::Engine.routes.url_for(
45
+ **arguments.except(:controller),
46
+ controller: "spree/#{name}",
47
+ only_path: false,
48
+ host: request.host,
49
+ protocol: request.protocol,
50
+ port: request.optional_port
51
+ )
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -268,6 +268,7 @@ module Spree
268
268
  preload_associations_lazily.
269
269
  ransack(ransack_params, auth_object: ransack_auth_object)
270
270
  result = @search.result(distinct: collection_distinct?)
271
+ result = result.with_translation_fallback_order if model_class.include?(Spree::TranslatableResource)
271
272
  pagy_options = { limit: limit, page: page }
272
273
  result = apply_collection_sort(result)
273
274
  @pagy, @collection = pagy(result, **pagy_options)
@@ -123,7 +123,7 @@ module Spree
123
123
  sweep_unavailable_coupon_code!
124
124
  render_cart
125
125
  else
126
- render_service_error(result.error, code: ERROR_CODES[:validation_error])
126
+ render_result_failure(result, code: ERROR_CODES[:validation_error])
127
127
  end
128
128
  end
129
129
  end
@@ -181,11 +181,10 @@ module Spree
181
181
  if result.success?
182
182
  @cart = result.value
183
183
  render_order
184
+ elsif result.error.to_s.present?
185
+ render_result_failure(result, code: ERROR_CODES[:cart_cannot_complete])
184
186
  else
185
- render_service_error(
186
- result.error.to_s.presence || 'Could not complete checkout',
187
- code: ERROR_CODES[:cart_cannot_complete]
188
- )
187
+ render_service_error('Could not complete checkout', code: ERROR_CODES[:cart_cannot_complete])
189
188
  end
190
189
  rescue ActiveRecord::RecordNotFound
191
190
  # Gateway-race retry with the id the client got back: the webhook
@@ -25,6 +25,10 @@ module Spree
25
25
  Spree::Emails::Branding::COLORS.each { |name| branding.attribute name, :string, nullable: true }
26
26
  branding.attribute :font, :string, nullable: true, choices: Spree::Emails::Branding::FONTS.keys
27
27
  end
28
+ attribute :mailer_logo_signed_id, :string,
29
+ nullable: true,
30
+ permit: false,
31
+ description: 'An unsaved email logo: the signed id of an uploaded image, or null to preview without one.'
28
32
  end
29
33
  end
30
34
  end
@@ -3,6 +3,8 @@ module Spree
3
3
  module V3
4
4
  module Admin
5
5
  class FulfillmentSerializer < V3::FulfillmentSerializer
6
+ include Concerns::ExternalReferencesAttribute
7
+
6
8
  without_formatted_money
7
9
 
8
10
  # The Admin API has no guest gating — money fields inherited from the
@@ -5,6 +5,8 @@ module Spree
5
5
  # Admin API Line Item Serializer
6
6
  # Extends the store serializer with metadata visibility
7
7
  class LineItemSerializer < V3::LineItemSerializer
8
+ include Concerns::ExternalReferencesAttribute
9
+
8
10
  without_formatted_money
9
11
 
10
12
  # The Admin API has no guest gating — money fields inherited from the
@@ -0,0 +1,62 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # One person's live access through a connected client.
6
+ #
7
+ # The token itself is never serialized: it is a bearer credential, and
8
+ # the screen that lists connections only needs to say whose access it
9
+ # is and what that person approved.
10
+ class OauthAccessTokenSerializer < V3::BaseSerializer
11
+ typelize owner_id: [:string, nullable: true],
12
+ owner_type: [:string, nullable: true],
13
+ scopes: [:string, multi: true],
14
+ expired: :boolean
15
+
16
+ attributes created_at: :iso8601
17
+
18
+ # Who connected, without expanding: the id and the kind, so a
19
+ # client can render a reference or follow it.
20
+ attribute :owner_id do |token|
21
+ token.resource_owner.try(:prefixed_id)
22
+ end
23
+
24
+ attribute :owner_type do |token|
25
+ Spree::Base.polymorphic_api_type(token.resource_owner_type)
26
+ end
27
+
28
+ # Who connected, as the record itself rather than a summary of it,
29
+ # so the dashboard shows a person the way it does everywhere else:
30
+ # a name over an email, with an avatar.
31
+ #
32
+ # The serializer follows the record's kind, because a grant's owner
33
+ # is polymorphic — an admin today, a customer when a storefront
34
+ # client needs one. Alba hands the proc the object for exactly this.
35
+ one :resource_owner,
36
+ key: :owner,
37
+ resource: proc { |owner| owner_serializer_for(owner) },
38
+ if: proc { expand?('owner') }
39
+
40
+ # @param owner [Object] the grant's resource owner
41
+ # @return [Class] the serializer for that kind of owner
42
+ def self.owner_serializer_for(owner)
43
+ case Spree::Base.polymorphic_api_type(owner.class.name)
44
+ when 'customer' then Spree.api.admin_customer_serializer
45
+ else Spree.api.admin_admin_user_serializer
46
+ end.to_s.constantize
47
+ end
48
+
49
+ # What this person approved, as permission keys.
50
+ attribute :scopes do |token|
51
+ token.scopes.to_a
52
+ end
53
+
54
+ # Whether the current access token has lapsed. The connection is
55
+ # still live — a refresh mints another — but a merchant reading the
56
+ # list deserves to know which agents hold access right now.
57
+ attribute :expired, &:expired?
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,56 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin API serializer for {Spree::OauthApplication}.
6
+ #
7
+ # Exposes `client_id` but never `secret`. The id is public by
8
+ # design — it is what a merchant pastes into a connector — while the
9
+ # secret is null here anyway, because every MCP client is a public
10
+ # client that proves itself with PKCE instead.
11
+ #
12
+ # `scopes` and `last_authorized_at` come from the live tokens rather than
13
+ # the registration — a client is registered once and granted per
14
+ # consent, so what the merchant approved lives on the token.
15
+ class OauthApplicationSerializer < V3::BaseSerializer
16
+ typelize name: :string,
17
+ client_id: :string,
18
+ connected: :boolean,
19
+ scopes: [:string, multi: true],
20
+ last_authorized_at: [:string, nullable: true],
21
+ authorized_at: [:string, nullable: true],
22
+ authorized_by: [:string, nullable: true],
23
+ redirect_uri: [:string, nullable: true]
24
+
25
+ attributes :name, :redirect_uri, created_at: :iso8601, updated_at: :iso8601
26
+
27
+ attribute :client_id, &:uid
28
+
29
+ # A registration nobody has connected and one whose access was
30
+ # revoked both carry no scopes, so the list alone cannot tell them
31
+ # apart — and a merchant pressing Revoke has to see that it worked.
32
+ attribute :connected, &:connected?
33
+
34
+ attribute :scopes do |application|
35
+ application.live_token_scopes
36
+ end
37
+
38
+ attribute :last_authorized_at do |application|
39
+ application.last_authorized_at&.iso8601
40
+ end
41
+
42
+ attribute :authorized_at do |application|
43
+ application.authorized_at&.iso8601
44
+ end
45
+
46
+ # Who allowed this, in the words a person is known by. On the screen
47
+ # that decides whether to revoke, the permission alone is half the
48
+ # story.
49
+ attribute :authorized_by do |application|
50
+ application.authorized_by.try(:actor_label)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin API serializer for {Spree::OauthConsentRequest}.
6
+ #
7
+ # Alba directly rather than through {BaseSerializer}: a consent
8
+ # request is a computed view with no id to expose, and inheriting one
9
+ # would put a null `id` in every response.
10
+ class OauthConsentRequestSerializer
11
+ include Alba::Resource
12
+ include Typelizer::DSL
13
+
14
+ typelize client_name: :string,
15
+ scopes: [:string, multi: true],
16
+ grantable_scopes: [:string, multi: true],
17
+ resource: [:string, nullable: true],
18
+ redirect_uri: :string
19
+
20
+ attributes :client_name, :scopes, :grantable_scopes, :resource, :redirect_uri
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Serializer for {Spree::OauthProtectedResource}, the RFC 9728
6
+ # document a client reads to find the authorization server.
7
+ #
8
+ # Alba directly rather than through {BaseSerializer}: the document is
9
+ # a computed view with no id, and inheriting one would put a null
10
+ # `id` in a document the spec defines exactly.
11
+ class OauthProtectedResourceSerializer
12
+ include Alba::Resource
13
+ include Typelizer::DSL
14
+
15
+ typelize resource: :string,
16
+ authorization_servers: [:string, multi: true],
17
+ scopes_supported: [:string, multi: true],
18
+ bearer_methods_supported: [:string, multi: true],
19
+ resource_documentation: :string
20
+
21
+ attributes :resource, :authorization_servers, :scopes_supported,
22
+ :bearer_methods_supported, :resource_documentation
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin API serializer for {Spree::OauthRedirect}.
6
+ class OauthRedirectSerializer
7
+ include Alba::Resource
8
+ include Typelizer::DSL
9
+
10
+ typelize redirect_uri: :string
11
+
12
+ attributes :redirect_uri
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Serializes Spree::PaymentCaptureEvent — one capture of money a
6
+ # payment had authorized. A payment captured in parts has one per part.
7
+ class PaymentCaptureEventSerializer < V3::BaseSerializer
8
+ attributes created_at: :iso8601, updated_at: :iso8601
9
+
10
+ money_attributes :amount
11
+ typelize amount: [:string, nullable: false]
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -13,11 +13,18 @@ module Spree
13
13
  order_id: [:string, nullable: true],
14
14
  avs_response: [:string, nullable: true],
15
15
  cvv_response_code: [:string, nullable: true],
16
- cvv_response_message: [:string, nullable: true]
16
+ cvv_response_message: [:string, nullable: true],
17
+ gateway_dashboard_payment_url: [:string, nullable: true],
18
+ avs_risky: [:boolean, comment: 'Whether the card issuer reported that the billing address did not match.'],
19
+ cvv_risky: [:boolean, comment: 'Whether the card issuer reported that the security code did not match.']
17
20
 
18
21
  attributes :metadata, :avs_response, :cvv_response_code, :cvv_response_message,
22
+ :gateway_dashboard_payment_url,
19
23
  created_at: :iso8601, updated_at: :iso8601
20
24
 
25
+ attribute(:avs_risky, &:is_avs_risky?)
26
+ attribute(:cvv_risky, &:is_cvv_risky?)
27
+
21
28
  money_attributes :captured_amount
22
29
  typelize captured_amount: [:string, nullable: false]
23
30
 
@@ -41,6 +48,10 @@ module Spree
41
48
  serializer.new(payment.source).to_h
42
49
  end
43
50
 
51
+ many :capture_events,
52
+ resource: proc { Spree.api.admin_payment_capture_event_serializer },
53
+ if: proc { expand?('capture_events') }
54
+
44
55
  one :order,
45
56
  resource: proc { Spree.api.admin_order_serializer },
46
57
  if: proc { expand?('order') }
@@ -5,6 +5,8 @@ module Spree
5
5
  module V3
6
6
  module Admin
7
7
  class ReturnSerializer < V3::ReturnSerializer
8
+ include Concerns::ExternalReferencesAttribute
9
+
8
10
  without_formatted_money
9
11
 
10
12
  typelize memo: [:string, nullable: true],
@@ -5,7 +5,7 @@ module Spree
5
5
  # Admin-only: certificates are back-office evidence with no storefront
6
6
  # surface, so there is no store serializer to extend.
7
7
  class TaxExemptionCertificateSerializer < V3::BaseSerializer
8
- typelize certificate_number: :string, reason_code: :string, status: [:string, enum: Spree::TaxExemptionCertificate.statuses, enum_type_name: 'TaxExemptionCertificateStatus'],
8
+ typelize certificate_number: :string, tax_exemption_reason_id: :string, status: [:string, enum: Spree::TaxExemptionCertificate.statuses, enum_type_name: 'TaxExemptionCertificateStatus'],
9
9
  issuing_authority: [:string, nullable: true],
10
10
  issued_at: [:string, nullable: true], expires_at: [:string, nullable: true],
11
11
  verified_at: [:string, nullable: true],
@@ -17,11 +17,15 @@ module Spree
17
17
  document_url: [:string, nullable: true],
18
18
  metadata: 'Record<string, unknown> | null'
19
19
 
20
- attributes :certificate_number, :reason_code, :status, :issuing_authority, :metadata,
20
+ attributes :certificate_number, :status, :issuing_authority, :metadata,
21
21
  issued_at: :iso8601, expires_at: :iso8601, verified_at: :iso8601,
22
22
  created_at: :iso8601, updated_at: :iso8601
23
23
 
24
- prefixed_id_attributes :company
24
+ prefixed_id_attributes :company, :tax_exemption_reason
25
+
26
+ one :tax_exemption_reason,
27
+ resource: proc { Spree.api.admin_tax_exemption_reason_serializer },
28
+ if: proc { expand?('tax_exemption_reason') }
25
29
 
26
30
  # The jurisdiction the certificate holds in, in the same vocabulary the
27
31
  # tax lines use.
@@ -0,0 +1,20 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin-only: exemption reasons are recorded on certificates, which are
6
+ # back-office evidence with no storefront surface.
7
+ class TaxExemptionReasonSerializer < V3::BaseSerializer
8
+ typelize name: :string, code: :string, active: :boolean, can_be_deleted: :boolean,
9
+ metadata: 'Record<string, unknown> | null'
10
+
11
+ attributes :name, :code, :active, :metadata, created_at: :iso8601, updated_at: :iso8601
12
+
13
+ # Lets the dashboard hide destructive controls instead of offering a
14
+ # delete that the model will refuse.
15
+ attribute :can_be_deleted, &:can_be_deleted?
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -6,7 +6,6 @@ module Spree
6
6
  # customer-facing. What the customer sees is the label on a tax line.
7
7
  class TaxRateSerializer < V3::BaseSerializer
8
8
  typelize name: :string,
9
- included_in_price: :boolean,
10
9
  show_rate_in_label: :boolean,
11
10
  tax_category_id: [:string, nullable: true],
12
11
  store_id: [:string, nullable: true],
@@ -15,7 +14,7 @@ module Spree
15
14
  metadata: ['Record<string, unknown>', nullable: true],
16
15
  deleted_at: [:string, nullable: true]
17
16
 
18
- attributes :name, :included_in_price, :show_rate_in_label, :metadata,
17
+ attributes :name, :show_rate_in_label, :metadata,
19
18
  created_at: :iso8601, updated_at: :iso8601, deleted_at: :iso8601
20
19
 
21
20
  rate_attributes :rate, :rate_percent
@@ -32,8 +32,11 @@ module Spree
32
32
  # shipped still hold live credentials in the column. Null when the
33
33
  # caller cannot read the record the event is about.
34
34
  attribute :payload do |delivery|
35
+ # Fails closed: a reader that passes no gate gets nothing. The
36
+ # payload keeps the record body by design, so a caller holding
37
+ # `read_webhooks` alone must not see customer or payment data.
35
38
  visible = params[:payload_visible]
36
- next nil if visible && !visible.call(delivery)
39
+ next nil unless visible&.call(delivery)
37
40
 
38
41
  Spree::WebhookPayloadRedaction.split(delivery.payload).first
39
42
  end
@@ -224,6 +224,20 @@ module Spree
224
224
 
225
225
  private
226
226
 
227
+ # Adds the fields extensions registered for this class
228
+ # (Spree.api.extension_fields), read from the registry on every
229
+ # render as Spree.ransack is, so nothing is copied onto the class. A
230
+ # field never replaces an attribute the serializer writes itself.
231
+ def attributes_to_hash(object, hash)
232
+ super
233
+ Spree.api.extension_fields.for(self.class).each do |field|
234
+ next if @_attributes.key?(field.name)
235
+
236
+ set_key_and_attribute_body_from(object, field.name, field.reader, hash)
237
+ end
238
+ hash
239
+ end
240
+
227
241
  # Override Alba's fetch_attribute to automatically inject nested expand params
228
242
  # into child serializers via nested_params.
229
243
  # Uses `key` (the public API name, e.g. :categories) for expand lookup,