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
@@ -72,12 +72,17 @@ module Spree
72
72
  fulfillments.sum { |fulfillment| fulfillment.discounted_cost.to_d }
73
73
  end
74
74
 
75
+ # @return [String] the currency the parcel was quoted in
76
+ def currency
77
+ primary.currency
78
+ end
79
+
75
80
  # @return [String, Spree::Money] the forwarder's placeholder while a
76
81
  # freight rate is unpriced, money otherwise
77
82
  def display_cost(**options)
78
83
  return primary.display_cost(**options) if unpriced?
79
84
 
80
- Spree::Money.new(cost, { currency: primary.currency }.merge(options))
85
+ Spree::Money.new(cost, { currency: currency }.merge(options))
81
86
  end
82
87
 
83
88
  # @return [Boolean]
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class Gateway::Bogus < Gateway
3
+ self.accepts_money_amounts = true
4
+
3
5
  TEST_VISA = ['4111111111111111', '4012888888881881', '4222222222222']
4
6
  TEST_MC = ['5500000000000004', '5555555555554444', '5105105105105100', '2223000010309703']
5
7
  TEST_AMEX = ['378282246310005', '371449635398431', '378734493671000', '340000000000009']
@@ -1,6 +1,6 @@
1
1
  module Spree
2
2
  class Gateway < PaymentMethod
3
- FROM_DOLLAR_TO_CENT_RATE = 100.0
3
+ FROM_DOLLAR_TO_CENT_RATE = 100
4
4
 
5
5
  delegate :authorize, :purchase, :capture, :void, :credit, to: :provider
6
6
 
@@ -5,6 +5,9 @@ module Spree
5
5
  extend DisplayMoney
6
6
  include Spree::SingleStoreResource
7
7
  include Spree::HasCustomFields
8
+ include Spree::CanonicalDecimals
9
+
10
+ canonical_decimals :amount
8
11
  include Spree::Security::GiftCards if defined?(Spree::Security::GiftCards)
9
12
 
10
13
  publishes_lifecycle_events
@@ -62,10 +65,12 @@ module Spree
62
65
  # Ransack
63
66
  #
64
67
  ransack_alias :state, :status # @deprecated filter alias — removed in 6.1
65
- self.whitelisted_ransackable_attributes = %w[code customer_id status state gift_card_batch_id created_by_id]
68
+ self.whitelisted_ransackable_attributes = %w[code customer_id status state gift_card_batch_id created_by_id currency expires_at]
69
+ search_by :code
66
70
  # `users` is the pre-6.0 name for `customers` — removed in 6.1.
67
71
  self.whitelisted_ransackable_associations = %w[customers users orders batch]
68
72
  self.whitelisted_ransackable_scopes = %w[active expired redeemed partially_redeemed]
73
+ self.ransackable_scope_types = %w[active expired redeemed partially_redeemed].index_with('boolean')
69
74
 
70
75
  normalizes :code, with: ->(value) { value&.to_s&.squish&.presence }
71
76
 
@@ -82,12 +87,6 @@ module Spree
82
87
  #
83
88
  money_methods :amount, :amount_used, :amount_authorized, :amount_remaining
84
89
 
85
- # Sets the amount
86
- # @param amount [String]
87
- def amount=(amount)
88
- self[:amount] = Spree::LocalizedNumber.parse(amount)
89
- end
90
-
91
90
  # Calculates the remaining amount
92
91
  # @return [Decimal]
93
92
  def amount_remaining
@@ -4,6 +4,9 @@ module Spree
4
4
 
5
5
  extend DisplayMoney
6
6
  include Spree::SingleStoreResource
7
+ include Spree::CanonicalDecimals
8
+
9
+ canonical_decimals :amount
7
10
 
8
11
  publishes_lifecycle_events
9
12
 
@@ -34,10 +37,6 @@ module Spree
34
37
 
35
38
  money_methods :amount
36
39
 
37
- def amount=(amount)
38
- self[:amount] = Spree::LocalizedNumber.parse(amount)
39
- end
40
-
41
40
  self.whitelisted_ransackable_attributes = %w[prefix]
