spree_api 6.0.0.rc4 → 6.0.0.rc6

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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +23 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/refund_authorization.rb +26 -0
  4. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +1 -0
  5. data/app/controllers/concerns/spree/api/v3/allowed_redirect_url.rb +27 -0
  6. data/app/controllers/concerns/spree/api/v3/api_key_authentication.rb +14 -2
  7. data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +23 -4
  8. data/app/controllers/concerns/spree/api/v3/digital_assets_resource.rb +68 -0
  9. data/app/controllers/concerns/spree/api/v3/error_handler.rb +3 -0
  10. data/app/controllers/concerns/spree/api/v3/jwt_authentication.rb +27 -1
  11. data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +6 -1
  12. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +6 -2
  13. data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +4 -1
  14. data/app/controllers/spree/api/v3/admin/auth_controller.rb +32 -5
  15. data/app/controllers/spree/api/v3/admin/commission_lines_controller.rb +2 -2
  16. data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
  17. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +27 -8
  18. data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +4 -0
  19. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
  20. data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +1 -1
  21. data/app/controllers/spree/api/v3/admin/customers_controller.rb +14 -14
  22. data/app/controllers/spree/api/v3/admin/delivery_profiles/origin_groups_controller.rb +1 -1
  23. data/app/controllers/spree/api/v3/admin/delivery_profiles_controller.rb +1 -1
  24. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
  25. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +2 -0
  26. data/app/controllers/spree/api/v3/admin/me_controller.rb +2 -0
  27. data/app/controllers/spree/api/v3/admin/option_types_controller.rb +13 -0
  28. data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +2 -0
  29. data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +9 -0
  30. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
  31. data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +7 -4
  32. data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +2 -0
  33. data/app/controllers/spree/api/v3/admin/orders_controller.rb +21 -7
  34. data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
  35. data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +7 -1
  36. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +18 -0
  37. data/app/controllers/spree/api/v3/admin/product_types_controller.rb +12 -1
  38. data/app/controllers/spree/api/v3/admin/products/digital_assets_controller.rb +5 -109
  39. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +7 -5
  40. data/app/controllers/spree/api/v3/admin/products_controller.rb +33 -6
  41. data/app/controllers/spree/api/v3/admin/sellers/payouts_controller.rb +4 -3
  42. data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +3 -2
  43. data/app/controllers/spree/api/v3/admin/store_controller.rb +8 -1
  44. data/app/controllers/spree/api/v3/admin/tags_controller.rb +11 -9
  45. data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
  46. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +5 -2
  47. data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +17 -6
  48. data/app/controllers/spree/api/v3/resource_controller.rb +50 -7
  49. data/app/controllers/spree/api/v3/seller/auth_controller.rb +10 -0
  50. data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +2 -0
  51. data/app/controllers/spree/api/v3/seller/me_controller.rb +4 -0
  52. data/app/controllers/spree/api/v3/seller/orders_controller.rb +11 -3
  53. data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
  54. data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +5 -1
  55. data/app/controllers/spree/api/v3/seller/products/digital_assets_controller.rb +67 -0
  56. data/app/controllers/spree/api/v3/seller/products_controller.rb +11 -63
  57. data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
  58. data/app/controllers/spree/api/v3/seller/team_controller.rb +1 -1
  59. data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
  60. data/app/controllers/spree/api/v3/store/carts/store_credits_controller.rb +9 -1
  61. data/app/controllers/spree/api/v3/store/carts_controller.rb +17 -3
  62. data/app/controllers/spree/api/v3/store/companies/addresses_controller.rb +4 -0
  63. data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +4 -0
  64. data/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb +19 -1
  65. data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +3 -14
  66. data/app/controllers/spree/api/v3/store/customers_controller.rb +13 -1
  67. data/app/controllers/spree/api/v3/store/data_request_confirmations_controller.rb +23 -0
  68. data/app/controllers/spree/api/v3/store/data_requests_controller.rb +56 -0
  69. data/app/controllers/spree/api/v3/store/delivery_methods_controller.rb +6 -1
  70. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
  71. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +19 -4
  72. data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
  73. data/app/request_schemas/spree/api/v3/digital_asset_request_schema.rb +25 -0
  74. data/app/request_schemas/spree/api/v3/seller/price_request_schema.rb +20 -0
  75. data/app/request_schemas/spree/api/v3/seller/variant_request_schema.rb +19 -0
  76. data/app/request_schemas/spree/api/v3/variant_request_schema.rb +8 -13
  77. data/app/serializers/concerns/spree/api/v3/digital_asset_details.rb +70 -0
  78. data/app/serializers/spree/api/v3/address_owner_serializer.rb +27 -0
  79. data/app/serializers/spree/api/v3/address_serializer.rb +5 -1
  80. data/app/serializers/spree/api/v3/admin/address_owner_serializer.rb +10 -0
  81. data/app/serializers/spree/api/v3/admin/address_serializer.rb +1 -10
  82. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +1 -1
  83. data/app/serializers/spree/api/v3/admin/catalog_assignment_serializer.rb +1 -1
  84. data/app/serializers/spree/api/v3/admin/catalog_price_serializer.rb +1 -1
  85. data/app/serializers/spree/api/v3/admin/collection_rule_serializer.rb +1 -1
  86. data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -1
  87. data/app/serializers/spree/api/v3/admin/commission_line_serializer.rb +2 -2
  88. data/app/serializers/spree/api/v3/admin/commission_rate_serializer.rb +1 -1
  89. data/app/serializers/spree/api/v3/admin/consent_record_serializer.rb +1 -1
  90. data/app/serializers/spree/api/v3/admin/delivery_origin_group_serializer.rb +2 -2
  91. data/app/serializers/spree/api/v3/admin/delivery_profile_serializer.rb +3 -5
  92. data/app/serializers/spree/api/v3/admin/digital_asset_serializer.rb +3 -47
  93. data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
  94. data/app/serializers/spree/api/v3/admin/newsletter_subscriber_serializer.rb +1 -0
  95. data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
  96. data/app/serializers/spree/api/v3/admin/package_type_serializer.rb +3 -3
  97. data/app/serializers/spree/api/v3/admin/permission_serializer.rb +1 -1
  98. data/app/serializers/spree/api/v3/admin/price_history_serializer.rb +1 -1
  99. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +9 -4
  100. data/app/serializers/spree/api/v3/admin/price_serializer.rb +1 -1
  101. data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
  102. data/app/serializers/spree/api/v3/admin/seller_payout_serializer.rb +2 -2
  103. data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
  104. data/app/serializers/spree/api/v3/admin/seller_serializer.rb +3 -5
  105. data/app/serializers/spree/api/v3/admin/seller_transfer_serializer.rb +1 -1
  106. data/app/serializers/spree/api/v3/admin/shipping_label_serializer.rb +3 -3
  107. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +1 -1
  108. data/app/serializers/spree/api/v3/admin/stock_receipt_item_serializer.rb +2 -2
  109. data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +1 -1
  110. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +1 -1
  111. data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +3 -5
  112. data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -0
  113. data/app/serializers/spree/api/v3/admin/tax_identifier_serializer.rb +2 -2
  114. data/app/serializers/spree/api/v3/admin/tax_line_serializer.rb +1 -1
  115. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
  116. data/app/serializers/spree/api/v3/base_serializer.rb +33 -6
  117. data/app/serializers/spree/api/v3/claim_serializer.rb +1 -1
  118. data/app/serializers/spree/api/v3/collection_serializer.rb +1 -1
  119. data/app/serializers/spree/api/v3/company_invitation_serializer.rb +1 -1
  120. data/app/serializers/spree/api/v3/company_serializer.rb +1 -1
  121. data/app/serializers/spree/api/v3/data_request_confirmation_requested_event_serializer.rb +28 -0
  122. data/app/serializers/spree/api/v3/data_request_event_serializer.rb +1 -1
  123. data/app/serializers/spree/api/v3/data_request_serializer.rb +8 -4
  124. data/app/serializers/spree/api/v3/delivery_serializer.rb +1 -1
  125. data/app/serializers/spree/api/v3/delivery_zone_member_serializer.rb +1 -1
  126. data/app/serializers/spree/api/v3/discount_serializer.rb +1 -1
  127. data/app/serializers/spree/api/v3/file_upload_serializer.rb +1 -1
  128. data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
  129. data/app/serializers/spree/api/v3/media_event_serializer.rb +1 -1
  130. data/app/serializers/spree/api/v3/media_serializer.rb +1 -1
  131. data/app/serializers/spree/api/v3/newsletter_subscriber_serializer.rb +5 -1
  132. data/app/serializers/spree/api/v3/option_type_serializer.rb +1 -1
  133. data/app/serializers/spree/api/v3/order_group_serializer.rb +2 -2
  134. data/app/serializers/spree/api/v3/order_serializer.rb +4 -2
  135. data/app/serializers/spree/api/v3/payment_serializer.rb +1 -1
  136. data/app/serializers/spree/api/v3/product_filter_availability_option_serializer.rb +1 -1
  137. data/app/serializers/spree/api/v3/product_filter_option_serializer.rb +1 -1
  138. data/app/serializers/spree/api/v3/seller/address_serializer.rb +3 -2
  139. data/app/serializers/spree/api/v3/seller/digital_asset_serializer.rb +16 -0
  140. data/app/serializers/spree/api/v3/seller/order_serializer.rb +3 -3
  141. data/app/serializers/spree/api/v3/seller/package_type_serializer.rb +3 -3
  142. data/app/serializers/spree/api/v3/seller/payout_serializer.rb +2 -2
  143. data/app/serializers/spree/api/v3/seller/profile_serializer.rb +2 -2
  144. data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +1 -1
  145. data/app/serializers/spree/api/v3/seller/store_serializer.rb +18 -0
  146. data/app/serializers/spree/api/v3/seller/tax_identifier_serializer.rb +1 -1
  147. data/app/serializers/spree/api/v3/seller/variant_serializer.rb +1 -1
  148. data/app/serializers/spree/api/v3/shipping_label_event_serializer.rb +1 -1
  149. data/app/serializers/spree/api/v3/stock_movement_serializer.rb +1 -1
  150. data/app/serializers/spree/api/v3/stock_receipt_event_serializer.rb +1 -1
  151. data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
  152. data/app/services/spree/api/v3/filter_table.rb +22 -3
  153. data/app/services/spree/api/v3/filters_aggregator.rb +63 -18
  154. data/app/services/spree/api/v3/request_schema.rb +64 -17
  155. data/config/locales/en.yml +2 -0
  156. data/config/routes.rb +22 -11
  157. data/lib/spree/api/agent_write_schemas.rb +15 -3
  158. data/lib/spree/api/dependencies.rb +4 -4
  159. data/lib/spree/api/engine.rb +6 -0
  160. data/lib/spree/api/enum_types.rb +118 -0
  161. data/lib/spree/api/middleware/no_index.rb +33 -0
  162. data/lib/spree/api/openapi/filter_documentation.rb +8 -1
  163. data/lib/spree/api/openapi/schema_helper.rb +2 -1
  164. data/lib/spree/api/testing_support/v3/base.rb +18 -0
  165. data/lib/spree/api/typelizer_generation.rb +5 -0
  166. data/lib/typelizer/open_enums.rb +40 -16
  167. metadata +25 -6
