spree_core 6.0.0.rc1 → 6.0.0.rc2

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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/app/jobs/spree/search_provider/index_job.rb +4 -0
  4. data/app/jobs/spree/uploads/purge_unattached_job.rb +49 -0
  5. data/app/mailers/spree/base_mailer.rb +4 -3
  6. data/app/models/concerns/spree/admin_user_methods.rb +2 -0
  7. data/app/models/concerns/spree/calculated_adjustments.rb +41 -13
  8. data/app/models/concerns/spree/calculator/currency_amounts.rb +1 -1
  9. data/app/models/concerns/spree/calculator/tiers.rb +1 -14
  10. data/app/models/concerns/spree/canonical_decimals.rb +61 -0
  11. data/app/models/concerns/spree/customer_methods.rb +8 -4
  12. data/app/models/concerns/spree/derived_number.rb +11 -13
  13. data/app/models/concerns/spree/preference_schema/json_schema.rb +139 -0
  14. data/app/models/concerns/spree/preference_schema.rb +59 -21
  15. data/app/models/concerns/spree/product_scopes.rb +5 -1
  16. data/app/models/concerns/spree/purchase/payment_processing.rb +1 -1
  17. data/app/models/concerns/spree/purchase/purchase_order.rb +7 -2
  18. data/app/models/concerns/spree/purchase/store_credits.rb +36 -1
  19. data/app/models/concerns/spree/purchase/totals.rb +1 -8
  20. data/app/models/concerns/spree/ransackable_attributes.rb +46 -0
  21. data/app/models/concerns/spree/signed_id_attachments.rb +54 -0
  22. data/app/models/concerns/spree/stores/email_branding.rb +2 -1
  23. data/app/models/concerns/spree/stores/markets.rb +5 -2
  24. data/app/models/concerns/spree/typed_adjustment_line.rb +4 -5
  25. data/app/models/concerns/spree/typed_associations.rb +6 -18
  26. data/app/models/concerns/spree/vat_price_calculation.rb +5 -6
  27. data/app/models/spree/address.rb +1 -1
  28. data/app/models/spree/adjusters/largest_remainder.rb +14 -1
  29. data/app/models/spree/adjusters/promotion.rb +3 -4
  30. data/app/models/spree/allowed_origin.rb +3 -0
  31. data/app/models/spree/base.rb +29 -0
  32. data/app/models/spree/calculator/flat_percent_item_total.rb +1 -1
  33. data/app/models/spree/calculator/flat_rate.rb +2 -2
  34. data/app/models/spree/calculator/flexi_rate.rb +3 -3
  35. data/app/models/spree/calculator/percent_on_line_item.rb +1 -1
  36. data/app/models/spree/calculator/price_sack.rb +4 -4
  37. data/app/models/spree/calculator/shipping/digital_delivery.rb +2 -2
  38. data/app/models/spree/calculator/shipping/flat_percent_item_total.rb +6 -4
  39. data/app/models/spree/calculator/shipping/flat_rate.rb +4 -4
  40. data/app/models/spree/calculator/shipping/flexi_rate.rb +3 -3
  41. data/app/models/spree/calculator/shipping/per_item.rb +2 -2
  42. data/app/models/spree/calculator/shipping/price_sack.rb +4 -4
  43. data/app/models/spree/calculator/tiered_flat_rate.rb +2 -2
  44. data/app/models/spree/calculator/tiered_percent.rb +1 -1
  45. data/app/models/spree/catalog.rb +1 -0
  46. data/app/models/spree/catalog_price.rb +0 -7
  47. data/app/models/spree/catalog_price_tier.rb +0 -8
  48. data/app/models/spree/category.rb +3 -1
  49. data/app/models/spree/channel/gating.rb +2 -1
  50. data/app/models/spree/channel.rb +2 -0
  51. data/app/models/spree/claim.rb +1 -0
  52. data/app/models/spree/collection.rb +2 -0
  53. data/app/models/spree/commission_rate.rb +5 -1
  54. data/app/models/spree/commission_rules/category_rule.rb +2 -5
  55. data/app/models/spree/commission_rules/item_total_rule.rb +2 -2
  56. data/app/models/spree/commission_rules/seller_rule.rb +2 -5
  57. data/app/models/spree/company.rb +1 -0
  58. data/app/models/spree/custom_field_definition.rb +11 -12
  59. data/app/models/spree/customer_group.rb +1 -0
  60. data/app/models/spree/data_request.rb +1 -0
  61. data/app/models/spree/delivery_method.rb +6 -1
  62. data/app/models/spree/delivery_method_rules/channel_rule.rb +2 -5
  63. data/app/models/spree/delivery_method_rules/item_total_rule.rb +2 -2
  64. data/app/models/spree/delivery_profile.rb +1 -0
  65. data/app/models/spree/delivery_rate.rb +3 -3
  66. data/app/models/spree/delivery_zone.rb +1 -1
  67. data/app/models/spree/exchange.rb +1 -0
  68. data/app/models/spree/exports/report.rb +2 -2
  69. data/app/models/spree/file_upload.rb +155 -0
  70. data/app/models/spree/fulfillment.rb +13 -3
  71. data/app/models/spree/fulfillment_group.rb +6 -1
  72. data/app/models/spree/gateway/bogus.rb +2 -0
  73. data/app/models/spree/gateway.rb +1 -1
  74. data/app/models/spree/gift_card.rb +6 -7
  75. data/app/models/spree/gift_card_batch.rb +3 -4
  76. data/app/models/spree/import.rb +3 -0
  77. data/app/models/spree/integration.rb +1 -1
  78. data/app/models/spree/line_item.rb +2 -2
  79. data/app/models/spree/market.rb +7 -2
  80. data/app/models/spree/media.rb +1 -9
  81. data/app/models/spree/newsletter_subscriber.rb +1 -0
  82. data/app/models/spree/option_type.rb +2 -1
  83. data/app/models/spree/option_value.rb +2 -1
  84. data/app/models/spree/order.rb +5 -5
  85. data/app/models/spree/order_group.rb +7 -2
  86. data/app/models/spree/package_type.rb +1 -0
  87. data/app/models/spree/payment/gateway_options.rb +1 -1
  88. data/app/models/spree/payment.rb +3 -10
  89. data/app/models/spree/payment_method/check.rb +2 -0
  90. data/app/models/spree/payment_method/store_credit.rb +11 -9
  91. data/app/models/spree/payment_method.rb +42 -2
  92. data/app/models/spree/policy.rb +1 -0
  93. data/app/models/spree/price.rb +17 -14
  94. data/app/models/spree/price_adjustment_tier.rb +4 -0
  95. data/app/models/spree/price_list.rb +13 -7
  96. data/app/models/spree/price_rules/channel_rule.rb +2 -5
  97. data/app/models/spree/price_rules/customer_group_rule.rb +2 -5
  98. data/app/models/spree/price_rules/market_rule.rb +2 -5
  99. data/app/models/spree/price_rules/user_rule.rb +1 -3
  100. data/app/models/spree/product/channels.rb +9 -2
  101. data/app/models/spree/product.rb +23 -23
  102. data/app/models/spree/product_type.rb +1 -0
  103. data/app/models/spree/promotion/actions/create_line_items.rb +1 -1
  104. data/app/models/spree/promotion/rules/category.rb +1 -1
  105. data/app/models/spree/promotion/rules/channel.rb +2 -5
  106. data/app/models/spree/promotion/rules/country.rb +3 -4
  107. data/app/models/spree/promotion/rules/currency.rb +1 -1
  108. data/app/models/spree/promotion/rules/customer_group.rb +2 -5
  109. data/app/models/spree/promotion/rules/item_total.rb +2 -2
  110. data/app/models/spree/promotion/rules/market.rb +2 -5
  111. data/app/models/spree/promotion/rules/option_value.rb +2 -3
  112. data/app/models/spree/promotion/rules/product.rb +1 -1
  113. data/app/models/spree/promotion.rb +1 -0
  114. data/app/models/spree/purchase_order.rb +2 -0
  115. data/app/models/spree/refund.rb +13 -15
  116. data/app/models/spree/return.rb +1 -0
  117. data/app/models/spree/saved_report.rb +1 -0
  118. data/app/models/spree/search_provider/base.rb +4 -0
  119. data/app/models/spree/seller.rb +8 -1
  120. data/app/models/spree/seller_payout.rb +1 -0
  121. data/app/models/spree/seller_requirement.rb +1 -0
  122. data/app/models/spree/seller_requirement_submission.rb +1 -0
  123. data/app/models/spree/seller_transfer.rb +1 -0
  124. data/app/models/spree/shipping_label.rb +1 -0
  125. data/app/models/spree/stock/estimator.rb +1 -1
  126. data/app/models/spree/stock_level.rb +2 -0
  127. data/app/models/spree/stock_location.rb +1 -0
  128. data/app/models/spree/stock_transfer.rb +2 -0
  129. data/app/models/spree/store.rb +37 -12
  130. data/app/models/spree/store_credit.rb +5 -4
  131. data/app/models/spree/supplier.rb +1 -0
  132. data/app/models/spree/tax_category.rb +1 -0
  133. data/app/models/spree/tax_exemption_certificate.rb +1 -0
  134. data/app/models/spree/tax_provider/internal.rb +12 -9
  135. data/app/models/spree/tax_rate.rb +29 -6
  136. data/app/models/spree/taxon.rb +1 -1
  137. data/app/models/spree/update_status.rb +25 -0
  138. data/app/models/spree/variant.rb +6 -9
  139. data/app/models/spree/webhook_delivery.rb +1 -0
  140. data/app/models/spree/webhook_endpoint.rb +36 -0
  141. data/app/presenters/spree/csv/order_line_item_presenter.rb +10 -6
  142. data/app/presenters/spree/csv/price_list_price_presenter.rb +3 -8
  143. data/app/presenters/spree/csv/product_variant_presenter.rb +17 -8
  144. data/app/presenters/spree/csv/purchase_order_item_presenter.rb +1 -3
  145. data/app/services/spree/carts/update.rb +3 -3
  146. data/app/services/spree/imports/row_processors/product_variant.rb +12 -0
  147. data/app/services/spree/locales.rb +39 -5
  148. data/app/services/spree/orders/create.rb +3 -3
  149. data/app/services/spree/orders/discounts/create.rb +8 -8
  150. data/app/services/spree/prices/bulk_upsert.rb +32 -25
  151. data/app/services/spree/seeds/stores.rb +1 -1
  152. data/app/services/spree/stores/provision_defaults.rb +13 -16
  153. data/app/services/spree/update_check.rb +160 -0
  154. data/app/services/spree/uploads/assign_stores.rb +92 -0
  155. data/app/validators/spree/attachment_content_type_validator.rb +19 -2
  156. data/app/validators/spree/locale_codes_validator.rb +22 -0
  157. data/app/workflows/spree/fulfillments/create.rb +2 -2
  158. data/app/workflows/spree/payments/capture.rb +20 -5
  159. data/app/workflows/spree/payments/process.rb +3 -1
  160. data/app/workflows/spree/price_lists/update.rb +4 -4
  161. data/app/workflows/spree/purchase_orders/create.rb +3 -1
  162. data/app/workflows/spree/shipping_labels/record.rb +2 -2
  163. data/app/workflows/spree/stock_transfers/create.rb +3 -1
  164. data/config/currency_exponents.json +180 -0
  165. data/config/locales/content_locales.rb +9 -0
  166. data/config/locales/en.yml +10 -2
  167. data/db/migrate/20261009000001_change_money_columns_to_decimal_19_4.rb +116 -0
  168. data/db/migrate/20261010000001_add_store_id_to_active_storage_blobs.rb +12 -0
  169. data/lib/generators/spree/dummy/templates/rails/test.rb +21 -2
  170. data/lib/spree/core/configuration.rb +18 -0
  171. data/lib/spree/core/engine.rb +26 -0
  172. data/lib/spree/core/preferences/invalid_preferences.rb +18 -0
  173. data/lib/spree/core/preferences/json_conversion.rb +5 -1
  174. data/lib/spree/core/preferences/masking.rb +51 -1
  175. data/lib/spree/core/preferences/preferable.rb +212 -26
  176. data/lib/spree/core/preferences/preferable_class_methods.rb +121 -4
  177. data/lib/spree/core/ransack_configuration.rb +13 -2
  178. data/lib/spree/core/version.rb +1 -1
  179. data/lib/spree/core.rb +6 -3
  180. data/lib/spree/iso_data.rb +7 -0
  181. data/lib/spree/localized_number.rb +6 -0
  182. data/lib/spree/migrations.rb +4 -1
  183. data/lib/spree/money.rb +196 -15
  184. data/lib/spree/reporting/adapters/base.rb +7 -6
  185. data/lib/spree/reporting/registry.rb +13 -0
  186. data/lib/spree/testing_support/factories/product_factory.rb +9 -0
  187. data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +22 -1
  188. data/lib/spree/uploads.rb +101 -0
  189. data/lib/tasks/markets.rake +2 -1
  190. data/lib/tasks/migrate_users_to_customers.rake +23 -18
  191. data/lib/tasks/money.rake +46 -0
  192. data/lib/tasks/seller_sample_data.rake +2 -2
  193. data/lib/tasks/store_settings.rake +13 -6
  194. data/lib/tasks/typed_adjustments_migration.rake +1 -1
  195. data/lib/tasks/uploads.rake +19 -0
  196. metadata +35 -6
  197. data/app/jobs/spree/products/touch_categories_job.rb +0 -15
  198. data/app/jobs/spree/products/touch_taxons_job.rb +0 -13
