spree_api 6.0.0.rc3 → 6.0.0.rc5

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 (156) 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/allowed_redirect_url.rb +27 -0
  7. data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +8 -2
  8. data/app/controllers/concerns/spree/api/v3/error_handler.rb +49 -0
  9. data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +76 -7
  10. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +66 -5
  11. data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
  12. data/app/controllers/spree/api/v3/admin/companies/tax_exemption_certificates_controller.rb +11 -5
  13. data/app/controllers/spree/api/v3/admin/concerns/external_references.rb +3 -0
  14. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +30 -8
  15. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
  16. data/app/controllers/spree/api/v3/admin/customers_controller.rb +1 -6
  17. data/app/controllers/spree/api/v3/admin/exports_controller.rb +15 -3
  18. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
  19. data/app/controllers/spree/api/v3/admin/imports_controller.rb +12 -3
  20. data/app/controllers/spree/api/v3/admin/markets_controller.rb +3 -1
  21. data/app/controllers/spree/api/v3/admin/me_controller.rb +21 -1
  22. data/app/controllers/spree/api/v3/admin/oauth/application_tokens_controller.rb +56 -0
  23. data/app/controllers/spree/api/v3/admin/oauth/applications_controller.rb +55 -0
  24. data/app/controllers/spree/api/v3/admin/oauth/authorizations_controller.rb +294 -0
  25. data/app/controllers/spree/api/v3/admin/order_groups_controller.rb +1 -1
  26. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +8 -0
  27. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +1 -0
  28. data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +7 -0
  29. data/app/controllers/spree/api/v3/admin/orders_controller.rb +20 -7
  30. data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
  31. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +3 -4
  32. data/app/controllers/spree/api/v3/admin/products_controller.rb +5 -3
  33. data/app/controllers/spree/api/v3/admin/purchase_orders_controller.rb +1 -0
  34. data/app/controllers/spree/api/v3/admin/reporting_controller.rb +20 -14
  35. data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +1 -1
  36. data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -1
  37. data/app/controllers/spree/api/v3/admin/tags_controller.rb +8 -5
  38. data/app/controllers/spree/api/v3/admin/tax_exemption_reasons_controller.rb +58 -0
  39. data/app/controllers/spree/api/v3/admin/tax_rates_controller.rb +0 -1
  40. data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +17 -11
  41. data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
  42. data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +72 -0
  43. data/app/controllers/spree/api/v3/oauth/metadata_controller.rb +57 -0
  44. data/app/controllers/spree/api/v3/oauth/server_metadata_controller.rb +57 -0
  45. data/app/controllers/spree/api/v3/resource_controller.rb +36 -5
  46. data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
  47. data/app/controllers/spree/api/v3/seller/products_controller.rb +7 -61
  48. data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
  49. data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
  50. data/app/controllers/spree/api/v3/store/carts_controller.rb +7 -8
  51. data/app/controllers/spree/api/v3/store/companies/addresses_controller.rb +4 -0
  52. data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +4 -0
  53. data/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb +19 -1
  54. data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +3 -14
  55. data/app/controllers/spree/api/v3/store/data_request_confirmations_controller.rb +23 -0
  56. data/app/controllers/spree/api/v3/store/data_requests_controller.rb +56 -0
  57. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
  58. data/app/request_schemas/spree/api/v3/admin/email_template_preview_request_schema.rb +4 -0
  59. data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
  60. data/app/request_schemas/spree/api/v3/seller/price_request_schema.rb +20 -0
  61. data/app/request_schemas/spree/api/v3/seller/variant_request_schema.rb +19 -0
  62. data/app/request_schemas/spree/api/v3/variant_request_schema.rb +8 -13
  63. data/app/serializers/spree/api/v3/address_owner_serializer.rb +27 -0
  64. data/app/serializers/spree/api/v3/address_serializer.rb +5 -1
  65. data/app/serializers/spree/api/v3/admin/address_owner_serializer.rb +10 -0
  66. data/app/serializers/spree/api/v3/admin/address_serializer.rb +1 -10
  67. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +1 -1
  68. data/app/serializers/spree/api/v3/admin/catalog_assignment_serializer.rb +1 -1
  69. data/app/serializers/spree/api/v3/admin/catalog_price_serializer.rb +1 -1
  70. data/app/serializers/spree/api/v3/admin/collection_rule_serializer.rb +1 -1
  71. data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -1
  72. data/app/serializers/spree/api/v3/admin/commission_line_serializer.rb +2 -2
  73. data/app/serializers/spree/api/v3/admin/commission_rate_serializer.rb +1 -1
  74. data/app/serializers/spree/api/v3/admin/consent_record_serializer.rb +1 -1
  75. data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
  76. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -0
  77. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +2 -0
  78. data/app/serializers/spree/api/v3/admin/oauth_access_token_serializer.rb +62 -0
  79. data/app/serializers/spree/api/v3/admin/oauth_application_serializer.rb +56 -0
  80. data/app/serializers/spree/api/v3/admin/oauth_consent_request_serializer.rb +25 -0
  81. data/app/serializers/spree/api/v3/admin/oauth_protected_resource_serializer.rb +27 -0
  82. data/app/serializers/spree/api/v3/admin/oauth_redirect_serializer.rb +17 -0
  83. data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
  84. data/app/serializers/spree/api/v3/admin/package_type_serializer.rb +3 -3
  85. data/app/serializers/spree/api/v3/admin/payment_capture_event_serializer.rb +16 -0
  86. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +12 -1
  87. data/app/serializers/spree/api/v3/admin/permission_serializer.rb +1 -1
  88. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +1 -1
  89. data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
  90. data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -0
  91. data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
  92. data/app/serializers/spree/api/v3/admin/seller_serializer.rb +2 -2
  93. data/app/serializers/spree/api/v3/admin/seller_transfer_serializer.rb +1 -1
  94. data/app/serializers/spree/api/v3/admin/shipping_label_serializer.rb +3 -3
  95. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +1 -1
  96. data/app/serializers/spree/api/v3/admin/stock_receipt_item_serializer.rb +2 -2
  97. data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +1 -1
  98. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +1 -1
  99. data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -0
  100. data/app/serializers/spree/api/v3/admin/tax_exemption_certificate_serializer.rb +7 -3
  101. data/app/serializers/spree/api/v3/admin/tax_exemption_reason_serializer.rb +20 -0
  102. data/app/serializers/spree/api/v3/admin/tax_identifier_serializer.rb +2 -2
  103. data/app/serializers/spree/api/v3/admin/tax_line_serializer.rb +1 -1
  104. data/app/serializers/spree/api/v3/admin/tax_rate_serializer.rb +1 -2
  105. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
  106. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +4 -1
  107. data/app/serializers/spree/api/v3/base_serializer.rb +41 -5
  108. data/app/serializers/spree/api/v3/claim_serializer.rb +1 -1
  109. data/app/serializers/spree/api/v3/collection_serializer.rb +1 -1
  110. data/app/serializers/spree/api/v3/company_invitation_serializer.rb +1 -1
  111. data/app/serializers/spree/api/v3/company_serializer.rb +1 -1
  112. data/app/serializers/spree/api/v3/data_request_confirmation_requested_event_serializer.rb +28 -0
  113. data/app/serializers/spree/api/v3/data_request_event_serializer.rb +1 -1
  114. data/app/serializers/spree/api/v3/data_request_serializer.rb +8 -4
  115. data/app/serializers/spree/api/v3/delivery_serializer.rb +1 -1
  116. data/app/serializers/spree/api/v3/delivery_zone_member_serializer.rb +1 -1
  117. data/app/serializers/spree/api/v3/discount_serializer.rb +1 -1
  118. data/app/serializers/spree/api/v3/file_upload_serializer.rb +1 -1
  119. data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
  120. data/app/serializers/spree/api/v3/media_event_serializer.rb +1 -1
  121. data/app/serializers/spree/api/v3/media_serializer.rb +1 -1
  122. data/app/serializers/spree/api/v3/option_type_serializer.rb +1 -1
  123. data/app/serializers/spree/api/v3/order_group_serializer.rb +2 -2
  124. data/app/serializers/spree/api/v3/order_serializer.rb +4 -2
  125. data/app/serializers/spree/api/v3/payment_serializer.rb +1 -1
  126. data/app/serializers/spree/api/v3/product_filter_availability_option_serializer.rb +1 -1
  127. data/app/serializers/spree/api/v3/product_filter_option_serializer.rb +1 -1
  128. data/app/serializers/spree/api/v3/seller/address_serializer.rb +3 -2
  129. data/app/serializers/spree/api/v3/seller/order_serializer.rb +3 -3
  130. data/app/serializers/spree/api/v3/seller/package_type_serializer.rb +3 -3
  131. data/app/serializers/spree/api/v3/seller/profile_serializer.rb +2 -2
  132. data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +1 -1
  133. data/app/serializers/spree/api/v3/seller/tax_identifier_serializer.rb +1 -1
  134. data/app/serializers/spree/api/v3/seller/variant_serializer.rb +1 -1
  135. data/app/serializers/spree/api/v3/shipping_label_event_serializer.rb +1 -1
  136. data/app/serializers/spree/api/v3/stock_movement_serializer.rb +1 -1
  137. data/app/serializers/spree/api/v3/stock_receipt_event_serializer.rb +1 -1
  138. data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
  139. data/app/services/spree/api/v3/filter_table.rb +22 -3
  140. data/app/services/spree/api/v3/request_schema.rb +64 -17
  141. data/config/locales/en.yml +1 -0
  142. data/config/routes.rb +80 -12
  143. data/lib/spree/api/agent_membership_map.rb +73 -0
  144. data/lib/spree/api/agent_resource_map.rb +350 -0
  145. data/lib/spree/api/agent_workflow_endpoints.rb +113 -0
  146. data/lib/spree/api/agent_write_schemas.rb +131 -0
  147. data/lib/spree/api/dependencies.rb +5 -4
  148. data/lib/spree/api/engine.rb +40 -0
  149. data/lib/spree/api/enum_types.rb +118 -0
  150. data/lib/spree/api/extension_fields.rb +167 -0
  151. data/lib/spree/api/oauth.rb +243 -0
  152. data/lib/spree/api/openapi/filter_documentation.rb +8 -1
  153. data/lib/spree/api/typelizer_generation.rb +5 -0
  154. data/lib/spree/api.rb +8 -0
  155. data/lib/typelizer/open_enums.rb +48 -10
  156. metadata +37 -6