@@ -57,7 +57,7 @@ module Spree
57
57
  end
58
58
 
59
59
  def collection_includes
60
- [:stock_locations]
60
+ [:stock_locations, :delivery_zones, :delivery_methods]
61
61
  end
62
62
 
63
63
  private
@@ -74,7 +74,7 @@ module Spree
74
74
  end
75
75
 
76
76
  def collection_includes
77
- [delivery_origin_groups: :stock_locations]
77
+ [:delivery_methods, :delivery_zones, { delivery_origin_groups: :stock_locations }]
78
78
  end
79
79
 
80
80
  private
@@ -59,14 +59,7 @@ module Spree
59
59
  return unless params.key?(:customer_id)
60
60
  return @customer = nil if params[:customer_id].blank?
61
61
 
62
- unless holds_permission?('read_customers')
63
- return render_error(
64
- code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
65
- message: 'Missing permission: read_customers',
66
- status: :forbidden,
67
- details: { required_permission: 'read_customers' }
68
- )
69
- end
62
+ return render_missing_permission('read_customers') unless holds_permission?('read_customers')
70
63
 
71
64
  @customer = Spree.customer_class.
72
65
  accessible_by(current_ability, :show).
@@ -37,6 +37,8 @@ module Spree
37
37
 
38
38
  user = resolve_or_create_invitee(@invitation)