@@ -3,14 +3,14 @@ module Spree
3
3
  class FlatRate < ShippingCalculator
4
4
  include Spree::Calculator::CurrencyAmounts
5
5
 
6
- preference :amount, :decimal, default: 0
6
+ preference :amount, :money, default: 0
7
7
  # @deprecated Together with the single `amount`, superseded by the
8
8
  # per-currency `amounts` hash; kept as the fallback for its own
9
9
  # currency so upgraded stores quote unchanged.
10
- preference :currency, :string, default: -> { Spree::Store.default.default_currency }
10
+ preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
11
11
 
12
- preference :minimum_item_total, :decimal, default: nil, nullable: true, deprecated: 'Use a Spree::DeliveryMethodRules rule on the delivery method instead; removed in Spree 6.1.'
13
- preference :maximum_item_total, :decimal, default: nil, nullable: true, deprecated: 'Use a Spree::DeliveryMethodRules rule on the delivery method instead; removed in Spree 6.1.'
12
+ preference :minimum_item_total, :money, default: nil, nullable: true, deprecated: 'Use a Spree::DeliveryMethodRules rule on the delivery method instead; removed in Spree 6.1.'
13
+ preference :maximum_item_total, :money, default: nil, nullable: true, deprecated: 'Use a Spree::DeliveryMethodRules rule on the delivery method instead; removed in Spree 6.1.'
14
14
 