42
41
 
43
42
  def generate_gift_cards
@@ -55,6 +55,7 @@ module Spree
55
55
  # Ransack configuration
56
56
  #
57
57
  self.whitelisted_ransackable_attributes = %w[number type status seller_id]
58
+ search_by :number
58
59
  # Lets an operator filter their list by who ran the job — the seller's
59
60
  # name, not an id they would have to look up first.
60
61
  self.whitelisted_ransackable_associations = %w[seller]
@@ -72,6 +73,7 @@ module Spree
72
73
  # Attachments
73
74
  #
74
75
  has_one_attached :attachment, service: Spree.private_storage_service_name
76
+ signed_id_attachments :attachment
75
77
 
76
78
  #
77
79
  # Status
@@ -87,6 +89,7 @@ module Spree
87
89
  # Preferences
88
90
  #
89
91
  preference :delimiter, :string, default: ','
92
+ exposes_preferences :delimiter
90
93
  # Run the pipeline in-process instead of enqueuing it. For rake tasks, seeds
91
94
  # and the console, where the caller needs the data to exist when the call
92
95
  # returns and there may be no worker attached. Never set it from a request:
@@ -98,7 +98,7 @@ module Spree
98
98
  group: klass.integration_group,
99
99
  description: klass.human_description,
100
100
  **klass.provider_listing,
101
- preference_schema: klass.serialized_preference_schema
101
+ schema: klass.preference_json_schema
102
102
  }
103
103
  end.sort_by { |entry| entry[:name] }
104
104
  end
@@ -119,7 +119,7 @@ module Spree
119
119
 
120
120
  self.whitelisted_ransackable_associations = %w[variant order tax_category]
121
121
  self.whitelisted_ransackable_attributes = %w[variant_id order_id tax_category_id quantity
122
- price cost_price cost_currency adjustment_total
122
+ price cost_price adjustment_total
123
123
  additional_tax_total discount_total included_tax_total
124
124
  pre_tax_amount taxable_adjustment_total
125
125
  non_taxable_adjustment_total]
@@ -231,7 +231,7 @@ module Spree
231
231
  #
232
232
  # @return [BigDecimal]
233
233
  def amount
234
- price * quantity
234
+ Spree::Money::Rounding.to_currency(price * quantity, currency)
235
235
  end
236
236
 
237
237
  # Returns the compare at amount (compare at price * quantity) of the line item
@@ -8,6 +8,9 @@ module Spree
8
8
  acts_as_paranoid
9
9
  acts_as_list scope: :store_id
10
10
 
11
+ self.whitelisted_ransackable_attributes = %w[currency default_locale]
12
+ search_by :name
13
+
11
14
  #
12
15
  # Associations
13
16
  #
@@ -74,7 +77,9 @@ module Spree
74
77
  inclusion: { in: ->(_market) { Spree.tax_providers.map(&:to_s) } },
75
78
  allow_blank: true
76
79
  validates :currency, presence: true
80
+ normalizes :default_locale, with: ->(value) { value.to_s.strip.presence }
77
81
  validates :default_locale, presence: true
82
+ validates_with Spree::LocaleCodesValidator, attributes: %i[default_locale supported_locales]
78
83
  validates :countries, presence: true
79
84
 
80
85
  #
@@ -142,7 +147,7 @@ module Spree
142
147
  #
143
148
  # @return [Array<String>]
144
149
  def supported_locales_list
145
- @supported_locales_list ||= (supported_locales.to_s.split(',').map(&:strip) << default_locale).compact.uniq.sort
150
+ @supported_locales_list ||= (supported_locales.to_s.split(',').map(&:strip) << default_locale).compact_blank.uniq.sort
146
151
  end
147
152
 
148
153
  # Accepts an Array of locale codes and persists them as a comma-separated
@@ -153,7 +158,7 @@ module Spree
153
158
  # @param value [Array<String>, String, nil]
154
159
  def supported_locales=(value)
