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
@@ -9,7 +9,7 @@ module Spree
9
9
  body.attribute :name, :string, example: 'My Store'
10
10
  body.attribute :admin_locale, :string, example: 'en'
11
11
  body.attribute :timezone, :string, example: 'Europe/Berlin'
12
- body.attribute :weight_unit, :string, choices: Spree::Variant::WEIGHT_UNITS
12
+ body.attribute :weight_unit, :weight_unit
13
13
  body.attribute :unit_system, :string, choices: Spree::Store::UNIT_SYSTEMS
14
14
  body.attribute :storefront_access, :string,
15
15
  choices: Spree::Channel::Gating::STOREFRONT_ACCESS,
@@ -65,6 +65,13 @@ module Spree
65
65
  description: 'Admits a seller as soon as they finish onboarding, with no operator review.'
66
66
  body.attribute :auto_approve_seller_products, :boolean,
67
67
  description: 'Puts a seller product on sale the moment it is submitted, with no review.'
68
+ body.attribute :seller_digital_assets_enabled, :boolean,
69
+ description: 'Lets sellers attach downloadable files to their own products.'
70
+ body.attribute :seller_digital_asset_content_types, :array, of: :string,
71
+ example: %w[application/pdf application/zip],
72
+ description: "The content types a seller's file may have, checked against the file's own signature where it has one. An entry names one type or, with `*` as the subtype, a whole family. The operator's own uploads are not restricted."
73
+ body.attribute :review_seller_digital_asset_changes, :boolean,
74
+ description: 'Sends a seller product already on sale back to review when the seller adds or replaces a file on it.'
68
75
  body.attribute :send_seller_transactional_emails, :boolean,
69
76
  description: 'When false, transactional seller emails (approval, payouts and so on) are not sent.'
70
77
  body.attribute :default_commission_tax_rate, :decimal,
@@ -63,22 +63,24 @@ module Spree
63
63
  false
64
64
  end
65
65
 
66
- # Tagging filter. Store-owned taggables (`Spree::Product`,
67
- # `Spree::Order`) tenant their taggings by `store_id` via
68
- # `acts_as_taggable_tenant`, so their vocabulary is bounded to the
69
- # current store with a single indexed `tenant` filter. Global
70
- # taggables like the customer/user type carry no store, so their tags
71
- # are not store-scoped here.
66
+ # Tagging filter. A taggable declaring `acts_as_taggable_tenant :store_id`
67
+ # stores the owning store on each tagging, so its vocabulary is bounded
68
+ # to the current store with a single indexed `tenant` filter. Taggables
69
+ # without a store tenant are not store-scoped here.
72
70
  def taggings_conditions
73
71
  conditions = { taggable_type: taggable_type, context: 'tags' }
74
72
  conditions[:tenant] = current_store.id.to_s if store_tenanted_taggable?
75
73
  conditions
76
74
  end
77
75
 
78
- # True for taggables whose taggings carry a store `tenant`
79
- # (`acts_as_taggable_tenant :store_id`), i.e. store-owned records.
80
76
  def store_tenanted_taggable?
81
- %w[Spree::Product Spree::Order].include?(taggable_type)
77
+ taggable_class.try(:tenant_column).to_s == 'store_id'
78
+ end
79
+
80
+ # `taggable_type` is already matched against `Spree.taggable_types`,
81
+ # so only a registered class name is ever constantized.
82
+ def taggable_class
83
+ taggable_type.constantize
82
84
  end
83
85
 
84
86
  # Per-type scope check for API-key principals: a key listing product
@@ -5,6 +5,30 @@ module Spree
5
5
  class VariantsController < ResourceController
6
6
  scoped_resource :products
7
7
 
