spree_core 6.0.2 → 6.0.3

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/app/jobs/spree/addresses/geocode_address_job.rb +6 -3
  3. data/app/jobs/spree/variants/remove_line_item_job.rb +5 -0
  4. data/app/models/concerns/spree/preference_schema.rb +5 -0
  5. data/app/models/concerns/spree/typed_associations.rb +53 -20
  6. data/app/models/spree/address.rb +2 -2
  7. data/app/models/spree/collection.rb +7 -2
  8. data/app/models/spree/order.rb +1 -1
  9. data/app/models/spree/product.rb +15 -5
  10. data/app/models/spree/promotion.rb +1 -2
  11. data/app/services/spree/newsletter/subscribe.rb +1 -1
  12. data/app/services/spree/orders/create_from_cart.rb +6 -2
  13. data/app/tools/spree/agent_tools/create_export.rb +3 -1
  14. data/app/tools/spree/agent_tools/create_import.rb +1 -0
  15. data/app/tools/spree/agent_tools/create_media.rb +2 -1
  16. data/app/tools/spree/agent_tools/create_resource.rb +12 -4
  17. data/app/tools/spree/agent_tools/delete_resource.rb +12 -4
  18. data/app/tools/spree/agent_tools/describe_resource.rb +69 -6
  19. data/app/tools/spree/agent_tools/get_resource.rb +7 -4
  20. data/app/tools/spree/agent_tools/resource_write.rb +15 -2
  21. data/app/tools/spree/agent_tools/search_resources.rb +10 -11
  22. data/app/tools/spree/agent_tools/update_resource.rb +6 -6
  23. data/app/tools/spree/agent_tools/upload_file.rb +1 -0
  24. data/app/workflows/spree/carts/complete.rb +4 -1
  25. data/app/workflows/spree/carts/merge.rb +25 -4
  26. data/app/workflows/spree/orders/complete.rb +39 -14
  27. data/app/workflows/spree/products/activate.rb +7 -2
  28. data/app/workflows/spree/products/archive.rb +7 -2
  29. data/app/workflows/spree/products/draft.rb +7 -2
  30. data/app/workflows/spree/products/status_transition.rb +24 -0
  31. data/config/locales/en.yml +3 -0
  32. data/lib/generators/spree/api_resource/api_resource_generator.rb +35 -3
  33. data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +9 -4
  34. data/lib/generators/spree/api_resource/templates/admin_integration_spec.rb.tt +74 -0
  35. data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +11 -6
  36. data/lib/spree/agent_tool.rb +105 -1
  37. data/lib/spree/agent_tools/default_catalog.rb +16 -21
  38. data/lib/spree/agent_tools/legacy_tools.rb +212 -0
  39. data/lib/spree/agent_tools/operation_catalog.rb +206 -0
  40. data/lib/spree/agent_tools/operation_tool.rb +189 -0
  41. data/lib/spree/agent_tools/registry.rb +65 -31
  42. data/lib/spree/agent_tools/resource_map.rb +128 -5
  43. data/lib/spree/agent_tools.rb +3 -3
  44. data/lib/spree/core/dependencies_helper.rb +96 -40
  45. data/lib/spree/core/engine.rb +2 -1
  46. data/lib/spree/core/version.rb +1 -1
  47. data/lib/spree/core.rb +19 -2
  48. data/lib/spree/testing_support/extension_rake.rb +12 -0
  49. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +34 -0
  50. data/lib/tasks/dependencies.rake +2 -2
  51. metadata +23 -7
  52. data/lib/spree/agent_tools/workflow_endpoints.rb +0 -88
  53. data/lib/spree/agent_tools/workflow_schema.rb +0 -279
  54. data/lib/spree/agent_tools/workflow_tool.rb +0 -654
@@ -14,19 +14,19 @@ module Spree
14
14
  param :resource, description: 'Resource key — call describe_resource for the list', required: true
15
15
  param :id, description: 'The record id as shown in search results', required: true
16
16
  param :attributes, type: :object, description: 'Attributes to change', required: true