39
39
  return if performed?
40
+ # Checked before accepting: a deactivated account gains no role.
41
+ return render_account_deactivated if account_deactivated?(user)
40
42
 
41
43
  @invitation.invitee = user
42
44
  result = Spree.invitation_accept_workflow.call(invitation: @invitation)
@@ -103,6 +103,8 @@ module Spree
103
103
  # role would both disclose more than was handed over and mislead a
104
104
  # client that reads /me to decide what to attempt.
105
105
  def me_response
106
+ ActiveRecord::Associations::Preloader.new(records: [current_user], associations: { spree_roles: :resource }).call
107
+
106
108
  {
107
109
  user: admin_user_serializer.new(current_user, params: serializer_params).to_h,
108
110
  permissions: serialize_permissions(current_ability),
@@ -42,6 +42,19 @@ module Spree
42
42
  [:option_values]
43
43
  end
44
44
 
45
+ # Values created or changed by this write were never loaded with
46
+ # their swatches, so fetch every value's image in one query before
47
+ # rendering.
48
+ def serialize_resource(resource)
49
+ if expand_list.include?('option_values')
50
+ ActiveRecord::Associations::Preloader.new(
51
+ records: resource.option_values.to_a,
52
+ associations: { image_attachment: :blob }
53
+ ).call
54
+ end
55
+ super
56
+ end
57
+
45
58
  def permitted_params
46
59
  params.permit(*permitted_attributes)
47
60
  end
@@ -6,6 +6,7 @@ module Spree
6
6
  # Claims on a completed order.
7
7
  class ClaimsController < BaseController
8
8
  include Spree::Api::V3::Orders::ClaimActions
9
+ include Spree::Api::V3::Admin::RefundAuthorization
9
10
 
10
11
  # Claims are a subject of the `orders` catalog resource, so
11
12
  # `read_orders`/`write_orders` gate these endpoints. `:claims`
@@ -51,6 +52,7 @@ module Spree
51
52
  end
52
53
 
53
54
  before_action :set_resource, only: [:show, :update, :approve, :resolve, :deny, :cancel]
55
+ before_action :require_refund_permission!, only: :resolve, if: -> { params[:resolution].to_s.include?('refund') }
54
56
 
55
57
  # PATCH /api/v3/admin/orders/:order_id/claims/:id
56
58
  #
@@ -7,6 +7,7 @@ module Spree
7
7
  # member action, because each carries different arguments.
8
8
  class ExchangesController < BaseController
9
9
  include Spree::Api::V3::Orders::ExchangeActions
10
+ include Spree::Api::V3::Admin::RefundAuthorization
10
11
 
11
12
  # Exchanges are a subject of the `orders` catalog resource, so
12
13
  # `read_orders`/`write_orders` gate these endpoints. `:exchanges`
@@ -49,6 +50,14 @@ module Spree
49
50
  end
50
51
 
51
52
  before_action :set_resource, only: [:show, :update, :approve, :receive, :fulfill, :cancel]
53
+ # Fulfilling settles the difference on the units that actually came
54
+ # back, a credit to the customer when the replacements cost less.
55
+ # That figure is only known once the tax rows are rewritten for
56
+ # those units, inside the fulfillment itself, so the gate cannot be
57
+ # narrowed on the announced difference: a line received at zero
58
+ # units would offset a credit still paid on another. Every
59
+ # fulfillment settles money, so all of them take the refund authority.
60
+ before_action :require_refund_permission!, only: :fulfill
52
61
 
53
62
  # PATCH /api/v3/admin/orders/:order_id/exchanges/:id
54
63
  #
@@ -186,7 +186,7 @@ module Spree
186
186
  def delivery_method_for_create
187
187
  return if create_params[:delivery_method_id].blank?
188
188
 
189
- Spree::DeliveryMethod.accessible_by(current_ability, :show).find_by_prefix_id!(create_params[:delivery_method_id])
189
+ current_store.delivery_methods.accessible_by(current_ability, :show).find_by_prefix_id!(create_params[:delivery_method_id])
190
190
  end
191
191
 
192
192
  def source_fulfillment_for_create
@@ -28,10 +28,13 @@ module Spree
28
28
  def create
29
29
  with_order_lock do
30
30
  payment_method = current_store.payment_methods.accessible_by(current_ability, :show).find_by_prefix_id!(params[:payment_method_id])
31
- @resource = @parent.payments.build(
32
- amount: params[:amount] || @parent.order_total_after_store_credit,
33
- payment_method: payment_method
34
- )
31
+ # The default is what the order still owes, which is nothing
32
+ # once store credit covers it — a zero payment is refused the
33
+ # same way a zero amount is.
34
+ amount = money_param(:amount, @parent.currency) || @parent.order_total_after_store_credit
35
+ raise Spree::Money::InvalidFormat.new('must be greater than 0', field: :amount) unless amount.positive?
36
+
37
+ @resource = @parent.payments.build(amount: amount, payment_method: payment_method)
35
38
 
36
39
  if params[:source_id].present? && payment_method.source_required?
37
40
  @resource.source = find_source!(payment_method, params[:source_id])
@@ -10,6 +10,7 @@ module Spree
10
10
  # an amount.
11
11
  class ReturnsController < BaseController
12
12
  include Spree::Api::V3::Orders::ReturnActions
13
+ include Spree::Api::V3::Admin::RefundAuthorization
13
14
 
14
15
  # Returns are a subject of the `orders` catalog resource (see
15
16
  # Spree::PermissionConfiguration), so `read_orders`/`write_orders`
@@ -57,6 +58,7 @@ module Spree
57
58
  end
58
59
 
59
60
  before_action :set_resource, only: [:show, :update, :approve, :receive, :refund, :cancel]
61
+ before_action :require_refund_permission!, only: :refund
60
62
 
61
63
  # PATCH /api/v3/admin/orders/:order_id/returns/:id
62
64
  #
@@ -4,6 +4,7 @@ module Spree
4
4
  module Admin
5
5
  class OrdersController < ResourceController
6
6
  include Spree::Api::V3::OrderLock
7
+ include Spree::Api::V3::Admin::RefundAuthorization
7
8
 
8
9
  scoped_resource :orders
9
10
 
@@ -17,10 +18,12 @@ module Spree
17
18
  request_params :order do |body|
18
19
  body.external_references
19
20
  body.attribute :email, :string, example: 'customer@example.com'
20
- body.attribute :customer_id, :id
21
+ body.attribute :customer_id, :id,
22
+ nullable: true,
23
+ description: 'Null detaches the customer, leaving a guest order under its `email`.'
21
24
  body.attribute :use_customer_default_address, :boolean,
22
25
  only: :create,
23
- description: "With `customer_id`, copies the customer's saved billing and shipping addresses onto the order."
26
+ description: "Copies the saved default billing and shipping addresses onto the order. Sent with `company_id`, it takes that company's defaults, inherited from its parent nodes when the company holds none of its own; otherwise it takes the customer's."
24
27
  body.attribute :currency, :currency, example: 'USD'
25
28
  body.attribute :market_id, :id
26
29
  body.attribute :channel_id, :id, description: 'Defaults to the store primary channel when omitted.'
@@ -35,10 +38,10 @@ module Spree
35
38
  description: "The `signed_id` of the buyer's purchase order, from `files.upload()` with `visibility: 'private'`. Null removes the document the order carries."
36
39
  body.attribute :shipping_address_id, :id,
37
40
  only: :create,
38
- description: 'An existing address of the customer.'
41
+ description: "An existing address from the customer's address book, or from the address book of the company the order is for."
39
42
  body.attribute :billing_address_id, :id,
40
43
  only: :create,
41
- description: 'An existing address of the customer.'
44
+ description: "An existing address from the customer's address book, or from the address book of the company the order is for."
42
45
  body.attribute :preferred_stock_location_id, :id,
43
46
  description: "Stock location to prefer for fulfillment. Order Routing's built-in preferred-location rule ranks it first; routing falls back to the next rule when the location can't cover the cart."
44
47
  body.attribute :company_id, :id,
@@ -51,6 +54,8 @@ module Spree
51
54
  body.attribute :tags, :array, of: :string
52
55
  body.attribute :shipping_address, :object, shape: ADDRESS
53
56
  body.attribute :billing_address, :object, shape: ADDRESS
57
+ body.attribute :use_shipping, :boolean,
58
+ description: 'When true, copies the shipping address to the billing address.'
54
59
  body.attribute :items, :array,
55
60
  description: 'Line items to upsert, keyed by variant. An entry sets that variant\'s quantity, creating the line item when the order has none; `quantity: 0` removes it. Variants you do not send are left alone.' do |item|
56
61
  item.attribute :variant_id, :id, required: true
@@ -82,6 +87,7 @@ module Spree
82
87
 
83
88
  skip_before_action :set_resource, only: [:index, :create]
84
89
  before_action :set_resource, only: [:show, :update, :destroy, :complete, :cancel, :approve, :resend_confirmation, :resend_digital_links, :po_document]
90
+ before_action :require_refund_permission!, only: :cancel, if: -> { params[:refund_payments].to_b }
85
91
 
86
92
  # POST /api/v3/admin/orders
87
93
  def create
@@ -227,7 +233,9 @@ module Spree
227
233
  # `fields` list names an order's attributes, and narrowing a group by
228
234
  # it can drop the very keys that say it is one.
229
235
  def serialize_completion(record)
230
- return serialize_resource(record.reload) unless record.is_a?(Spree::OrderGroup)
236
+ # Read back lazily preloading, as #show is, so its fulfillments and
237
+ # their rows load once for all of them rather than per fulfillment.
238
+ return serialize_resource(Spree::Order.preload_associations_lazily.find(record.id)) unless record.is_a?(Spree::OrderGroup)
231
239
 
232
240
  Spree.api.admin_order_group_serializer.new(completed_group(record), params: serializer_params).to_h
233
241
  end
@@ -239,7 +247,7 @@ module Spree
239
247
  :customer, :ship_address, :bill_address,
240
248
  { payments: %i[payment_method source] },
241
249
  { orders: [:seller, :market, :gift_card, :customer, { line_items: { variant: :prices } }] }
242
- ).find(group.id)
250
+ ).preload_associations_lazily.find(group.id)
243
251
  end