15
15
  preference :minimum_weight, :decimal, default: nil, nullable: true, deprecated: 'Use a Spree::DeliveryMethodRules rule on the delivery method instead; removed in Spree 6.1.'
16
16
  preference :maximum_weight, :decimal, default: nil, nullable: true, deprecated: 'Use a Spree::DeliveryMethodRules rule on the delivery method instead; removed in Spree 6.1.'
@@ -1,10 +1,10 @@
1
1
  module Spree
2
2
  module Calculator::Shipping
3
3
  class FlexiRate < ShippingCalculator
4
- preference :first_item, :decimal, default: 0.0
5
- preference :additional_item, :decimal, default: 0.0
4
+ preference :first_item, :money, default: 0.0
5
+ preference :additional_item, :money, default: 0.0
6
6
  preference :max_items, :integer, default: 0
7
- preference :currency, :string, default: -> { Spree::Store.default.default_currency }
7
+ preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
8
8
 
9
9
  # Single-currency amounts: quoting another currency would mislabel
10
10
  # the configured numbers, so the method stays hidden there.
@@ -3,8 +3,8 @@ module Spree
3
3
  class PerItem < ShippingCalculator
4
4
  include Spree::Calculator::CurrencyAmounts
5
5
 
6
- preference :amount, :decimal, default: 0
7
- preference :currency, :string, default: -> { Spree::Store.default.default_currency }
6
+ preference :amount, :money, default: 0
7
+ preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
8
8
 