155
160
  @supported_locales_list = nil
156
- normalized = value.is_a?(Array) ? value.compact.uniq.reject(&:blank?).join(',') : value
161
+ normalized = value.is_a?(Array) ? value.map { |code| code.to_s.strip }.reject(&:empty?).uniq.join(',') : value
157
162
  super(normalized)
158
163
  end
159
164
 
@@ -31,15 +31,6 @@ module Spree
31
31
  EXTERNAL_URL_CUSTOM_FIELD_KEY = 'external.url'
32
32
  MEDIA_TYPES = %w[image video external_video].freeze
33
33
 
34
- # Domain attributes a client may write, shared by every write path — the
35
- # media endpoints and the inline `media:` list on a product. Each path adds
36
- # its own transport keys (an upload's signed id, a row id) on top. Adding a
37
- # field here reaches both, so it can't be dropped on the one a client uses.
38
- WRITABLE_ATTRIBUTES = %i[
39
- alt position media_type external_video_url poster_signed_id
40
- focal_point_x focal_point_y
41
- ].freeze
42
-
43
34
  after_initialize { self.media_type ||= 'image' }
44
35
 
45
36
  belongs_to :viewable, polymorphic: true, touch: true, optional: true
@@ -93,6 +84,7 @@ module Spree
93
84
 
94
85
  self.whitelisted_ransackable_attributes = %w[alt media_type created_at updated_at]
95
86
  self.whitelisted_ransackable_scopes = %w[attached unattached filename_cont]
87
+ self.ransackable_scope_types = { 'attached' => 'boolean', 'unattached' => 'boolean' }
96
88
 
97
89
  delegate :key, :attached?, :variant, :variable?, :blob, :filename, :variation, to: :attachment
98
90
 
@@ -48,6 +48,7 @@ module Spree
48
48
  #
49
49
  self.whitelisted_ransackable_attributes = %w[email verified_at]
50
50
  self.whitelisted_ransackable_scopes = %w[verified unverified]
51
+ self.ransackable_scope_types = %w[verified unverified].index_with('boolean')
51
52
 
52
53
  def accepts_email_marketing
53
54
  return customer.accepts_email_marketing if customer.present?
@@ -24,7 +24,8 @@ module Spree
24
24
  self.whitelisted_ransackable_scopes = %w[search_by_name]
25
25
  # `label` is what admin surfaces display, so it has to be filterable —
26
26
  # matching Spree::OptionValue, which already allows it.
27
- self.whitelisted_ransackable_attributes = %w[label name]
27
+ self.whitelisted_ransackable_attributes = %w[label name kind]
28
+ search_by :name, :label
28
29
 
29
30
  normalizes :name, with: ->(value) { value&.to_s&.squish&.presence }
30
31
 
@@ -18,7 +18,8 @@ module Spree
18
18
  #
19
19
  # Attachments
20
20
  #
21
- has_one_attached :image
21
+ has_one_attached :image, service: Spree.public_storage_service_name
22
+ signed_id_attachments :image
22
23
 
23
24
  #
24
25
  # Associations
@@ -154,6 +154,7 @@ module Spree
154
154
  order_group_id po_number
155
155
  ]
156
156
  self.whitelisted_ransackable_scopes = %w[complete incomplete refunded partially_refunded search]
157
+ self.ransackable_scope_types = %w[complete incomplete refunded partially_refunded].index_with('boolean')
157
158
  # A seller never sees the buyer's email, and a company member filtering the
158
159
  # company's orders must not learn a colleague's; the risk flag and coupon
159
160
  # are back-office data. `search` matches on the email too.
@@ -415,7 +416,7 @@ module Spree
415
416
 
416
417
  # Returns the subtotal used for analytics integrations
417
418
  # It's a sum of the item total and the promo total
418
- # @return [Float]
419
+ # @return [Float] a JSON number, as analytics providers expect
419
420
  def analytics_subtotal
420
421
  (item_total + line_items.sum(:discount_total)).to_f
421
422
  end
@@ -458,7 +459,7 @@ module Spree
458
459
  end