244
252
 
245
253
  # Override scope — Order uses SingleStoreResource (for_store).
@@ -294,8 +302,12 @@ module Spree
294
302
  # are loaded with their selected rate because the freight summary
295
303
  # reads it — otherwise a page of orders costs a query per
296
304
  # fulfillment for a field that is nil on every parcel order.
305
+ # Both addresses are named because they are two associations on
306
+ # one table, so a row that reads them separately costs a lookup
307
+ # each.
297
308
  [:customer, :channel, :seller, :external_references, :cancel_reason,
298
309
  :market, { fulfillments: %i[selected_delivery_rate external_references] },
310
+ :bill_address, :ship_address,
299
311
  { line_items: [:external_references, { variant: :prices }, { price_list: :catalog }] },
300
312
  { po_document_attachment: :blob }]
301
313
  end
@@ -343,7 +355,9 @@ module Spree
343
355
  # Named rather than read from the action, since an upsert reaches
344
356
  # update from the create action.
345
357
  def order_update_params
346
- resolve_company_param(normalize_params(params.permit(*request_params_schema.permit_list(:update))))
358
+ permitted = resolve_company_param(normalize_params(params.permit(*request_params_schema.permit_list(:update))))
359
+ permitted[:customer_id] = resolve_customer&.id if permitted.key?(:customer_id)
360
+ permitted
347
361
  end
