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
|
@@ -268,6 +268,7 @@ module Spree
|
|
|
268
268
|
preload_associations_lazily.
|
|
269
269
|
ransack(ransack_params, auth_object: ransack_auth_object)
|
|
270
270
|
result = @search.result(distinct: collection_distinct?)
|
|
271
|
+
result = result.with_translation_fallback_order if model_class.include?(Spree::TranslatableResource)
|
|
271
272
|
pagy_options = { limit: limit, page: page }
|
|
272
273
|
result = apply_collection_sort(result)
|
|
273
274
|
@pagy, @collection = pagy(result, **pagy_options)
|
|
@@ -558,7 +559,17 @@ module Spree
|
|
|
558
559
|
# declared on the model. Override +resource_permitted_attributes+ rather
|
|
559
560
|
# than this, so the extension union is not lost.
|
|
560
561
|
def permitted_attributes
|
|
561
|
-
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
|
+
{}
|
|
562
573
|
end
|
|
563
574
|
|
|
564
575
|
# The resource's writable attributes: its `request_params`
|
|
@@ -579,13 +590,33 @@ module Spree
|
|
|
579
590
|
def model_additional_permitted_attributes
|
|
580
591
|
return [] unless respond_to?(:model_class, true)
|
|
581
592
|
|
|
582
|
-
|
|
583
|
-
return [] unless model.respond_to?(:additional_permitted_attributes)
|
|
584
|
-
|
|
585
|
-
Array(model.additional_permitted_attributes)
|
|
593
|
+
additional_permitted_attributes_for(model_class)
|
|
586
594
|
rescue NotImplementedError
|
|
587
595
|
[]
|
|
588
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
|
|
589
620
|
end
|
|
590
621
|
end
|
|
591
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
|
|
@@ -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:
|
|
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: '
|
|
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
|
|
|
@@ -270,6 +212,10 @@ module Spree
|
|
|
270
212
|
Spree.product_update_workflow
|
|
271
213
|
end
|
|
272
214
|
|
|
215
|
+
def nested_permitted_models
|
|
216
|
+
{ media: Spree::Media, variants: Spree::Variant }
|
|
217
|
+
end
|
|
218
|
+
|
|
273
219
|
# The workflow builds its own product, which would leave a seller's
|
|
274
220
|
# listing unowned. Handing it one built through `current_seller.products`
|
|
275
221
|
# 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
|
|
73
|
-
|
|
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)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Store
|
|
5
|
+
module Carts
|
|
6
|
+
# Every address this cart may be shipped or billed to, in one call:
|
|
7
|
+
# the buyer's own book, plus the book of the company the cart names
|
|
8
|
+
# — that node's entries and the ones it inherits from the nodes above
|
|
9
|
+
# it — limited to the countries the cart's market sells to.
|
|
10
|
+
#
|
|
11
|
+
# A checkout address step would otherwise merge two books and filter
|
|
12
|
+
# them by the market itself. Asking the cart keeps the picker and the
|
|
13
|
+
# checkout from ever disagreeing about what is usable.
|
|
14
|
+
#
|
|
15
|
+
# Signed in only, unlike the rest of the cart surface. What comes
|
|
16
|
+
# back is not the cart's own data but the books behind it — every
|
|
17
|
+
# address the buyer has saved, and their company's sites. A guest
|
|
18
|
+
# token says the caller holds this cart, which it may well do
|
|
19
|
+
# because the account holder adopted a cart the attacker started;
|
|
20
|
+
# it says nothing about the account. A genuine guest cart has
|
|
21
|
+
# neither book, so requiring the account costs nothing but the 401.
|
|
22
|
+
class AddressesController < Store::ResourceController
|
|
23
|
+
include Spree::Api::V3::CartResolvable
|
|
24
|
+
|
|
25
|
+
prepend_before_action :require_authentication!
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def set_parent
|
|
30
|
+
@parent = @cart = find_cart(token: nil)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def model_class
|
|
34
|
+
Spree::Address
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def serializer_class
|
|
38
|
+
Spree.api.address_serializer
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def collection_includes
|
|
42
|
+
[:owner]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parent_association
|
|
46
|
+
:selectable_addresses
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -13,7 +13,7 @@ module Spree
|
|
|
13
13
|
|
|
14
14
|
CART_ADDRESS = Spree::Api::V3::RequestSchema.shape('CartAddressParams') do |schema|
|
|
15
15
|
schema.attribute :id, :id,
|
|
16
|
-
description:
|
|
16
|
+
description: "An existing address from the buyer's address book, or from the book of the company the cart is for. The other fields are then ignored."
|
|
17
17
|
schema.include_attributes Spree::Api::V3::Store::AddressRequestSchema
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -25,10 +25,10 @@ module Spree
|
|
|
25
25
|
schema.attribute :currency, :currency
|
|
26
26
|
schema.attribute :locale, :locale
|
|
27
27
|
schema.attribute :shipping_address_id, :id,
|
|
28
|
-
description:
|
|
28
|
+
description: "An existing address to ship to, from the buyer's address book or from the book of the company the cart is for. List both with `GET /store/carts/:id/addresses`.",
|
|
29
29
|
example: 'addr_abc123'
|
|
30
30
|
schema.attribute :billing_address_id, :id,
|
|
31
|
-
description:
|
|
31
|
+
description: "An existing address to bill, from the buyer's address book or from the book of the company the cart is for. List both with `GET /store/carts/:id/addresses`.",
|
|
32
32
|
example: 'addr_def456'
|
|
33
33
|
schema.attribute :preferred_stock_location_id, :id
|
|
34
34
|
schema.attribute :company_id, :id, nullable: true, description: <<~TEXT.squish
|
|
@@ -123,7 +123,7 @@ module Spree
|
|
|
123
123
|
sweep_unavailable_coupon_code!
|
|
124
124
|
render_cart
|
|
125
125
|
else
|
|
126
|
-
|
|
126
|
+
render_result_failure(result, code: ERROR_CODES[:validation_error])
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
end
|
|
@@ -181,11 +181,10 @@ module Spree
|
|
|
181
181
|
if result.success?
|
|
182
182
|
@cart = result.value
|
|
183
183
|
render_order
|
|
184
|
+
elsif result.error.to_s.present?
|
|
185
|
+
render_result_failure(result, code: ERROR_CODES[:cart_cannot_complete])
|
|
184
186
|
else
|
|
185
|
-
render_service_error(
|
|
186
|
-
result.error.to_s.presence || 'Could not complete checkout',
|
|
187
|
-
code: ERROR_CODES[:cart_cannot_complete]
|
|
188
|
-
)
|
|
187
|
+
render_service_error('Could not complete checkout', code: ERROR_CODES[:cart_cannot_complete])
|
|
189
188
|
end
|
|
190
189
|
rescue ActiveRecord::RecordNotFound
|
|
191
190
|
# Gateway-race retry with the id the client got back: the webhook
|
|
@@ -69,6 +69,10 @@ module Spree
|
|
|
69
69
|
Spree.api.address_serializer
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
def collection_includes
|
|
73
|
+
[:owner]
|
|
74
|
+
end
|
|
75
|
+
|
|
72
76
|
# Reading a book and keeping one are different rights: the listing
|
|
73
77
|
# includes what this node inherits, writes stay with the node that
|
|
74
78
|
# owns the row. The Store API authorizes purely by what this scope
|
|
@@ -10,14 +10,30 @@ module Spree
|
|
|
10
10
|
# irreversible, and an unattended session should not be enough to
|
|
11
11
|
# trigger it — the same bar the account already applies to changing
|
|
12
12
|
# an email address.
|
|
13
|
+
#
|
|
14
|
+
# Nothing happens until the request is confirmed from the link emailed
|
|
15
|
+
# to the account's address (see DataRequestConfirmationsController),
|
|
16
|
+
# and the export link is never shown to this session.
|
|
13
17
|
class DataRequestsController < ResourceController
|
|
14
18
|
include Spree::Api::V3::CurrentPasswordConfirmation
|
|
19
|
+
include Spree::Api::V3::AllowedRedirectUrl
|
|
15
20
|
|
|
16
21
|
prepend_before_action :require_authentication!
|
|
17
22
|
|
|
23
|
+
# Each request while one waits sends the confirmation email again.
|
|
24
|
+
rate_limit to: Spree::Api::Config[:rate_limit_password_reset],
|
|
25
|
+
within: Spree::Api::Config[:rate_limit_window].seconds,
|
|
26
|
+
store: Rails.cache,
|
|
27
|
+
only: :create,
|
|
28
|
+
with: RATE_LIMIT_RESPONSE
|
|
29
|
+
|
|
18
30
|
request_params 'CreateDataRequestParams', action: :create do |body|
|
|
19
31
|
body.attribute :kind, :string, choices: Spree::DataRequest::KINDS, description: 'Defaults to `access`.'
|
|
20
32
|
body.attribute :current_password, :string, description: 'Required for `erasure`.'
|
|
33
|
+
body.attribute :redirect_url, :string,
|
|
34
|
+
format: :uri,
|
|
35
|
+
example: 'https://myshop.com/account/data-requests/confirm',
|
|
36
|
+
description: "The storefront page the confirmation link opens, with the token appended. Ignored unless it matches one of the store's allowed origins."
|
|
21
37
|
end
|
|
22
38
|
|
|
23
39
|
# POST /api/v3/store/customers/me/data_requests
|
|
@@ -27,7 +43,9 @@ module Spree
|
|
|
27
43
|
result = Spree::DataRequests::Create.call(
|
|
28
44
|
store: current_store,
|
|
29
45
|
customer: current_user,
|
|
30
|
-
kind: requested_kind
|
|
46
|
+
kind: requested_kind,
|
|
47
|
+
confirm_by_email: true,
|
|
48
|
+
redirect_url: validated_redirect_url
|
|
31
49
|
)
|
|
32
50
|
|
|
33
51
|
return render_result_error(result) unless result.success?
|
|
@@ -6,6 +6,8 @@ module Spree
|
|
|
6
6
|
module Store
|
|
7
7
|
module Customer
|
|
8
8
|
class PasswordResetsController < Store::BaseController
|
|
9
|
+
include Spree::Api::V3::AllowedRedirectUrl
|
|
10
|
+
|
|
9
11
|
allow_guest_storefront_access!
|
|
10
12
|
rate_limit to: Spree::Api::Config[:rate_limit_password_reset],
|
|
11
13
|
within: Spree::Api::Config[:rate_limit_window].seconds,
|
|
@@ -33,26 +35,13 @@ module Spree
|
|
|
33
35
|
|
|
34
36
|
# POST /api/v3/store/password_resets
|
|
35
37
|
def create
|
|
36
|
-
redirect_url = params[:redirect_url]
|
|
37
|
-
|
|
38
|
-
# Validate redirect_url against allowed origins (secure by default).
|
|
39
|
-
# If no allowed origins are configured, redirect_url is silently ignored
|
|
40
|
-
# to prevent open redirect / token exfiltration attacks.
|
|
41
|
-
if redirect_url.present?
|
|
42
|
-
if current_store.allowed_origins.exists? && current_store.allowed_origin?(redirect_url)
|
|
43
|
-
# redirect_url is valid — keep it
|
|
44
|
-
else
|
|
45
|
-
redirect_url = nil
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
38
|
user = Spree.customer_class.find_by(email: params[:email])
|
|
50
39
|
|
|
51
40
|
if user
|
|
52
41
|
event_name = 'customer.password_reset_requested'
|
|
53
42
|
payload = user.event_payload_for(
|
|
54
43
|
event_name,
|
|
55
|
-
reset_token: user.generate_token_for(:password_reset), store: current_store, redirect_url:
|
|
44
|
+
reset_token: user.generate_token_for(:password_reset), store: current_store, redirect_url: validated_redirect_url
|
|
56
45
|
)
|
|
57
46
|
user.publish_event(event_name, payload)
|
|
58
47
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Store
|
|
5
|
+
# Confirms a request a customer raised, from the page the emailed link
|
|
6
|
+
# opens. The page calls this when the person presses its button, never
|
|
7
|
+
# on load: some mail scanners open links and run the page's scripts.
|
|
8
|
+
class DataRequestConfirmationsController < DataRequestsController
|
|
9
|
+
# POST /api/v3/store/data_requests/:token/confirmation
|
|
10
|
+
def create
|
|
11
|
+
return render_confirmation_invalid(status: :unprocessable_content) unless data_request
|
|
12
|
+
|
|
13
|
+
result = Spree::DataRequests::Confirm.call(data_request: data_request)
|
|
14
|
+
|
|
15
|
+
return render_confirmation_invalid(status: :unprocessable_content) unless result.success?
|
|
16
|
+
|
|
17
|
+
render json: serialize_resource(result.value)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Store
|
|
5
|
+
# A data request as seen by whoever holds its confirmation token: the
|
|
6
|
+
# person reading mail at the address the request names.
|
|
7
|
+
#
|
|
8
|
+
# Token-addressed rather than session-authenticated, because the link
|
|
9
|
+
# arrives by email and may be opened on a device that was never signed
|
|
10
|
+
# in. Besides the export email, it is the only place the export link is
|
|
11
|
+
# shown, since the token is what reached the address the request names.
|
|
12
|
+
class DataRequestsController < Store::BaseController
|
|
13
|
+
allow_guest_storefront_access!
|
|
14
|
+
|
|
15
|
+
# The response can carry a live link to a person's whole export, the
|
|
16
|
+
# same credential the download itself keeps out of shared caches.
|
|
17
|
+
before_action { response.headers['Cache-Control'] = 'no-store' }
|
|
18
|
+
|
|
19
|
+
# GET /api/v3/store/data_requests/:token
|
|
20
|
+
def show
|
|
21
|
+
return render_confirmation_invalid(status: :not_found) unless data_request
|
|
22
|
+
|
|
23
|
+
render json: serialize_resource(data_request)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def serializer_class
|
|
29
|
+
Spree.api.data_request_serializer
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def serializer_params
|
|
33
|
+
super.merge(download_url_visible: true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def data_request
|
|
39
|
+
return @data_request if defined?(@data_request)
|
|
40
|
+
|
|
41
|
+
token = params[:token].presence
|
|
42
|
+
@data_request = token && Spree::DataRequest.for_store(current_store).find_by_token_for(:confirmation, token)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render_confirmation_invalid(status:)
|
|
46
|
+
render_error(
|
|
47
|
+
code: ERROR_CODES[:invalid_token],
|
|
48
|
+
message: I18n.t('spree.data_request_errors.confirmation_invalid'),
|
|
49
|
+
status: status
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -3,6 +3,8 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Store
|
|
5
5
|
class NewsletterSubscribersController < Store::BaseController
|
|
6
|
+
include Spree::Api::V3::AllowedRedirectUrl
|
|
7
|
+
|
|
6
8
|
allow_guest_storefront_access!
|
|
7
9
|
rate_limit to: Spree::Api::Config[:rate_limit_register],
|
|
8
10
|
within: Spree::Api::Config[:rate_limit_window].seconds,
|
|
@@ -139,17 +141,6 @@ module Spree
|
|
|
139
141
|
subscriber = Spree::NewsletterSubscriber.for_store(current_store).find_by_token_for(:unsubscribe, params[:token])
|
|
140
142
|
subscriber if subscriber&.prefixed_id == params[:id]
|
|
141
143
|
end
|
|
142
|
-
|
|
143
|
-
# Drop redirect_url when it isn't in the store's allow-list — secure-by-default,
|
|
144
|
-
# mirrors password_resets. Returning nil omits it from the webhook payload rather
|
|
145
|
-
# than rejecting the request, so callers can't probe the allow-list via 4xx errors.
|
|
146
|
-
def validated_redirect_url
|
|
147
|
-
redirect_url = params[:redirect_url]
|
|
148
|
-
return nil if redirect_url.blank?
|
|
149
|
-
return nil unless current_store.allowed_origin?(redirect_url)
|
|
150
|
-
|
|
151
|
-
redirect_url
|
|
152
|
-
end
|
|
153
144
|
end
|
|
154
145
|
end
|
|
155
146
|
end
|
|
@@ -25,6 +25,10 @@ module Spree
|
|
|
25
25
|
Spree::Emails::Branding::COLORS.each { |name| branding.attribute name, :string, nullable: true }
|
|
26
26
|
branding.attribute :font, :string, nullable: true, choices: Spree::Emails::Branding::FONTS.keys
|
|
27
27
|
end
|
|
28
|
+
attribute :mailer_logo_signed_id, :string,
|
|
29
|
+
nullable: true,
|
|
30
|
+
permit: false,
|
|
31
|
+
description: 'An unsaved email logo: the signed id of an uploaded image, or null to preview without one.'
|
|
28
32
|
end
|
|
29
33
|
end
|
|
30
34
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# The shared variant attributes, plus the marketplace configuration
|
|
6
|
+
# only an operator sets.
|
|
7
|
+
class VariantRequestSchema < Spree::Api::V3::VariantRequestSchema
|
|
8
|
+
publishes 'VariantParams'
|
|
9
|
+
|
|
10
|
+
attribute :seller_id, :id
|
|
11
|
+
attribute :delivery_profile_id, :id, nullable: true
|
|
12
|
+
attribute :tax_category_id, :id
|
|
13
|
+
attribute :prices, :array, of: Spree::Api::V3::PriceRequestSchema,
|
|
14
|
+
description: 'Per-currency prices. Upserted by currency.'
|
|
15
|
+
attribute :stock_levels, :array, of: Spree::Api::V3::StockLevelRequestSchema,
|
|
16
|
+
description: 'Stock per location. Upserted by location; omitted locations are removed.'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Seller
|
|
5
|
+
# The operator's price shape, except `currency` may be left out: the
|
|
6
|
+
# store's currency is assumed (see `default_price_currencies`).
|
|
7
|
+
class PriceRequestSchema < Spree::Api::V3::RequestSchema
|
|
8
|
+
attribute :currency, :currency,
|
|
9
|
+
example: 'USD',
|
|
10
|
+
description: "Omit to price in the store's currency. A seller has no currency of their own."
|
|
11
|
+
attribute :amount, :decimal,
|
|
12
|
+
required: true,
|
|
13
|
+
example: '24.00',
|
|
14
|
+
description: 'A canonical decimal string (`"1234.56"`: period decimal, no grouping). The API does not parse localized input.'
|
|
15
|
+
attribute :compare_at_amount, :decimal, nullable: true, example: '30.00'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Seller
|
|
5
|
+
# The shared variant attributes. Narrower than the operator's set:
|
|
6
|
+
# `seller_id` would let a payload hand a variant to somebody else, and
|
|
7
|
+
# `tax_category_id`/`delivery_profile_id` are marketplace configuration.
|
|
8
|
+
class VariantRequestSchema < Spree::Api::V3::VariantRequestSchema
|
|
9
|
+
publishes 'VariantParams'
|
|
10
|
+
|
|
11
|
+
attribute :prices, :array, of: Spree::Api::V3::Seller::PriceRequestSchema,
|
|
12
|
+
description: 'Per-currency prices. Upserted by currency.'
|
|
13
|
+
attribute :stock_levels, :array, of: Spree::Api::V3::StockLevelRequestSchema,
|
|
14
|
+
description: "Stock per location, at this seller's own locations only: a location belonging to anyone else is ignored."
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Api
|
|
3
3
|
module V3
|
|
4
|
-
# A variant's own attributes,
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# A variant's own attributes, the ones a seller may write too. Each API
|
|
5
|
+
# publishes its own `VariantParams` from a subclass, which the variant
|
|
6
|
+
# endpoints and the variants nested in a product payload both extend.
|
|
7
7
|
class VariantRequestSchema < Spree::Api::V3::RequestSchema
|
|
8
8
|
attribute :sku, :string
|
|
9
9
|
attribute :barcode, :string
|
|
@@ -13,14 +13,14 @@ module Spree
|
|
|
13
13
|
attribute :height, :number, nullable: true
|
|
14
14
|
attribute :width, :number, nullable: true
|
|
15
15
|
attribute :depth, :number, nullable: true
|
|
16
|
-
attribute :weight_unit, :
|
|
17
|
-
attribute :dimensions_unit, :
|
|
16
|
+
attribute :weight_unit, :weight_unit, nullable: true, description: "Null falls back to the store's weight unit."
|
|
17
|
+
attribute :dimensions_unit, :dimension_unit, nullable: true, description: "Null falls back to the store's unit system."
|
|
18
18
|
attribute :hs_code, :string
|
|
19
19
|
attribute :customs_description, :string
|
|
20
20
|
attribute :country_of_origin, :country
|
|
21
21
|
attribute :minimum_order_quantity, :integer, nullable: true
|
|
22
22
|
attribute :order_multiple, :integer, nullable: true
|
|
23
|
-
attribute :purchase_unit, :string
|
|
23
|
+
attribute :purchase_unit, :string, nullable: true, choices: Spree::Variant::PURCHASE_UNITS
|
|
24
24
|
attribute :units_per_carton, :integer, nullable: true, description: 'How many units fill one carton.'
|
|
25
25
|
attribute :carton_package_type_id, :id,
|
|
26
26
|
nullable: true,
|
|
@@ -29,8 +29,6 @@ module Spree
|
|
|
29
29
|
nullable: true,
|
|
30
30
|
description: "What one packed carton weighs, in the variant's weight unit."
|
|
31
31
|
attribute :cartons_per_pallet, :integer, nullable: true, description: 'How many cartons stack on one pallet.'
|
|
32
|
-
attribute :seller_id, :id
|
|
33
|
-
attribute :delivery_profile_id, :id, nullable: true
|
|
34
32
|
attribute :track_inventory, :boolean
|
|
35
33
|
attribute :preorderable, :boolean,
|
|
36
34
|
description: "Sell this variant before it's in stock. Adds the pre-order label and ships-by promise; the cap comes from `backorder_limit`, an incoming stock count, or backorder."
|
|
@@ -40,12 +38,9 @@ module Spree
|
|
|
40
38
|
attribute :backorder_limit, :integer,
|
|
41
39
|
nullable: true,
|
|
42
40
|
description: 'Maximum units sellable beyond available stock, capping backorders and pre-orders. Null is unlimited.'
|
|
43
|
-
attribute :tax_category_id, :id
|
|
44
41
|
attribute :position, :integer
|
|
45
|
-
attribute :
|
|
46
|
-
description: '
|
|
47
|
-
attribute :stock_levels, :array, of: Spree::Api::V3::StockLevelRequestSchema,
|
|
48
|
-
description: 'Stock per location. Upserted by location; omitted locations are removed.'
|
|
42
|
+
attribute :options, :array, of: Spree::Api::V3::VariantOptionRequestSchema,
|
|
43
|
+
description: 'One pair per option type the variant takes part in (size, color). Option types and values are created when missing.'
|
|
49
44
|
end
|
|
50
45
|
end
|
|
51
46
|
end
|