spree_api 6.0.0.rc3 → 6.0.0.rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/spree/api/v3/admin/email_template_lookup.rb +6 -1
- data/app/controllers/concerns/spree/api/v3/admin/reporting_authorization.rb +17 -8
- data/app/controllers/concerns/spree/api/v3/admin/store_context.rb +7 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +61 -0
- 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/error_handler.rb +49 -0
- data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +76 -7
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +66 -5
- data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
- data/app/controllers/spree/api/v3/admin/companies/tax_exemption_certificates_controller.rb +11 -5
- data/app/controllers/spree/api/v3/admin/concerns/external_references.rb +3 -0
- 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/exports_controller.rb +15 -3
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
- data/app/controllers/spree/api/v3/admin/imports_controller.rb +12 -3
- data/app/controllers/spree/api/v3/admin/markets_controller.rb +3 -1
- data/app/controllers/spree/api/v3/admin/me_controller.rb +21 -1
- data/app/controllers/spree/api/v3/admin/oauth/application_tokens_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/oauth/applications_controller.rb +55 -0
- data/app/controllers/spree/api/v3/admin/oauth/authorizations_controller.rb +294 -0
- data/app/controllers/spree/api/v3/admin/order_groups_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +8 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +7 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +20 -7
- 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/purchase_orders_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/reporting_controller.rb +20 -14
- data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +8 -5
- data/app/controllers/spree/api/v3/admin/tax_exemption_reasons_controller.rb +58 -0
- data/app/controllers/spree/api/v3/admin/tax_rates_controller.rb +0 -1
- data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +17 -11
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
- data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +72 -0
- data/app/controllers/spree/api/v3/oauth/metadata_controller.rb +57 -0
- data/app/controllers/spree/api/v3/oauth/server_metadata_controller.rb +57 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +36 -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 +7 -8
- 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/email_template_preview_request_schema.rb +4 -0
- 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/fulfillment_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/oauth_access_token_serializer.rb +62 -0
- data/app/serializers/spree/api/v3/admin/oauth_application_serializer.rb +56 -0
- data/app/serializers/spree/api/v3/admin/oauth_consent_request_serializer.rb +25 -0
- data/app/serializers/spree/api/v3/admin/oauth_protected_resource_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/admin/oauth_redirect_serializer.rb +17 -0
- 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/payment_capture_event_serializer.rb +16 -0
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +12 -1
- 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/return_serializer.rb +2 -0
- 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_exemption_certificate_serializer.rb +7 -3
- data/app/serializers/spree/api/v3/admin/tax_exemption_reason_serializer.rb +20 -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/tax_rate_serializer.rb +1 -2
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/base_serializer.rb +41 -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 +80 -12
- data/lib/spree/api/agent_membership_map.rb +73 -0
- data/lib/spree/api/agent_resource_map.rb +350 -0
- data/lib/spree/api/agent_workflow_endpoints.rb +113 -0
- data/lib/spree/api/agent_write_schemas.rb +131 -0
- data/lib/spree/api/dependencies.rb +5 -4
- data/lib/spree/api/engine.rb +40 -0
- data/lib/spree/api/enum_types.rb +118 -0
- data/lib/spree/api/extension_fields.rb +167 -0
- data/lib/spree/api/oauth.rb +243 -0
- data/lib/spree/api/openapi/filter_documentation.rb +8 -1
- data/lib/spree/api/typelizer_generation.rb +5 -0
- data/lib/spree/api.rb +8 -0
- data/lib/typelizer/open_enums.rb +48 -10
- metadata +37 -6
|
@@ -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
|
|
@@ -295,8 +299,8 @@ module Spree
|
|
|
295
299
|
# reads it — otherwise a page of orders costs a query per
|
|
296
300
|
# fulfillment for a field that is nil on every parcel order.
|
|
297
301
|
[:customer, :channel, :seller, :external_references, :cancel_reason,
|
|
298
|
-
:market, { fulfillments:
|
|
299
|
-
{ line_items: [{ variant: :prices }, { price_list: :catalog }] },
|
|
302
|
+
:market, { fulfillments: %i[selected_delivery_rate external_references] },
|
|
303
|
+
{ line_items: [:external_references, { variant: :prices }, { price_list: :catalog }] },
|
|
300
304
|
{ po_document_attachment: :blob }]
|
|
301
305
|
end
|
|
302
306
|
|
|
@@ -333,10 +337,19 @@ module Spree
|
|
|
333
337
|
resolve_company_param(normalize_params(params.permit(*request_params_schema.permit_list(:create))))
|
|
334
338
|
end
|
|
335
339
|
|
|
340
|
+
# `items` is the public name for line items, so the normalizer needs
|
|
341
|
+
# telling which model the rows describe — otherwise a `prod_` id in
|
|
342
|
+
# `variant_id` is not recognised as the wrong kind.
|
|
343
|
+
def nested_param_models
|
|
344
|
+
{ 'items' => Spree::LineItem }
|
|
345
|
+
end
|
|
346
|
+
|
|
336
347
|
# Named rather than read from the action, since an upsert reaches
|
|
337
348
|
# update from the create action.
|
|
338
349
|
def order_update_params
|
|
339
|
-
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
|
|
340
353
|
end
|
|
341
354
|
|
|
342
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
|
|
@@ -47,6 +47,7 @@ module Spree
|
|
|
47
47
|
item.attribute :variant_id, :id, required: true, example: 'variant_1234567890'
|
|
48
48
|
item.attribute :quantity_ordered, :integer, required: true, example: 100
|
|
49
49
|
item.attribute :unit_cost, :decimal,
|
|
50
|
+
required: true,
|
|
50
51
|
example: '12.50',
|
|
51
52
|
description: 'What the merchant agreed to pay per unit.'
|
|
52
53
|
end
|
|
@@ -87,27 +87,33 @@ module Spree
|
|
|
87
87
|
params.to_unsafe_h.slice(*QUERY_KEYS)
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
90
|
+
# Every referenced member is authorized against what this
|
|
91
|
+
# credential holds, on top of the `read_reports` endpoint gate: a
|
|
92
|
+
# caller without `read_products` cannot query the product dimension
|
|
93
|
+
# whatever its principal type.
|
|
94
|
+
#
|
|
95
|
+
# Asked through `holds_permission?` rather than of the ability,
|
|
96
|
+
# because an OAuth grant authenticates as the staff member who
|
|
97
|
+
# approved it and carries no API key. Measured against their role,
|
|
98
|
+
# a grant for `read_reports` alone could group by customer and read
|
|
99
|
+
# names, emails and spend — data the merchant never consented to.
|
|
95
100
|
def authorize_reporting_members!(reporting_query)
|
|
96
|
-
|
|
97
|
-
missing = reporting_query.required_key_scopes.reject { |scope| current_api_key.has_scope?(scope) }
|
|
98
|
-
return if missing.empty?
|
|
101
|
+
missing = reporting_query.required_key_scopes.reject { |scope| holds_permission?(scope) }
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
if missing.any?
|
|
104
|
+
return render_error(
|
|
101
105
|
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
102
|
-
message: "
|
|
106
|
+
message: "Missing permission: #{missing.join(', ')}",
|
|
103
107
|
status: :forbidden,
|
|
104
108
|
details: { required_scopes: missing }
|
|
105
109
|
)
|
|
106
|
-
else
|
|
107
|
-
authorize!(:read, :reports)
|
|
108
|
-
forbidden = reporting_query.unreadable_subjects(current_ability).first
|
|
109
|
-
raise CanCan::AccessDenied.new(nil, :read, forbidden) if forbidden
|
|
110
110
|
end
|
|
111
|
+
|
|
112
|
+
# Then the record-level rules, which a host app uses to narrow
|
|
113
|
+
# what a role sees within data it may read at all.
|
|
114
|
+
authorize!(:read, :reports)
|
|
115
|
+
forbidden = reporting_query.unreadable_subjects(current_ability).first
|
|
116
|
+
raise CanCan::AccessDenied.new(nil, :read, forbidden) if forbidden
|
|
111
117
|
end
|
|
112
118
|
|
|
113
119
|
# An unknown member carries what it was and what was valid instead.
|
|
@@ -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,
|
|
@@ -82,18 +82,21 @@ module Spree
|
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
# Per-type scope check for API-key principals: a key listing product
|
|
85
|
-
# tags needs `read_products`, order tags `read_orders`, etc.
|
|
86
|
-
#
|
|
85
|
+
# tags needs `read_products`, order tags `read_orders`, etc.
|
|
86
|
+
#
|
|
87
|
+
# Asked of the credential, not of its kind: an OAuth grant carries
|
|
88
|
+
# no API key, so a check that began `return unless current_api_key`
|
|
89
|
+
# skipped itself entirely and handed an agent every order and
|
|
90
|
+
# customer tag the approver could see.
|
|
87
91
|
def authorize_taggable_scope!
|
|
88
|
-
return unless current_api_key
|
|
89
92
|
return unless taggable_type
|
|
90
93
|
|
|
91
94
|
required = self.class.scope_for_taggable_type.fetch(taggable_type, 'read_all')
|
|
92
|
-
return if
|
|
95
|
+
return if holds_permission?(required)
|
|
93
96
|
|
|
94
97
|
render_error(
|
|
95
98
|
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
96
|
-
message: "
|
|
99
|
+
message: "Missing permission: #{required}",
|
|
97
100
|
status: :forbidden,
|
|
98
101
|
details: { required_scope: required }
|
|
99
102
|
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class TaxExemptionReasonsController < ResourceController
|
|
6
|
+
scoped_resource :settings
|
|
7
|
+
|
|
8
|
+
request_params :tax_exemption_reason do |body|
|
|
9
|
+
body.include_attributes Spree::Api::V3::Admin::ReasonRequestSchema
|
|
10
|
+
body.attribute :code, :string, required: true, on: :create, example: 'RESALE',
|
|
11
|
+
description: 'What the tax provider receives for every certificate holding this reason — for Avalara, an entity use code such as `G`. ' \
|
|
12
|
+
'Saved in capitals. Changing it changes what those certificates send from then on.'
|
|
13
|
+
body.attribute :metadata, :hash
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
def model_class
|
|
19
|
+
Spree::TaxExemptionReason
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def serializer_class
|
|
23
|
+
Spree.api.admin_tax_exemption_reason_serializer
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def permitted_params
|
|
27
|
+
params.permit(*permitted_attributes)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The certificate form picks from this list, so staff who manage
|
|
31
|
+
# customers read it without the settings key. Writing stays a
|
|
32
|
+
# settings matter.
|
|
33
|
+
def scoped_resource_name
|
|
34
|
+
reads_through_customers? ? :customers : super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def scope
|
|
38
|
+
reads_through_customers? ? current_store.tax_exemption_reasons : super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def authorize_resource!(resource = @resource, action = action_name.to_sym)
|
|
42
|
+
return authorize!(:show, Spree::TaxExemptionCertificate) if reads_through_customers?
|
|
43
|
+
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reads_through_customers?
|
|
48
|
+
return @reads_through_customers if defined?(@reads_through_customers)
|
|
49
|
+
|
|
50
|
+
@reads_through_customers = action_kind == 'read' &&
|
|
51
|
+
!holds_permission?('read_settings') &&
|
|
52
|
+
holds_permission?('read_customers')
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -13,7 +13,6 @@ module Spree
|
|
|
13
13
|
example: '0.19',
|
|
14
14
|
description: 'The rate as a fraction. Send this or `rate_percent`.'
|
|
15
15
|
body.attribute :rate_percent, :decimal, example: '19', description: 'The rate as a percentage.'
|
|
16
|
-
body.attribute :included_in_price, :boolean
|
|
17
16
|
body.attribute :show_rate_in_label, :boolean
|
|
18
17
|
body.attribute :tax_category_id, :id, required: true, on: :create
|
|
19
18
|
body.attribute :country_code, :country,
|
|
@@ -20,9 +20,10 @@ module Spree
|
|
|
20
20
|
# authorization, and rendering.
|
|
21
21
|
class BatchesController < Admin::BaseController
|
|
22
22
|
# The batch spans heterogeneous resource types, so a single static
|
|
23
|
-
# scope can't gate it. We verify the
|
|
24
|
-
# write_<resource>
|
|
25
|
-
# which is stricter than the static check
|
|
23
|
+
# scope can't gate it. We verify the caller holds the matching
|
|
24
|
+
# write_<resource> permission for EVERY entry (see
|
|
25
|
+
# require_batch_scopes!), which is stricter than the static check
|
|
26
|
+
# and keeps the audit model.
|
|
26
27
|
skip_scope_check!
|
|
27
28
|
|
|
28
29
|
request_params 'TranslationBatchParams' do |body|
|
|
@@ -63,19 +64,24 @@ module Spree
|
|
|
63
64
|
|
|
64
65
|
private
|
|
65
66
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
67
|
+
# Require write_<resource> for every distinct resource type in the
|
|
68
|
+
# batch.
|
|
69
|
+
#
|
|
70
|
+
# Asked of the caller rather than of a secret key, because there
|
|
71
|
+
# are three principals and only one of them carries scopes on a
|
|
72
|
+
# key. An OAuth token resolves as a staff user with no key, so
|
|
73
|
+
# reading `current_api_key` here waved it through and the ceiling
|
|
74
|
+
# the merchant consented to went unenforced on the one write this
|
|
75
|
+
# controller performs — `holds_permission?` answers for all three.
|
|
76
|
+
#
|
|
77
|
+
# Returns false (and renders 403) when a permission is missing.
|
|
70
78
|
def require_batch_scopes!(batch)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
missing = batch.required_scopes.reject { |scope| current_api_key.has_scope?(scope) }
|
|
79
|
+
missing = batch.required_scopes.reject { |scope| holds_permission?(scope) }
|
|
74
80
|
return true if missing.empty?
|
|
75
81
|
|
|
76
82
|
render_error(
|
|
77
83
|
code: ERROR_CODES[:access_denied],
|
|
78
|
-
message: "
|
|
84
|
+
message: "You do not have permission to #{missing.map { |key| key.tr('_', ' ') }.to_sentence}.",
|
|
79
85
|
status: :forbidden,
|
|
80
86
|
details: { required_scopes: missing }
|
|
81
87
|
)
|
|
@@ -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
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Oauth
|
|
5
|
+
# The authorization endpoint a client sends the browser to.
|
|
6
|
+
#
|
|
7
|
+
# It renders nothing itself: consent belongs in the dashboard, where
|
|
8
|
+
# the merchant is already signed in and the screen matches the rest of
|
|
9
|
+
# the admin. This hands the request over with its parameters intact,
|
|
10
|
+
# and the dashboard reads them back through the JSON endpoints under
|
|
11
|
+
# /api/v3/admin/oauth.
|
|
12
|
+
#
|
|
13
|
+
# It is a real route rather than a skipped one because RFC 8414
|
|
14
|
+
# requires the metadata document to name an `authorization_endpoint`,
|
|
15
|
+
# and a consumer client refuses a document that advertises the
|
|
16
|
+
# authorization-code flow without one.
|
|
17
|
+
class AuthorizationsController < ActionController::Base
|
|
18
|
+
include Spree::Core::ControllerHelpers::Store
|
|
19
|
+
|
|
20
|
+
# The authorization endpoint is reached by a cross-site GET on
|
|
21
|
+
# purpose — a client sends the browser here — and forgery
|
|
22
|
+
# protection never applies to a GET, so `:exception` costs nothing
|
|
23
|
+
# while leaving the endpoint reachable. Nothing is granted here
|
|
24
|
+
# either way: the action only redirects, and consent is submitted
|
|
25
|
+
# to the Admin API under the merchant's own session.
|
|
26
|
+
protect_from_forgery with: :exception
|
|
27
|
+
|
|
28
|
+
# Carried across to the dashboard. Everything else is dropped, so a
|
|
29
|
+
# crafted link cannot smuggle extra parameters into the consent page.
|
|
30
|
+
FORWARDED_PARAMETERS = %w[
|
|
31
|
+
client_id redirect_uri response_type scope state
|
|
32
|
+
code_challenge code_challenge_method resource
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
def new
|
|
36
|
+
redirect_to consent_url, allow_other_host: true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# The client id names the store, not the hostname. An installation
|
|
42
|
+
# serving several stores from one API origin would otherwise send
|
|
43
|
+
# every consent to whichever store that host happens to resolve as,
|
|
44
|
+
# and the merchant would approve on the wrong one.
|
|
45
|
+
def consent_url
|
|
46
|
+
query = params.permit(*FORWARDED_PARAMETERS).to_h.compact_blank.to_query
|
|
47
|
+
base = Spree::Stores::DashboardUrl.call(store: current_store)
|
|
48
|
+
|
|
49
|
+
"#{base}/#{current_store.prefixed_id}/oauth/authorize?#{query}"
|
|
50
|
+
end
|
|
51
|
+
|
|
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])
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Oauth
|
|
5
|
+
# RFC 9728 Protected Resource Metadata — the one discovery document
|
|
6
|
+
# Doorkeeper does not ship (it serves RFC 8414 authorization-server
|
|
7
|
+
# metadata itself).
|
|
8
|
+
#
|
|
9
|
+
# A client reads this to learn which authorization server guards a
|
|
10
|
+
# resource. It is deliberately unauthenticated: a client has no
|
|
11
|
+
# credential at the point it asks.
|
|
12
|
+
class MetadataController < ActionController::API
|
|
13
|
+
|
|
14
|
+
# A public discovery document, read before any credential exists.
|
|
15
|
+
# The module has to be included because `ActionController::API`
|
|
16
|
+
# carries none of it; `:exception` is free here because the only
|
|
17
|
+
# action is a GET, which forgery protection never applies to.
|
|
18
|
+
include ActionController::RequestForgeryProtection
|
|
19
|
+
protect_from_forgery with: :exception
|
|
20
|
+
|
|
21
|
+
# The `resource` value must equal the URL the client was pointed at,
|
|
22
|
+
# path and all, or a consumer client refuses to continue — so it is
|
|
23
|
+
# built from this request's own origin rather than from anything
|
|
24
|
+
# configured elsewhere.
|
|
25
|
+
def show
|
|
26
|
+
resource = Spree::Api::Oauth.resource_identifier(params[:resource_key], request.base_url)
|
|
27
|
+
return head :not_found if resource.blank?
|
|
28
|
+
|
|
29
|
+
document = Spree::OauthProtectedResource.new(
|
|
30
|
+
resource: resource,
|
|
31
|
+
authorization_servers: [issuer],
|
|
32
|
+
# Exactly what the authorization server will accept.
|
|
33
|
+
#
|
|
34
|
+
# The spec tells a client with no scope challenge to request
|
|
35
|
+
# everything advertised here, and Doorkeeper refuses a request
|
|
36
|
+
# naming a scope it does not know — so advertising one more
|
|
37
|
+
# than it accepts turns every connection into "The requested
|
|
38
|
+
# scope is invalid".
|
|
39
|
+
scopes_supported: Spree::Api::Oauth.staff_scope_keys,
|
|
40
|
+
resource_documentation: 'https://spreecommerce.org/docs/developer/mcp/overview'
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
render json: Spree.api.admin_oauth_protected_resource_serializer.new(document).to_h
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# The authorization server's own origin. This request reached it, so
|
|
49
|
+
# the origin it arrived on is the one a client can use.
|
|
50
|
+
def issuer
|
|
51
|
+
::Doorkeeper.config.issuer.presence || request.base_url
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Oauth
|
|
5
|
+
# RFC 8414 Authorization Server Metadata.
|
|
6
|
+
#
|
|
7
|
+
# The document itself is entirely Doorkeeper's. Only URL generation is
|
|
8
|
+
# Spree's problem: Doorkeeper asks for each endpoint by controller name
|
|
9
|
+
# anchored at the application root, and these routes are drawn inside
|
|
10
|
+
# an isolated engine, which cannot generate from such a name. The
|
|
11
|
+
# injected resolver answers from the engine's own router instead.
|
|
12
|
+
class ServerMetadataController < ::Doorkeeper::MetadataController
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def metadata_response
|
|
16
|
+
@metadata_response ||= ::Doorkeeper::OAuth::MetadataResponse.new(
|
|
17
|
+
request.base_url,
|
|
18
|
+
->(**arguments) { resolve_endpoint(**arguments) }
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Doorkeeper hands over a controller name anchored at the
|
|
23
|
+
# application root (`/api/v3/…`), while the engine's router knows
|
|
24
|
+
# its own controllers by their engine-qualified names
|
|
25
|
+
# (`spree/api/v3/…`). Doorkeeper's own controllers are already
|
|
26
|
+
# absolute and generate from the application router unchanged.
|
|
27
|
+
def resolve_endpoint(**arguments)
|
|
28
|
+
# Doorkeeper prepends a slash to whatever the route mapping
|
|
29
|
+
# held, so a name configured as "/doorkeeper/tokens" arrives
|
|
30
|
+
# doubled.
|
|
31
|
+
name = arguments[:controller].to_s.sub(%r{\A/+}, '')
|
|
32
|
+
|
|
33
|
+
if name.start_with?('doorkeeper/')
|
|
34
|
+
return Spree::Core::Engine.routes.url_for(
|
|
35
|
+
**arguments.except(:controller),
|
|
36
|
+
controller: "/#{name}",
|
|
37
|
+
only_path: false,
|
|
38
|
+
host: request.host,
|
|
39
|
+
protocol: request.protocol,
|
|
40
|
+
port: request.optional_port
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Spree::Core::Engine.routes.url_for(
|
|
45
|
+
**arguments.except(:controller),
|
|
46
|
+
controller: "spree/#{name}",
|
|
47
|
+
only_path: false,
|
|
48
|
+
host: request.host,
|
|
49
|
+
protocol: request.protocol,
|
|
50
|
+
port: request.optional_port
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|