459
460
 
460
461
  def refunds_total
461
- refunds.loaded? ? refunds.sum(&:amount) : refunds.sum(:amount)
462
+ read_association_in_memory?(:refunds) ? refunds.sum(0.to_d, &:amount) : refunds.sum(:amount)
462
463
  end
463
464
 
464
465
  # Checks if the order is partially refunded
@@ -707,7 +708,7 @@ module Spree
707
708
  #
708
709
  # @return [BigDecimal]
709
710
  def returned_items_refund_total
710
- return refunds.select(&:for_returned_items?).sum(0.to_d, &:amount) if refunds.loaded?
711
+ return refunds.select(&:for_returned_items?).sum(0.to_d, &:amount) if read_association_in_memory?(:refunds)
711
712
 
712
713
  refunds.for_returned_items.sum(:amount)
713
714
  end
@@ -829,7 +830,6 @@ module Spree
829
830
  end
830
831
 
831
832
  def fulfill!
832
- fulfillments.each { |shipment| shipment.update!(self) if shipment.persisted? }
833
833
  save!
834
834
  update_statuses!
835
835
  end
@@ -1274,7 +1274,7 @@ module Spree
1274
1274
  end
1275
1275
 
1276
1276
  def credit_card_nil_payment?(attributes)
1277
- payments.store_credits.present? && attributes[:amount].to_f.zero?
1277
+ payments.store_credits.present? && Spree::Money::Rounding.blank_or_zero?(attributes[:amount])
1278
1278
  end
1279
1279
  end
1280
1280
  end
@@ -100,9 +100,14 @@ module Spree
100
100
  # the children loaded already (a serializer renders them, a gateway call
101
101
  # reads four of these in a row), and an aggregate per figure would issue a
102
102
  # query each time even against rows sitting in memory.
103
+ #
104
+ # +tax_total+ is each child's own derivation rather than a column, summed
105
+ # like the rest: asking the children keeps the group's answer the one they
106
+ # gave, where re-deriving it here from the two halves above would be a
107
+ # second way to reach the same figure.
103
108
  ROLLED_UP_TOTALS = %i[
104
109
  total item_total delivery_total additional_tax_total included_tax_total
105
- discount_total fee_total
110
+ tax_total discount_total fee_total
106
111
  ].freeze
107
112
 
108
113
  ROLLED_UP_TOTALS.each do |figure|
@@ -112,7 +117,7 @@ module Spree
112
117
  end
113
118
 
114
119
  money_methods(*ROLLED_UP_TOTALS)
115
- money_methods :gift_card_total
120
+ money_methods :gift_card_total, :amount_due
116
121
 
117
122
  alias ship_total delivery_total
118
123
 
@@ -94,6 +94,7 @@ module Spree
94
94
  # `seller_id` is what the operator's list filters by to see one seller's
95
95
  # packaging, or (blank) the marketplace's own.
96
96
  self.whitelisted_ransackable_attributes = %w[name kind default seller_id]
97
+ search_by :name
97
98
  self.whitelisted_ransackable_associations = %w[seller]
98
99
 
99
100
  # The unit the geometry is expressed in, falling back to what the store's
@@ -67,7 +67,7 @@ module Spree
67
67
  private
68
68
 
69
69
  def exchange_multiplier
70
- payment.payment_method.try(:exchange_multiplier) || 1.0
70
+ payment.payment_method.try(:exchange_multiplier) || 1
71
71
  end
72
72
  end
73
73
  end
@@ -6,6 +6,9 @@ module Spree
6
6
  derives_number infix: 'P'
7
7
  include Spree::HasCustomFields
8
8
  include Spree::Metadata
9
+ include Spree::CanonicalDecimals
10
+
11
+ canonical_decimals :amount
9
12
  if defined?(Spree::Security::Payments)
10
13
  include Spree::Security::Payments
11
14
  end
@@ -255,16 +258,6 @@ module Spree
255
258
  end
256
259
  end
257
260
 