9
9
  def self.description
10
10
  I18n.t('spree.shipping_flat_rate_per_item')
@@ -1,10 +1,10 @@
1
1
  module Spree
2
2
  module Calculator::Shipping
3
3
  class PriceSack < ShippingCalculator
4
- preference :minimal_amount, :decimal, default: 0
5
- preference :normal_amount, :decimal, default: 0
6
- preference :discount_amount, :decimal, default: 0
7
- preference :currency, :string, default: -> { Spree::Store.default.default_currency }
4
+ preference :minimal_amount, :money, default: 0
5
+ preference :normal_amount, :money, default: 0
6
+ preference :discount_amount, :money, default: 0
7
+ preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
8
8
 
9
9
  # Single-currency amounts: quoting another currency would mislabel
10
10
  # the configured numbers, so the method stays hidden there.
@@ -2,8 +2,8 @@ module Spree
2
2
  class Calculator::TieredFlatRate < Calculator
3
3
  include Spree::Calculator::Tiers
4
4
 
5
- preference :base_amount, :decimal, default: 0
6
- preference :currency, :string, default: -> { Spree::Store.default.default_currency }
5
+ preference :base_amount, :money, default: 0
6
+ preference :currency, :string, format: :currency, default: -> { Spree::Store.default.default_currency }
7
7
 
8
8
  def self.description
9
9
  I18n.t('spree.tiered_flat_rate')
@@ -15,7 +15,7 @@ module Spree
15
15
 
16
16
  def compute(object)
17
17
  percent = tier_value_for(object.amount) || preferred_base_percent
18
- (object.amount * percent / 100).round(2)
18
+ Spree::Money::Rounding.to_currency(object.amount * percent / 100, object.try(:currency))
19
19
  end
20
20
 
21
21
  private
@@ -86,6 +86,7 @@ module Spree
86
86
  scope :by_position, -> { ordered }
87
87
 
88
88
  self.whitelisted_ransackable_attributes = %w[name active]
89
+ search_by :name
89
90
 
90
91
  # The catalogs that apply to a purchase node, nearest node first: the
91
92
  # node's own assignments before its parent's, catalog position breaking
@@ -80,13 +80,6 @@ module Spree
80
80
  break_count.to_i.positive?
81
81
  end
82
82
 
83
- # The amount as the wire carries it — see CatalogPriceTier#display_value.
84
- #
85
- # @return [String]
86
- def display_value
87
- amount.to_s
88
- end
89
-
90
83
  # @return [Spree::Money]
91
84
  def money
92
85
  Spree::Money.new(amount, currency: currency)
@@ -15,14 +15,6 @@ module Spree
15
15
 
16
16
  validates :min_quantity, numericality: { greater_than: 1 }
17
17
 
18
- # The amount as the wire carries it — a string, like every other
19
- # money-shaped value, since a float would round the merchant's figure.
20
- #
21
- # @return [String]
22
- def display_value
23
- amount.to_s
24
- end
25
-
26
18
  # @return [Spree::Money]
27
19
  def money
28
20
  Spree::Money.new(amount, currency: currency)
@@ -61,6 +61,7 @@ module Spree
61
61
  #
62
62
  has_one_attached :image, service: Spree.public_storage_service_name
63
63
  has_one_attached :square_image, service: Spree.public_storage_service_name
64
+ signed_id_attachments :image, :square_image
64
65
  # The slots double as media-library placements, so an upload here is
65
66
  # visible and reusable there (Spree::HasLibraryMedia).
66
67
  has_library_media :image, :square_image
@@ -133,8 +134,9 @@ module Spree
133
134
  # Ransack
134
135
  #
135
136
  self.whitelisted_ransackable_associations = %w[parent]