8
+ SALES_CONTEXT = Spree::Api::V3::RequestSchema.shape('VariantSalesContextParams') do |query|
9
+ query.attribute :order_id, :id,
10
+ description: 'A draft order to sell to: lists only what its buyer can be sold on its channel, ' \
11
+ 'priced in its market. Needs `read_orders`, and cannot be combined with the fields below.'
12
+ query.attribute :market_id, :id,
13
+ description: 'Prices in this market and lists only variants with a price in its currency.'
14
+ query.attribute :channel_id, :id,
15
+ description: 'Lists only variants published on this channel. When a market, customer, company or ' \
16
+ "currency is given without it, the store default channel applies; an order uses its own."
17
+ query.attribute :customer_id, :id,
18
+ description: "Narrows to the customer's catalogs and prices as they would pay. Needs `read_customers`."
19
+ query.attribute :company_id, :id,
20
+ description: "Narrows to the company's catalogs and prices under its agreement. Needs `read_customers`."
21
+ query.attribute :currency, :currency,
22
+ description: 'Lists only variants with a price in this currency. ' \
23
+ "With `market_id` it must be the market's currency."
24
+ end
25
+ SALES_CONTEXT_PARAMS = (SALES_CONTEXT.fields.keys - [:order_id]).freeze
26
+
27
+ # A draft order's item picker asks with the order's market, channel and
28
+ # buyer, so it lists only what that buyer can be sold there, priced as
29
+ # they would pay.
30
+ before_action :load_sales_context, only: :index
31
+
8
32
  protected
9
33
 
10
34
  def model_class
@@ -19,7 +43,21 @@ module Spree
19
43
  # relation from scratch dropped `scope_includes`, so every listed
20
44
  # variant fell back to per-row price and stock queries.
21
45
  def scope
22
- super.eligible.accessible_by(current_ability, ability_action_for_request)
46
+ variants = super.eligible.accessible_by(current_ability, ability_action_for_request)
47
+ @sales_context ? variants.where(id: @sales_context.sellable_variants.select(:id)) : variants
48
+ end
49
+
50
+ def serializer_params
51
+ return super unless @sales_context
52
+
53
+ super.merge(
54
+ currency: @sales_context.currency,
55
+ user: @sales_context.customer,
56
+ company: @sales_context.company,
57
+ market: @sales_context.market,
58
+ channel: @sales_context.channel,
59
+ country: @sales_context.country
60
+ )
23
61
  end
24
62
 
25
63
  # `active_stock_reservations` rides along with the stock levels:
@@ -33,6 +71,63 @@ module Spree
33
71
  { primary_media: [{ attachment_attachment: :blob }, { poster_attachment: :blob }] }
34
72
  ]
35
73
  end
74
+
75
+ private
76
+
77
+ def load_sales_context
78
+ requested = SALES_CONTEXT_PARAMS.select { |key| params[key].present? }
79
+ return if requested.empty? && params[:order_id].blank?
80
+
81
+ permission = required_sales_context_permission
82
+ return render_missing_permission(permission) if permission && !holds_permission?(permission)
83
+
84
+ if params[:order_id].present?
85
+ return refuse_order_with_context(requested) if requested.any?
86
+
87
+ @sales_context = Spree::SalesContext.from_order(context_order)
88
+ else
89
+ @sales_context = requested_sales_context
90
+ render_errors(@sales_context.errors) unless @sales_context.valid?
91
+ end
92
+ end
93
+
94
+ # An order already fixes its market, channel, buyer and currency, so
95
+ # naming them too could only disagree with it.
96
+ def refuse_order_with_context(requested)
97
+ render_error(
98
+ code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:parameter_invalid],
99
+ message: I18n.t('spree.api.errors.order_sales_context_exclusive', params: requested.join(', ')),
100
+ status: :unprocessable_content,
101
+ details: { order_id: requested.map(&:to_s) }
102
+ )
103
+ end
104
+
105
+ def required_sales_context_permission
106
+ return 'read_orders' if params[:order_id].present?
107
+
108
+ 'read_customers' if params[:customer_id].present? || params[:company_id].present?
109
+ end
110
+
111
+ def requested_sales_context
112
+ Spree::SalesContext.new(
113
+ store: current_store,
114
+ market: find_requested(current_store.markets, :market_id),
115
+ channel: find_requested(current_store.channels, :channel_id),
116
+ customer: find_requested(
117
+ Spree.customer_class.for_store(current_store).accessible_by(current_ability, :show), :customer_id
118
+ ),
119
+ company: find_requested(current_store.companies, :company_id),
120
+ currency: params[:currency]
121
+ )
122
+ end
123
+
124
+ def find_requested(relation, key)
125
+ relation.find_by_prefix_id!(params[key]) if params[key].present?
126
+ end
127
+
128
+ def context_order
129
+ current_store.orders.accessible_by(current_ability, :show).find_by_prefix_id!(params[:order_id])
130
+ end
36
131
  end
37
132
  end
38
133
  end
@@ -21,7 +21,7 @@ module Spree
21
21
  description: 'Defaults to true on create. Prefer the disable and enable endpoints to pause one.'
