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
@@ -7,6 +7,9 @@ module Spree
7
7
 
8
8
  include Spree::SingleStoreResource
9
9
  include Spree::HasListPosition
10
+ include Spree::CanonicalDecimals
11
+
12
+ canonical_decimals :price_adjustment_percentage, places: 3
10
13
 
11
14
  MATCH_POLICIES = %w[all any].freeze
12
15
 
@@ -94,6 +97,7 @@ module Spree
94
97
  # `catalog_id` is queryable so a picker can offer the lists that are
95
98
  # actually available — unowned, plus the one the catalog already holds.
96
99
  self.whitelisted_ransackable_attributes = %w[status match_policy starts_at ends_at catalog_id]
100
+ search_by :name
97
101
 
98
102
  scope :by_position, -> { ordered }
99
103
  scope :for_store, ->(store) { where(store: store) }
@@ -333,9 +337,9 @@ module Spree
333
337
 
334
338
  return if prices_to_insert.empty?
335
339
 
336
- # Use upsert_all with on_duplicate: :skip to handle race conditions
340
+ # Use upsert_all with on_duplicate: :skip to handle race conditions.
341
+ # Placeholders charge nothing, so the variants are not touched.
337
342
  Spree::Price.upsert_all(prices_to_insert, on_duplicate: :skip)
338
- touch_variants(variant_ids)
339
343
  touch
340
344
  end
341
345
 
@@ -354,10 +358,13 @@ module Spree
354
358
  variant_ids = Spree::Variant.where(product_id: product_ids).distinct.pluck(:id)
355
359
  return if variant_ids.empty?
356
360
 
361
+ removed_prices = prices.where(variant_id: variant_ids)
362
+ priced_variant_ids = removed_prices.where.not(amount: nil).distinct.pluck(:variant_id)
363
+
357
364
  # Use delete_all for hard delete - this bypasses acts_as_paranoid
358
365
  # which is required for the unique index to work when re-adding products
359
- prices.where(variant_id: variant_ids).delete_all
360
- touch_variants(variant_ids)
366
+ removed_prices.delete_all
367
+ touch_variants(priced_variant_ids)
361
368
  touch
362
369
  end
363
370
 
@@ -386,9 +393,8 @@ module Spree
386
393
 
387
394
  current = current_values[price_id]
388
395
 
389
- # Parse amounts using LocalizedNumber for proper decimal handling
390
- amount = attrs[:amount].present? ? Spree::LocalizedNumber.parse(attrs[:amount]) : nil
391
- compare_at_amount = attrs[:compare_at_amount].present? ? Spree::LocalizedNumber.parse(attrs[:compare_at_amount]) : nil
396
+ amount = Spree::Money::Rounding.parse_decimal(attrs[:amount])
397
+ compare_at_amount = Spree::Money::Rounding.parse_decimal(attrs[:compare_at_amount])
392
398
 
393
399
  # Clear compare_at_amount if it equals amount
394
400
  compare_at_amount = nil if compare_at_amount == amount
@@ -9,11 +9,8 @@ module Spree
9
9
  # against raw `channel_id` rows directly. Scope confines the
10
10
  # existence check to the price-list's store so cross-store channel
11
11
  # IDs can't sneak in.
12
- preference :channel_ids, :array, default: [],
13
- parse_on_set: normalize_id_preference(
14
- klass: Spree::Channel,
15
- scope: ->(rule) { rule.store.channels }
16
- )
12
+ preference :channel_ids, :array, of: :id, model: 'Spree::Channel', default: [],
13
+ scope: ->(rule) { rule.store.channels }
17
14
 
18
15
  def channels
19
16
  return [] if preferred_channel_ids.blank?
@@ -10,11 +10,8 @@ module Spree
10
10
  # callers and decodes them on write so eligibility checks compare
11
11
  # against raw `customer_group_id` rows directly. Scope confines
12
12
  # the existence check to the price-list's store.
13
- preference :customer_group_ids, :array, default: [],
14
- parse_on_set: normalize_id_preference(
15
- klass: Spree::CustomerGroup,
16
- scope: ->(rule) { rule.store.customer_groups }
17
- )
13
+ preference :customer_group_ids, :array, of: :id, model: 'Spree::CustomerGroup', default: [],
14
+ scope: ->(rule) { rule.store.customer_groups }
18
15
 