258
- def amount=(amount)
259
- self[:amount] =
260
- case amount
261
- when String
262
- separator = I18n.t('number.currency.format.separator')
263
- number = amount.delete("^0-9-#{separator}\.").tr(separator, '.')
264
- number.to_d if number.present?
265
- end || amount
266
- end
267
-
268
261
  # Resizes a store credit payment that has not been taken yet. Completion
269
262
  # takes it against the eligibility event recorded for its amount, so the
270
263
  # new amount needs one of its own.
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class PaymentMethod::Check < ::Spree::PaymentMethod
3
+ self.accepts_money_amounts = true
4
+
3
5
  def actions
4
6
  %w{capture void}
5
7
  end
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class PaymentMethod::StoreCredit < ::Spree::PaymentMethod
3
+ self.accepts_money_amounts = true
4
+
3
5
  def payment_source_class
4
6
  ::Spree::StoreCredit
5
7
  end
@@ -20,13 +22,13 @@ module Spree
20
22
  payment.pending?
21
23
  end
22
24
 
23
- def authorize(amount_in_cents, store_credit, gateway_options = {})
25
+ def authorize(money, store_credit, gateway_options = {})
24
26
  if store_credit.nil?
25
27
  Spree::PaymentResponse.new(false, I18n.t('spree.store_credit_payment_method.unable_to_find'), {}, {})
26
28
  else
27
29
  action = lambda do |store_credit|
28
30
  store_credit.authorize(
29
- amount_in_cents / 100.0.to_d,
31
+ money.to_d,
30
32
  gateway_options[:currency],
31
33
  action_originator: gateway_options[:originator]
32
34
  )
@@ -35,10 +37,10 @@ module Spree
35
37
  end
36
38
  end
37
39
 
38
- def capture(amount_in_cents, auth_code, gateway_options = {})
40
+ def capture(money, auth_code, gateway_options = {})
39
41
  action = lambda do |store_credit|