17
+ param :parent_id, description: PARENT_ID_DESCRIPTION
17
18
 
18
- def call(resource:, id:, attributes: {})
19
+ def call(resource:, id:, attributes: {}, parent_id: nil)
19
20
  entry, refusal = writable_entry(resource, :update)
20
21
  return refusal if refusal
21
22
 
22
23
  body, rejection = body_for(entry, attributes)
23
24
  return rejection if rejection
24
25
 
25
- response = dispatch.call(
26
- method: :patch,
27
- path: entry.api_path(:update).sub(':id', id.to_s),
28
- body: body
29
- )
26
+ path, missing = write_path(entry, :update, id: id, parent_id: parent_id)
27
+ return missing if missing
28
+
29
+ response = dispatch.call(method: :patch, path: path, body: body)
30
30
 
31
31
  result_for(response, entry, summary: summary(resource: resource, id: id, attributes: attributes))
32
32
  end
@@ -21,6 +21,7 @@ module Spree
21
21
  # passes; whatever uses the file checks its own permission after.
22
22
  permission 'write_products'
23
23
  mutating!
24
+ additive!
24
25
 
25
26
  param :filename, description: 'The file name, with its extension', required: true
26
27
  param :content, description: 'The file contents — text as text, binary base64 encoded',
@@ -292,6 +292,7 @@ module Spree
292
292
  copy_tax_exemptions!(cart, order, line_item_map, fulfillment_map, fee_map)
293
293
  copy_po_document!(cart, order)
294
294
  repoint_money_records!(cart, order)
295
+ Spree.cart_copiers.each { |copier| copier.call(cart: cart, order: order, line_item_map: line_item_map) }
295
296
 