348
362
 
349
363
  # An incidental lookup like any other: resolved through the store so
@@ -27,9 +27,8 @@ module Spree
27
27
  body.attribute :length, :number, nullable: true
28
28
  body.attribute :width, :number, nullable: true
29
29
  body.attribute :height, :number, nullable: true
30
- body.attribute :dimensions_unit, :string,
30
+ body.attribute :dimensions_unit, :dimension_unit,
31
31
  nullable: true,
32
- choices: Spree::Variant::DIMENSION_UNITS,
33
32
  example: 'cm',
34
33
  description: "Unit the three dimensions are in; falls back to the store's unit system."
35
34
  body.attribute :weight, :number,
@@ -38,7 +37,7 @@ module Spree
38
37
  body.attribute :max_weight, :number,
39
38
  nullable: true,
40
39
  description: 'What the package can hold, not the same as its own weight.'
41
- body.attribute :weight_unit, :string, nullable: true, choices: Spree::Variant::WEIGHT_UNITS, example: 'kg'
40
+ body.attribute :weight_unit, :weight_unit, nullable: true, example: 'kg'
42
41
  body.attribute :default, :boolean,
43
42
  description: 'The box every parcel quote is built on. Setting this demotes whichever row held it.'
44
43
  body.attribute :metadata, :hash
@@ -36,7 +36,9 @@ module Spree
36
36
  def create