19
16
  def customer_groups
20
17
  return [] if preferred_customer_group_ids.blank?
@@ -6,11 +6,8 @@ module Spree
6
6
  # against raw `market_id` rows directly. Scope confines the
7
7
  # existence check to the price-list's store so cross-store market
8
8
  # IDs can't sneak in.
9
- preference :market_ids, :array, default: [],
10
- parse_on_set: normalize_id_preference(
11
- klass: Spree::Market,
12
- scope: ->(rule) { rule.store.markets }
13
- )
9
+ preference :market_ids, :array, of: :id, model: 'Spree::Market', default: [],
10
+ scope: ->(rule) { rule.store.markets }
14
11
 
15
12
  def markets
16
13
  return [] if preferred_market_ids.blank?
@@ -10,9 +10,7 @@ module Spree
10
10
  # e.g. `usr_…`) from API callers and decodes them on write. Resolves
11
11
  # `Spree.customer_class` lazily — the user class is configured at boot,
12
12
  # and class-body evaluation runs before that on cold loads.
13
- preference :user_ids, :array, default: [], parse_on_set: ->(values) {
14
- normalize_id_preference(klass: Spree.customer_class).call(values)
15
- }
13
+ preference :user_ids, :array, of: :id, model: -> { Spree.customer_class }, default: []
16
14
 
17
15
  def users
18
16
  return [] if preferred_user_ids.blank?
@@ -43,8 +43,7 @@ module Spree
43
43
  "Spree::Product##{legacy_attr}= is deprecated; set #{publication_attr} on " \
44
44
  "ProductPublication instead (writes to every channel's publication). "
45
45
  )
46
- super(value)
47
- product_publications.each { |publication| publication.public_send("#{publication_attr}=", value) }
46
+ write_publication_date(legacy_attr, value)
48
47
  end
49
48
 
50
49
  # Reading +available_on+/+discontinue_on+ prefers the current-channel
@@ -92,6 +91,14 @@ module Spree
92
91
 
93
92
  private
94
93
 
94
+ # Writes a legacy date column together with the matching date on every
95
+ # channel's publication, so the two never disagree.
96
+ def write_publication_date(legacy_attr, value)
97
+ write_attribute(legacy_attr, value)
98
+ publication_attr = DEPRECATED_DATE_TO_PUBLICATION_FIELD.fetch(legacy_attr.to_sym)
99
+ product_publications.each { |publication| publication.public_send("#{publication_attr}=", value) }
100
+ end
101
+
95
102
  def assign_default_store
96
103
  self.store ||= Spree::Current.store || Spree::Store.default
97
104
  end
@@ -40,8 +40,7 @@ module Spree
40
40
  publishes_lifecycle_events
41
41
  publishes_events :activated, :approved, :archived, :back_in_stock, :drafted, :out_of_stock, :proposed, :rejected
42
42
 
43
- MEMOIZED_METHODS = %w[total_on_hand category_and_ancestors
44
- default_variant_id tax_category default_variant variant_for_images
43
+ MEMOIZED_METHODS = %w[total_on_hand default_variant_id tax_category default_variant variant_for_images
45
44
  primary_category buy_box_variants resolved_delivery_profile
46
45
  purchasable? in_stock? backorderable? digital?]
47
46
 
@@ -243,8 +242,6 @@ module Spree
243
242
  after_create :set_default_variant
244
243
 
245
244
  after_save :auto_promote_default_variant
246
- after_save :run_touch_callbacks, if: :saved_changes?
247
- after_touch :touch_categories
248
245
 
249
246
  after_commit :auto_match_collections, if: :eligible_for_collection_matching?
250
247
 
@@ -364,7 +361,21 @@ module Spree
364
361
  super(manual_ids | collections.automatic.ids)
365
362
  end
366
363
 
367
- self.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status available_on created_at updated_at seller_id]
364
+ self.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status available_on created_at updated_at seller_id price]
365
+
366
+ # The default variant's base price in the current currency — what the
367
+ # product lists show — so a list can filter on it like a column.
368
+ ransacker :price, type: :decimal do |parent|
369
+ prices = Spree::Price.arel_table
370
+ Arel::Nodes::Grouping.new(
371
+ prices.project(prices[:amount]).
372
+ where(prices[:variant_id].eq(parent.table[:default_variant_id])).
373
+ where(prices[:currency].eq(Spree::Current.currency)).
374
+ where(prices[:price_list_id].eq(nil)).
375
+ where(prices[:deleted_at].eq(nil)).
376
+ take(1)
377
+ )
378
+ end
368
379
  self.whitelisted_ransackable_associations = %w[categories collections store channels variants default_variant tags labels
369
380
  product_type product_categories option_types seller]