40
42
  store_credit.capture(
41
- amount_in_cents / 100.0.to_d,
43
+ money.to_d,
42
44
  auth_code,
43
45
  gateway_options[:currency],
44
46
  action_originator: gateway_options[:originator]
@@ -47,9 +49,9 @@ module Spree
47
49
  handle_action(action, :capture, auth_code)
48
50
  end
49
51
 
50
- def purchase(amount_in_cents, store_credit, gateway_options = {})
52
+ def purchase(money, store_credit, gateway_options = {})
51
53
  eligible_events = store_credit.store_credit_events.where(
52
- amount: amount_in_cents / 100.0.to_d,
54
+ amount: money.to_d,
53
55
  action: Spree::StoreCredit::ELIGIBLE_ACTION
54
56
  )
55
57
  event = eligible_events.detect do |eligible_event|
@@ -60,7 +62,7 @@ module Spree
60
62
  if event.blank?
61
63
  Spree::PaymentResponse.new(false, I18n.t('spree.store_credit_payment_method.unable_to_find'), {}, {})
62
64
  else
63
- capture(amount_in_cents, event.authorization_code, gateway_options)
65
+ capture(money, event.authorization_code, gateway_options)
64
66
  end
65
67
  end
66
68
 
@@ -71,12 +73,12 @@ module Spree
71
73
  handle_action(action, :void, auth_code)
72
74
  end
73
75
 
74
- def credit(amount_in_cents, auth_code, gateway_options)
76
+ def credit(money, auth_code, gateway_options)
75
77
  action = lambda do |store_credit|
76
78
  currency = gateway_options[:currency] || store_credit.currency
77
79
  originator = gateway_options[:originator]
78
80
 
79
- store_credit.credit(amount_in_cents / 100.0.to_d, auth_code, currency, action_originator: originator)
81
+ store_credit.credit(money.to_d, auth_code, currency, action_originator: originator)
80
82
  end
81
83
 
82
84
  handle_action(action, :credit, auth_code)
@@ -20,6 +20,13 @@ module Spree
20
20
  include Spree::Security::PaymentMethods
21
21
  end
22
22
 
23
+ # Whether authorize, purchase, capture and credit take the amount as a
24
+ # Spree::Money (amount and currency) and convert it to the provider's own
25
+ # unit. Methods that have not opted in receive an integer in hundredths of
26
+ # the currency, which is wrong for yen and dinar; that bridge is removed in
27
+ # Spree 6.1.
28
+ class_attribute :accepts_money_amounts, default: false, instance_writer: false
29
+
23
30
  # Blank falls back to the store, so a method only overrides when a merchant
24
31
  # deliberately picks one — the meaning the auto_capture boolean already had.
25
32
  normalizes :capture_method, with: ->(value) { value.presence }
@@ -48,8 +55,9 @@ module Spree
48
55
  scope :storefront_visible, -> { where(storefront_visible: true) }
49
56
  scope :admin_only, -> { where(storefront_visible: false) }
50
57
 
51
- # Real column, so admin clients filter it directly — no ransacker needed.
52
- self.whitelisted_ransackable_attributes = %w[storefront_visible]
58
+ # Real columns, so admin clients filter them directly — no ransacker needed.
59
+ self.whitelisted_ransackable_attributes = %w[storefront_visible active type]
60
+ search_by :name
53
61
 
54
62
  after_initialize :set_name, if: :new_record?
55
63
 
@@ -339,6 +347,27 @@ module Spree
339
347
  self.storefront_visible = value.to_s != 'back_end'
340
348
  end
341
349
 
350
+
351
+ # The amount in the form this payment method's gateway calls take.
352
+ #
353
+ # @param amount [BigDecimal] in the currency's own units ("19.99")
354
+ # @param currency [String]
355
+ # @return [Spree::Money, Integer] a Spree::Money for methods that accept
356
+ # money amounts, otherwise hundredths of the currency (deprecated)
357
+ def gateway_amount(amount, currency)
358
+ return Spree::Money.new(amount, currency: currency) if accepts_money_amounts
359
+
360
+ warn_about_hundredths
361
+ hundredths = Spree::Money::Rounding.to_hundredths(amount)
362
+ # A hundredth is the smallest amount these gateways can be sent; zero
363
+ # reads as "the whole amount" to many of them.
364
+ if hundredths.zero? && !amount.zero?
365
+ raise Spree::Core::GatewayError, "#{self.class.name} cannot take an amount below a hundredth of #{currency}"
366
+ end
367
+
368
+ hundredths
369
+ end
370
+
342
371
  protected
343
372
 
344
373
  def public_preference_keys
@@ -348,5 +377,16 @@ module Spree
348
377
  def set_name
349
378
  self.name ||= default_name
350
379
  end
380
+
381
+ def warn_about_hundredths
382
+ return if self.class.instance_variable_get(:@warned_about_hundredths)
383
+
384
+ self.class.instance_variable_set(:@warned_about_hundredths, true)
385
+ Spree::Deprecation.warn(
386
+ "#{self.class.name} receives amounts as an integer in hundredths, which is wrong for currencies " \
387
+ 'without two decimals. Set `self.accepts_money_amounts = true` and take a Spree::Money ' \
388
+ '(amount and currency); hundredths are removed in Spree 6.1.'
389
+ )
390
+ end
351
391
  end
352
392
  end
@@ -62,6 +62,7 @@ module Spree
62
62
  # Ransack
63
63
  #
64
64
  self.whitelisted_ransackable_attributes = %w[name owner_type owner_id]
65
+ search_by :name
65
66
 
66
67
  before_destroy :really_destroy_slugs!
67
68
 
@@ -11,12 +11,16 @@ module Spree
11
11
 
12
12
  include Spree::VatPriceCalculation
13
13
  include Spree::StorePreferences
14
+ include Spree::CanonicalDecimals
15
+
16
+ canonical_decimals :amount, :compare_at_amount, places: Spree::Money::Rounding::UNIT_PRICE_DECIMALS
14
17
 
15
18
  publishes_lifecycle_events
16
19
 
17
20
  acts_as_paranoid
18
21
 
19
- MAXIMUM_AMOUNT = BigDecimal('99_999_999.99')
22
+ # The largest amount decimal(19,4) holds.
23
+ MAXIMUM_AMOUNT = BigDecimal('999_999_999_999_999.9999')
20
24
 
21
25
  # How many breaks one variant may carry on one list in one currency. A UI
22
26
  # sanity bound rather than a technical one — but enforced here so no
@@ -24,7 +28,7 @@ module Spree
24
28
  # every priced line (docs/plans/6.0-volume-pricing.md).
25
29
  MAXIMUM_BREAKS_PER_VARIANT = 10
26
30
 
27
- belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant', inverse_of: :prices, touch: true
31
+ belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant', inverse_of: :prices
28
32
  belongs_to :price_list, class_name: 'Spree::PriceList', optional: true
29
33
 
30
34
  has_many :price_histories, class_name: 'Spree::PriceHistory', dependent: :delete_all
@@ -32,6 +36,8 @@ module Spree
32
36
  before_validation :ensure_currency
33
37
  before_save :remove_compare_at_amount_if_equals_amount
34
38
  after_save :record_price_history, if: :should_record_price_history?
39
+ after_save :touch_variant, if: -> { saved_changes? && (amount.present? || amount_before_last_save.present?) }
40
+ after_destroy :touch_variant, if: -> { amount.present? }
35
41
 
36
42
  # legacy behavior
37
43
  validates :amount, allow_nil: true, numericality: {
@@ -100,10 +106,6 @@ module Spree
100
106
  Spree::Money.new(amount || 0, currency: currency.upcase)
101
107
  end
102
108
 
103
- def amount=(amount)
104
- self[:amount] = amount.blank? ? nil : Spree::LocalizedNumber.parse(amount)
105
- end
106
-
107
109
  # Returns the amount in cents
108
110
  # @return [Integer]
109
111
  def amount_in_cents
@@ -114,12 +116,6 @@ module Spree
114
116
  Spree::Money.new(compare_at_amount || 0, currency: currency)
115
117
  end
116
118
 
117
- def compare_at_amount=(value)
118
- calculated_value = Spree::LocalizedNumber.parse(value) if value.present?
119
-
120
- self[:compare_at_amount] = calculated_value
121
- end
122
-
123
119
  # Returns the compare at amount for display
124
120
  # @return [Spree::Money, nil]
125
121
  def display_compare_at_amount
@@ -137,9 +133,7 @@ module Spree
137
133
  end
138
134
 
139
135
  alias_attribute :price, :amount
140
- alias_method :price=, :amount=
141
136
  alias_attribute :compare_at_price, :compare_at_amount
142
- alias_method :compare_at_price=, :compare_at_amount=
143
137
 
144
138
  # Whether this figure is the merchant's answer for the buyer's geography
145
139
  # already. A price list narrowed by market or country states its prices for
@@ -342,6 +336,15 @@ module Spree
342
336
  rungs.sort_by(&:first)
343
337
  end
344
338
 
339
+ # Stands in for `touch: true` on the variant, skipping placeholder rows: a
340
+ # price list holds one without an amount for every variant and currency
341
+ # nobody has priced yet, and it charges nothing, so writing one changes no
342
+ # price a shopper sees — yet the touch would expire the product's cached
343
+ # responses and reindex it for search.
344
+ def touch_variant
345
+ variant&.touch_later
346
+ end
347
+
345
348
  def should_record_price_history?
346
349
  price_list_id.nil? &&
347
350
  amount.present? &&
@@ -10,6 +10,10 @@ module Spree
10
10
  # and not twenty percent off an already-discounted number
11
11
  # (docs/plans/6.0-volume-pricing.md).
12
12
  class PriceAdjustmentTier < Spree.base_class
13
+ include Spree::CanonicalDecimals
14
+
15
+ canonical_decimals :percentage, places: 3
16
+
13
17
  has_prefix_id :pat
14
18
 
15
19
  # How many bands one list may carry, matching the break cap so a merchant