37
37
  user = Spree.admin_user_class.find_by(email: params[:email])
38
38
 
39
- if user
39
+ # A deactivated account is treated as no account: no email is sent,
40
+ # and the response is the same 202.
41
+ if user && !account_deactivated?(user)
40
42
  event_name = 'admin_user.password_reset_requested'
41
43
  payload = user.event_payload_for(
42
44
  event_name,
@@ -62,6 +64,10 @@ module Spree
62
64
  )
63
65
  end
64
66
 
67
+ # A link sent before the account was switched off must not change
68
+ # its password or sign it in.
69
+ return render_account_deactivated if account_deactivated?(user)
70
+
65
71
  if user.update(password: params[:password], password_confirmation: params[:password_confirmation])
66
72
  user.publish_event('admin_user.password_reset')
67
73
  # A password reset must kill every existing session — a stolen
@@ -123,8 +123,26 @@ module Spree
123
123
  [:price_adjustment_tiers]
124
124
  end
125
125
 
126
+ def serializer_params
127
+ return super unless action_name == 'index'
128
+
129
+ super.merge(page_counts)
130
+ end
131
+
126
132
  private
127
133
 
134
+ # One grouped count per figure for the whole page rather than two
135
+ # queries per list.
136
+ def page_counts
137
+ @page_counts ||= begin
138
+ price_list_ids = collection.map(&:id)
139
+ {
140
+ products_counts: Spree::PriceList.products_count_by_list(price_list_ids),
141
+ prices_counts: Spree::PriceList.prices_count_by_list(price_list_ids)
142
+ }
143
+ end
144
+ end
145
+
128
146
  # Loads the record without the action-derived authorization
129
147
  # `set_resource` runs (which would check `:activate` /
130
148
  # `:deactivate` — actions that abilities don't grant). The
@@ -76,9 +76,21 @@ module Spree
76
76
  def permitted_params
77
77
  attrs = params.permit(*permitted_attributes)
78
78
  attrs = scope_delivery_profile(attrs)
79
+ attrs = scope_option_type_ids(attrs)
79
80
  scope_category_ids(attrs)
80
81
  end
81
82
 
83
+ # `option_type_ids=` resolves prefixed ids with no store scoping, so a
84
+ # type could otherwise seed another store's option types onto every
85
+ # product it is applied to.
86
+ def scope_option_type_ids(attrs)
87
+ return attrs if attrs[:option_type_ids].blank?
88
+
89
+ option_type_ids = decode_ids(attrs[:option_type_ids], Spree::OptionType)
90
+ attrs[:option_type_ids] = Spree::OptionType.for_store(current_store).where(id: option_type_ids).pluck(:id)
91
+ attrs
92
+ end
93
+
82
94
  # The template profile must belong to this store — a cross-store id
83
95
  # 404s instead of silently linking.
84
96
  def scope_delivery_profile(attrs)
@@ -93,7 +105,6 @@ module Spree
93
105
  # `category_ids=` resolves prefixed ids with no store scoping, so a
94
106
  # type in this store could otherwise be pointed at another store's
95
107
  # categories. Mirrors ProductsController#apply_categories.
96
- # (OptionType is global — nothing to scope there.)
97
108
  def scope_category_ids(attrs)
98
109
  return attrs if attrs[:category_ids].blank?
99
110
 
@@ -4,61 +4,19 @@ module Spree
4
4
  module Admin
5
5
  module Products
6
6
  class DigitalAssetsController < ResourceController
7
+ include Spree::Api::V3::DigitalAssetsResource
8
+
7
9
  scoped_resource :products
8
10
 
9
11
  request_params :digital_asset do |body|
10
- # Names a blob rather than a column, so it is attached
11
- # separately (+attach_uploaded_file+).
12
- body.attribute :signed_id, :string,
13
- permit: false,
14
- description: "A `signed_id` from `files.upload()` with `visibility: 'private'`. Required for an uploaded file, omitted for a provider-backed asset. On update it replaces the file for everyone who already bought it."
15
- # Resolved through the parent rather than assigned, so an asset
16
- # can't be pointed at a variant of another product or store.
17
- body.attribute :variant_id, :id,
18
- permit: false,
19
- description: "Defaults to the product's default variant when omitted."
12
+ body.include_attributes Spree::Api::V3::DigitalAssetRequestSchema
20
13
  # The model validates it names a registered provider.
21
14
  body.attribute :provider_type, :string,
22
- description: 'A provider shorthand from the providers endpoint. Omit for an uploaded file.'
15
+ description: 'A provider shorthand from the providers endpoint. Omit for an uploaded file; a provider-backed asset sends no `signed_id`.'
23
16
  # Its keys are declared by the provider, so it lands verbatim
24
17
  # under one key in the asset's metadata.
25
18
  body.attribute :provider_settings, :hash,
26
19
  description: "Values for the provider's settings schema, keyed by field name."