370
381
  # The storefront filters by tag, category and collection; every other hop
@@ -376,6 +387,12 @@ module Spree
376
387
  search in_stock out_of_stock with_option_value_ids
377
388
 
378
389
  ascend_by_price descend_by_price]
390
+ self.ransackable_scope_types = {
391
+ 'in_taxon' => 'id', 'in_category' => 'id', 'in_collection' => 'id',
392
+ 'in_categories' => { list: 'id' }, 'with_option_value_ids' => { list: 'id' },
393
+ 'price_between' => %w[decimal decimal], 'price_lte' => 'decimal', 'price_gte' => 'decimal',
394
+ 'ascend_by_price' => 'boolean', 'descend_by_price' => 'boolean'
395
+ }
379
396
 
380
397
  # All product-level convenience attributes delegate to the default variant —
381
398
  # one target, no branching. Reads resolve the existing default variant
@@ -696,7 +713,7 @@ module Spree
696
713
  end
697
714
 
698
715
  def discontinue!
699
- self.discontinue_on = Time.current
716
+ write_publication_date(:discontinue_on, Time.current)
700
717
  self.status = 'archived'
701
718
  save(validate: false)
702
719
  end
@@ -996,23 +1013,6 @@ module Spree
996
1013
  errors.add(:seller, :invalid)
997
1014
  end
998
1015
 
999
- def run_touch_callbacks
1000
- run_callbacks(:touch)
1001
- end
1002
-
1003
- def category_and_ancestors
1004
- @category_and_ancestors ||= categories.map(&:self_and_ancestors).flatten.uniq
1005
- end
1006
-
1007
- # Iterate through this product's categories and touch their timestamps in a batch
1008
- def touch_categories
1009
- if categories.any?
1010
- Spree::Products::TouchCategoriesJob.
1011
- set(wait: 5.seconds).
1012
- perform_later(category_and_ancestors.map(&:id))
1013
- end
1014
- end
1015
-
1016
1016
  def ensure_not_in_complete_orders
1017
1017
  if orders.complete.any?
1018
1018
  errors.add(:base, :cannot_destroy_if_attached_to_line_items)
@@ -37,6 +37,7 @@ module Spree
37
37
  validates :store, presence: true
38
38
 
39
39
  self.whitelisted_ransackable_attributes = %w[name]
40
+ search_by :name
40
41
  self.whitelisted_ransackable_associations = %w[option_types]
41
42
 
42
43
  # Replace-set writer for the flat API payload. Each entry is
@@ -42,7 +42,7 @@ module Spree
42
42
  # @param line_item [Spree::LineItem]
43
43
  # @return [BigDecimal] never positive
44
44
  def compute_amount(line_item)
45
- line_item.price * gifted_quantity_of(line_item) * -1
45
+ -Spree::Money::Rounding.to_currency(line_item.price * gifted_quantity_of(line_item), line_item.currency)
46
46
  end
47
47
 
48
48
  # Tops the order up to the promised quantity and discounts it.
@@ -35,7 +35,7 @@ module Spree
35
35
  # Preferences
36
36
  #
37
37
  MATCH_POLICIES = %w(any all)
38
- preference :match_policy, :string, default: MATCH_POLICIES.first
38
+ preference :match_policy, :string, default: MATCH_POLICIES.first, choices: MATCH_POLICIES
39
39
 
40
40
  #
41
41
  # Attributes
@@ -8,11 +8,8 @@ module Spree
8
8
  # against the order's raw `channel_id` directly. Scope confines the
9
9
  # existence check to the promotion's store so cross-store channel
10
10
  # IDs can't sneak in.