@@ -25,6 +25,17 @@ module Spree
25
25
  boolean: 'boolean'
26
26
  }.freeze
27
27
 
28
+ # Columns holding a value from one of the closed code lists the API
29
+ # publishes as a shared type, matched by name (`locale`,
30
+ # `default_locale`, `cost_currency`), so clients filter with that type,
31
+ # not free text.
32
+ CODE_TYPES = {
33
+ 'locale' => 'LocaleCode',
34
+ 'currency' => 'CurrencyCode',
35
+ 'country_code' => 'CountryCode',
36
+ 'country_of_origin' => 'CountryCode'
37
+ }.freeze
38
+
28
39
  # Raised when an endpoint's table cannot be described — a column of a
29
40
  # kind the contract has no predicates for, or two models sharing a name.
30
41
  class Error < StandardError; end
@@ -124,8 +135,9 @@ module Spree
124
135
  self.class.name_for(model)
125
136
  end
126
137
 
127
- # @return [Hash{String => String, Hash}] attribute => value kind, or
128
- # `{ 'enum' => [values] }` for a status
138
+ # @return [Hash{String => String, Hash}] attribute => value kind,
139
+ # `{ 'enum' => [values] }` for a status, or `{ 'code' => type }` for
140
+ # a value from a shared code list
129
141
  def attributes