136
- self.whitelisted_ransackable_attributes = %w[name permalink automatic depth is_root children_count
137
+ self.whitelisted_ransackable_attributes = %w[name permalink automatic depth children_count
137
138
  products_count pretty_name parent_id]
139
+ search_by :name, :pretty_name
138
140
 
139
141
  #
140
142
  # Translations
@@ -17,8 +17,9 @@ module Spree
17
17
  # Empty -> falls back to the Store-level preference. Both are `nullable`
18
18
  # so a blank write stays nil (inherit) rather than coercing to "" or
19
19
  # false, while an explicit value remains a channel override.
20
- preference :storefront_access, :string, default: nil, nullable: true
20
+ preference :storefront_access, :string, default: nil, nullable: true, choices: STOREFRONT_ACCESS
21
21
  preference :guest_checkout, :boolean, default: nil, nullable: true
22
+ exposes_preferences :storefront_access, :guest_checkout
22
23
 
23
24
  validate :storefront_access_must_be_valid
24
25
  end
@@ -16,6 +16,7 @@ module Spree
16
16
  # Empty -> falls back to the Store-level preference.
17
17
  preference :order_routing_strategy, :string, default: nil,
18
18
  parse_on_set: ->(value) { Spree::ApiTyped.class_name_for(Spree.order_routing.strategies, value) }
19
+ exposes_preferences :order_routing_strategy
19
20
 
20
21
 
21
22
  has_many :orders, class_name: 'Spree::Order', inverse_of: :channel, dependent: :nullify
@@ -59,6 +60,7 @@ module Spree
59
60
  scope :default, -> { where(default: true) }
60
61
 
61
62
  self.whitelisted_ransackable_attributes = %w[name code active default store_id]
63
+ search_by :name
62
64
 
63
65
  # Publishes the given products on this channel by creating/upserting ProductPublications.
64
66
  # Optionally sets the publication window; if not given, the products will be published immediately
@@ -44,6 +44,7 @@ module Spree
44
44
  delegate :currency, to: :order
45
45
 
46
46
  self.whitelisted_ransackable_attributes = %w[number status created_at]
47
+ search_by :number
47
48
  self.whitelisted_ransackable_associations = %w[order reason]
48
49
 
49
50
  def refund_total
@@ -72,6 +72,7 @@ module Spree
72
72
  #
73
73
  has_one_attached :image, service: Spree.public_storage_service_name
74
74
  has_one_attached :square_image, service: Spree.public_storage_service_name
75
+ signed_id_attachments :image, :square_image
75
76
  # The slots double as media-library placements, so an upload here is
76
77
  # visible and reusable there (Spree::HasLibraryMedia).
77
78
  has_library_media :image, :square_image
@@ -101,6 +102,7 @@ module Spree
101
102
  # Ransack
102
103
  #
103
104
  self.whitelisted_ransackable_attributes = %w[permalink automatic sort_order products_count]
105
+ search_by :name
104
106
 
105
107
  #
106
108
  # Automatic (rule-based) membership
@@ -36,6 +36,9 @@ module Spree
36
36
  include Spree::SingleStoreResource
37
37
  include Spree::HasListPosition
38
38
  include Spree::Metadata
39
+ include Spree::CanonicalDecimals
40
+
41
+ canonical_decimals :value, places: 5
39
42
  include Spree::TypedAssociations
40
43
 
41
44
  KINDS = %w[percentage fixed].freeze
@@ -103,6 +106,7 @@ module Spree
103
106
 
104
107
  self.whitelisted_ransackable_attributes = %w[name code kind enabled position value
105
108
  tax_inclusive include_shipping]
109
+ search_by :name
106
110
  self.whitelisted_ransackable_associations = %w[commission_rules]
107
111
 
108
112
  # @return [Boolean]
@@ -244,7 +248,7 @@ module Spree
244
248
  # shape a price list's rules use.
245
249
  #
246
250
  # Ids inside a rule's preferences are scope-checked against this rate's own
247
- # store as they are written (see normalize_id_preference), so a rule can
251
+ # store as they are written (see `of: :id, scope:`), so a rule can
248
252
  # never be pointed at another marketplace's records.
249
253
  #
250
254
  # @param rows [Array<Hash>, nil]
@@ -9,11 +9,8 @@ module Spree
9
9
  # restate the rule on every new leaf. The walk is done once per order by
10
10
  # Spree::Commissions::Context, not per rule.
11
11
  class CategoryRule < Spree::CommissionRule
12
- preference :category_ids, :array, default: [],
13
- parse_on_set: normalize_id_preference(
14
- klass: Spree::Category,
15
- scope: ->(rule) { rule.store.categories }
16
- )
12
+ preference :category_ids, :array, of: :id, model: 'Spree::Category', default: [],
13
+ scope: ->(rule) { rule.store.categories }
17
14
 
18
15
  # @return [Array<Spree::Category>]
19
16
  def categories
@@ -14,8 +14,8 @@ module Spree
14
14
  # This rule is the reason commission rules became classes. It could not
15
15
  # exist while a rule was only able to name a record.
16
16
  class ItemTotalRule < Spree::CommissionRule
17
- preference :min_amount, :decimal, nullable: true
18
- preference :max_amount, :decimal, nullable: true
17
+ preference :min_amount, :money, nullable: true
18
+ preference :max_amount, :money, nullable: true
19
19
 
20
20
  def applicable?(context)
21
21
  # Weighed exactly as the fee will weigh it, per this rate's own base.
@@ -7,11 +7,8 @@ module Spree
7
7
  # Ids are checked against the rate's own store on write, so a rule can
8
8
  # never be pointed at another marketplace's seller — and the client is
9
9
  # told, rather than having the id quietly dropped.
10
- preference :seller_ids, :array, default: [],
11
- parse_on_set: normalize_id_preference(
12
- klass: Spree::Seller,
13
- scope: ->(rule) { rule.store.sellers }
14
- )
10
+ preference :seller_ids, :array, of: :id, model: 'Spree::Seller', default: [],
11
+ scope: ->(rule) { rule.store.sellers }
15
12
 
16
13
  # @return [Array<Spree::Seller>]
17
14
  def sellers
@@ -101,6 +101,7 @@ module Spree
101
101
  }
102
102
 
103
103
  self.whitelisted_ransackable_attributes = %w[name kind parent_id po_number_required]