11
- preference :channel_ids, :array, default: [],
12
- parse_on_set: normalize_id_preference(
13
- klass: Spree::Channel,
14
- scope: ->(rule) { rule.promotion.store.channels }
15
- )
11
+ preference :channel_ids, :array, of: :id, model: 'Spree::Channel', default: [],
12
+ scope: ->(rule) { rule.promotion.store.channels }
16
13
 
17
14
  def channels
18
15
  return Spree::Channel.none if preferred_channel_ids.blank?
@@ -9,11 +9,10 @@ module Spree
9
9
  class Promotion
10
10
  module Rules
11
11
  class Country < Spree::PromotionRule
12
- preference :country_codes, :array, default: [], parse_on_set: lambda { |values|
13
- normalize_id_preference.call(values).map(&:upcase)
14
- }
12
+ preference :country_codes, :array, of: :string, format: :iso_country, default: [],
13
+ parse_on_set: ->(values) { Array(values).flat_map { |value| value.to_s.split(',') }.map { |code| code.strip.upcase }.compact_blank }
15
14
  preference :country_id, :integer # legacy single-country shortcut
16
- preference :country_code, :string # legacy ISO-based shortcut
15
+ preference :country_code, :string, format: :iso_country # legacy ISO-based shortcut
17
16
 
18
17
  def countries
19
18
  isos = configured_country_codes.presence || [configured_country_code].compact_blank
@@ -3,7 +3,7 @@ module Spree
3
3
  class Promotion
4
4
  module Rules
5
5
  class Currency < Spree::PromotionRule
6
- preference :currency, :string
6
+ preference :currency, :string, format: :currency
7
7
 
8
8
  def eligible?(order, options = {})
9
9
  return true if order.currency == preferred_currency
@@ -9,11 +9,8 @@ module Spree
9
9
  # Scoped to the promotion's own store: groups are store-owned, so an
10
10
  # id from elsewhere must raise rather than link a promotion to an
11
11
  # audience its store cannot see.
12
- preference :customer_group_ids, :array, default: [],
13
- parse_on_set: normalize_id_preference(
14
- klass: Spree::CustomerGroup,
15
- scope: ->(rule) { rule.promotion.store.customer_groups }
16
- )
12
+ preference :customer_group_ids, :array, of: :id, model: 'Spree::CustomerGroup', default: [],
13
+ scope: ->(rule) { rule.promotion.store.customer_groups }
17
14
 
18
15
  def customer_groups
19
16
  return Spree::CustomerGroup.none if preferred_customer_group_ids.blank?
@@ -4,9 +4,9 @@ module Spree
4
4
  class Promotion
5
5
  module Rules
6
6
  class ItemTotal < Spree::PromotionRule
7
- preference :amount_min, :decimal, default: 100.00
7
+ preference :amount_min, :money, default: 100.00
8
8
  preference :operator_min, :string, default: '>'
9
- preference :amount_max, :decimal, default: nil, nullable: true
9
+ preference :amount_max, :money, default: nil, nullable: true
10
10
  preference :operator_max, :string, default: '<'
11
11
 
12
12
  OPERATORS_MIN = ['gt', 'gte']
@@ -8,11 +8,8 @@ module Spree
8
8
  # against the order's raw `market_id` directly. Scope confines the
9
9
  # existence check to the promotion's store so cross-store market
10
10
  # IDs can't sneak in.
11
- preference :market_ids, :array, default: [],
12
- parse_on_set: normalize_id_preference(
13
- klass: Spree::Market,
14
- scope: ->(rule) { rule.promotion.store.markets }
15
- )
11
+ preference :market_ids, :array, of: :id, model: 'Spree::Market', default: [],
12
+ scope: ->(rule) { rule.promotion.store.markets }
16
13
 
17
14
  def markets
18
15
  return Spree::Market.none if preferred_market_ids.blank?
@@ -3,13 +3,12 @@ module Spree
3
3
  module Rules
4
4
  class OptionValue < Spree::PromotionRule
5
5
  MATCH_POLICIES = %w(any)
6
- preference :match_policy, :string, default: MATCH_POLICIES.first
6
+ preference :match_policy, :string, default: MATCH_POLICIES.first, choices: MATCH_POLICIES
7
7
  # Stored as raw Spree::OptionValue ids. Accepts prefixed IDs (`optval_…`)
