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.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/email_template_lookup.rb +6 -1
  3. data/app/controllers/concerns/spree/api/v3/admin/reporting_authorization.rb +17 -8
  4. data/app/controllers/concerns/spree/api/v3/admin/store_context.rb +7 -0
  5. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +61 -0
  6. data/app/controllers/concerns/spree/api/v3/allowed_redirect_url.rb +27 -0
  7. data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +8 -2
  8. data/app/controllers/concerns/spree/api/v3/error_handler.rb +49 -0
  9. data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +76 -7
  10. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +66 -5
  11. data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
  12. data/app/controllers/spree/api/v3/admin/companies/tax_exemption_certificates_controller.rb +11 -5
  13. data/app/controllers/spree/api/v3/admin/concerns/external_references.rb +3 -0
  14. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +30 -8
  15. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
  16. data/app/controllers/spree/api/v3/admin/customers_controller.rb +1 -6
  17. data/app/controllers/spree/api/v3/admin/exports_controller.rb +15 -3
  18. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
  19. data/app/controllers/spree/api/v3/admin/imports_controller.rb +12 -3
  20. data/app/controllers/spree/api/v3/admin/markets_controller.rb +3 -1
  21. data/app/controllers/spree/api/v3/admin/me_controller.rb +21 -1
  22. data/app/controllers/spree/api/v3/admin/oauth/application_tokens_controller.rb +56 -0
  23. data/app/controllers/spree/api/v3/admin/oauth/applications_controller.rb +55 -0
  24. data/app/controllers/spree/api/v3/admin/oauth/authorizations_controller.rb +294 -0
  25. data/app/controllers/spree/api/v3/admin/order_groups_controller.rb +1 -1
  26. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +8 -0
  27. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +1 -0
  28. data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +7 -0
  29. data/app/controllers/spree/api/v3/admin/orders_controller.rb +20 -7
  30. data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
  31. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +3 -4
  32. data/app/controllers/spree/api/v3/admin/products_controller.rb +5 -3
  33. data/app/controllers/spree/api/v3/admin/purchase_orders_controller.rb +1 -0
  34. data/app/controllers/spree/api/v3/admin/reporting_controller.rb +20 -14
  35. data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +1 -1
  36. data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -1
  37. data/app/controllers/spree/api/v3/admin/tags_controller.rb +8 -5
  38. data/app/controllers/spree/api/v3/admin/tax_exemption_reasons_controller.rb +58 -0
  39. data/app/controllers/spree/api/v3/admin/tax_rates_controller.rb +0 -1
  40. data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +17 -11
  41. data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
  42. data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +72 -0
  43. data/app/controllers/spree/api/v3/oauth/metadata_controller.rb +57 -0
  44. data/app/controllers/spree/api/v3/oauth/server_metadata_controller.rb +57 -0
  45. data/app/controllers/spree/api/v3/resource_controller.rb +36 -5
  46. data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
  47. data/app/controllers/spree/api/v3/seller/products_controller.rb +7 -61
  48. data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
  49. data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
  50. data/app/controllers/spree/api/v3/store/carts_controller.rb +7 -8
  51. data/app/controllers/spree/api/v3/store/companies/addresses_controller.rb +4 -0
  52. data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +4 -0
  53. data/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb +19 -1
  54. data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +3 -14
  55. data/app/controllers/spree/api/v3/store/data_request_confirmations_controller.rb +23 -0
  56. data/app/controllers/spree/api/v3/store/data_requests_controller.rb +56 -0
  57. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
  58. data/app/request_schemas/spree/api/v3/admin/email_template_preview_request_schema.rb +4 -0
  59. data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
  60. data/app/request_schemas/spree/api/v3/seller/price_request_schema.rb +20 -0
  61. data/app/request_schemas/spree/api/v3/seller/variant_request_schema.rb +19 -0
  62. data/app/request_schemas/spree/api/v3/variant_request_schema.rb +8 -13
  63. data/app/serializers/spree/api/v3/address_owner_serializer.rb +27 -0
  64. data/app/serializers/spree/api/v3/address_serializer.rb +5 -1
  65. data/app/serializers/spree/api/v3/admin/address_owner_serializer.rb +10 -0
  66. data/app/serializers/spree/api/v3/admin/address_serializer.rb +1 -10
  67. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +1 -1
  68. data/app/serializers/spree/api/v3/admin/catalog_assignment_serializer.rb +1 -1
  69. data/app/serializers/spree/api/v3/admin/catalog_price_serializer.rb +1 -1
  70. data/app/serializers/spree/api/v3/admin/collection_rule_serializer.rb +1 -1
  71. data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -1
  72. data/app/serializers/spree/api/v3/admin/commission_line_serializer.rb +2 -2
  73. data/app/serializers/spree/api/v3/admin/commission_rate_serializer.rb +1 -1
  74. data/app/serializers/spree/api/v3/admin/consent_record_serializer.rb +1 -1
  75. data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
  76. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -0
  77. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +2 -0
  78. data/app/serializers/spree/api/v3/admin/oauth_access_token_serializer.rb +62 -0
  79. data/app/serializers/spree/api/v3/admin/oauth_application_serializer.rb +56 -0
  80. data/app/serializers/spree/api/v3/admin/oauth_consent_request_serializer.rb +25 -0
  81. data/app/serializers/spree/api/v3/admin/oauth_protected_resource_serializer.rb +27 -0
  82. data/app/serializers/spree/api/v3/admin/oauth_redirect_serializer.rb +17 -0
  83. data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
  84. data/app/serializers/spree/api/v3/admin/package_type_serializer.rb +3 -3
  85. data/app/serializers/spree/api/v3/admin/payment_capture_event_serializer.rb +16 -0
  86. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +12 -1
  87. data/app/serializers/spree/api/v3/admin/permission_serializer.rb +1 -1
  88. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +1 -1
  89. data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
  90. data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -0
  91. data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
  92. data/app/serializers/spree/api/v3/admin/seller_serializer.rb +2 -2
  93. data/app/serializers/spree/api/v3/admin/seller_transfer_serializer.rb +1 -1
  94. data/app/serializers/spree/api/v3/admin/shipping_label_serializer.rb +3 -3
  95. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +1 -1
  96. data/app/serializers/spree/api/v3/admin/stock_receipt_item_serializer.rb +2 -2
  97. data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +1 -1
  98. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +1 -1
  99. data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -0
  100. data/app/serializers/spree/api/v3/admin/tax_exemption_certificate_serializer.rb +7 -3
  101. data/app/serializers/spree/api/v3/admin/tax_exemption_reason_serializer.rb +20 -0
  102. data/app/serializers/spree/api/v3/admin/tax_identifier_serializer.rb +2 -2
  103. data/app/serializers/spree/api/v3/admin/tax_line_serializer.rb +1 -1
  104. data/app/serializers/spree/api/v3/admin/tax_rate_serializer.rb +1 -2
  105. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
  106. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +4 -1
  107. data/app/serializers/spree/api/v3/base_serializer.rb +41 -5
  108. data/app/serializers/spree/api/v3/claim_serializer.rb +1 -1
  109. data/app/serializers/spree/api/v3/collection_serializer.rb +1 -1
  110. data/app/serializers/spree/api/v3/company_invitation_serializer.rb +1 -1
  111. data/app/serializers/spree/api/v3/company_serializer.rb +1 -1
  112. data/app/serializers/spree/api/v3/data_request_confirmation_requested_event_serializer.rb +28 -0
  113. data/app/serializers/spree/api/v3/data_request_event_serializer.rb +1 -1
  114. data/app/serializers/spree/api/v3/data_request_serializer.rb +8 -4
  115. data/app/serializers/spree/api/v3/delivery_serializer.rb +1 -1
  116. data/app/serializers/spree/api/v3/delivery_zone_member_serializer.rb +1 -1
  117. data/app/serializers/spree/api/v3/discount_serializer.rb +1 -1
  118. data/app/serializers/spree/api/v3/file_upload_serializer.rb +1 -1
  119. data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
  120. data/app/serializers/spree/api/v3/media_event_serializer.rb +1 -1
  121. data/app/serializers/spree/api/v3/media_serializer.rb +1 -1
  122. data/app/serializers/spree/api/v3/option_type_serializer.rb +1 -1
  123. data/app/serializers/spree/api/v3/order_group_serializer.rb +2 -2
  124. data/app/serializers/spree/api/v3/order_serializer.rb +4 -2
  125. data/app/serializers/spree/api/v3/payment_serializer.rb +1 -1
  126. data/app/serializers/spree/api/v3/product_filter_availability_option_serializer.rb +1 -1
  127. data/app/serializers/spree/api/v3/product_filter_option_serializer.rb +1 -1
  128. data/app/serializers/spree/api/v3/seller/address_serializer.rb +3 -2
  129. data/app/serializers/spree/api/v3/seller/order_serializer.rb +3 -3
  130. data/app/serializers/spree/api/v3/seller/package_type_serializer.rb +3 -3
  131. data/app/serializers/spree/api/v3/seller/profile_serializer.rb +2 -2
  132. data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +1 -1
  133. data/app/serializers/spree/api/v3/seller/tax_identifier_serializer.rb +1 -1
  134. data/app/serializers/spree/api/v3/seller/variant_serializer.rb +1 -1
  135. data/app/serializers/spree/api/v3/shipping_label_event_serializer.rb +1 -1
  136. data/app/serializers/spree/api/v3/stock_movement_serializer.rb +1 -1
  137. data/app/serializers/spree/api/v3/stock_receipt_event_serializer.rb +1 -1
  138. data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
  139. data/app/services/spree/api/v3/filter_table.rb +22 -3
  140. data/app/services/spree/api/v3/request_schema.rb +64 -17
  141. data/config/locales/en.yml +1 -0
  142. data/config/routes.rb +80 -12
  143. data/lib/spree/api/agent_membership_map.rb +73 -0
  144. data/lib/spree/api/agent_resource_map.rb +350 -0
  145. data/lib/spree/api/agent_workflow_endpoints.rb +113 -0
  146. data/lib/spree/api/agent_write_schemas.rb +131 -0
  147. data/lib/spree/api/dependencies.rb +5 -4
  148. data/lib/spree/api/engine.rb +40 -0
  149. data/lib/spree/api/enum_types.rb +118 -0
  150. data/lib/spree/api/extension_fields.rb +167 -0
  151. data/lib/spree/api/oauth.rb +243 -0
  152. data/lib/spree/api/openapi/filter_documentation.rb +8 -1
  153. data/lib/spree/api/typelizer_generation.rb +5 -0
  154. data/lib/spree/api.rb +8 -0
  155. data/lib/typelizer/open_enums.rb +48 -10
  156. 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