104
+ search_by :name
104
105
  self.whitelisted_ransackable_associations = %w[parent children memberships external_references]
105
106
 
106
107
  # The node a buyer browses and buys as when they name none: the single
@@ -25,16 +25,6 @@ module Spree
25
25
  scope :for_resource_type, ->(resource_type) { where(resource_type: resource_type) }
26
26
  scope :storefront_visible, -> { where(storefront_visible: true) }
27
27
  scope :admin_only, -> { where(storefront_visible: false) }
28
- scope :search, ->(query) do
29
- return all if query.blank?
30
-
31
- search_term = "%#{query.downcase}%"
32
- namespace_condition = arel_table[:namespace].lower.matches(search_term)
33
- key_condition = arel_table[:key].lower.matches(search_term)
34
- label_condition = arel_table[:label].lower.matches(search_term)
35
-
36
- where(namespace_condition.or(key_condition).or(label_condition))
37
- end
38
28
 
39
29
  #
40
30
  # Callbacks
@@ -52,8 +42,17 @@ module Spree
52
42
  #
53
43
  # Ransack
54
44
  #
55
- self.whitelisted_ransackable_attributes = %w[key namespace label resource_type storefront_visible searchable sortable]
56
- self.whitelisted_ransackable_scopes = %w[search]
45
+ self.whitelisted_ransackable_attributes = %w[key namespace label resource_type field_type storefront_visible searchable sortable]
46
+
47
+ # Both columns store class names; filters take the short names clients see.
48
+ def self.api_type_resolver(attribute)
49
+ case attribute
50
+ when 'resource_type' then ->(api_type) { normalize_value_for(:resource_type, api_type) }
51
+ when 'field_type' then ->(api_type) { Spree::CustomField::TYPE_TOKENS[api_type] }
52
+ else super
53
+ end
54
+ end
55
+ search_by :namespace, :key, :label
57
56
 
58
57
  # API-facing token for the STI subclass name stored in the `field_type`
59
58
  # column. Reader returns the registered token (`short_text`); writer
@@ -5,6 +5,7 @@ module Spree
5
5
  acts_as_paranoid
6
6
 
7
7
  include Spree::SingleStoreResource
8
+ search_by :name
8
9
 
9
10
  #
10
11
  # Associations
@@ -56,6 +56,7 @@ module Spree
56
56
 
57
57
  self.whitelisted_ransackable_attributes = %w[number kind status email requested_at completed_at]
58
58
  self.whitelisted_ransackable_scopes = %w[access erasure in_progress]
59
+ self.ransackable_scope_types = %w[access erasure in_progress].index_with('boolean')
59
60
 
60
61
  before_validation :set_defaults, on: :create
61
62
 
@@ -122,6 +122,7 @@ module Spree
122
122
  # needed. `seller_id` is what the operator's list filters by to see one
123
123
  # seller's methods, or (blank) the marketplace's own.
124
124
  self.whitelisted_ransackable_attributes = %w[storefront_visible available_to_sellers seller_id]
125
+ search_by :name
125
126
  self.whitelisted_ransackable_associations = %w[seller]
126
127
 
127
128
  validates :name, presence: true
@@ -227,7 +228,11 @@ module Spree
227
228
  end
228
229
 
229
230
  def ensure_calculator
230
- self.calculator ||= Spree::Calculator::Shipping::FlatRate.new(preferred_amount: 0)
231
+ self.calculator ||= default_calculator
232
+ end
233
+
234
+ def default_calculator
235
+ Spree::Calculator::Shipping::FlatRate.new(preferred_amount: 0)
231
236
  end
232
237
 
233
238
  def assign_default_delivery_profile
@@ -13,11 +13,8 @@ module Spree
13
13
  # decodes them on write, so eligibility compares against raw
14
14
  # `channel_id` rows. The scope confines the existence check to the
15
15
  # method's store, so cross-store channel IDs can't sneak in.
16
- preference :channel_ids, :array, default: [],
17
- parse_on_set: normalize_id_preference(
18
- klass: Spree::Channel,
19
- scope: ->(rule) { rule.store.channels }
20
- )
16
+ preference :channel_ids, :array, of: :id, model: 'Spree::Channel', default: [],
17
+ scope: ->(rule) { rule.store.channels }
21
18
 
22
19
  def channels
23
20
  return [] if preferred_channel_ids.blank?
@@ -4,8 +4,8 @@ module Spree
4
4
  # owner's currency). Empty preferences pass — half-configured rules fail
5
5
  # open per-rule, matching the PromotionRule/PriceRule convention.
6
6
  class ItemTotalRule < Spree::DeliveryMethodRule
7
- preference :minimum_amount, :decimal, default: nil, nullable: true
8
- preference :maximum_amount, :decimal, default: nil, nullable: true
7
+ preference :minimum_amount, :money, default: nil, nullable: true
8
+ preference :maximum_amount, :money, default: nil, nullable: true
9
9
 
10
10
  def eligible?(package)
11
11
  total = package.item_total
@@ -13,6 +13,7 @@ module Spree
13
13
  include Spree::HasListPosition
14
14
 
15
15
  has_prefix_id :fp
16
+ search_by :name
16
17
 
17
18
  acts_as_list scope: :store
18
19
 