8
8
  # from API callers and decodes them on write so eligibility checks compare
9
9
  # against stable option-value rows, not join-table ids that shift when
10
10
  # variants are reconfigured.
11
- preference :eligible_values, :array, default: [],
12
- parse_on_set: normalize_id_preference(klass: Spree::OptionValue)
11
+ preference :eligible_values, :array, of: :id, model: 'Spree::OptionValue', default: []
13
12
 
14
13
  # @param promotable [Object]
15
14
  # @return [Boolean]
@@ -28,7 +28,7 @@ module Spree
28
28
  # Preferences
29
29
  #
30
30
  MATCH_POLICIES = %w(any all none)
31
- preference :match_policy, :string, default: MATCH_POLICIES.first
31
+ preference :match_policy, :string, default: MATCH_POLICIES.first, choices: MATCH_POLICIES
32
32
 
33
33
  #
34
34
  # Attributes
@@ -87,6 +87,7 @@ module Spree
87
87
  # `name` is whitelisted so the admin global search / command palette can
88
88
  # filter via the `name_or_code_cont` predicate without a dedicated scope.
89
89
  self.whitelisted_ransackable_attributes = ['name', 'path', 'promotion_category_id', 'code', 'kind', 'starts_at', 'expires_at']
90
+ search_by :name, :code
90
91
  self.whitelisted_ransackable_associations = %w[coupon_codes]
91
92
 
92
93
  def self.with_coupon_code(coupon_code)
@@ -58,7 +58,9 @@ module Spree
58
58
  self.whitelisted_ransackable_attributes = %w[number status currency reference expected_at
59
59
  cancel_by ordered_at received_at closed_short_at
60
60
  supplier_id destination_location_id created_at]
61
+ search_by :number, :reference
61
62
  self.whitelisted_ransackable_scopes = %w[open closed overdue past_cancel_by]
63
+ self.ransackable_scope_types = %w[open closed overdue past_cancel_by].index_with('boolean')
62
64
  self.whitelisted_ransackable_associations = %w[supplier destination_location items]
63
65
 
64
66
  # What the supplier will invoice for everything ordered.
@@ -5,6 +5,9 @@ module Spree
5
5
  include Spree::ActedBy
6
6
  include Spree::HasCustomFields
7
7
  include Spree::Metadata
8
+ include Spree::CanonicalDecimals
9
+
10
+ canonical_decimals :amount
8
11
  include Spree::InstrumentsGatewayCalls
9
12
  if defined?(Spree::Security::Refunds)
10
13
  include Spree::Security::Refunds
@@ -56,10 +59,6 @@ module Spree
56
59
  # back through a payment that may belong to several orders.
57
60
  before_validation :assign_order_from_payment, on: :create
58
61
 
59
- def amount=(amount)
60
- self[:amount] = Spree::LocalizedNumber.parse(amount)
61
- end
62
-
63
62
  def money
64
63
  Spree::Money.new(amount, currency: currency)
65
64
  end
@@ -121,9 +120,7 @@ module Spree
121
120
  def perform!
122
121
  return true if transaction_id.present?
123
122
 
124
- credit_cents = Spree::Money.new(amount.to_f, currency: currency).amount_in_cents
125
-
126
- @response = process!(credit_cents)
123
+ @response = process!(payment.payment_method.gateway_amount(amount, currency))
127
124
 
128
125
  self.transaction_id = @response.authorization
129
126
  update_columns(transaction_id: transaction_id)
@@ -166,19 +163,20 @@ module Spree
166
163
  end
167
164
 
168
165
  # return a payment response object if successful or else raise an error
169
- def process!(credit_cents)
170
- refund_total_in_cents = calculate_refund_amount(credit_cents)
166
+ # @param credit_amount [Spree::Money, Integer] as PaymentMethod#gateway_amount returns it
167
+ def process!(credit_amount)
168
+ refund_total = calculate_refund_amount(credit_amount)
171
169
 
172
170
  response = instrument_gateway_call(:credit, payment.payment_method) do
173
171
  if payment.payment_method.payment_profiles_supported?