130
142
  @attributes ||= kinds.reject { |_, kind| kind == :unclassified }.compact
131
143
  end
@@ -210,6 +222,10 @@ module Spree
210
222
  # attribute the model does not have (`position` on a table without
211
223
  # one), `:unclassified` for one the contract cannot describe
212
224
  def attribute_kind(attribute)
225
+ if (code_type = code_type(attribute))
226
+ return { 'code' => code_type }
227
+ end
228
+
213
229
  aliased = model._ransack_aliases[attribute] || model.attribute_aliases[attribute]
214
230
  return attribute_kind(aliased) if aliased
215
231
  return ransacker_kind(model._ransackers[attribute]) if model._ransackers.key?(attribute)
@@ -220,7 +236,6 @@ module Spree
220
236
  if (values = inclusion_values(attribute))
221
237
  return { 'enum' => values }
222
238
  end
223
-
224
239
  column = model.columns_hash[attribute]
225
240
  return COLUMN_KINDS.fetch(column.type, :unclassified) if column
226
241
  return 'text' if translated_attribute?(attribute)
@@ -229,6 +244,10 @@ module Spree
229
244
  :unclassified
230
245
  end
231
246
 
247
+ def code_type(attribute)
248
+ CODE_TYPES.find { |name, _| attribute == name || attribute.end_with?("_#{name}") }&.last
249
+ end
250
+
232
251
  # The fixed list an inclusion validation allows, for a column that