22
22
  body.attribute :subscriptions, :array, of: :string,
23
23
  example: %w[order.placed order.canceled],
24
- description: 'Event names this endpoint receives. An empty list or `["*"]` subscribes to every event; otherwise list exact names (`order.completed`) or patterns (`order.*`).'
24
+ description: 'Event names this endpoint receives. An empty list or `["*"]` subscribes to every event; otherwise list exact names (`order.completed`) or patterns with `*` in place of a segment (`order.*`, `*.created`). At most 200 entries of up to 100 characters.'
25
25
  end
26
26
 
27
27
  request_params 'WebhookEndpointDisableParams', action: :disable do |body|
@@ -112,8 +112,11 @@ module Spree
112
112
  )
113
113
  end
114
114
 
115
+ # Checked after the model's normalization, so what the gate sees is
116
+ # exactly what would be stored.
115
117
  def requested_credential_events
116
- Spree::Events.catalog.credential_events(Array(permitted_params[:subscriptions]))
118
+ subscriptions = Spree::WebhookEndpoint.normalize_value_for(:subscriptions, permitted_params[:subscriptions])
119
+ Spree::Events.catalog.credential_events(subscriptions)
117
120
  end
118
121
 
119
122
  def repointed_credential_events
@@ -44,15 +44,26 @@ module Spree
44
44
  # and the merchant would approve on the wrong one.
45
45
  def consent_url
46
46
  query = params.permit(*FORWARDED_PARAMETERS).to_h.compact_blank.to_query
47
- base = Spree::Stores::DashboardUrl.call(store: consent_store)
47
+ base = Spree::Stores::DashboardUrl.call(store: current_store)
48
48
 
49
- "#{base}/#{consent_store.prefixed_id}/oauth/authorize?#{query}"
49
+ "#{base}/#{current_store.prefixed_id}/oauth/authorize?#{query}"
50
50
  end
51
51
 
52
- # @return [Spree::Store]
53
- def consent_store
54
- @consent_store ||=
55
- Spree::OauthApplication.find_by(uid: params[:client_id])&.store || current_store
52
+ # The client's store wins over the hostname's, and is looked up
53
+ # before the hostname is resolved at all: resolving it assigns the
54
+ # default store to Spree::Current, and an app scoping queries by
55
+ # that store would then never find another store's client.
56
+ #
57
+ # @return [Spree::Store, nil]
58
+ def current_store
59
+ @current_store ||= client_application&.store || super
60
+ end
61
+
62
+ # @return [Spree::OauthApplication, nil]
63
+ def client_application
64
+ return if params[:client_id].blank?
65
+
66
+ Spree::OauthApplication.find_by(uid: params[:client_id])
56
67
  end
57
68
  end
58
69
  end
@@ -248,8 +248,11 @@ module Spree
248
248
  end
249
249
 
250
250
  # Finds a single resource within scope using prefixed ID
251
+ # Loaded through the lazy-preload relation, as the collection is, so
252
+ # the serializer's walk over the record's children batches their
253
+ # associations instead of querying once per child.
251
254
  def find_resource
252
- scope.find_by_prefix_id!(params[:id])
255
+ scope.preload_associations_lazily.find_by_prefix_id!(params[:id])
253
256
  end
254
257
 
255
258
  # Per-record authorization hook. A no-op in the shared base: the Store
@@ -481,13 +484,23 @@ module Spree
481
484
  base_scope = if @parent.present?
482
485
  @parent.send(parent_association)
483
486
  else
484
- model_class.for_store(current_store)
487
+ store_scope
485
488
  end
486
489
  base_scope = base_scope.includes(scope_includes) if scope_includes.any?
487
490
  base_scope = base_scope.preload_associations_lazily
488
491
  model_class.include?(Spree::TranslatableResource) ? base_scope.i18n : base_scope
489
492
  end
490
493
 
494
+ # The store's rows of this model when the resource has no parent. A
495
+ # model the store reaches through another record rather than an
496
+ # association of its own — a commission line through its order —
497
+ # names that path here, since `for_store` answers nothing for it.
498
+ #
499
+ # @return [ActiveRecord::Relation]
500
+ def store_scope
501
+ model_class.for_store(current_store)
502
+ end
503
+
491
504
  # Action names treated as reads. Override in subclasses with custom