174
- payment.payment_method.credit(refund_total_in_cents, payment.source, payment.transaction_id, originator: self)
172
+ payment.payment_method.credit(refund_total, payment.source, payment.transaction_id, originator: self)
175
173
  else
176
- payment.payment_method.credit(refund_total_in_cents, payment.transaction_id, originator: self)
174
+ payment.payment_method.credit(refund_total, payment.transaction_id, originator: self)
177
175
  end
178
176
  end
179
177
 
180
178
  if response.success?
181
- track_order_as_refunded(refund_total_in_cents)
179
+ track_order_as_refunded(refund_total)
182
180
  else
183
181
  Rails.logger.error(I18n.t('spree.gateway_error') + " #{response.to_yaml}")
184
182
  text = response.params['message'] || response.params['response_reason_text'] || response.message
@@ -191,12 +189,12 @@ module Spree
191
189
  raise Core::GatewayError, I18n.t('spree.unable_to_connect_to_gateway')
192
190
  end
193
191
 
194
- def calculate_refund_amount(credit_cents)
192
+ def calculate_refund_amount(credit_amount)
195
193
  # Overwrite this for more complex calculations
196
- credit_cents
194
+ credit_amount
197
195
  end
198
196
 
199
- def track_order_as_refunded(credit_cents)
197
+ def track_order_as_refunded(credit_amount)
200
198
  # You can track refunds here
201
199
  end
202
200
 
@@ -52,6 +52,7 @@ module Spree
52
52
  delegate :currency, to: :order
53
53
 
54
54
  self.whitelisted_ransackable_attributes = %w[number status created_at]
55
+ search_by :number
55
56
  self.whitelisted_ransackable_associations = %w[order reason]
56
57
 
57
58
  # The label that currently binds the inbound parcel — bought or uploaded,
@@ -23,6 +23,7 @@ module Spree
23
23
  scope :seeded, -> { where(seeded: true) }
24
24
 
25
25
  self.whitelisted_ransackable_attributes = %w[name seeded created_at updated_at]
26
+ search_by :name
26
27
 
27
28
  def event_serializer_class
28
29
  'Spree::Api::V3::SavedReportEventSerializer'.safe_constantize
@@ -1,6 +1,10 @@
1
1
  module Spree
2
2
  module SearchProvider
3
3
  class Base
4
+ # Sort keys a provider accepts beyond the product's sortable
5
+ # attributes. `price` also sorts descending as `-price`.
6
+ SORT_FIELDS = %w[price best_selling manual].freeze
7
+
4
8
  attr_reader :store
5
9
 
6
10
  # Whether this provider requires background indexing jobs.
@@ -41,6 +41,12 @@ module Spree
41
41
  has_status :pending, :invited, :canceled, :onboarding, :ready_for_review,
42
42
  :approved, :rejected, :suspended, default: :pending
43
43
 
44
+ # The storefront lists approved sellers only, and a seller's contact
45
+ # address is back-office data.
46
+ self.whitelisted_ransackable_attributes = %w[status contact_email]
47
+ search_by :name
48
+ self.private_ransackable_attributes = { store: %w[status contact_email], seller: %w[status contact_email] }
49
+
44
50
  # Who remits consumer tax on this seller's sales. `seller` means they are
45
51
  # merchant of record; `platform` is the marketplace-facilitator case, and
46
52
  # only Enterprise decides a seller belongs there — core just reads it.
@@ -196,6 +202,7 @@ module Spree
196
202
  has_one_attached :logo, service: Spree.public_storage_service_name
197
203
  has_one_attached :square_logo, service: Spree.public_storage_service_name
198
204
  has_one_attached :cover_photo, service: Spree.public_storage_service_name
205
+ signed_id_attachments :logo, :square_logo, :cover_photo
199
206
 
200
207
  #
201
208
  # Validations
@@ -340,7 +347,7 @@ module Spree
340
347
  # A seller keeps no clock of their own: a date means whatever the
341
348
  # marketplace says it means, so their panel reads timestamps in the
342
349
  # store's zone rather than in whichever one their browser sits in.
343
- delegate :preferred_timezone, to: :store, allow_nil: true
350
+ delegate :preferred_timezone, :timezone, to: :store, allow_nil: true
344
351
 
345
352
  # Sellers holding an account with one provider, for the reverse lookup a