233
252
  # holds one of a known set of values (`payment_status`, `kind`).
234
253
  def inclusion_values(attribute)
@@ -40,20 +40,24 @@ module Spree
40
40
  datetime: { type: :string, format: :'date-time' },
41
41
  id: { type: :string },
42
42
  any: {},
43
- # ISO codes, from the closed lists below.
43
+ # Codes from the closed lists below.
44
44
  country: { type: :string },
45
45
  currency: { type: :string },
46
- locale: { type: :string }
46
+ locale: { type: :string },
47
+ weight_unit: { type: :string },
48
+ dimension_unit: { type: :string }
47
49
  }.freeze
48
50
 
49
- # The ISO lists a `country` or `currency` field takes, published once
50
- # per API as a named component — the same `CountryCode` and
51
- # `CurrencyCode` types the serializers declare, so a value read from
52
- # a response is the type a request takes.
51
+ # The closed lists a code field takes, published once per API as a
52
+ # named component — the same `CountryCode`, `CurrencyCode` and
53
+ # `WeightUnit` types the serializers declare, so a value read from a
54
+ # response is the type a request takes.
53
55
  CODE_LISTS = {
54
56
  country: ['CountryCode', -> { Spree::IsoData.country_codes }],
55
57
  currency: ['CurrencyCode', -> { Spree::Money::Rounding::CURRENCY_CODES }],
56
- locale: ['LocaleCode', -> { Spree::Locales::ALL }]
58
+ locale: ['LocaleCode', -> { Spree::Locales::ALL }],
59
+ weight_unit: ['WeightUnit', -> { Spree::Variant::WEIGHT_UNITS }],
60
+ dimension_unit: ['DimensionUnit', -> { Spree::Variant::DIMENSION_UNITS }]
57
61
  }.freeze
58
62
 
59
63
  ACTIONS = %i[create update].freeze
@@ -119,7 +123,7 @@ module Spree
119
123
  end
120
124
  end
121
125
 
122
- attr_reader :name, :fields
126
+ attr_reader :name, :fields, :base
123
127
 
124
128
  # The types an attribute takes: the scalars above, plus `:hash` (any
125
129
  # keys), `:object` (a block or a `shape:`) and `:array` (`of:` a type
@@ -172,7 +176,7 @@ module Spree
172
176
  value.is_a?(Class) ? value.definition : value
173
177
  end
174
178
 
175
- %i[attribute include_attributes require_attributes external_references].each do |method|
179
+ %i[attribute extends include_attributes require_attributes external_references].each do |method|
176
180
  define_method(method) { |*args, **options, &block| definition.public_send(method, *args, **options, &block) }
177
181
  end
178
182
  end
@@ -229,6 +233,19 @@ module Spree
229
233
  TEXT
230
234
  end
231
235
 
236
+ # Builds on a published shape: the body accepts all of its attributes,
237
+ # and the published type is that shape's type plus what this schema
238
+ # adds, so clients write one type wherever the shape is accepted.
239
+ #
240
+ # @param other [RequestSchema, Class] a shape with a published name
241
+ def extends(other)
242
+ parent = RequestSchema.resolve(other)
243
+ raise ArgumentError, "#{parent.class.name} has no published name to extend" unless shape?(parent)
244
+
245
+ include_attributes(parent)
246
+ @base = parent
247
+ end
248
+
232
249
  # Adds the attributes of another schema, for endpoints that extend a
233
250
  # shared set.
234
251
  #
@@ -253,6 +270,19 @@ module Spree
253
270
  @permit_lists = nil
254
271
  end
255
272
 
273
+ # Replaces the description of an attribute this schema took from
274
+ # another, for an endpoint where it behaves differently.
275
+ #
276
+ # @param name [Symbol]
277
+ # @param description [String]
278
+ # @return [void]
279
+ def describe_attribute(name, description)
280
+ field = fields.fetch(name) { raise ArgumentError, "#{name} is not an attribute of this schema" }
281
+ copy = field.dup
282
+ copy.instance_variable_set(:@description, description)
283
+ fields[name] = copy
284
+ end
285
+
256
286
  # The `params.permit` list for an action.
257
287
  #
258
288
  # @param action [Symbol, String, nil] `create` or `update`; nil