492
505
  # read-only member/collection actions (e.g. add `analytics`, `types`)
493
506
  # so they map to the `:show` ability instead of a write.
@@ -559,7 +572,17 @@ module Spree
559
572
  # declared on the model. Override +resource_permitted_attributes+ rather
560
573
  # than this, so the extension union is not lost.
561
574
  def permitted_attributes
562
- resource_permitted_attributes + model_additional_permitted_attributes
575
+ with_nested_additional_permitted_attributes(resource_permitted_attributes) + model_additional_permitted_attributes
576
+ end
577
+
578
+ # Nested lists this resource writes, keyed by param name, mapped to the
579
+ # model each entry becomes — +{ media: Spree::Media }+ lets an
580
+ # extension's +Spree::Media.additional_permitted_attributes+ ride the
581
+ # product's +media+ entries, not only the media endpoint.
582
+ #
583
+ # @return [Hash{Symbol => Class}]
584
+ def nested_permitted_models
585
+ {}
563
586
  end
564
587
 
565
588
  # The resource's writable attributes: its `request_params`
@@ -580,13 +603,33 @@ module Spree
580
603
  def model_additional_permitted_attributes
581
604
  return [] unless respond_to?(:model_class, true)
582
605
 
583
- model = model_class
584
- return [] unless model.respond_to?(:additional_permitted_attributes)
585
-
586
- Array(model.additional_permitted_attributes)
606
+ additional_permitted_attributes_for(model_class)
587
607
  rescue NotImplementedError
588
608
  []
589
609
  end
610
+
611
+ # What extensions declared writable on +model+. `Spree.base_class` is
612
+ # host-overridable, so the class_attribute may not be there at all.
613
+ #
614
+ # @param model [Class]
615
+ # @return [Array]
616
+ def additional_permitted_attributes_for(model)
617
+ Array(model.try(:additional_permitted_attributes))
618
+ end
619
+
620
+ def with_nested_additional_permitted_attributes(attributes)
621
+ return attributes if nested_permitted_models.empty?
622
+
623
+ attributes.map do |attribute|
624
+ next attribute unless attribute.is_a?(Hash)
625
+
626
+ attribute.to_h do |name, nested|
627
+ model = nested_permitted_models[name.to_sym]
628
+ additional = model ? additional_permitted_attributes_for(model) : []
629
+ [name, additional.any? && nested.is_a?(Array) ? nested + additional : nested]
630
+ end
631
+ end
632
+ end
590
633
  end
591
634
  end
592
635
  end
@@ -52,6 +52,8 @@ module Spree
52
52
  return render_authentication_failed(result.error) unless result.success?
53
53
 
54
54
  user = result.value
55
+ return render_account_deactivated if account_deactivated?(user)
56
+
55
57
  # A store's own staff share this user class, so authenticating is
56
58
  # not enough: without a role on some seller there is no panel to
57
59
  # sign in to, and minting a token would hand out an audience the
@@ -101,6 +103,14 @@ module Spree
101
103
  end
102
104
 
103
105
  user = refresh_token.user
106
+ # Deactivating deletes the account's refresh tokens; this catches a
107
+ # refresh already under way when that happened.
108
+ if account_deactivated?(user)
109
+ refresh_token.destroy
110
+ clear_refresh_cookie
111
+ return render_account_deactivated
112
+ end
113
+
104
114
  # Membership can be revoked while a session is live; the refresh is
105
115
  # where that takes effect.
106
116
  unless user.seller_member?
@@ -46,6 +46,8 @@ module Spree
46
46
 
47
47
  user = resolve_or_create_invitee(@invitation)
48
48
  return if performed?
49
+ # Checked before accepting: a deactivated account gains no role.
50
+ return render_account_deactivated if account_deactivated?(user)
49
51
 
50
52
  @invitation.invitee = user
51
53
  result = Spree.invitation_accept_workflow.call(invitation: @invitation)
@@ -58,6 +58,10 @@ module Spree
58
58
  {
59
59
  user: Spree.api.seller_account_serializer.new(current_user, params: serializer_params).to_h,
60
60
  sellers: serialized_sellers,
61
+ # The marketplace's own settings that shape what the panel lets a
62
+ # seller do, read before any seller is chosen since there is one
63
+ # store per installation.
64
+ store: Spree.api.seller_store_serializer.new(serializer_params[:store], params: serializer_params).to_h,
61
65
  # Both shapes, exactly as the admin `/me`: the panel's `<Can>`
62
66
  # reads CanCanCan rules, the key gate reads keys. Sending only
63
67
  # keys left `<Can>` answering false for everything on the seller
@@ -84,7 +84,7 @@ module Spree
84
84
  )