346
353
  # provider webhook does — it knows the account, not the seller.
@@ -60,6 +60,7 @@ module Spree
60
60
  scope :owed, -> { where(status: %w[pending processing]) }
61
61
 
62
62
  self.whitelisted_ransackable_attributes = %w[amount currency status provider reference period_start period_end created_at seller_id]
63
+ search_by :reference
63
64
  # Filtered by the provider's API shorthand (`system`); the column holds
64
65
  # its class name.
65
66
  ransacker :provider, formatter: ->(value) { Spree::ApiTyped.class_name_for(Spree.payout_providers, value) } do |parent|
@@ -21,6 +21,7 @@ module Spree
21
21
  self.type_labels_scope = 'spree.seller_requirement_types'
22
22
 
23
23
  has_prefix_id :selreq
24
+ search_by :name
24
25
 
25
26
  acts_as_list scope: :store
26
27
 
@@ -38,6 +38,7 @@ module Spree
38
38
  # Private, and served only through the admin and seller branches: these
39
39
  # are business registrations and identity documents.
40
40
  has_one_attached :file, service: Spree.private_storage_service_name
41
+ signed_id_attachments :file
41
42
 
42
43
  #
43
44
  # Validations
@@ -112,6 +112,7 @@ module Spree
112
112
  end
113
113
 
114
114
  self.whitelisted_ransackable_attributes = %w[amount currency kind status provider reference created_at seller_id order_id payout_id]
115
+ search_by :reference
115
116
  # Filtered by the provider's API shorthand (`system`); the column holds
116
117
  # its class name.
117
118
  ransacker :provider, formatter: ->(value) { Spree::ApiTyped.class_name_for(Spree.payout_providers, value) } do |parent|
@@ -39,6 +39,7 @@ module Spree
39
39
  # merchant's account details, and is served only through an
40
40
  # authenticated, streamed download.
41
41
  has_one_attached :file, service: Spree.private_storage_service_name
42
+ signed_id_attachments :file
42
43
 
43
44
  validates :source, inclusion: { in: SOURCES }
44
45
  validates :owner_type, inclusion: { in: OWNER_TYPES }
@@ -121,7 +121,7 @@ module Spree
121
121
 
122
122
  percent = service_row&.markup_percent || delivery_method.markup_percent || 0
123
123
  flat = service_row&.markup_flat || delivery_method.markup_flat || 0
124
- (cost * (1 + (percent / 100)) + flat).round(2)
124
+ Spree::Money::Rounding.to_currency(cost * (1 + (percent / 100)) + flat, currency)
125
125
  end
126
126
 
127
127
  # Display name for provider-priced rates: the merchant's label override,
@@ -38,7 +38,9 @@ module Spree
38
38
 
39
39
  self.whitelisted_ransackable_attributes = %w[count_on_hand allocated_count reserved_count incoming_count
40
40
  stock_location_id variant_id]
41
+ search_by :variant_sku, :variant_product_name
41
42
  self.whitelisted_ransackable_scopes = %w[with_stock_status]
43
+ self.ransackable_scope_types = { 'with_stock_status' => { list: 'text' } }
42
44
  self.whitelisted_ransackable_associations = %w[variant stock_location]
43
45
 
44
46
  scope :with_active_stock_location, -> { joins(:stock_location).merge(Spree::StockLocation.active) }
@@ -50,6 +50,7 @@ module Spree
50
50
  country_code state_code created_at updated_at
51
51
  seller_id
52
52
  ]
53
+ search_by :name
53
54
  self.whitelisted_ransackable_associations = %w[seller]
54
55
 
55
56
  scope :active, -> { where(active: true) }
@@ -66,7 +66,9 @@ module Spree
66
66
  self.whitelisted_ransackable_attributes = %w[number status reference source_location_id
67
67
  destination_location_id shipped_at received_at
68
68
  closed_short_at created_at]
69
+ search_by :number, :reference
69
70
  self.whitelisted_ransackable_scopes = %w[open closed]
71
+ self.ransackable_scope_types = %w[open closed].index_with('boolean')
70
72
  self.whitelisted_ransackable_associations = %w[source_location destination_location items]
71
73
 
72
74
  # Whether the merchant may still edit the lines. Once the box is sealed and