@@ -269,12 +299,21 @@ module Spree
269
299
  # @return [Hash]
270
300
  def json_schema(action = nil)
271
301
  action = action&.to_sym
272
- fields = fields_for(action)
273
- required = fields.select { |field| field.required_on?(action) }.map { |field| field.name.to_s }
302
+ # A base field this action does not take would still reach clients
303
+ # through the base's type, so such a body is published in full.
304
+ unless base && base.fields.each_value.all? { |field| field.accepted_on?(action) }
305
+ return object_schema(fields_for(action), action)
306
+ end
274
307
 
275
- schema = { type: :object, properties: fields.to_h { |field| [field.name, property_schema(field)] } }
276
- schema[:required] = required if required.any?
277
- schema
308
+ # Attributes the base describes the same way for this action are
309
+ # left to its type.
310
+ own = fields_for(action).reject do |field|
311
+ inherited = base.fields[field.name]
312
+ inherited && inherited.required_on?(action) == field.required_on?(action) &&
313
+ property_schema(inherited) == property_schema(field)
314
+ end
315
+ reference = { '$ref': "#/components/schemas/#{base.name}" }
316
+ own.any? ? { allOf: [reference, object_schema(own, action)] } : reference
278
317
  end
279
318
 
280
319
  # The component name published for an action.
@@ -291,15 +330,15 @@ module Spree
291
330
  #
292
331
  # @return [Array<RequestSchema>]
293
332
  def shapes
294
- fields.each_value.flat_map do |field|
333
+ (base ? [base, *base.shapes] : []).concat(fields.each_value.flat_map do |field|
295
334
  next [] unless field.schema
296
335
 
297
336
  nested = field.schema.shapes
298
337
  shape?(field.schema) ? [field.schema, *nested] : nested
299
- end.uniq
338
+ end).uniq
300
339
  end
301
340
 
302
- # The ISO code lists this schema's fields reach, nested ones included,
341
+ # The code lists this schema's fields reach, nested ones included,
303
342
  # as the components they publish.
304
343
  #
305
344
  # @return [Hash{String => Hash}]
@@ -374,6 +413,14 @@ module Spree
374
413
  end
375
414
 
376
415
 
416
+ def object_schema(fields, action)
417
+ required = fields.select { |field| field.required_on?(action) }.map { |field| field.name.to_s }
418
+
419
+ schema = { type: :object, properties: fields.to_h { |field| [field.name, property_schema(field)] } }
420
+ schema[:required] = required if required.any?
421
+ schema
422
+ end
423
+
377
424
  def fields_for(action)
378
425
  fields.values.select { |field| field.accepted_on?(action) }
379
426
  end
@@ -14,6 +14,7 @@ en:
14
14
  media_in_use: This file is still in use (%{places}). Pass detach=true to remove it everywhere and delete it, or clear those places first.
15
15
  media_not_deleted: The file could not be deleted. Nothing was changed.
16
16
  media_url_import_requires_product: Importing media from a URL needs a product to import it into. Upload the file instead, or post it to a product's media.
17
+ order_sales_context_exclusive: An order already fixes its sales context, so order_id cannot be combined with %{params}.
17
18
  store_mismatch: The requested store does not match the store this API key belongs to.
18
19
  storefront_login_required: Authentication required to access this store.
19
20
  unsupported_export_type: 'Unsupported export type. Supported types: %{types}'
data/config/routes.rb CHANGED
@@ -43,6 +43,7 @@ Spree::Core::Engine.add_routes do
43
43
  patch :associate
44
44
  post :complete
45
45
  end
46
+ resources :addresses, only: [:index], controller: 'carts/addresses'
46
47
  resources :items, only: [:create, :update, :destroy], controller: 'carts/items'
47
48
  resources :discount_codes, only: [:create, :destroy], controller: 'carts/discount_codes'
48
49
  resources :gift_cards, only: [:create, :destroy], controller: 'carts/gift_cards'
@@ -154,6 +155,10 @@ Spree::Core::Engine.add_routes do
154
155
  # Token-addressed so the emailed link works from a device that was
155
156
  # never signed in.
156
157
  get 'data_requests/:token/download', to: 'data_request_downloads#show', as: :data_request_download