- model = model_class
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, :string, nullable: true, choices: Spree::Variant::DIMENSION_UNITS
31
+ body.attribute :dimensions_unit, :dimension_unit, nullable: true
32
32
  body.attribute :weight, :number,
33
33
  nullable: true,
34
34
  description: "The empty package's own weight, added to every quote."
35
35
  body.attribute :max_weight, :number, nullable: true
36
- body.attribute :weight_unit, :string, nullable: true, choices: Spree::Variant::WEIGHT_UNITS
36
+ body.attribute :weight_unit, :weight_unit, nullable: true
37
37
  body.attribute :default, :boolean,
38
38
  description: "Makes this the box this seller's parcels are quoted with. One default per seller, so it never displaces the marketplace's."
39
39
  body.attribute :metadata, :hash, nullable: true
@@ -41,19 +41,6 @@ module Spree
41
41
  'archived' => -> { Spree.product_archive_workflow }
42
42
  }.freeze
43
43
 
44
- # The operator's price shape, except `currency` may be left out: the
45
- # store's currency is assumed (see `default_price_currencies`).
46
- PRICE = Spree::Api::V3::RequestSchema.new do |schema|
47
- schema.attribute :currency, :currency,
48
- example: 'USD',
49
- description: "Omit to price in the store's currency. A seller has no currency of their own."
50
- schema.attribute :amount, :decimal,
51
- required: true,
52
- example: '24.00',
53
- description: 'A canonical decimal string (`"1234.56"`: period decimal, no grouping). The API does not parse localized input.'
54
- schema.attribute :compare_at_amount, :decimal, nullable: true, example: '30.00'
55
- end
56
-
57
44
  scoped_resource :products