85
85
 
86
86
  if result.success?
87
- render json: serialize_resource(@resource.reload)
87
+ render json: serialize_resource(reloaded_resource)
88
88
  else
89
89
  render_service_error(@resource.errors.presence || result.error)
90
90
  end
@@ -115,7 +115,7 @@ module Spree
115
115
  next render_validation_error(invalid.errors) if invalid
116
116
 
117
117
  if @resource.update(addresses)
118
- render json: serialize_resource(@resource.reload)
118
+ render json: serialize_resource(reloaded_resource)
119
119
  else
120
120
  render_validation_error(@resource.errors)
121
121
  end
@@ -143,12 +143,20 @@ module Spree
143
143
  super.not_drafts
144
144
  end
145
145
 
146
+ # Both addresses are named because they are two associations on one
147
+ # table, so a row that reads them separately costs a lookup each.
146
148
  def collection_includes
147
- [:line_items, :customer, :channel]
149
+ [:line_items, :customer, :channel, :bill_address, :ship_address]
148
150
  end
149
151
 
150
152
  private
151
153
 
154
+ # Read back as #show reads it, lazily preloading, so its
155
+ # fulfillments' rates and methods load once rather than per row.
156
+ def reloaded_resource
157
+ scope.preload_associations_lazily.find(@resource.id)
158
+ end
159
+
152
160
  # Either address, as a correction rather than a replacement: the
153
161
  # sent fields are laid over what the order already holds, so a
154
162
  # request naming one line does not blank out the country and
@@ -28,12 +28,12 @@ module Spree
28
28
  body.attribute :length, :number, nullable: true
29
29
  body.attribute :width, :number, nullable: true
30
30
  body.attribute :height, :number, nullable: true
31
- body.attribute :dimensions_unit, :string, nullable: true, choices: Spree::Variant::DIMENSION_UNITS
31
+ body.attribute :dimensions_unit, :dimension_unit, nullable: true
32
32
  body.attribute :weight, :number,
33
33
  nullable: true,
34
34
  description: "The empty package's own weight, added to every quote."
35
35
  body.attribute :max_weight, :number, nullable: true
36
- body.attribute :weight_unit, :string, nullable: true, choices: Spree::Variant::WEIGHT_UNITS
36
+ body.attribute :weight_unit, :weight_unit, nullable: true
37
37
  body.attribute :default, :boolean,
38
38
  description: "Makes this the box this seller's parcels are quoted with. One default per seller, so it never displaces the marketplace's."
39
39
  body.attribute :metadata, :hash, nullable: true
@@ -43,7 +43,8 @@ module Spree
43
43
  # admin who runs no seller has no panel to be sent to, and mailing
44
44
  # them a seller reset link would be an invitation to a session they
45
45
  # cannot hold.
46
- if user&.seller_member?
46
+ # A deactivated account is treated as no account: no email is sent.
47
+ if user&.seller_member? && !account_deactivated?(user)
47
48
  user.publish_event(RESET_REQUESTED_EVENT, event_payload(user))
48
49
  end
49
50
 
@@ -60,6 +61,9 @@ module Spree
60
61
  # it can be revoked between the email being sent and the link being
61
62
  # opened, and signing in is what the success path does.
62
63
  return render_token_invalid unless user&.seller_member?
64
+ # A link sent before the account was switched off must not change
65
+ # its password or sign it in.
66
+ return render_account_deactivated if account_deactivated?(user)
63
67
 
64
68
  unless user.update(password: params[:password], password_confirmation: params[:password_confirmation])
65
69
  return render_errors(user.errors)