27
- body.attribute :authorized_clicks, :integer,
28
- nullable: true,
29
- description: "Null means the store's download settings apply."
30
- body.attribute :authorized_days, :integer,
31
- nullable: true,
32
- description: "Null means the store's download settings apply."
33
- end
34
-
35
- # PATCH /api/v3/admin/products/:product_id/digital_assets/:id
36
- #
37
- # Replacing the file keeps every already-issued link working, since
38
- # links resolve through the asset rather than the blob.
39
- def update
40
- return render_private_storage_error unless uploaded_blob_private?
41
-
42
- # The asset already exists, so attaching a replacement blob writes
43
- # to the database immediately, while the update below renders a 422
44
- # without raising when it fails. Wrap both so a failed update rolls
45
- # the new attachment back rather than leaving the file swapped but
46
- # the record unchanged.
47
- model_class.transaction do
48
- @resource.variant = requested_variant if params[:variant_id].present?
49
- attach_uploaded_file(@resource)
50
-
51
- super
52
-
53
- raise ActiveRecord::Rollback if @resource.errors.any?
54
- end
55
- end
56
-
57
- # POST /api/v3/admin/products/:product_id/digital_assets
58
- def create
59
- return render_private_storage_error unless uploaded_blob_private?
60
-
61
- super
62
20
  end
63
21
 
64
22
  # GET /api/v3/admin/products/:product_id/digital_assets/providers
@@ -83,75 +41,13 @@ module Spree
83
41
 
84
42
  protected
85
43
 
86
- def model_class
87
- Spree::DigitalAsset
88
- end
89
-
90
44
  def serializer_class
91
45
  Spree.api.admin_digital_asset_serializer
92
46
  end
93
47
 
94
48
  def set_parent
95
49
  @parent = current_store.products.find_by_prefix_id!(params[:product_id])
96
- authorize!(:show, @parent)
97
- end
98
-
99
- # The parent association reaches assets through `variants`, which
100
- # carries an ORDER BY on the variants table. Combined with the
101
- # collection's DISTINCT, PostgreSQL rejects the query outright
102
- # (ordering columns must appear in the select list), so order by
103
- # the assets' own table instead. SQLite accepts either form, so
104
- # this only fails on a real deployment.
105
- def scope
106
- super.reorder(created_at: :asc)
107
- end
108
-
109
- def scope_includes
110
- [:variant, { attachment_attachment: :blob }]
111
- end
112
-
113
- def permitted_params
114
- params.permit(*declared_permitted_attributes)
115
- end
116
-
117
- private
118
-
119
- # A product without explicit variants keeps its file on the default
120
- # variant, mirroring how the rest of the admin treats such products.
121
- def build_resource
122
- digital_asset = super
123
- digital_asset.variant = requested_variant || @parent.default_variant
124
- attach_uploaded_file(digital_asset)
125
- digital_asset
126
- end
127
-
128
- # Scoped to the parent, not merely to the store: an asset reached
129
- # through a product's route has no business pointing at another
130
- # product's variant. An id from anywhere else is a 404.
131
- def requested_variant
132
- return if params[:variant_id].blank?
133
-
134
- @parent.variants.find_by_prefix_id!(params[:variant_id])
135
- end
136
-
137
- def attach_uploaded_file(digital_asset)
138
- return if params[:signed_id].blank?
139
-
140
- digital_asset.attachment.attach(params[:signed_id])
141
- end
142
-
143
- # Attaching never moves a blob between storage services, so a file
144
- # uploaded to the public bucket would stay publicly readable while
145
- # looking attached. Refuse it rather than storing it wrongly.
146
- def uploaded_blob_private?
147
- return true if params[:signed_id].blank?
148
-
149
- blob = ActiveStorage::Blob.find_signed(params[:signed_id])
150
- blob.present? && blob.service_name.to_s == Spree.private_storage_service_name.to_s
151
- end
152
-
153
- def render_private_storage_error
154
- render_validation_error(I18n.t('spree.digital_assets_attachment_must_be_private'))
50
+ authorize_parent!(@parent)
155
51
  end
156
52
  end
157
53
  end
@@ -7,11 +7,10 @@ module Spree
7
7
  scoped_resource :products
8
8
 
9
9
  request_params :variant do |body|
10
+ body.extends Spree::Api::V3::Admin::VariantRequestSchema
10
11
  body.external_references
11
- body.include_attributes Spree::Api::V3::VariantRequestSchema
12
- body.attribute :options, :array, of: Spree::Api::V3::VariantOptionRequestSchema,
13
- required: true, on: :create,
14
- description: 'One pair per option type the variant takes part in (size, color). Option types and values are created when missing. On update, omit to leave the option values untouched.'
12
+ body.require_attributes :options, on: :create
13
+ body.describe_attribute :options, 'One pair per option type the variant takes part in (size, color). Option types and values are created when missing. On update, omit to leave the option values untouched.'
15
14
  end
16
15
 
17
16
  protected
@@ -29,8 +28,11 @@ module Spree
29
28
  authorize!(:show, @parent)
30
29
  end
31
30
 
31
+ # `active_stock_reservations` is what lets the quantifier count
32
+ # held stock in memory; without it the serializer's availability
33
+ # column sums reservations once per variant.
32
34
  def scope_includes
33
- [:prices, stock_levels: :stock_location]
35
+ [:prices, { stock_levels: [:stock_location, :active_stock_reservations] }]
34
36
  end
35
37
 
36
38
  def create_workflow