58
45
 
59
46
  request_params :product do |body|
@@ -69,7 +56,7 @@ module Spree
69
56
  description: "The marketplace's product type this product is listed against, from the product types endpoint. Null detaches it. Saving with a type seeds its option types onto the product."
70
57
  body.attribute :delivery_profile_id, :id,
71
58
  description: "Which of the marketplace's delivery profiles the product ships under. A new product sent without one lands on the profile marked default."
72
- body.attribute :prices, :array, of: PRICE,
59
+ body.attribute :prices, :array, of: Spree::Api::V3::Seller::PriceRequestSchema,
73
60
  description: "Shorthand for a simple (no-options) product: per-currency prices that forward to the product's sole variant."
74
61
  # `external_url` stays out for the same reason it does on the
75
62
  # operator's endpoint: it would make the server fetch a URL the
@@ -83,56 +70,11 @@ module Spree
83
70
  media_item.attribute :variant_ids, :array, of: :id,
84
71
  description: 'Replaces the variants this media is linked to. An empty list clears them.'
85
72
  end
86
- # Narrower than the operator's variant set: `seller_id` would let a
87
- # payload hand a variant to somebody else, and
88
- # `tax_category_id`/`delivery_profile_id` are marketplace
89
- # configuration.
90
73
  body.attribute :variants, :array,