@@ -0,0 +1,67 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Seller
5
+ module Products
6
+ # The downloadable files on one of this seller's own products.
7
+ #
8
+ # Uploaded files only: a provider is the operator's own code
9
+ # reaching into their internal systems, so a seller can neither pick
10
+ # one nor read its settings. An asset the operator set up with one
11
+ # is still listed, since it is part of what the seller's product
12
+ # delivers.
13
+ #
14
+ # The marketplace's rules for a seller's file — whether sellers may
15
+ # upload at all, which content types, and whether an approved
16
+ # listing goes back to review — apply because the workflow is told
17
+ # who is writing.
18
+ class DigitalAssetsController < Seller::ResourceController
19
+ include Spree::Api::V3::DigitalAssetsResource
20
+
21
+ scoped_resource :products
22
+
23
+ request_params :digital_asset do |body|
24
+ body.include_attributes Spree::Api::V3::DigitalAssetRequestSchema
25
+ body.require_attributes :signed_id, on: :create
26
+ end
27
+
28
+ # DELETE /api/v3/seller/products/:product_id/digital_assets/:id
29
+ #
30
+ # Removing a file is covered by the same switch as adding one: a
31
+ # marketplace that keeps digital files to itself keeps the ones it
32
+ # attached to a seller's product.
33
+ def destroy
34
+ unless current_store.preferred_seller_digital_assets_enabled
35
+ return render_validation_error(I18n.t('spree.digital_assets_sellers_disabled'))
36
+ end
37
+
38
+ super
39
+ end
40
+
41
+ protected
42
+
43
+ def serializer_class
44
+ Spree.api.seller_digital_asset_serializer
45
+ end
46
+
47
+ def create_workflow_arguments
48
+ super.merge(seller: current_seller, submitted_by: current_user)
49
+ end
50
+
51
+ def update_workflow_arguments
52
+ super.merge(seller: current_seller, submitted_by: current_user)
53
+ end
54
+
55
+ # Rooted in the seller's own catalog, so another seller's product
56
+ # is a 404. Changing a product's files is a change to the product,
57
+ # so writes need `:update` on it, not just `:show`.
58
+ def set_parent
59
+ @parent = current_seller.products.find_by_prefix_id!(params[:product_id])
60
+ authorize_parent!(@parent)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -41,19 +41,6 @@ module Spree
41
41
  'archived' => -> { Spree.product_archive_workflow }
42
42
  }.freeze
43
43
 
44
- # The operator's price shape, except `currency` may be left out: the
45
- # store's currency is assumed (see `default_price_currencies`).
46
- PRICE = Spree::Api::V3::RequestSchema.new do |schema|
47
- schema.attribute :currency, :currency,
48
- example: 'USD',
49
- description: "Omit to price in the store's currency. A seller has no currency of their own."
50
- schema.attribute :amount, :decimal,
51
- required: true,
52
- example: '24.00',
53
- description: 'A canonical decimal string (`"1234.56"`: period decimal, no grouping). The API does not parse localized input.'
54
- schema.attribute :compare_at_amount, :decimal, nullable: true, example: '30.00'
55
- end
56
-
57
44
  scoped_resource :products
58
45
 
59
46
  request_params :product do |body|
@@ -69,7 +56,7 @@ module Spree
69
56
  description: "The marketplace's product type this product is listed against, from the product types endpoint. Null detaches it. Saving with a type seeds its option types onto the product."
70
57
  body.attribute :delivery_profile_id, :id,
71
58
  description: "Which of the marketplace's delivery profiles the product ships under. A new product sent without one lands on the profile marked default."
72
- body.attribute :prices, :array, of: PRICE,
59
+ body.attribute :prices, :array, of: Spree::Api::V3::Seller::PriceRequestSchema,
73
60
  description: "Shorthand for a simple (no-options) product: per-currency prices that forward to the product's sole variant."
74
61
  # `external_url` stays out for the same reason it does on the
75
62
  # operator's endpoint: it would make the server fetch a URL the
@@ -83,56 +70,11 @@ module Spree
83
70
  media_item.attribute :variant_ids, :array, of: :id,
84
71
  description: 'Replaces the variants this media is linked to. An empty list clears them.'
85
72
  end
86
- # Narrower than the operator's variant set: `seller_id` would let a
87
- # payload hand a variant to somebody else, and
88
- # `tax_category_id`/`delivery_profile_id` are marketplace
89
- # configuration.
90
73
  body.attribute :variants, :array,
91
- as: 'SellerProductVariantParams',
74
+ as: 'ProductVariantParams',
92
75
  description: 'The variants, as a whole: one absent from the list is removed. Pass `id` to update an existing variant.' do |variant|
76
+ variant.extends Spree::Api::V3::Seller::VariantRequestSchema
93
77
  variant.attribute :id, :id