@@ -80,9 +80,9 @@ module Spree
80
80
  @tax_amount ||= if tax_rate.nil?
81
81
  BigDecimal(0)
82
82
  elsif tax_rate.included_in_price?
83
- (cost / (1 + tax_rate.amount) * tax_rate.amount).round(2)
83
+ Spree::Money::Rounding.to_currency(cost / (1 + tax_rate.amount) * tax_rate.amount, currency)
84
84
  else
85
- (cost * tax_rate.amount).round(2)
85
+ Spree::Money::Rounding.to_currency(cost * tax_rate.amount, currency)
86
86
  end
87
87
  end
88
88
 
@@ -157,7 +157,7 @@ module Spree
157
157
  end
158
158
 
159
159
  def discount_amount
160
- fulfillment.discounts.promotion.sum(:amount)
160
+ fulfillment.promotion_discount_total
161
161
  end
162
162
  end
163
163
  end
@@ -33,7 +33,7 @@ module Spree
33
33
  validate :origin_group_must_belong_to_profile,
34
34
  if: -> { delivery_origin_group_id_changed? || delivery_profile_id_changed? }
35
35
 
36
- self.whitelisted_ransackable_attributes = %w[name]
36
+ self.whitelisted_ransackable_attributes = %w[name delivery_profile_id]
37
37
 
38
38
  # @param address [Spree::Address, nil]
39
39
  # @return [Boolean] whether any member of this zone matches the address
@@ -42,6 +42,7 @@ module Spree
42
42
  delegate :currency, to: :order
43
43
 
44
44
  self.whitelisted_ransackable_attributes = %w[number status created_at]
45
+ search_by :number
45
46
  self.whitelisted_ransackable_associations = %w[order reason]
46
47
 
47
48
  # Positive when the replacements cost more than what came back (the
@@ -46,10 +46,10 @@ module Spree
46
46
  result.rows.each do |row|
47
47
  csv << Spree::CSV::FormulaSanitizer.row(
48
48
  row[:dimensions].map { |name, raw| hydration.label(name, raw) } +
49
- query.metrics.map { |m| row[:metrics][m.name][:value] }
49
+ query.metrics.map { |m| m.format_value(row[:metrics][m.name][:value], result.meta[:currency]) }
50
50
  )
51
51
  end
52
- totals = query.metrics.map { |m| result.totals[m.name][:value] }
52
+ totals = query.metrics.map { |m| m.format_value(result.totals[m.name][:value], result.meta[:currency]) }
53
53
  # With a breakdown the totals close the file under a "Total" label in
54
54
  # the first dimension column; without one the totals are the only row.
55
55
  label = query.dimensions.empty? ? [] : [I18n.t('spree.reporting.export.total')] + Array.new(query.dimensions.size - 1, '')
