spree_api 6.0.0.rc4 → 6.0.0.rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/spree/api/v3/allowed_redirect_url.rb +27 -0
- data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +8 -2
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +6 -2
- data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +30 -8
- data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +1 -6
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +11 -5
- data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +3 -4
- data/app/controllers/spree/api/v3/admin/products_controller.rb +5 -3
- data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
- data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +17 -6
- data/app/controllers/spree/api/v3/resource_controller.rb +35 -5
- data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
- data/app/controllers/spree/api/v3/seller/products_controller.rb +7 -61
- data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
- data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
- data/app/controllers/spree/api/v3/store/carts_controller.rb +3 -3
- data/app/controllers/spree/api/v3/store/companies/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb +19 -1
- data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +3 -14
- data/app/controllers/spree/api/v3/store/data_request_confirmations_controller.rb +23 -0
- data/app/controllers/spree/api/v3/store/data_requests_controller.rb +56 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
- data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
- data/app/request_schemas/spree/api/v3/seller/price_request_schema.rb +20 -0
- data/app/request_schemas/spree/api/v3/seller/variant_request_schema.rb +19 -0
- data/app/request_schemas/spree/api/v3/variant_request_schema.rb +8 -13
- data/app/serializers/spree/api/v3/address_owner_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/address_serializer.rb +5 -1
- data/app/serializers/spree/api/v3/admin/address_owner_serializer.rb +10 -0
- data/app/serializers/spree/api/v3/admin/address_serializer.rb +1 -10
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/catalog_assignment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/catalog_price_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/collection_rule_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/commission_line_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/commission_rate_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/consent_record_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/package_type_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/admin/permission_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/seller_transfer_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/shipping_label_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/stock_receipt_item_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -0
- data/app/serializers/spree/api/v3/admin/tax_identifier_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/tax_line_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/base_serializer.rb +27 -5
- data/app/serializers/spree/api/v3/claim_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/collection_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/company_invitation_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/company_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/data_request_confirmation_requested_event_serializer.rb +28 -0
- data/app/serializers/spree/api/v3/data_request_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/data_request_serializer.rb +8 -4
- data/app/serializers/spree/api/v3/delivery_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/delivery_zone_member_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/discount_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/file_upload_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/media_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/media_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/option_type_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/order_group_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/order_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/payment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_filter_availability_option_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_filter_option_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/address_serializer.rb +3 -2
- data/app/serializers/spree/api/v3/seller/order_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/seller/package_type_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/seller/profile_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/tax_identifier_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/shipping_label_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/stock_movement_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/stock_receipt_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
- data/app/services/spree/api/v3/filter_table.rb +22 -3
- data/app/services/spree/api/v3/request_schema.rb +64 -17
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +19 -11
- data/lib/spree/api/agent_write_schemas.rb +15 -3
- data/lib/spree/api/dependencies.rb +2 -2
- data/lib/spree/api/enum_types.rb +118 -0
- data/lib/spree/api/openapi/filter_documentation.rb +8 -1
- data/lib/spree/api/typelizer_generation.rb +5 -0
- data/lib/typelizer/open_enums.rb +40 -16
- metadata +17 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf869734311256969147c858239c279f8aec3faac383f6bf030937eedc9fc54e
|
|
4
|
+
data.tar.gz: e8a7fa2d529fd09e06f6d9b3ad7007e5122c2b56ad0a0e3b32ce8fccb0d4d073
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef0d1a79bbdcd73ce34176fbe9054090214f326458c2840ac0937582334f639a4907df1cfa7bae30b566976bec1770fa32f455369eded75a8fc456b01efcbb08
|
|
7
|
+
data.tar.gz: 439f5b98f55aab51ce2d07f037336b175dce46c5e3bc9383685beebe35c34a7d519c004cf2697f68821b52ce538023a3de0dd68ee3b2522062a119db8906b59c
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
# Reads the `redirect_url` a storefront passes for an emailed link.
|
|
5
|
+
#
|
|
6
|
+
# The link carries a live token, so a page outside the store's allowed
|
|
7
|
+
# origins would hand that token to whoever named the page. An address
|
|
8
|
+
# that does not match is dropped rather than refused, so callers cannot
|
|
9
|
+
# probe the allow-list through error responses.
|
|
10
|
+
module AllowedRedirectUrl
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# @return [String, nil] the redirect URL when it matches one of the
|
|
16
|
+
# store's allowed origins
|
|
17
|
+
def validated_redirect_url
|
|
18
|
+
redirect_url = params[:redirect_url]
|
|
19
|
+
return if redirect_url.blank?
|
|
20
|
+
return unless current_store.allowed_origin?(redirect_url)
|
|
21
|
+
|
|
22
|
+
redirect_url
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -11,10 +11,16 @@ module Spree
|
|
|
11
11
|
# Find cart by prefixed ID and authorize read access (owner or guest
|
|
12
12
|
# token). Completed carts are not carts anymore — they 404 unless the
|
|
13
13
|
# caller opts in (idempotent completion, payment-session confirm races).
|
|
14
|
+
#
|
|
15
|
+
# Pass +token: nil+ to demand ownership. A guest token proves the
|
|
16
|
+
# caller holds this cart, which is the right bar for the cart's own
|
|
17
|
+
# contents and no higher — an endpoint reaching past the cart into
|
|
18
|
+
# something the account holds asks for the account instead.
|
|
19
|
+
#
|
|
14
20
|
# @return [Spree::Cart]
|
|
15
|
-
def find_cart(include_completed: false)
|
|
21
|
+
def find_cart(include_completed: false, token: cart_token)
|
|
16
22
|
@cart = resolve_cart(include_completed: include_completed)
|
|
17
|
-
authorize_storefront_read!(@cart, token:
|
|
23
|
+
authorize_storefront_read!(@cart, token: token)
|
|
18
24
|
@cart
|
|
19
25
|
end
|
|
20
26
|
|
|
@@ -159,11 +159,15 @@ module Spree
|
|
|
159
159
|
"held=#{ability.permission_keys.join(',')}"
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
+
render_missing_permission(required)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def render_missing_permission(permission)
|
|
162
166
|
render_error(
|
|
163
167
|
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
164
|
-
message: "Missing permission: #{
|
|
168
|
+
message: "Missing permission: #{permission}",
|
|
165
169
|
status: :forbidden,
|
|
166
|
-
details: { required_permission:
|
|
170
|
+
details: { required_permission: permission }
|
|
167
171
|
)
|
|
168
172
|
end
|
|
169
173
|
|
|
@@ -61,8 +61,14 @@ module Spree
|
|
|
61
61
|
Spree.api.admin_address_serializer
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
def collection_includes
|
|
65
|
+
[:owner]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The listing includes what the node inherits: a division's orders may ship to its headquarters' sites,
|
|
69
|
+
# so staff placing one must be able to see them — while writes stay with the node that owns the row.
|
|
64
70
|
def scope
|
|
65
|
-
@parent.addresses
|
|
71
|
+
action_name == 'index' ? @parent.address_book : @parent.addresses
|
|
66
72
|
end
|
|
67
73
|
end
|
|
68
74
|
end
|
|
@@ -64,16 +64,37 @@ module Spree
|
|
|
64
64
|
# store can't read or write custom fields on another store's records.
|
|
65
65
|
# Users are intentionally global in Spree (`User.for_store` is a
|
|
66
66
|
# no-op), so the store boundary for customer parents is enforced by
|
|
67
|
-
# the `authorize!(:
|
|
67
|
+
# the `authorize!(:update, @parent)` ability check above.
|
|
68
|
+
#
|
|
69
|
+
# Fails closed: `Spree::Base.for_store` answers the bare class when the
|
|
70
|
+
# store has no association for the model, which would search every
|
|
71
|
+
# store's records.
|
|
68
72
|
def parent_relation
|
|
69
73
|
klass = parent_lookup.klass
|
|
70
|
-
klass.
|
|
74
|
+
return klass.where(id: current_store.id) if klass <= Spree::Store
|
|
75
|
+
return klass if klass <= Spree.customer_class
|
|
76
|
+
|
|
77
|
+
relation = klass.for_store(current_store)
|
|
78
|
+
raise ActiveRecord::RecordNotFound, 'Parent resource not found' if relation.equal?(klass)
|
|
79
|
+
|
|
80
|
+
relation
|
|
71
81
|
end
|
|
72
82
|
|
|
73
83
|
# Product-family parents have no scope pair of their own — their
|
|
74
84
|
# custom fields are catalog data, gated by `products` like the rest
|
|
75
85
|
# of the variant/option-type surface.
|
|
76
|
-
|
|
86
|
+
# Parents whose own controller is gated by a scope other than their
|
|
87
|
+
# plural name share that controller's scope.
|
|
88
|
+
SCOPE_OVERRIDES = {
|
|
89
|
+
'option_type' => :products,
|
|
90
|
+
'variant' => :products,
|
|
91
|
+
'company' => :customers,
|
|
92
|
+
'stock_transfer' => :stock,
|
|
93
|
+
'purchase_order' => :purchasing,
|
|
94
|
+
'payment_method' => :settings,
|
|
95
|
+
'tax_rate' => :settings,
|
|
96
|
+
'store' => :settings
|
|
97
|
+
}.freeze
|
|
77
98
|
|
|
78
99
|
# Per-parent scope check: a key holding `write_products` may write a
|
|
79
100
|
# product's custom fields, `write_orders` may write an order's, etc.
|
|
@@ -104,12 +125,13 @@ module Spree
|
|
|
104
125
|
# code reloads — `enabled_resources` is captured at boot and its
|
|
105
126
|
# class references go stale. Aliases `'customer'` because the route
|
|
106
127
|
# uses `customer_id` while user_class.model_name.element is `'user'`,
|
|
107
|
-
#
|
|
108
|
-
# (5.5 rename) while the model's element is still `'taxon'
|
|
128
|
+
# `'category'` because the routes expose taxons as categories
|
|
129
|
+
# (5.5 rename) while the model's element is still `'taxon'`, and
|
|
130
|
+
# `'medium'` because Rails singularizes the `media` route to `medium_id`.
|
|
109
131
|
def parent_route_map
|
|
110
132
|
@parent_route_map ||= Spree.custom_fields.enabled_resources.each_with_object({}) do |klass, m|
|
|
111
133
|
m[klass.model_name.element.to_s] = klass.name
|
|
112
|
-
end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category')
|
|
134
|
+
end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category', 'medium' => 'Spree::Media')
|
|
113
135
|
end
|
|
114
136
|
|
|
115
137
|
# Returns the first segment whose `<segment>_id` is in the route,
|
|
@@ -117,8 +139,8 @@ module Spree
|
|
|
117
139
|
# by both `set_parent` and `scoped_resource_name`.
|
|
118
140
|
#
|
|
119
141
|
# Read from the path only: a `store_id` or `refund_id` added to the
|
|
120
|
-
# query string or body must not switch the parent
|
|
121
|
-
#
|
|
142
|
+
# query string or body must not switch the parent away from the record
|
|
143
|
+
# the route names.
|
|
122
144
|
def parent_lookup
|
|
123
145
|
return @parent_lookup if defined?(@parent_lookup)
|
|
124
146
|
|
|
@@ -248,12 +248,7 @@ module Spree
|
|
|
248
248
|
missing = EXPORT_PERMISSIONS.reject { |permission| holds_permission?(permission) }
|
|
249
249
|
return true if missing.empty?
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
253
|
-
message: "Missing permission: #{missing.first}",
|
|
254
|
-
status: :forbidden,
|
|
255
|
-
details: { required_permission: missing.first }
|
|
256
|
-
)
|
|
251
|
+
render_missing_permission(missing.first)
|
|
257
252
|
false
|
|
258
253
|
end
|
|
259
254
|
|
|
@@ -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).
|
|
@@ -17,10 +17,12 @@ module Spree
|
|
|
17
17
|
request_params :order do |body|
|
|
18
18
|
body.external_references
|
|
19
19
|
body.attribute :email, :string, example: 'customer@example.com'
|
|
20
|
-
body.attribute :customer_id, :id
|
|
20
|
+
body.attribute :customer_id, :id,
|
|
21
|
+
nullable: true,
|
|
22
|
+
description: 'Null detaches the customer, leaving a guest order under its `email`.'
|
|
21
23
|
body.attribute :use_customer_default_address, :boolean,
|
|
22
24
|
only: :create,
|
|
23
|
-
description: "
|
|
25
|
+
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
26
|
body.attribute :currency, :currency, example: 'USD'
|
|
25
27
|
body.attribute :market_id, :id
|
|
26
28
|
body.attribute :channel_id, :id, description: 'Defaults to the store primary channel when omitted.'
|
|
@@ -35,10 +37,10 @@ module Spree
|
|
|
35
37
|
description: "The `signed_id` of the buyer's purchase order, from `files.upload()` with `visibility: 'private'`. Null removes the document the order carries."
|
|
36
38
|
body.attribute :shipping_address_id, :id,
|
|
37
39
|
only: :create,
|
|
38
|
-
description:
|
|
40
|
+
description: "An existing address from the customer's address book, or from the address book of the company the order is for."
|
|
39
41
|
body.attribute :billing_address_id, :id,
|
|
40
42
|
only: :create,
|
|
41
|
-
description:
|
|
43
|
+
description: "An existing address from the customer's address book, or from the address book of the company the order is for."
|
|
42
44
|
body.attribute :preferred_stock_location_id, :id,
|
|
43
45
|
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
46
|
body.attribute :company_id, :id,
|
|
@@ -51,6 +53,8 @@ module Spree
|
|
|
51
53
|
body.attribute :tags, :array, of: :string
|
|
52
54
|
body.attribute :shipping_address, :object, shape: ADDRESS
|
|
53
55
|
body.attribute :billing_address, :object, shape: ADDRESS
|
|
56
|
+
body.attribute :use_shipping, :boolean,
|
|
57
|
+
description: 'When true, copies the shipping address to the billing address.'
|
|
54
58
|
body.attribute :items, :array,
|
|
55
59
|
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
60
|
item.attribute :variant_id, :id, required: true
|
|
@@ -343,7 +347,9 @@ module Spree
|
|
|
343
347
|
# Named rather than read from the action, since an upsert reaches
|
|
344
348
|
# update from the create action.
|
|
345
349
|
def order_update_params
|
|
346
|
-
resolve_company_param(normalize_params(params.permit(*request_params_schema.permit_list(:update))))
|
|
350
|
+
permitted = resolve_company_param(normalize_params(params.permit(*request_params_schema.permit_list(:update))))
|
|
351
|
+
permitted[:customer_id] = resolve_customer&.id if permitted.key?(:customer_id)
|
|
352
|
+
permitted
|
|
347
353
|
end
|
|
348
354
|
|
|
349
355
|
# 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, :
|
|
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, :
|
|
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
|
|
@@ -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.
|
|
12
|
-
body.
|
|
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
|
|
@@ -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
|
|
|
@@ -291,6 +289,10 @@ 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
|
+
|
|
294
296
|
# Use SearchProvider::Database for collection to handle price/best_selling
|
|
295
297
|
# sorting correctly (counts before sorting, avoiding PG/Mobility issues).
|
|
296
298
|
def collection
|
|
@@ -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, :
|
|
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,
|
|
@@ -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
|
|
@@ -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:
|
|
47
|
+
base = Spree::Stores::DashboardUrl.call(store: current_store)
|
|
48
48
|
|
|
49
|
-
"#{base}/#{
|
|
49
|
+
"#{base}/#{current_store.prefixed_id}/oauth/authorize?#{query}"
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
@@ -559,7 +559,17 @@ module Spree
|
|
|
559
559
|
# declared on the model. Override +resource_permitted_attributes+ rather
|
|
560
560
|
# than this, so the extension union is not lost.
|
|
561
561
|
def permitted_attributes
|
|
562
|
-
resource_permitted_attributes + model_additional_permitted_attributes
|
|
562
|
+
with_nested_additional_permitted_attributes(resource_permitted_attributes) + model_additional_permitted_attributes
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Nested lists this resource writes, keyed by param name, mapped to the
|
|
566
|
+
# model each entry becomes — +{ media: Spree::Media }+ lets an
|
|
567
|
+
# extension's +Spree::Media.additional_permitted_attributes+ ride the
|
|
568
|
+
# product's +media+ entries, not only the media endpoint.
|
|
569
|
+
#
|
|
570
|
+
# @return [Hash{Symbol => Class}]
|
|
571
|
+
def nested_permitted_models
|
|
572
|
+
{}
|
|
563
573
|
end
|
|
564
574
|
|
|
565
575
|
# The resource's writable attributes: its `request_params`
|
|
@@ -580,13 +590,33 @@ module Spree
|
|
|
580
590
|
def model_additional_permitted_attributes
|
|
581
591
|
return [] unless respond_to?(:model_class, true)
|
|
582
592
|
|
|
583
|
-
|
|
584
|
-
return [] unless model.respond_to?(:additional_permitted_attributes)
|
|
585
|
-
|
|
586
|
-
Array(model.additional_permitted_attributes)
|
|
593
|
+
additional_permitted_attributes_for(model_class)
|
|
587
594
|
rescue NotImplementedError
|
|
588
595
|
[]
|
|
589
596
|
end
|
|
597
|
+
|
|
598
|
+
# What extensions declared writable on +model+. `Spree.base_class` is
|
|
599
|
+
# host-overridable, so the class_attribute may not be there at all.
|
|
600
|
+
#
|
|
601
|
+
# @param model [Class]
|
|
602
|
+
# @return [Array]
|
|
603
|
+
def additional_permitted_attributes_for(model)
|
|
604
|
+
Array(model.try(:additional_permitted_attributes))
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def with_nested_additional_permitted_attributes(attributes)
|
|
608
|
+
return attributes if nested_permitted_models.empty?
|
|
609
|
+
|
|
610
|
+
attributes.map do |attribute|
|
|
611
|
+
next attribute unless attribute.is_a?(Hash)
|
|
612
|
+
|
|
613
|
+
attribute.to_h do |name, nested|
|
|
614
|
+
model = nested_permitted_models[name.to_sym]
|
|
615
|
+
additional = model ? additional_permitted_attributes_for(model) : []
|
|
616
|
+
[name, additional.any? && nested.is_a?(Array) ? nested + additional : nested]
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
end
|
|
590
620
|
end
|
|
591
621
|
end
|
|
592
622
|
end
|
|
@@ -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, :
|
|
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, :
|
|
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
|