94
- variant.attribute :sku, :string
95
- variant.attribute :barcode, :string
96
- variant.attribute :cost_price, :decimal, nullable: true
97
- variant.attribute :cost_currency, :currency
98
- variant.attribute :weight, :number, nullable: true
99
- variant.attribute :height, :number, nullable: true
100
- variant.attribute :width, :number, nullable: true
101
- variant.attribute :depth, :number, nullable: true
102
- variant.attribute :weight_unit, :string, choices: Spree::Variant::WEIGHT_UNITS
103
- variant.attribute :dimensions_unit, :string, choices: Spree::Variant::DIMENSION_UNITS
104
- variant.attribute :hs_code, :string
105
- variant.attribute :customs_description, :string
106
- variant.attribute :country_of_origin, :country
107
- variant.attribute :minimum_order_quantity, :integer, nullable: true
108
- variant.attribute :order_multiple, :integer, nullable: true
109
- variant.attribute :purchase_unit, :string
110
- variant.attribute :units_per_carton, :integer,
111
- nullable: true,
112
- description: 'How many units fill one carton.'
113
- variant.attribute :carton_package_type_id, :id,
114
- nullable: true,
115
- description: 'The carton this variant is packed into: a package type of kind `carton`.'
116
- variant.attribute :carton_weight, :number,
117
- nullable: true,
118
- description: "What one packed carton weighs, in the variant's weight unit."
119
- variant.attribute :cartons_per_pallet, :integer,
120
- nullable: true,
121
- description: 'How many cartons stack on one pallet.'
122
- variant.attribute :track_inventory, :boolean
123
- variant.attribute :preorderable, :boolean
124
- variant.attribute :preorder_ships_at, :datetime,
125
- nullable: true,
126
- description: 'The "ships by" date shown to customers while the variant is a pre-order. Null clears it.'
127
- variant.attribute :backorder_limit, :integer,
128
- nullable: true,
129
- description: 'Maximum units sellable beyond available stock, capping backorders and pre-orders. Null is unlimited.'
130
- variant.attribute :position, :integer
131
- variant.attribute :options, :array, of: Spree::Api::V3::VariantOptionRequestSchema,
132
- description: 'One pair per option type the variant takes part in (size, color).'
133
- variant.attribute :prices, :array, of: PRICE, description: 'Per-currency prices. Upserted by currency.'
134
- variant.attribute :stock_levels, :array, of: Spree::Api::V3::StockLevelRequestSchema,
135
- description: "Stock per location, at this seller's own locations only: a location belonging to anyone else is ignored."
136
78
  end
137
79
  end
138
80
 
@@ -200,8 +142,10 @@ module Spree
200
142
  submitted_by: try_spree_current_user)
201
143
 
202
144
  # A product Propose refused is untouched; one it accepted is in
203
- # review or already through it.
204
- product.reload.status != was
145
+ # review or already through it. The saved value, not the
146
+ # attribute: a refused update leaves the new status unsaved on
147
+ # the record, and a rolled-back one restores what was stored.
148
+ product.status_in_database != was
205
149
  }
206
150
  end
207
151
 
@@ -270,6 +214,10 @@ module Spree
270
214
  Spree.product_update_workflow
271
215
  end
272
216
 
217
+ def nested_permitted_models
218
+ { media: Spree::Media, variants: Spree::Variant }
219
+ end
220
+
273
221
  # The workflow builds its own product, which would leave a seller's
274
222
  # listing unowned. Handing it one built through `current_seller.products`
275
223
  # keeps the seller on the record, whatever the payload claims.
@@ -69,12 +69,8 @@ module Spree
69
69
  # What extensions opened to sellers, never the operator-level list:
70
70
  # an attribute opened to operators must not become seller-writable
71
71
  # by accident.
72
- def model_additional_permitted_attributes
73
- return [] unless respond_to?(:model_class, true)
74
-
75
- Array(model_class.try(:additional_seller_permitted_attributes))
76
- rescue NotImplementedError
77
- []
72
+ def additional_permitted_attributes_for(model)
73
+ Array(model.try(:additional_seller_permitted_attributes))
78
74
  end
79
75
 
80
76
  def authorize_resource!(resource = @resource, action = action_name.to_sym)
@@ -86,7 +86,7 @@ module Spree
86
86
  private
87
87
 
88
88
  def members
89
- @members ||= current_seller.users.to_a
89
+ @members ||= current_seller.users.with_attached_avatar.to_a
90
90
  end
91
91
 
92
92
  def set_member