158
+ # Addressed by the confirmation token emailed to the address a
159
+ # customer's own request names, for the same reason.
160
+ get 'data_requests/:token', to: 'data_requests#show', as: :data_request
161
+ post 'data_requests/:token/confirmation', to: 'data_request_confirmations#create', as: :data_request_confirmation
157
162
  # Legacy path — emailed and bookmarked URLs outlive the rename. Removed in 6.1.
158
163
  get 'digitals/:token', to: 'digital_links#show', as: :digital_download
159
164
 
@@ -238,6 +243,27 @@ Spree::Core::Engine.add_routes do
238
243
  post 'auth/password_resets', to: 'password_resets#create'
239
244
  patch 'auth/password_resets/:id', to: 'password_resets#update'
240
245
 
246
+ # OAuth 2.1 consent, read and decided by the dashboard. The admin's
247
+ # own session authorizes it: a secret key may authenticate here but
248
+ # can never grant authority on a person's behalf.
249
+ namespace :oauth do
250
+ get 'authorize', to: 'authorizations#show'
251
+ post 'authorize', to: 'authorizations#create'
252
+ delete 'authorize', to: 'authorizations#destroy'
253
+
254
+ # The OAuth clients registered against this store. Deleting one
255
+ # removes the registration; revoking a connection is the nested
256
+ # `tokens` resource, because that is what revoking deletes.
257
+ #
258
+ # Tokens are plural and addressable: a store registers a client once
259
+ # and every staff member connects to that same registration, so the
260
+ # live tokens are who is connected. Revoking one has to cut off that
261
+ # person and leave their colleagues working.
262
+ resources :applications, except: :show do
263
+ resources :tokens, only: %i[index destroy], controller: 'application_tokens'
264
+ end
265
+ end
266
+
241
267
  # Semantic reporting (docs/plans/6.0-analytics-semantic-layer.md)
242
268
  namespace :reporting do
243
269
  post :query
@@ -261,6 +287,9 @@ Spree::Core::Engine.add_routes do
261
287
  resource :store, only: [:show, :update], controller: 'store' do
262
288
  get :data_sources
263
289
  end
290
+ # The store's own custom fields, addressed by its id like every other
291
+ # parent. The controller only ever resolves the current store.
292
+ resources :stores, only: [], concerns: :custom_fieldable
264
293
 
265
294
  # Staff & access (invitations, admin users, roles, API keys)
266
295
  resources :admin_users, only: [:index, :show, :update, :destroy]
@@ -324,7 +353,7 @@ Spree::Core::Engine.add_routes do
324
353
  end
325
354
 
326
355
  # Products
327
- resources :product_types do
356
+ resources :product_types, concerns: :custom_fieldable do
328
357
  post :apply_to_products, on: :member
329
358
  end
330
359
  resources :products, concerns: [:custom_fieldable, :translatable] do
@@ -366,7 +395,7 @@ Spree::Core::Engine.add_routes do
366
395
  # Media library — every file in the store, placed or not. Files are put
367
396
  # ON a product through the nested media routes above; this is where they
368
397
  # are uploaded, browsed and deleted.
369
- resources :media, controller: 'media_library', only: [:index, :show, :create, :update, :destroy] do
398
+ resources :media, controller: 'media_library', only: [:index, :show, :create, :update, :destroy], concerns: :custom_fieldable do
370
399
  get :usage, on: :member
371
400
  end
372
401
 
@@ -401,7 +430,7 @@ Spree::Core::Engine.add_routes do
401
430
  resources :tax_categories
402
431
 
403
432
  # Tax Rates — the internal tax provider's configuration
404
- resources :tax_rates
433
+ resources :tax_rates, concerns: :custom_fieldable
405
434
 
406
435
  # Selectable tax engines and their declared limits (discovery only)
407
436
  resources :tax_providers, only: [:index]
@@ -409,11 +438,12 @@ Spree::Core::Engine.add_routes do
409
438
  # preference, so there is nothing here to create.
410
439
  resources :payout_providers, only: [:index]
411
440
 
412
- # Return / claim / refund / cancellation reasons (dropdowns + settings management)
441
+ # Return / claim / refund / cancellation / tax exemption reasons (dropdowns + settings management)
413
442
  resources :return_reasons
414
443
  resources :claim_reasons
415
444
  resources :refund_reasons
416
445
  resources :order_cancellation_reasons
446
+ resources :tax_exemption_reasons
417
447
 
418
448
  # Markets
419
449
  resources :markets
@@ -425,7 +455,7 @@ Spree::Core::Engine.add_routes do
425
455
  post :bulk_upsert, on: :collection