@@ -0,0 +1,155 @@
1
+ module Spree
2
+ # A file sent to `POST /files` (docs/plans/6.0-uploads-and-file-ownership.md),
3
+ # before anything uses it.
4
+ #
5
+ # Two ways in, one result. A presigned upload carries only the file's
6
+ # metadata and gets back a storage target the client sends the bytes to; a
7
+ # multipart upload carries the bytes, which are stored here. Either way the
8
+ # result is a blob owned by +store+ and an expiring, opaque `signed_id` the
9
+ # client passes to whichever endpoint uses the file.
10
+ #
11
+ # Never persisted: the blob is the record.
12
+ class FileUpload
13
+ include ActiveModel::Model
14
+ include ActiveModel::Attributes
15
+
16
+ VISIBILITIES = %w[public private].freeze
17
+
18
+ attribute :filename, :string
19
+ attribute :content_type, :string
20
+ attribute :byte_size, :integer
21
+ attribute :checksum, :string
22
+ attribute :visibility, :string, default: 'public'
23
+ # The bytes of a multipart upload (an uploaded file or any IO).
24
+ attribute :file
25
+ attribute :store
26
+ # Narrower limits for a caller with its own rules (a buyer's purchase
27
+ # order document); the installation-wide ones apply otherwise.
28
+ attribute :max_byte_size, :integer
29
+ attribute :allowed_content_types
30
+
31
+ attr_reader :blob, :signed_id, :expires_at
32
+
33
+ validates :store, presence: true
34
+ validates :visibility, inclusion: { in: VISIBILITIES }
35
+ validates :filename, :content_type, presence: true
36
+ validates :checksum, presence: true, unless: :multipart?
37
+ validates :byte_size, numericality: { only_integer: true, greater_than: 0 }
38
+ validate :byte_size_within_limit
39
+ validate :content_type_allowed
40
+
41
+ # The content types a public upload may have. Private uploads accept any
42
+ # type: digital products are ebooks, archives and audio, and each
43
+ # attachment validates its own file.
44
+ #
45
+ # @return [Array<String>]
46
+ def self.public_content_types
47
+ Rails.application.config.active_storage.web_image_content_types +
48
+ Spree::Config.video_content_types +
49
+ %w[text/csv]
50
+ end
51
+
52
+ # @return [Boolean] whether the bytes travel in the request
53
+ def multipart?
54
+ file.respond_to?(:read)
55
+ end
56
+
57
+ # @return [Boolean] whether the file lands on private storage
58
+ def private?
59
+ visibility == 'private'
60
+ end
61
+
62
+ # Validates and creates the blob. A multipart upload is stored here; a
63
+ # presigned one waits for the client to send its bytes to {#upload_target}.
64
+ #
65
+ # @return [Boolean]
66
+ def save
67
+ read_file_facts if multipart?
68
+ return false unless valid?
69
+
70
+ @blob = multipart? ? store_bytes : reserve_blob
71
+ @expires_at = Spree::Uploads::SIGNED_ID_EXPIRY.from_now
72
+ @signed_id = Spree::Uploads.signed_id_for(@blob)
73
+ true
74
+ end
75
+
76
+ # Where a presigned upload's bytes go, or nil once the file is stored.
77
+ #
78
+ # @return [Spree::FileUpload::Target, nil]
79
+ def upload_target
80
+ return if multipart? || blob.nil?
81
+
82
+ Target.new(
83
+ http_method: 'PUT',
84
+ url: blob.service_url_for_direct_upload,
85
+ headers: blob.service_headers_for_direct_upload
86
+ )
87
+ end
88
+
89
+ # The storage target a presigned upload sends its bytes to.
90
+ class Target
91
+ include ActiveModel::Model
92
+ include ActiveModel::Attributes
93
+
94
+ # Not `method`, which every Ruby object already answers.
95
+ attribute :http_method, :string
96
+ attribute :url, :string
97
+ attribute :headers
98
+ end
99
+
100
+ private
101
+
102
+ # The size of a multipart upload comes from the bytes, and so does its type
103
+ # wherever the bytes carry a recognisable signature: Marcel reads the
104
+ # content first, so a renamed executable is not accepted as an image. Only
105
+ # bytes it cannot place fall back to the declared type and the file name.
106
+ def read_file_facts
107
+ self.filename = filename.presence || file.try(:original_filename)
108
+ self.byte_size = io.size
109
+ self.content_type = Marcel::MimeType.for(io, name: filename, declared_type: content_type.presence || file.try(:content_type))
110
+ io.rewind
111
+ end
112
+
113
+ def reserve_blob
114
+ ActiveStorage::Blob.create!(
115
+ filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type,
116
+ service_name: service_name, store_id: store.id
117
+ )
118
+ end
119
+
120
+ def store_bytes
121
+ ActiveStorage::Blob.build_after_unfurling(
122
+ io: io, filename: filename, content_type: content_type, service_name: service_name, identify: false
123
+ ).tap do |blob|
124
+ blob.store_id = store.id
125
+ blob.save!
126
+ blob.upload_without_unfurling(io)
127
+ end
128
+ end
129
+
130
+ def io
131
+ @io ||= file.respond_to?(:to_io) ? file.to_io : file
132
+ end
133
+
134
+ def service_name
135
+ private? ? Spree.private_storage_service_name : Spree.public_storage_service_name
136
+ end
137
+
138
+ def size_limit
139
+ max_byte_size || (multipart? ? Spree::Config.max_multipart_upload_size : Spree::Config.max_upload_size)
140
+ end
141
+
142
+ def byte_size_within_limit
143
+ return if byte_size.nil? || byte_size <= size_limit
144
+
145
+ errors.add(:byte_size, :less_than_or_equal_to, count: ActiveSupport::NumberHelper.number_to_human_size(size_limit))
146
+ end
147
+
148
+ def content_type_allowed
149
+ allowed = allowed_content_types || (self.class.public_content_types unless private?)
150
+ return if allowed.nil? || content_type.blank? || allowed.include?(content_type)
151
+
152
+ errors.add(:content_type, :inclusion, value: content_type)
153
+ end
154
+ end
155
+ end
@@ -374,9 +374,20 @@ module Spree
374
374
  #
375
375
  # @return [Boolean]
376
376
  def with_free_shipping_promotion?
377
+ return discounts.any?(&:promotion?) if read_association_in_memory?(:discounts)
378
+
377
379
  discounts.promotion.exists?
378
380
  end
379
381
 
382
+ # What promotions took off this shipment's cost, as a negative amount.
383
+ #
384
+ # @return [BigDecimal]
385
+ def promotion_discount_total
386
+ return discounts.select(&:promotion?).sum(BigDecimal('0'), &:amount) if read_association_in_memory?(:discounts)
387
+
388
+ discounts.promotion.sum(:amount)
389
+ end
390
+
380
391
  # Placement no longer moves physical stock — the units are promised in
381
392
  # Spree::Orders::Complete and only leave the shelf when the parcel does.
382
393
  def finalize!
@@ -701,7 +712,7 @@ module Spree
701
712
  #
702
713
  # @return [Spree::Delivery, nil]
703
714
  def primary_delivery
704
- deliveries.first
715
+ read_association_in_memory?(:deliveries) ? deliveries.to_a.first : deliveries.first
705
716
  end
706
717
 
707
718
  # The primary delivery's tracking number. Read-only since 6.0: tracking is
@@ -895,8 +906,7 @@ module Spree
895
906
  def capture_at_gateway(payment, amount)
896
907
  return 0 if amount <= 0
897
908
 
898
- units = Spree::Money::Rounding.to_minor_units(amount, payment.currency)
899
- result = Spree.payment_capture_workflow.call(payment: payment, amount: units)
909
+ result = Spree.payment_capture_workflow.call(payment: payment, amount: amount)
900
910
  raise Spree::Core::GatewayError, result.error.value.to_s if result.failure?
901
911
 
902
912
  amount