296
297
  order.update_columns(
297
298
  item_total: cart.item_total,
@@ -560,7 +561,9 @@ module Spree
560
561
  # its children is the order this checkout carries on with. No child holds
561
562
  # the confirmation: the customer is confirmed from the group.
562
563
  def complete_orders
563
- result = Spree.order_complete_workflow.call(order: order, payment_pending: payment_pending)
564
+ # carts.complete.validate ran before the payment was taken; a veto now
565
+ # would leave the customer charged for an order that never places.
566
+ result = Spree.order_complete_workflow.call(order: order, payment_pending: payment_pending, validated: true)
564
567
  failure(cart, code: 'completion_failed', message: result.error) if result.failure?
565
568
 
566
569
  return unless result.value.is_a?(Spree::OrderGroup)
@@ -10,7 +10,14 @@ module Spree
10
10
  # cap the merged quantity) and previously required replacing the whole
11
11
  # strategy class.
12
12
  class Merge < Spree::Workflow
13
- hooks :validate, :after_merge
13
+ hooks :validate, :after_items_moved, :after_merge
14
+
15
+ # Every line item of the folded-in cart → the line item now holding its
16
+ # quantity: itself when it moved across, or the surviving cart's line it
17
+ # was added to. Set before +after_items_moved+ runs.
18
+ #
19
+ # @return [Hash{Spree::LineItem => Spree::LineItem}]
20
+ attr_reader :line_item_map
14
21
 
15
22
  # @param cart [Spree::Cart] the surviving cart
16
23
  # @param other_cart [Spree::Cart, nil] the cart folded in and destroyed
@@ -28,6 +35,10 @@ module Spree
28
35
 
29
36
  ApplicationRecord.transaction do
30
37
  step :move_line_items
38
+ # Inside the transaction and before the absorbed lines are deleted,
39
+ # so an extension can move what it keeps against them.
40
+ run_hooks :after_items_moved
41
+ step :destroy_absorbed_line_items
31
42
  step :associate_customer
32
43
  step :destroy_drained_cart
33
44
  end
@@ -46,19 +57,29 @@ module Spree
46
57
  end
47
58
 
48
59
  # A matching item absorbs the quantity; everything else is re-pointed
49
- # at the surviving cart.
60
+ # at the surviving cart. Absorbed items are deleted only after the
61
+ # +after_items_moved+ hook.
50
62
  def move_line_items
51
- other_cart.line_items.reload.each do |other_line_item|
63
+ @line_item_map = other_cart.line_items.reload.to_a.index_with do |other_line_item|
52
64
  existing = matching_line_item(other_line_item)
53
65
 
54
66
  if existing
55
67
  existing.quantity += other_line_item.quantity
56
68
  existing.save!
57
- other_line_item.destroy!
69
+ existing
58
70
  else
59
71
  other_line_item.update!(cart: cart, order: nil)
72
+ other_line_item
60
73
  end
61
74
  end
75
+ # Matching loaded the surviving cart's items before any moved in.
76
+ cart.line_items.reset
77
+ end
78
+
79
+ def destroy_absorbed_line_items
80
+ line_item_map.each do |other_line_item, line_item|
81
+ other_line_item.destroy! unless other_line_item == line_item
82
+ end
62
83
  end
63
84
 
64
85
  # Options-aware comparison (personalization, gift wrapping) with a
@@ -7,6 +7,8 @@ module Spree
7
7
  # may still need to process payments). Idempotent: an already-placed
8
8
  # order halts successfully, so interrupted completions replay safely.
9
9
  class Complete < Spree::Workflow
10
+ hooks :validate
11
+
10
12
  # Set only when the order divided between sellers.
11
13
  attr_reader :order_group
12
14
 
@@ -18,9 +20,13 @@ module Spree
18
20
  # rollup work's call (docs/plans/6.0-b2b-wholesale-shipping.md phase 7)
19
21
  # @param notify_customer [Boolean, nil] admin drafts pass false to
20
22
  # complete silently; nil (checkout) leaves customer notification on
23
+ # @param validated [Boolean] the caller already ran its own veto, so the
24
+ # validate hook is skipped: checkout's finalize, whose
25
+ # carts.complete.validate ran before the payment was taken, and the
26
+ # orders a seller division places beside the one it came from
21
27
  # @return [Spree::ServiceModule::Result] value is the order, or the
22
28
  # Spree::OrderGroup when it divided between sellers
23
- def perform(order:, payment_pending: false, notify_customer: nil)
29
+ def perform(order:, payment_pending: false, notify_customer: nil, validated: false)
24
30
  super
25
31
 
26
32
  order.notify_customer = notify_customer unless notify_customer.nil?
@@ -37,20 +43,30 @@ module Spree
37
43
 
38
44
  step :ensure_not_canceled
39
45
 
40
- order.with_lock do
41
- unless order.reload.placed?
42
- step :process_payments unless payment_pending || !order.payment_required?
43
- step :split_by_seller
44
- step :finalize_fulfillments
45
- step :place_order
46
- step :freeze_tax_exemptions
47
- step :use_coupon_codes
48
- step :redeem_gift_card
49
- step :fulfill_auto_fulfillments
50
- step :ensure_digital_links
51
- end
46
+ placed_here = order.with_lock do
47
+ # A completion running alongside this one placed it while this one
48
+ # waited for the lock. Placed, not merely in that status: it may
49
+ # have been canceled since.
50
+ next false if order.reload.completed?
51
+
52
+ step :validate_order unless validated
53
+ step :process_payments unless payment_pending || !order.payment_required?
54
+ step :split_by_seller
55
+ step :finalize_fulfillments
56
+ step :place_order
57
+ step :freeze_tax_exemptions
58
+ step :use_coupon_codes
59
+ step :redeem_gift_card
60
+ step :fulfill_auto_fulfillments
61
+ step :ensure_digital_links
62
+ true
52
63
  end
53
64
 
65
+ # The run that placed it announces it and finishes what follows; this
66
+ # one answers as a replay would, so nothing after placement happens
67
+ # twice.
68
+ halt!(order.order_group || order) unless placed_here
69
+
54
70
  step :ensure_placed_status
55
71
  step :release_stock_reservations
56
72
  step :cancel_unused_payment_sessions
@@ -67,6 +83,14 @@ module Spree
67
83
  failure(order, 'Order is canceled') if order.canceled?
68
84
  end
69
85
 
86
+ # The extension veto, under the order's row lock: a handler sees the
87
+ # order as it is placed, and anything else that takes the lock to change
88
+ # it waits for the outcome. Rejecting rolls the lock back before any
89
+ # payment is processed.
90
+ def validate_order
91
+ run_hooks :validate
92
+ end
93
+
70
94
  def process_payments
71
95
  order.process_payments! unless payment_covered?
72
96
 
@@ -276,7 +300,8 @@ module Spree
276
300
  end
277
301
 
278
302
  def place_sibling(sibling)
279
- result = Spree.order_complete_workflow.call(order: sibling, payment_pending: true, notify_customer: false)
303
+ result = Spree.order_complete_workflow.call(order: sibling, payment_pending: true, notify_customer: false,
304
+ validated: true)
280
305
  failure(order, result.error) if result.failure?
281
306
  end
282
307
 
@@ -5,6 +5,8 @@ module Spree
5
5
  # after_transition, so a handler can veto the change before anything is
6
6
  # written and see the product it applies to.
7
7
  class Activate < Spree::Workflow
8
+ include StatusTransition
9
+
8
10
  hooks :validate, :after_activate
9
11
 
10
12
  # @param product [Spree::Product]
@@ -14,12 +16,15 @@ module Spree
14
16
 
15
17
  run_hooks :validate
16
18
 
17
- ApplicationRecord.transaction do
19
+ moved = ApplicationRecord.transaction do
20
+ next false if already_in?('active')
21
+
18
22
  step :mark_active
19
23
  run_hooks :after_activate
24
+ true
20
25
  end
21
26
 
22
- product.publish_event('product.activated')
27
+ product.publish_event('product.activated') if moved
23
28
  success(product)
24
29
  end
25
30
 
@@ -3,6 +3,8 @@ module Spree
3
3
  # Takes a product off sale for good. Replaces the `archive` state machine
4
4
  # event.
5
5
  class Archive < Spree::Workflow
6
+ include StatusTransition
7
+
6
8
  hooks :validate, :after_archive
7
9
 
8
10
  # @param product [Spree::Product]
@@ -12,13 +14,16 @@ module Spree
12
14
 
13
15
  run_hooks :validate
14
16
 
15
- ApplicationRecord.transaction do
17
+ moved = ApplicationRecord.transaction do
18
+ next false if already_in?('archived')
19
+
16
20
  step :mark_archived
17
21
  step :withdraw_submission
18
22
  run_hooks :after_archive
23
+ true
19
24
  end
20
25
 
21
- product.publish_event('product.archived')
26
+ product.publish_event('product.archived') if moved
22
27
  success(product)
23
28
  end
24
29
 
@@ -3,6 +3,8 @@ module Spree
3
3
  # Returns a product to draft, hiding it from the storefront while it is
4
4
  # worked on. Replaces the `draft` state machine event.
5
5
  class Draft < Spree::Workflow
6
+ include StatusTransition
7
+
6
8
  hooks :validate, :after_draft
7
9
 
8
10
  # @param product [Spree::Product]
@@ -12,17 +14,20 @@ module Spree
12
14
 
13
15
  run_hooks :validate
14
16
 
15
- ApplicationRecord.transaction do
17
+ moved = ApplicationRecord.transaction do
18
+ next false if already_in?('draft')
19
+
16
20
  step :mark_draft
17
21
  step :withdraw_submission
18
22
  run_hooks :after_draft
23
+ true
19
24
  end
20
25
 
21
26
  # Its siblings (Activate, Archive, Propose, Approve, Reject) all
22
27
  # announce themselves, and this one now carries a marketplace meaning
23
28
  # too: taking a listing back withdraws an open submission, which a
24
29
  # seller's integration has the same reason to hear about.
25
- product.publish_event('product.drafted')
30
+ product.publish_event('product.drafted') if moved
26
31
 
27
32
  success(product)
28
33
  end
@@ -0,0 +1,24 @@
1
+ module Spree
2
+ module Products
3
+ # What the status workflows share: whether the product is already in the
4
+ # status a move would put it in, read under a row lock inside the
5
+ # workflow's own transaction.
6
+ #
7
+ # Two requests for the same move otherwise both read the old status, both
8
+ # write the new one and both publish the event, so every webhook
9
+ # subscriber hears of one change twice. With the lock the second waits,
10
+ # finds the move made, and changes nothing.
11
+ #
12
+ # The status is read by query rather than by reloading the product, so a
13
+ # caller's unsaved edits on the record are kept.
14
+ module StatusTransition
15
+ private
16
+
17
+ # @param status [String]
18
+ # @return [Boolean]
19
+ def already_in?(status)
20
+ product.class.where(id: product.id).lock.pick(:status) == status
21
+ end
22
+ end
23
+ end
24
+ end
@@ -900,6 +900,7 @@ en:
900
900
  invalid_status: Invalid status
901
901
  invalid_storefront_access: is not a valid storefront access level
902
902
  items_not_a_list: items must be a list of objects
903
+ missing_row_type: 'is required (expected one of: %{types})'
903
904
  money_preference_too_many_decimals: has more than %{count} decimal places for %{currency}
904
905
  must_be_greater_than_min_amount: must be greater than or equal to the minimum
905
906
  no_pickup_point_provider: this delivery method has no pickup point provider configured
@@ -909,6 +910,7 @@ en:
909
910
  oidc_missing_code: The identity provider did not return an authorization code.
910
911
  oidc_requires_redirect: This provider requires signing in through its own login page.
911
912
  pickup_point_not_found: pickup point could not be found with this provider
913
+ product_in_review: A product awaiting review is approved or rejected, not moved to another status
912
914
  product_not_in_store: must all belong to this store
913
915
  profile_does_not_accept_provider: "%{provider} deliveries cannot join the %{profile} profile"
914
916
  promotion_discount_not_editable: promotion discounts are managed by recalculation and cannot be edited
@@ -924,6 +926,7 @@ en:
924
926
  store_is_already_set: Store is already set
925
927
  unknown_calculator_type: "%{type} is not a calculator offered here"
926
928
  unknown_custom_field_type: 'is not a known custom field type (expected one of: %{types})'
929
+ unknown_row_type: "%{type} is not a %{family} offered here (expected one of: %{types})"
927
930
  unknown_scopes: 'contains unknown scopes: %{scopes}'
928
931
  unregistered_actor: is not a registered actor class
929
932
  variant_product_mismatch: must belong to the same product as the variant
@@ -169,6 +169,10 @@ module Spree
169
169
  if options[:admin]
170
170
  template 'admin_controller_spec.rb.tt',
171
171
  "spec/controllers/spree/api/v3/admin/#{plural_name}_controller_spec.rb"
172
+ # Documents the endpoints in openapi/admin.yaml (`spree task
173
+ # openapi:generate`): the SDK request types and the agent tools come
174
+ # from there.
175
+ template 'admin_integration_spec.rb.tt', "spec/integration/admin/#{plural_name}_spec.rb"
172
176
  end
173
177
  end
174
178
 
@@ -297,6 +301,9 @@ module Spree
297
301
  if options[:store] || options[:admin]
298
302
  say " #{store_scoped? ? 5 : 4}. Run the specs: pnpm exec spree test spec/controllers/spree/api/v3/"
299
303
  end
304
+ if options[:admin] && !options[:skip_specs]
305
+ say " #{store_scoped? ? 6 : 5}. Document the Admin API (agent tools, SDK types): pnpm exec spree task openapi:generate"
306
+ end
300
307
  say ''
301
308
  end
302
309
 
@@ -344,9 +351,26 @@ module Spree
344
351
  # Attributes the controller permits on write. By default, every column
345
352
  # except references (FKs come through nested), attachments, rich text.
346
353
  def permitted_attribute_names
347
- attributes
348
- .reject { |a| a.reference? || a.attachment? || a.attachments? || a.rich_text? || a.token? || a.password_digest? }
349
- .map(&:name)
354
+ writable_attributes.map(&:name)
355
+ end
356
+
357
+ # @return [Array<Array(String, Symbol, Boolean)>] each writable attribute,
358
+ # its `request_params` type, and whether creating needs it — the model
359
+ # template validates the presence of every one but booleans
360
+ def request_param_attributes
361
+ writable_attributes.map { |a| [a.name, REQUEST_PARAM_TYPES.fetch(a.type, :string), a.type != :boolean] }
362
+ end
363
+
364
+ # Decimal attributes travel as decimal strings (`'1.00'`), never JSON
365
+ # numbers; a money attribute sent as a number is refused.
366
+ #
367
+ # @return [Array<String>]
368
+ def decimal_attribute_names
369
+ writable_attributes.select { |a| a.type == :decimal }.map(&:name)
370
+ end
371
+
372
+ def writable_attributes
373
+ attributes.reject { |a| a.reference? || a.attachment? || a.attachments? || a.rich_text? || a.token? || a.password_digest? }
350
374
  end
351
375
  end
352
376
 
@@ -354,6 +378,14 @@ module Spree
354
378
 
355
379
  INITIALIZER_PATH = 'config/initializers/spree.rb'.freeze
356
380
 
381
+ # A migration column type → the `request_params` type that documents it.
382
+ REQUEST_PARAM_TYPES = {
383
+ string: :string, text: :string, citext: :string,
384
+ integer: :integer, bigint: :integer, float: :number, decimal: :decimal,
385
+ boolean: :boolean, date: :date, datetime: :datetime, timestamp: :datetime,
386
+ json: :hash, jsonb: :hash
387
+ }.freeze
388
+
357
389
  def permission_scope_block
358
390
  <<~RUBY
359
391
 
@@ -9,6 +9,15 @@ module Spree
9
9
  # existing resource name instead if this endpoint belongs under one.
10
10
  scoped_resource :<%= plural_name %>
11
11
 
12
+ # What create and update accept: the controller permits from it, and
13
+ # the integration spec documents it (`request_body_for`), which is
14
+ # where the SDK request types come from.
15
+ request_params :<%= singular_name %> do |body|
16
+ <% request_param_attributes.each do |name, type, required| -%>
17
+ body.attribute :<%= name %>, :<%= type %><%= ', required: true, on: :create' if required %>
18
+ <% end -%>
19
+ end
20
+
12
21
  protected
13
22
 
14
23
  def model_class
@@ -18,10 +27,6 @@ module Spree
18
27
  def serializer_class
19
28
  Spree::Api::V3::Admin::<%= bare_class_name %>Serializer
20
29
  end
21
-
22
- def resource_permitted_attributes
23
- [<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>]
24
- end
25
30
  end
26
31
  end
27
32
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'swagger_helper'
4
+
5
+ # Documents the <%= plural_name.humanize(capitalize: false) %> Admin API in openapi/admin.yaml (`spree task
6
+ # openapi:generate`). The SDK request types come from these operations, and
7
+ # each action other than create, read, update and delete becomes an agent tool.
8
+ RSpec.describe '<%= plural_name.humanize %> Admin API', type: :request, swagger_doc: 'openapi/admin.yaml' do
9
+ let!(:<%= singular_name %>) { create(:<%= singular_name %>) }
10
+
11
+ path '/api/v3/admin/<%= plural_name %>' do
12
+ get 'List <%= plural_name.humanize(capitalize: false) %>' do
13
+ tags '<%= plural_name.humanize %>'
14
+ admin_operation :read, :<%= plural_name %>, description: 'Lists the <%= plural_name.humanize(capitalize: false) %>.'
15
+ parameter name: :page, in: :query, type: :integer, required: false, description: 'Page number'
16
+ parameter name: :limit, in: :query, type: :integer, required: false, description: 'Number of records per page'
17
+ filter_parameters_for
18
+
19
+ response '200', '<%= plural_name.humanize(capitalize: false) %> found' do
20
+ run_test! do |response|
21
+ expect(JSON.parse(response.body)['data'].map { |record| record['id'] }).to include(<%= singular_name %>.prefixed_id)
22
+ end
23
+ end
24
+ end
25
+
26
+ post 'Create a <%= singular_name.humanize(capitalize: false) %>' do
27
+ tags '<%= plural_name.humanize %>'
28
+ admin_operation :write, :<%= plural_name %>, description: 'Creates a <%= singular_name.humanize(capitalize: false) %>.'
29
+ request_body_for
30
+
31
+ response '201', '<%= singular_name.humanize(capitalize: false) %> created' do
32
+ let(:body) { attributes_for(:<%= singular_name %>).slice(<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>)<%= decimal_attribute_names.map { |a| ".merge(#{a}: '1.00')" }.join %> }
33
+
34
+ run_test!
35
+ end
36
+ end
37
+ end
38
+
39
+ path '/api/v3/admin/<%= plural_name %>/{id}' do
40
+ parameter name: :id, in: :path, type: :string, required: true, description: '<%= singular_name.humanize %> ID'
41
+
42
+ let(:id) { <%= singular_name %>.prefixed_id }
43
+
44
+ get 'Get a <%= singular_name.humanize(capitalize: false) %>' do
45
+ tags '<%= plural_name.humanize %>'
46
+ admin_operation :read, :<%= plural_name %>, description: 'Returns a <%= singular_name.humanize(capitalize: false) %>.'
47
+
48
+ response '200', '<%= singular_name.humanize(capitalize: false) %> found' do
49
+ run_test!
50
+ end
51
+ end
52
+
53
+ patch 'Update a <%= singular_name.humanize(capitalize: false) %>' do
54
+ tags '<%= plural_name.humanize %>'
55
+ admin_operation :write, :<%= plural_name %>, description: 'Updates a <%= singular_name.humanize(capitalize: false) %>.'
56
+ request_body_for
57
+
58
+ response '200', '<%= singular_name.humanize(capitalize: false) %> updated' do
59
+ let(:body) { attributes_for(:<%= singular_name %>).slice(<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>)<%= decimal_attribute_names.map { |a| ".merge(#{a}: '1.00')" }.join %> }
60
+
61
+ run_test!
62
+ end
63
+ end
64
+
65
+ delete 'Delete a <%= singular_name.humanize(capitalize: false) %>' do
66
+ tags '<%= plural_name.humanize %>'
67
+ admin_operation :write, :<%= plural_name %>, description: 'Deletes a <%= singular_name.humanize(capitalize: false) %>.'
68
+
69
+ response '204', '<%= singular_name.humanize(capitalize: false) %> deleted' do
70
+ run_test!
71
+ end
72
+ end
73
+ end
74
+ end
@@ -3,6 +3,17 @@ module Spree
3
3
  module V3
4
4
  module Store
5
5
  class <%= plural_name.camelize %>Controller < ResourceController
6
+ <% if writable? -%>
7
+ # What create and update accept: the controller permits from it, and
8
+ # the integration spec documents it (`request_body_for`), which is
9
+ # where the SDK request types come from.
10
+ request_params :<%= singular_name %> do |body|
11
+ <% request_param_attributes.each do |name, type, required| -%>
12
+ body.attribute :<%= name %>, :<%= type %><%= ', required: true, on: :create' if required %>
13
+ <% end -%>
14
+ end
15
+
16
+ <% end -%>
6
17
  protected
7
18
 
8
19
  def model_class
@@ -17,12 +28,6 @@ module Spree
17
28
  def serializer_class
18
29
  Spree::Api::V3::<%= bare_class_name %>Serializer
19
30
  end
20
- <% end -%>
21
- <% if writable? -%>
22
-
23
- def resource_permitted_attributes
24
- [<%= permitted_attribute_names.map { |a| ":#{a}" }.join(', ') %>]
25
- end
26
31
  <% end -%>
27
32
  end
28
33
  end