426
456
  end
427
457
  resources :stock_movements, only: [:index, :show]
428
- resources :stock_transfers do
458
+ resources :stock_transfers, concerns: :custom_fieldable do
429
459
  member do
430
460
  patch :mark_ready
431
461
  patch :mark_in_transit
@@ -438,7 +468,7 @@ Spree::Core::Engine.add_routes do
438
468
 
439
469
  # Purchasing
440
470
  resources :suppliers
441
- resources :purchase_orders do
471
+ resources :purchase_orders, concerns: :custom_fieldable do
442
472
  member do
443
473
  patch :mark_ordered
444
474
  patch :mark_draft
@@ -449,7 +479,7 @@ Spree::Core::Engine.add_routes do
449
479
  end
450
480
 
451
481
  # Payment Methods
452
- resources :delivery_methods do
482
+ resources :delivery_methods, concerns: :custom_fieldable do
453
483
  collection do
454
484
  get :calculators
455
485
  get :fulfillment_providers
@@ -470,7 +500,7 @@ Spree::Core::Engine.add_routes do
470
500
  only: [:index, :show, :create, :update, :destroy]
471
501
  end
472
502
 
473
- resources :payment_methods do
503
+ resources :payment_methods, concerns: :custom_fieldable do
474
504
  get :types, on: :collection
475
505
  end
476
506
 
@@ -480,7 +510,7 @@ Spree::Core::Engine.add_routes do
480
510
  end
481
511
 
482
512
  # Promotions, with nested actions/rules/coupon codes.
483
- resources :promotions do
513
+ resources :promotions, concerns: :custom_fieldable do
484
514
  resources :promotion_actions, only: [:index, :show, :create, :update, :destroy]
485
515
  resources :promotion_rules, only: [:index, :show, :create, :update, :destroy]
486
516
  resources :coupon_codes, only: [:index, :show]
@@ -613,7 +643,7 @@ Spree::Core::Engine.add_routes do
613
643
  # Business customers — a tree of company nodes. Nested collections are
614
644
  # listed and created under their node and then addressed directly, so
615
645
  # a caller holding an entry id does not have to know its company.
616
- resources :companies do
646
+ resources :companies, concerns: :custom_fieldable do
617
647
  # The node's address book.
618
648
  resources :addresses, controller: 'companies/addresses'
619
649
 
@@ -698,12 +728,12 @@ Spree::Core::Engine.add_routes do
698
728
  end
699
729
 
700
730
  # Gift cards
701
- resources :gift_cards
731
+ resources :gift_cards, concerns: :custom_fieldable
702
732
  resources :gift_card_batches, only: [:index, :show, :create]
703
733
 
704
734
  # Store credits across all customers. Read-only: issuing, editing and
705
735
  # deleting a credit stays nested under the customer that holds it.
706
- resources :store_credits, only: [:index, :show] do
736
+ resources :store_credits, only: [:index, :show], concerns: :custom_fieldable do
707
737
  resources :events, only: [:index], controller: 'store_credits/events'
708
738
  end
709
739
 
@@ -1052,6 +1082,14 @@ Spree::Core::Engine.add_routes do
1052
1082
  end
1053
1083
  end
1054
1084
 
1085
+ # RFC 9728 Protected Resource Metadata. One document per protected
1086
+ # resource, named by the surface that registered it, so a client
1087
+ # discovers which authorization server guards the URL it was given.
1088
+ # Unauthenticated: a client has no credential at the point it asks.
1089
+ get 'oauth/protected-resource/:resource_key',
1090
+ to: 'oauth/metadata#show',
1091
+ as: :oauth_protected_resource
1092
+
1055
1093
  # Webhooks (outside of store namespace — no API key authentication)
1056
1094
  namespace :webhooks do
1057
1095
  post 'payments/:payment_method_id', to: 'payments#create', as: :payment_webhook
@@ -1066,4 +1104,34 @@ Spree::Core::Engine.add_routes do
1066
1104
  end
1067
1105
  end
1068
1106
  end