@@ -77,10 +77,8 @@ module Spree
77
77
  body.attribute :variants, :array,
78
78
  as: 'ProductVariantParams',
79
79
  description: 'Every purchasable attribute (sku, prices, stock, weight, dimensions) lives on variants. An entry without `options` upserts onto the default variant of a simple product; pass `id` to update an existing variant.' do |variant|
80
+ variant.extends Spree::Api::V3::Admin::VariantRequestSchema
80
81
  variant.attribute :id, :id
81
- variant.include_attributes Spree::Api::V3::VariantRequestSchema
82
- variant.attribute :options, :array, of: Spree::Api::V3::VariantOptionRequestSchema,
83
- description: 'One pair per option type the variant takes part in (size, color). Option types and values are created when missing.'
84
82
  end
85
83
  end
86
84
 
@@ -221,7 +219,7 @@ module Spree
221
219
 
222
220
  channels = scoped_channels
223
221
  product_ids = bulk_collection.distinct.ids
224
- channels.find_each { |channel| channel.add_products(product_ids) }
222
+ channels.add_products(product_ids)
225
223
 
226
224
  render json: { product_count: product_ids.size, channel_count: channels.size }
227
225
  end
@@ -233,7 +231,7 @@ module Spree
233
231
 
234
232
  channels = scoped_channels
235
233
  product_ids = bulk_collection.distinct.ids
236
- removed = channels.sum { |channel| channel.remove_products(product_ids) }
234
+ removed = channels.remove_products(product_ids)
237
235
 
238
236
  render json: { product_count: product_ids.size, channel_count: channels.size, removed: removed }
239
237
  end
@@ -291,6 +289,18 @@ module Spree
291
289
  Spree.product_update_workflow
292
290
  end
293
291
 
292
+ def nested_permitted_models
293
+ { media: Spree::Media, variants: Spree::Variant }
294
+ end
295
+
296
+ # A written product is serialized from a fresh read: lazy preloading
297
+ # does not reach records a write built, so the instance the workflow
298
+ # returned would query once per variant.
299
+ def serialize_resource(resource)
300
+ resource = scope.where(id: resource.id).preload_associations_lazily.take! if resource.lazy_preload_context.nil?
301
+ super
302
+ end
303
+
294
304
  # Use SearchProvider::Database for collection to handle price/best_selling
295
305
  # sorting correctly (counts before sorting, avoiding PG/Mobility issues).
296
306
  def collection
@@ -321,7 +331,24 @@ module Spree
321
331
  end
322
332
 
323
333
  def permitted_params
324
- params.permit(*permitted_attributes).tap { |attrs| attrs.delete(:status) if review_status?(attrs[:status]) }
334
+ params.permit(*permitted_attributes).tap do |attrs|
335
+ attrs.delete(:status) if review_status?(attrs[:status])
336
+ scope_store_references(attrs)
337
+ end
338
+ end
339
+
340
+ # Read through the store here because the model only checks that the
341
+ # record exists: another store's type or tax category would otherwise
342
+ # be accepted, and the type would seed its option types onto the
343
+ # product.
344
+ STORE_REFERENCES = { product_type_id: :product_types, tax_category_id: :tax_categories }.freeze
345
+
346
+ def scope_store_references(attrs)
347
+ STORE_REFERENCES.each do |key, association|
348
+ next if attrs[key].blank?
349
+
350
+ attrs[key] = current_store.public_send(association).find_by_param!(attrs[key]).id
351
+ end
325
352
  end
326
353
 
327
354
  # A review status is an outcome, not a value to assign: `proposed`
@@ -30,7 +30,8 @@ module Spree
30
30
  # because another went through: the payout row exists and is
31
31
  # failed or unresolved, and an operator reading "settled" would
32
32
  # not know to go looking for it.
33
- body = { data: settled.map { |payout| serialize_payout(payout) } }
33
+ transfer_counts = Spree::SellerTransfer.where(payout_id: settled.map(&:id)).group(:payout_id).count
34
+ body = { data: settled.map { |payout| serialize_payout(payout, transfer_counts) } }
34
35
  body[:meta] = { failures: failures } if failures.any?
35
36
 
36
37
  render json: body, status: :created
@@ -96,8 +97,8 @@ module Spree
96
97
  @seller.payable_currencies
97
98
  end
98
99
 
99
- def serialize_payout(payout)
100
- Spree.api.admin_seller_payout_serializer.new(payout, params: { store: current_store }).to_h
100
+ def serialize_payout(payout, transfer_counts)
101
+ Spree.api.admin_seller_payout_serializer.new(payout, params: { store: current_store, transfer_counts: transfer_counts }).to_h
101
102
  end
102
103
  end
103
104
  end
@@ -202,9 +202,10 @@ module Spree
202
202
  end
203
203
 
204
204
  # The listing and the detail page both read a transfer's totals off
205
- # its lines.
205
+ # its lines. Both warehouses come from one table, so their external
206
+ # references are loaded here rather than once per warehouse.
206
207
  def scope_includes
207
- [line_includes]
208
+ [{ source_location: :external_references, destination_location: :external_references }, line_includes]
208
209
  end
209
210
  end
210
211
  end