91
- as: 'SellerProductVariantParams',
74
+ as: 'ProductVariantParams',
92
75
  description: 'The variants, as a whole: one absent from the list is removed. Pass `id` to update an existing variant.' do |variant|
76
+ variant.extends Spree::Api::V3::Seller::VariantRequestSchema
93
77
  variant.attribute :id, :id
94
- variant.attribute :sku, :string
95
- variant.attribute :barcode, :string
96
- variant.attribute :cost_price, :decimal, nullable: true
97
- variant.attribute :cost_currency, :currency
98
- variant.attribute :weight, :number, nullable: true
99
- variant.attribute :height, :number, nullable: true
100
- variant.attribute :width, :number, nullable: true
101
- variant.attribute :depth, :number, nullable: true
102
- variant.attribute :weight_unit, :string, choices: Spree::Variant::WEIGHT_UNITS
103
- variant.attribute :dimensions_unit, :string, choices: Spree::Variant::DIMENSION_UNITS
104
- variant.attribute :hs_code, :string
105
- variant.attribute :customs_description, :string
106
- variant.attribute :country_of_origin, :country
107
- variant.attribute :minimum_order_quantity, :integer, nullable: true
108
- variant.attribute :order_multiple, :integer, nullable: true
109
- variant.attribute :purchase_unit, :string
110
- variant.attribute :units_per_carton, :integer,
111
- nullable: true,
112
- description: 'How many units fill one carton.'
113
- variant.attribute :carton_package_type_id, :id,
114
- nullable: true,
115
- description: 'The carton this variant is packed into: a package type of kind `carton`.'
116
- variant.attribute :carton_weight, :number,
117
- nullable: true,
118
- description: "What one packed carton weighs, in the variant's weight unit."
119
- variant.attribute :cartons_per_pallet, :integer,
120
- nullable: true,
121
- description: 'How many cartons stack on one pallet.'
122
- variant.attribute :track_inventory, :boolean
123
- variant.attribute :preorderable, :boolean
124
- variant.attribute :preorder_ships_at, :datetime,
125
- nullable: true,
126
- description: 'The "ships by" date shown to customers while the variant is a pre-order. Null clears it.'
127
- variant.attribute :backorder_limit, :integer,
128
- nullable: true,
129
- description: 'Maximum units sellable beyond available stock, capping backorders and pre-orders. Null is unlimited.'
130
- variant.attribute :position, :integer
131
- variant.attribute :options, :array, of: Spree::Api::V3::VariantOptionRequestSchema,
132
- description: 'One pair per option type the variant takes part in (size, color).'
133
- variant.attribute :prices, :array, of: PRICE, description: 'Per-currency prices. Upserted by currency.'
134
- variant.attribute :stock_levels, :array, of: Spree::Api::V3::StockLevelRequestSchema,
135
- description: "Stock per location, at this seller's own locations only: a location belonging to anyone else is ignored."
136
78
  end
137
79
  end
138
80
 
@@ -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 model_additional_permitted_attributes
73
- return [] unless respond_to?(:model_class, true)
74
-
75
- Array(model_class.try(:additional_seller_permitted_attributes))
76
- rescue NotImplementedError
77
- []
72
+ def additional_permitted_attributes_for(model)
73
+ Array(model.try(:additional_seller_permitted_attributes))
78
74
  end
79
75
 
80
76
  def authorize_resource!(resource = @resource, action = action_name.to_sym)
@@ -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: 'An existing address of the customer to use. The other fields are then ignored.'
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: 'An existing address to ship to.',
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: 'An existing address to bill.',
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
- render_service_error(result.error, code: ERROR_CODES[:validation_error])
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
@@ -64,6 +64,10 @@ module Spree
64
64
  Spree.api.address_serializer
65
65
  end
66
66
 
67
+ def collection_includes
68
+ [:owner]
69
+ end
70
+
67
71
  def permitted_params
68
72
  params.permit(*permitted_attributes)
69
73
  end
@@ -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: redirect_url.presence
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, shared by the variant endpoints and the
5
- # variants nested in a product payload. Not published on its own: each
6
- # endpoint includes these fields into its own type.
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, :string, choices: Spree::Variant::WEIGHT_UNITS
17
- attribute :dimensions_unit, :string, choices: Spree::Variant::DIMENSION_UNITS
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 :prices, :array, of: Spree::Api::V3::PriceRequestSchema,
46
- description: 'Per-currency prices. Upserted by currency.'
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