1107
+
1108
+ # The authorization server's own endpoints: /oauth/authorize, /oauth/token,
1109
+ # /oauth/revoke and RFC 8414 metadata. Outside the versioned API namespace —
1110
+ # the OAuth protocol is not a Spree API resource and its paths are fixed by
1111
+ # the spec and by what clients expect to find.
1112
+ #
1113
+ # `authorizations` is NOT skipped. Skipping it would leave the metadata
1114
+ # document without an `authorization_endpoint`, which is required for the
1115
+ # code flow, so a consumer client would have nowhere to send the browser.
1116
+ # The controller is Spree's own and redirects to the dashboard's consent
1117
+ # screen instead of rendering Doorkeeper's HTML.
1118
+ #
1119
+ # The application-management controllers stay skipped: connected apps are
1120
+ # managed through /api/v3/admin/oauth/applications, under the Admin API's
1121
+ # own authentication.
1122
+ # `controllers` is a block method, not an option to `use_doorkeeper`.
1123
+ #
1124
+ # Doorkeeper's own controllers need a leading slash because these routes are
1125
+ # drawn inside Spree::Core::Engine, which isolates the namespace — a bare
1126
+ # `doorkeeper/tokens` resolves as `Spree::Doorkeeper::Tokens` and fails on
1127
+ # the first request. Spree's own authorizations controller is named from the
1128
+ # application root WITHOUT a slash, because the metadata document builds its
1129
+ # URL with `url_for(controller: "/#{name}")` and adds the slash itself.
1130
+ use_doorkeeper scope: 'oauth' do
1131
+ controllers authorizations: 'api/v3/oauth/authorizations',
1132
+ tokens: '/doorkeeper/tokens',
1133
+ token_info: '/doorkeeper/token_info',
1134
+ metadata: '/spree/api/v3/oauth/server_metadata'
1135
+ skip_controllers :applications, :authorized_applications
1136
+ end
1069
1137
  end
@@ -0,0 +1,73 @@
1
+ module Spree
2
+ module Api
3
+ # Which parents an agent may curate products into, derived from the
4
+ # controllers that adopt `Spree::Api::V3::Admin::ProductMembership`.
5
+ #
6
+ # Read off the controllers for the same reason {AgentResourceMap} is: a
7
+ # parent that adopts the concern later is curated without anyone editing
8
+ # this file, and one that drops it stops being offered.
9
+ module AgentMembershipMap
10
+ class << self
11
+ # @param map [Module]
12
+ # @return [Integer] how many parents were registered
13
+ def install(map: Spree::AgentTools::MembershipMap)
14
+ map.reset!
15
+
16
+ curating_controllers.each do |controller|
17
+ target = target_for(controller)
18
+ next if target.blank?
19
+
20
+ # Addressing a parent an agent cannot otherwise read would hand it
21
+ # a write with no matching read, and no way to name what it changed.
22
+ entry = Spree::AgentTools::ResourceMap.find(target.pluralize)
23
+ next if entry.nil?
24
+
25
+ map.register(
26
+ target: target,
27
+ permission: write_permission_for(entry),
28
+ api_path: membership_path_for(controller)
29
+ )
30
+ end
31
+
32
+ map.all.size
33
+ end
34
+
35
+ private
36
+
37
+ # Where this parent's products are added and removed, read from the
38
+ # routes. The nested path carries the parent's id, which a tool fills
39
+ # in from the parent it was given.
40
+ def membership_path_for(controller)
41
+ route = Spree::Core::Engine.routes.routes.find do |candidate|
42
+ candidate.defaults[:controller] == controller.controller_path &&
43
+ candidate.defaults[:action] == 'create'
44
+ end
45
+
46
+ route&.path&.spec.to_s.sub('(.:format)', '').presence
47
+ end
48
+
49
+ def curating_controllers
50
+ Rails.application.eager_load! unless Rails.application.config.eager_load
51
+
52
+ Spree::Api::V3::Admin::ResourceController.descendants.
53
+ select { |controller| controller.include?(Spree::Api::V3::Admin::ProductMembership) }.
54
+ sort_by(&:name)
55
+ end
56
+
57
+ # Curating is a write to the parent, so it is gated on the parent's
58
+ # own write scope — a price list rides `write_products`, a category
59
+ # `write_categories`, exactly as their controllers do.
60
+ def write_permission_for(entry)
61
+ entry.write_permission.presence || "write_#{entry.permission.to_s.sub(/\Aread_/, '')}"
62
+ end
63
+
64
+ # `Admin::Categories::ProductsController` curates a category. The
65
+ # parent is the namespace the nested controller sits in, which is how
66
+ # the routes name it too.
67
+ def target_for(controller)
68
+ controller.name.to_s.split('::')[-2]&.underscore&.singularize
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end