spree_core 5.4.2 → 5.5.0.rc1

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 (248) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/base_helper.rb +0 -82
  3. data/app/helpers/spree/currency_helper.rb +0 -12
  4. data/app/helpers/spree/products_helper.rb +0 -8
  5. data/app/jobs/spree/base_job.rb +18 -0
  6. data/app/jobs/spree/events/subscriber_job.rb +3 -2
  7. data/app/jobs/spree/exports/generate_job.rb +11 -0
  8. data/app/jobs/spree/images/save_from_url_job.rb +23 -8
  9. data/app/jobs/spree/imports/assign_tags_job.rb +11 -0
  10. data/app/jobs/spree/imports/base_job.rb +15 -0
  11. data/app/jobs/spree/imports/create_categories_job.rb +37 -0
  12. data/app/jobs/spree/imports/create_rows_job.rb +1 -3
  13. data/app/jobs/spree/imports/process_group_job.rb +8 -6
  14. data/app/jobs/spree/imports/process_rows_job.rb +1 -3
  15. data/app/jobs/spree/media/migrate_product_assets_job.rb +83 -0
  16. data/app/jobs/spree/products/refresh_metrics_job.rb +15 -4
  17. data/app/jobs/spree/reports/generate_job.rb +11 -0
  18. data/app/jobs/spree/search_provider/index_job.rb +5 -1
  19. data/app/jobs/spree/search_provider/remove_job.rb +4 -0
  20. data/app/jobs/spree/stock_reservations/expire_job.rb +11 -0
  21. data/app/models/concerns/spree/calculated_adjustments.rb +34 -1
  22. data/app/models/concerns/spree/display_on.rb +31 -0
  23. data/app/models/concerns/spree/metafields.rb +167 -5
  24. data/app/models/concerns/spree/preference_schema.rb +191 -0
  25. data/app/models/concerns/spree/prefixed_id.rb +94 -11
  26. data/app/models/concerns/spree/product_scopes.rb +36 -17
  27. data/app/models/concerns/spree/publishable.rb +1 -1
  28. data/app/models/concerns/spree/ransackable_attributes.rb +5 -1
  29. data/app/models/concerns/spree/search_indexable.rb +8 -7
  30. data/app/models/concerns/spree/searchable.rb +11 -2
  31. data/app/models/concerns/spree/stores/channels.rb +20 -0
  32. data/app/models/concerns/spree/stores/markets.rb +21 -5
  33. data/app/models/concerns/spree/typed_associations.rb +120 -0
  34. data/app/models/concerns/spree/user_methods.rb +71 -12
  35. data/app/models/spree/ability.rb +4 -117
  36. data/app/models/spree/api_key.rb +53 -0
  37. data/app/models/spree/asset.rb +37 -14
  38. data/app/models/spree/authentication/strategy_registry.rb +72 -0
  39. data/app/models/spree/base.rb +18 -1
  40. data/app/models/spree/channel.rb +159 -0
  41. data/app/models/spree/country.rb +2 -0
  42. data/app/models/spree/current.rb +5 -1
  43. data/app/models/spree/custom_field.rb +9 -0
  44. data/app/models/spree/custom_field_definition.rb +7 -0
  45. data/app/models/spree/customer_group.rb +8 -2
  46. data/app/models/spree/event.rb +6 -6
  47. data/app/models/spree/export.rb +32 -5
  48. data/app/models/spree/exports/product_translations.rb +1 -1
  49. data/app/models/spree/gateway/bogus.rb +6 -1
  50. data/app/models/spree/gateway.rb +25 -0
  51. data/app/models/spree/gift_card.rb +1 -1
  52. data/app/models/spree/gift_card_batch.rb +4 -1
  53. data/app/models/spree/import.rb +5 -0
  54. data/app/models/spree/import_row.rb +12 -0
  55. data/app/models/spree/line_item.rb +7 -2
  56. data/app/models/spree/market.rb +57 -1
  57. data/app/models/spree/metafield.rb +38 -0
  58. data/app/models/spree/metafield_definition.rb +29 -6
  59. data/app/models/spree/metafields/json.rb +10 -0
  60. data/app/models/spree/newsletter_subscriber.rb +19 -3
  61. data/app/models/spree/option_type.rb +48 -7
  62. data/app/models/spree/order/checkout.rb +3 -3
  63. data/app/models/spree/order.rb +102 -6
  64. data/app/models/spree/order_approval.rb +19 -0
  65. data/app/models/spree/order_cancellation.rb +19 -0
  66. data/app/models/spree/order_inventory.rb +24 -2
  67. data/app/models/spree/order_routing/has_strategy_preference.rb +28 -0
  68. data/app/models/spree/order_routing/rules/default_location.rb +16 -0
  69. data/app/models/spree/order_routing/rules/minimize_splits.rb +45 -0
  70. data/app/models/spree/order_routing/rules/preferred_location.rb +22 -0
  71. data/app/models/spree/order_routing/strategy/base.rb +47 -0
  72. data/app/models/spree/order_routing/strategy/legacy.rb +33 -0
  73. data/app/models/spree/order_routing/strategy/reducer.rb +68 -0
  74. data/app/models/spree/order_routing/strategy/rules.rb +81 -0
  75. data/app/models/spree/order_routing_rule.rb +75 -0
  76. data/app/models/spree/payment_setup_sessions/bogus.rb +4 -0
  77. data/app/models/spree/permission_sets/configuration_management.rb +16 -0
  78. data/app/models/spree/permission_sets/product_display.rb +2 -0
  79. data/app/models/spree/permission_sets/product_management.rb +2 -0
  80. data/app/models/spree/price.rb +14 -1
  81. data/app/models/spree/price_list.rb +129 -17
  82. data/app/models/spree/price_rule.rb +11 -1
  83. data/app/models/spree/price_rules/customer_group_rule.rb +15 -1
  84. data/app/models/spree/price_rules/market_rule.rb +16 -1
  85. data/app/models/spree/price_rules/user_rule.rb +21 -2
  86. data/app/models/spree/product/channels.rb +149 -0
  87. data/app/models/spree/product/legacy_multi_store_support.rb +40 -0
  88. data/app/models/spree/product/slugs.rb +1 -1
  89. data/app/models/spree/product.rb +172 -31
  90. data/app/models/spree/product_publication.rb +43 -0
  91. data/app/models/spree/promotion/actions/create_adjustment.rb +4 -0
  92. data/app/models/spree/promotion/actions/create_item_adjustments.rb +4 -0
  93. data/app/models/spree/promotion/actions/create_line_items.rb +32 -14
  94. data/app/models/spree/promotion/rules/country.rb +40 -18
  95. data/app/models/spree/promotion/rules/customer_group.rb +10 -1
  96. data/app/models/spree/promotion/rules/product.rb +4 -0
  97. data/app/models/spree/promotion/rules/taxon.rb +24 -1
  98. data/app/models/spree/promotion/rules/user.rb +21 -0
  99. data/app/models/spree/promotion/rules/user_logged_in.rb +6 -0
  100. data/app/models/spree/promotion.rb +22 -1
  101. data/app/models/spree/promotion_action.rb +17 -11
  102. data/app/models/spree/promotion_rule.rb +17 -18
  103. data/app/models/spree/search_provider/meilisearch.rb +12 -2
  104. data/app/models/spree/shipment.rb +10 -4
  105. data/app/models/spree/stock/availability_validator.rb +1 -1
  106. data/app/models/spree/stock/quantifier.rb +89 -9
  107. data/app/models/spree/stock_item.rb +36 -0
  108. data/app/models/spree/stock_location.rb +52 -0
  109. data/app/models/spree/stock_reservation.rb +38 -0
  110. data/app/models/spree/stock_reservations/insufficient_stock_error.rb +12 -0
  111. data/app/models/spree/store.rb +18 -72
  112. data/app/models/spree/store_credit.rb +0 -8
  113. data/app/models/spree/store_product.rb +11 -23
  114. data/app/models/spree/subscriber.rb +12 -12
  115. data/app/models/spree/taxon.rb +0 -5
  116. data/app/models/spree/user_identity.rb +1 -2
  117. data/app/models/spree/variant.rb +132 -18
  118. data/app/models/spree/variant_media.rb +46 -0
  119. data/app/models/spree/webhook_delivery.rb +1 -1
  120. data/app/models/spree/webhook_endpoint.rb +24 -0
  121. data/app/models/spree/wished_item.rb +0 -13
  122. data/app/presenters/spree/csv/formula_sanitizer.rb +28 -0
  123. data/app/presenters/spree/csv/product_variant_presenter.rb +23 -3
  124. data/app/presenters/spree/search_provider/product_presenter.rb +11 -4
  125. data/app/presenters/spree/variant_presenter.rb +4 -3
  126. data/app/services/spree/addresses/update.rb +6 -8
  127. data/app/services/spree/cart/add_item.rb +10 -0
  128. data/app/services/spree/cart/empty.rb +2 -0
  129. data/app/services/spree/cart/remove_line_item.rb +10 -0
  130. data/app/services/spree/cart/remove_out_of_stock_items.rb +1 -1
  131. data/app/services/spree/cart/set_quantity.rb +10 -0
  132. data/app/services/spree/carts/complete.rb +1 -0
  133. data/app/services/spree/carts/create.rb +1 -0
  134. data/app/services/spree/carts/update.rb +18 -2
  135. data/app/services/spree/carts/upsert_items.rb +6 -6
  136. data/app/services/spree/credit_cards/destroy.rb +1 -1
  137. data/app/services/spree/imports/row_processors/customer.rb +4 -1
  138. data/app/services/spree/imports/row_processors/product_variant.rb +95 -57
  139. data/app/services/spree/newsletter/link_user.rb +53 -0
  140. data/app/services/spree/newsletter/subscribe.rb +31 -9
  141. data/app/services/spree/orders/approve.rb +27 -6
  142. data/app/services/spree/orders/build_shipments.rb +29 -0
  143. data/app/services/spree/orders/cancel.rb +34 -3
  144. data/app/services/spree/orders/complete.rb +53 -0
  145. data/app/services/spree/orders/create.rb +156 -0
  146. data/app/services/spree/orders/update.rb +51 -0
  147. data/app/services/spree/orders/upsert_items.rb +70 -0
  148. data/app/services/spree/payments/handle_webhook.rb +3 -10
  149. data/app/services/spree/prices/bulk_upsert.rb +201 -0
  150. data/app/services/spree/products/duplicator.rb +1 -1
  151. data/app/services/spree/products/prepare_nested_attributes.rb +2 -30
  152. data/app/services/spree/sample_data/loader.rb +30 -0
  153. data/app/services/spree/stock_reservations/extend.rb +19 -0
  154. data/app/services/spree/stock_reservations/release.rb +12 -0
  155. data/app/services/spree/stock_reservations/reserve.rb +103 -0
  156. data/app/services/spree/taxons/remove_products.rb +7 -1
  157. data/app/subscribers/spree/event_log_subscriber.rb +1 -1
  158. data/app/subscribers/spree/product_metrics_subscriber.rb +3 -7
  159. data/app/views/spree/invitation_mailer/invitation_email.html.erb +4 -0
  160. data/config/locales/en.yml +35 -10
  161. data/config/routes.rb +9 -0
  162. data/db/migrate/20260429000001_create_spree_order_cancellations.rb +25 -0
  163. data/db/migrate/20260429000002_create_spree_order_approvals.rb +22 -0
  164. data/db/migrate/20260429000003_add_status_to_spree_orders.rb +6 -0
  165. data/db/migrate/20260429000004_add_scopes_to_spree_api_keys.rb +11 -0
  166. data/db/migrate/20260501000001_create_spree_stock_reservations.rb +19 -0
  167. data/db/migrate/20260504103113_add_type_to_spree_payment_setup_sessions.rb +6 -0
  168. data/db/migrate/20260507162651_create_spree_variant_media.rb +23 -0
  169. data/db/migrate/20260508175303_add_pickup_to_spree_stock_locations.rb +12 -0
  170. data/db/migrate/20260508204040_create_spree_channels.rb +18 -0
  171. data/db/migrate/20260508204041_create_spree_order_routing_rules.rb +18 -0
  172. data/db/migrate/20260508204042_add_preferred_stock_location_to_spree_orders.rb +5 -0
  173. data/db/migrate/20260508204043_add_channel_id_to_spree_orders.rb +10 -0
  174. data/db/migrate/20260511000001_backfill_status_on_spree_orders.rb +57 -0
  175. data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +25 -0
  176. data/db/migrate/20260529000001_add_unique_index_to_spree_price_rules.rb +41 -0
  177. data/db/migrate/20260529000002_add_unique_index_to_spree_promotion_rules.rb +37 -0
  178. data/db/migrate/20260601000001_create_spree_product_publications.rb +14 -0
  179. data/db/migrate/20260601000002_add_store_id_to_spree_products.rb +16 -0
  180. data/db/migrate/20260602000001_add_default_to_spree_channels.rb +14 -0
  181. data/db/sample_data/channels.rb +12 -0
  182. data/db/sample_data/orders.rb +1 -1
  183. data/db/sample_data/products.csv +212 -212
  184. data/lib/generators/spree/api_resource/api_resource_generator.rb +353 -0
  185. data/lib/generators/spree/api_resource/templates/admin_controller.rb.tt +23 -0
  186. data/lib/generators/spree/api_resource/templates/admin_controller_spec.rb.tt +59 -0
  187. data/lib/generators/spree/api_resource/templates/admin_serializer.rb.tt +11 -0
  188. data/lib/generators/spree/api_resource/templates/factory.rb.tt +26 -0
  189. data/lib/generators/spree/api_resource/templates/store_aliased_serializer.rb.tt +12 -0
  190. data/lib/generators/spree/api_resource/templates/store_controller.rb.tt +31 -0
  191. data/lib/generators/spree/api_resource/templates/store_controller_spec.rb.tt +61 -0
  192. data/lib/generators/spree/api_resource/templates/store_serializer.rb.tt +14 -0
  193. data/lib/generators/spree/controller_decorator/controller_decorator_generator.rb +66 -0
  194. data/lib/generators/spree/controller_decorator/templates/controller_decorator.rb.tt +25 -0
  195. data/lib/generators/spree/model/model_generator.rb +73 -7
  196. data/lib/generators/spree/model/templates/create_table_migration.rb.tt +40 -0
  197. data/lib/generators/spree/model/templates/model.rb.tt +28 -2
  198. data/lib/spree/core/configuration.rb +7 -0
  199. data/lib/spree/core/controller_helpers/auth.rb +0 -12
  200. data/lib/spree/core/controller_helpers/currency.rb +0 -17
  201. data/lib/spree/core/controller_helpers/order.rb +0 -19
  202. data/lib/spree/core/dependencies.rb +5 -2
  203. data/lib/spree/core/engine.rb +54 -7
  204. data/lib/spree/core/permission_configuration.rb +15 -0
  205. data/lib/spree/core/preferences/masking.rb +47 -0
  206. data/lib/spree/core/preferences/preferable_class_methods.rb +7 -1
  207. data/lib/spree/core/version.rb +1 -1
  208. data/lib/spree/core.rb +56 -5
  209. data/lib/spree/events/adapters/active_support_notifications.rb +1 -1
  210. data/lib/spree/events/adapters/base.rb +3 -3
  211. data/lib/spree/events/registry.rb +1 -1
  212. data/lib/spree/events.rb +1 -1
  213. data/lib/spree/permitted_attributes.rb +9 -7
  214. data/lib/spree/testing_support/factories/address_factory.rb +16 -9
  215. data/lib/spree/testing_support/factories/api_key_factory.rb +1 -0
  216. data/lib/spree/testing_support/factories/channel_factory.rb +8 -0
  217. data/lib/spree/testing_support/factories/line_item_factory.rb +2 -8
  218. data/lib/spree/testing_support/factories/newsletter_subscriber_factory.rb +2 -0
  219. data/lib/spree/testing_support/factories/product_factory.rb +16 -7
  220. data/lib/spree/testing_support/factories/product_publication_factory.rb +6 -0
  221. data/lib/spree/testing_support/factories/refresh_token_factory.rb +15 -0
  222. data/lib/spree/testing_support/factories/stock_location_factory.rb +2 -2
  223. data/lib/spree/testing_support/factories/stock_reservation_factory.rb +31 -0
  224. data/lib/spree/testing_support/factories/variant_factory.rb +3 -3
  225. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  226. data/lib/spree/testing_support/store.rb +10 -0
  227. data/lib/spree/upgrades/5_4_to_5_5/manifest.yml +53 -0
  228. data/lib/tasks/channels.rake +94 -0
  229. data/lib/tasks/core.rake +1 -0
  230. data/lib/tasks/media.rake +27 -0
  231. data/lib/tasks/products.rake +4 -6
  232. data/lib/tasks/publications.rake +60 -0
  233. data/lib/tasks/upgrade.rake +211 -0
  234. metadata +86 -18
  235. data/app/finders/spree/variants/visible_finder.rb +0 -23
  236. data/app/paginators/spree/shared/paginate.rb +0 -30
  237. data/app/presenters/spree/filters/price_presenter.rb +0 -23
  238. data/app/presenters/spree/filters/price_range_presenter.rb +0 -30
  239. data/app/presenters/spree/filters/quantified_price_range_presenter.rb +0 -45
  240. data/app/presenters/spree/product_summary_presenter.rb +0 -27
  241. data/app/presenters/spree/variants/options_presenter.rb +0 -82
  242. data/app/services/spree/classifications/reposition.rb +0 -23
  243. data/app/sorters/spree/orders/sort.rb +0 -10
  244. data/lib/spree/core/controller_helpers/common.rb +0 -14
  245. data/lib/spree/core/token_generator.rb +0 -23
  246. data/lib/spree/database_type_utilities.rb +0 -22
  247. data/lib/spree/testing_support/bar_ability.rb +0 -14
  248. data/lib/spree/testing_support/factories/store_product_factory.rb +0 -6
@@ -15,6 +15,9 @@ module Spree
15
15
  puts 'Loading sample markets...'
16
16
  load_ruby_file('markets')
17
17
 
18
+ puts 'Loading sample channels...'
19
+ load_ruby_file('channels')
20
+
18
21
  puts 'Loading sample metafield definitions...'
19
22
  load_ruby_file('metafield_definitions')
20
23
 
@@ -24,6 +27,12 @@ module Spree
24
27
  puts 'Loading sample products...'
25
28
  load_products
26
29
 
30
+ puts 'Publishing sample products on the default channel...'
31
+ publish_sample_products
32
+
33
+ puts 'Loading sample categories...'
34
+ load_categories
35
+
27
36
  puts 'Loading sample product translations...'
28
37
  load_product_translations
29
38
 
@@ -67,6 +76,27 @@ module Spree
67
76
  Spree::SampleData::ImportRunner.call(csv_path: csv_path, import_class: Spree::Imports::Products)
68
77
  end
69
78
 
79
+ def publish_sample_products
80
+ store = Spree::Store.default
81
+ store.default_channel.add_products(store.product_ids)
82
+ end
83
+
84
+ def load_categories
85
+ store = Spree::Store.default
86
+ csv_path = sample_data_path.join('products.csv')
87
+
88
+ require 'csv'
89
+ ::CSV.foreach(csv_path, headers: true) do |row|
90
+ product = store.products.find_by(slug: row['slug'])
91
+ next unless product
92
+
93
+ categories = [row['category1'], row['category2'], row['category3']].compact_blank
94
+ next if categories.empty?
95
+
96
+ Spree::Imports::CreateCategoriesJob.perform_now(product.id, store.id, categories)
97
+ end
98
+ end
99
+
70
100
  def load_product_translations
71
101
  csv_path = sample_data_path.join('product_translations.csv')
72
102
  return unless csv_path.exist?
@@ -0,0 +1,19 @@
1
+ module Spree
2
+ module StockReservations
3
+ class Extend
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call(order:)
7
+ return success(order) unless Spree::Config[:stock_reservations_enabled]
8
+
9
+ expires_at = Time.current + Spree::StockReservation.ttl_for(order)
10
+
11
+ Spree::StockReservation
12
+ .where(order_id: order.id)
13
+ .update_all(expires_at: expires_at, updated_at: Time.current)
14
+
15
+ success(order)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module Spree
2
+ module StockReservations
3
+ class Release
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call(order:)
7
+ Spree::StockReservation.where(order_id: order.id).delete_all
8
+ success(order)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,103 @@
1
+ module Spree
2
+ module StockReservations
3
+ class Reserve
4
+ prepend Spree::ServiceModule::Base
5
+
6
+ def call(order:)
7
+ return success(order) unless Spree::Config[:stock_reservations_enabled]
8
+
9
+ expires_at = Time.current + Spree::StockReservation.ttl_for(order)
10
+
11
+ ApplicationRecord.transaction do
12
+ targets = build_targets(order)
13
+ break if targets.empty?
14
+
15
+ # Pessimistic lock + fresh read of count_on_hand. The lock serializes
16
+ # concurrent checkouts and we use the locked rows below so we never
17
+ # check stock against a stale association cache.
18
+ locked_stock_items = Spree::StockItem
19
+ .where(id: targets.map { |_, si| si.id })
20
+ .lock
21
+ .index_by(&:id)
22
+
23
+ held = held_by_others(locked_stock_items.keys, order.id)
24
+ existing = existing_reservations_for(targets)
25
+
26
+ this_order_used = Hash.new(0)
27
+
28
+ targets.each do |line_item, stock_item|
29
+ stock_item = locked_stock_items.fetch(stock_item.id)
30
+ available = stock_item.count_on_hand - held.fetch(stock_item.id, 0) - this_order_used[stock_item.id]
31
+
32
+ if available < line_item.quantity
33
+ raise InsufficientStockError.new(
34
+ line_item,
35
+ Spree.t(
36
+ :insufficient_stock_for_reservation,
37
+ default: '%{item} has only %{available} available',
38
+ item: line_item.variant.name,
39
+ available: [available, 0].max
40
+ )
41
+ )
42
+ end
43
+
44
+ this_order_used[stock_item.id] += line_item.quantity
45
+
46
+ reservation = existing[[stock_item.id, line_item.id]] ||
47
+ Spree::StockReservation.new(stock_item: stock_item, line_item: line_item)
48
+ reservation.order = order
49
+ reservation.quantity = line_item.quantity
50
+ reservation.expires_at = expires_at
51
+ reservation.save!
52
+ end
53
+ end
54
+
55
+ success(order)
56
+ rescue InsufficientStockError => e
57
+ failure(e.line_item, e.message)
58
+ end
59
+
60
+ private
61
+
62
+ def build_targets(order)
63
+ order.line_items.includes(variant: { stock_items: :stock_location }).filter_map do |line_item|
64
+ variant = line_item.variant
65
+ next unless variant&.should_track_inventory?
66
+
67
+ stock_item = select_stock_item(variant)
68
+ next if stock_item.nil? || stock_item.backorderable?
69
+
70
+ [line_item, stock_item]
71
+ end
72
+ end
73
+
74
+ def select_stock_item(variant)
75
+ variant.stock_items.detect { |si| si.stock_location&.active? && si.available? }
76
+ end
77
+
78
+ def held_by_others(stock_item_ids, exclude_order_id)
79
+ return {} if stock_item_ids.empty?
80
+
81
+ Spree::StockReservation
82
+ .active
83
+ .where(stock_item_id: stock_item_ids)
84
+ .where.not(order_id: exclude_order_id)
85
+ .group(:stock_item_id)
86
+ .sum(:quantity)
87
+ end
88
+
89
+ # One SELECT for all (stock_item_id, line_item_id) pairs we need to
90
+ # upsert. Returns a hash keyed by [stock_item_id, line_item_id].
91
+ def existing_reservations_for(targets)
92
+ return {} if targets.empty?
93
+
94
+ stock_item_ids = targets.map { |_, si| si.id }
95
+ line_item_ids = targets.map { |li, _| li.id }
96
+
97
+ Spree::StockReservation
98
+ .where(stock_item_id: stock_item_ids, line_item_id: line_item_ids)
99
+ .index_by { |r| [r.stock_item_id, r.line_item_id] }
100
+ end
101
+ end
102
+ end
103
+ end
@@ -36,7 +36,7 @@ module Spree
36
36
 
37
37
  if classifications_params.any?
38
38
  opts = {}
39
- opts[:unique_by] = :index_spree_products_taxons_on_product_id_and_taxon_id unless ActiveRecord::Base.connection.adapter_name == 'Mysql2'
39
+ opts[:unique_by] = :index_spree_products_taxons_on_product_id_and_taxon_id unless mysql_adapter?
40
40
 
41
41
  Spree::Classification.upsert_all(
42
42
  classifications_params,
@@ -58,6 +58,12 @@ module Spree
58
58
 
59
59
  success(true)
60
60
  end
61
+
62
+ private
63
+
64
+ def mysql_adapter?
65
+ ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
66
+ end
61
67
  end
62
68
  end
63
69
  end
@@ -9,7 +9,7 @@ module Spree
9
9
  # Rails.application.config.filter_parameters.
10
10
  #
11
11
  # @example Output
12
- # [Spree Event] order.complete | payload: {"id"=>1} | 0.5ms
12
+ # [Spree Event] order.completed | payload: {"id"=>1} | 0.5ms
13
13
  #
14
14
  class EventLogSubscriber
15
15
  NAMESPACE = 'spree'
@@ -1,11 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spree
4
- # Handles order completion events to update product metrics.
5
- #
6
- # When an order is completed, this subscriber enqueues background jobs
7
- # to refresh the metrics (units_sold_count, revenue) for each product
8
- # in the order.
4
+ # Handles order completion events to update product metrics
5
+ # (+units_sold_count+, +revenue+).
9
6
  class ProductMetricsSubscriber < Spree::Subscriber
10
7
  subscribes_to 'order.completed'
11
8
 
@@ -17,12 +14,11 @@ module Spree
17
14
 
18
15
  order = Spree::Order.find_by_param(order_id)
19
16
  return unless order
20
- return unless order.store_id
21
17
 
22
18
  product_ids = order.line_items.includes(:variant).map { |li| li.variant.product_id }.uniq
23
19
  return if product_ids.empty?
24
20
 
25
- jobs = product_ids.map { |product_id| Spree::Products::RefreshMetricsJob.new(product_id, order.store_id) }
21
+ jobs = product_ids.map { |product_id| Spree::Products::RefreshMetricsJob.new(product_id) }
26
22
  ActiveJob.perform_all_later(jobs)
27
23
  end
28
24
  end
@@ -12,6 +12,10 @@
12
12
  <p>
13
13
  <%= link_to Spree.t(:accept), spree.admin_invitation_url(@invitation, token: @invitation.token, host: @invitation.store.formatted_url) %>
14
14
  </p>
15
+ <% else %>
16
+ <p>
17
+ <%= link_to Spree.t(:accept), main_app.admin_invitation_acceptance_url(@invitation) %>
18
+ </p>
15
19
  <% end %>
16
20
 
17
21
  <p>
@@ -299,6 +299,11 @@ en:
299
299
  attributes:
300
300
  currency:
301
301
  must_match_order_currency: Must match order currency
302
+ spree/market:
303
+ attributes:
304
+ base:
305
+ cannot_destroy_default_market: Cannot delete the default market.
306
+ cannot_destroy_last_market: Cannot delete the only market.
302
307
  spree/market_country:
303
308
  attributes:
304
309
  country:
@@ -318,6 +323,10 @@ en:
318
323
  cannot_destroy_if_attached_to_line_items: Cannot delete Products that are added to placed Orders. In such cases, please discontinue them.
319
324
  discontinue_on:
320
325
  invalid_date_range: must be later than available date
326
+ spree/product_publication:
327
+ attributes:
328
+ unpublished_at:
329
+ must_be_after_published_at: must be after published date
321
330
  spree/promotion:
322
331
  attributes:
323
332
  expires_at:
@@ -780,6 +789,8 @@ en:
780
789
  are_you_sure: Are you sure?
781
790
  are_you_sure_delete: Are you sure you want to delete this record?
782
791
  assets: Media
792
+ assigned_variants: Assigned variants
793
+ assigned_variants_help: Pick the variants this image represents. Leave blank to apply to all variants.
783
794
  associated_adjustment_closed: The associated adjustment is closed, and will not be recalculated. Do you want to open it?
784
795
  at_symbol: "@"
785
796
  attachments: Attachments
@@ -874,7 +885,8 @@ en:
874
885
  change: Change
875
886
  change_password: Change password
876
887
  changes_published: Changes published!
877
- channel: Channel
888
+ channel: Sales channel
889
+ channels: Sales channels
878
890
  charged: Charged
879
891
  checkout: Checkout
880
892
  checkout_message: Checkout message
@@ -1124,8 +1136,12 @@ en:
1124
1136
  errors:
1125
1137
  messages:
1126
1138
  blank: can't be blank
1139
+ cannot_delete_default_channel: Default channel cannot be deleted. Promote another channel to default first.
1127
1140
  cannot_remove_icon: Cannot remove image
1141
+ channel_store_mismatch: must belong to the same store
1128
1142
  could_not_create_taxon: Could not create taxon
1143
+ invalid_order_routing_rule: is not a registered order routing rule
1144
+ invalid_order_routing_strategy: is not a registered order routing strategy
1129
1145
  must_be_origin_only: must be an origin (scheme and host) without path, query, or fragment
1130
1146
  no_shipping_methods_available: No shipping methods available for selected location, please change your address and try again.
1131
1147
  store_association_can_not_be_changed: The store association can not be changed
@@ -1311,6 +1327,9 @@ en:
1311
1327
  inventory_error_flash_for_insufficient_quantity: An item in your cart has become unavailable.
1312
1328
  inventory_state: Inventory State
1313
1329
  invitation_accepted: Invitation accepted!
1330
+ invitation_expired:
1331
+ body: This invitation link is no longer valid. Please ask the person who invited you to send a new one.
1332
+ heading: Invitation invalid or expired
1314
1333
  invitation_mailer:
1315
1334
  greeting: Hi %{user_name},
1316
1335
  invitation_accepted:
@@ -1445,6 +1464,7 @@ en:
1445
1464
  new_api_key: New API Key
1446
1465
  new_balance: New balance
1447
1466
  new_billing_address: New Billing Address
1467
+ new_channel: New sales channel
1448
1468
  new_country: New Country
1449
1469
  new_custom_domain: New Custom Domain
1450
1470
  new_customer: New Customer
@@ -1633,6 +1653,10 @@ en:
1633
1653
  order_number: Order %{number}
1634
1654
  order_processed_successfully: Your order has been processed successfully
1635
1655
  order_resumed: Order resumed
1656
+ order_routing:
1657
+ strategies:
1658
+ legacy: Legacy
1659
+ rules: Rules (ordered)
1636
1660
  order_state:
1637
1661
  address: address
1638
1662
  awaiting_return: awaiting return
@@ -1878,15 +1902,24 @@ en:
1878
1902
  promotion_not_cloned: 'Promotion has not been cloned. Reason: %{error}'
1879
1903
  promotion_rule: Promotion Rule
1880
1904
  promotion_rule_types:
1905
+ category:
1906
+ description: Order includes products in specified categories
1907
+ name: Categories
1881
1908
  country:
1882
1909
  description: Limit to orders with shipping address in a specific country
1883
1910
  name: Country
1884
1911
  currency:
1885
1912
  description: Limit to orders in a specific currency
1886
1913
  name: Currency
1914
+ customer:
1915
+ description: Available only to the specified customers
1916
+ name: Customers
1887
1917
  customer_group:
1888
1918
  description: Available only to customers in specified customer group(s)
1889
1919
  name: Customer Group(s)
1920
+ customer_logged_in:
1921
+ description: Available only to logged in customers
1922
+ name: Only logged in customers
1890
1923
  first_order:
1891
1924
  description: Must be the customer's first order
1892
1925
  name: First order
@@ -1902,15 +1935,6 @@ en:
1902
1935
  product:
1903
1936
  description: Order includes specified product(s)
1904
1937
  name: Product(s)
1905
- taxon:
1906
- description: Order includes products with specified taxon(s)
1907
- name: Taxon(s)
1908
- user:
1909
- description: Available only to the specified customers
1910
- name: User
1911
- user_logged_in:
1912
- description: Available only to logged in users
1913
- name: User Logged In
1914
1938
  promotion_uses: Promotion uses
1915
1939
  promotionable: Promotable
1916
1940
  promotions: Promotions
@@ -2479,6 +2503,7 @@ en:
2479
2503
  view: View
2480
2504
  view_all: View all
2481
2505
  view_full_details: View full details
2506
+ view_full_size: View full size
2482
2507
  view_store: View store
2483
2508
  view_your_store: View your store
2484
2509
  visibility: Visibility
data/config/routes.rb CHANGED
@@ -40,6 +40,15 @@ Rails.application.routes.draw do
40
40
  )
41
41
  end
42
42
  end
43
+ # Used by admin mailers; the SPA derives the URL from `window.location.origin` instead.
44
+ direct :admin_invitation_acceptance do |invitation, _options = {}|
45
+ path = "/accept-invitation/#{invitation.prefixed_id}?token=#{invitation.token}"
46
+ base = Spree::Config[:admin_url].presence ||
47
+ (Rails.env.development? ? 'http://localhost:5173' : nil) ||
48
+ invitation.store&.formatted_url
49
+
50
+ base.present? ? "#{base.chomp('/')}#{path}" : path
51
+ end
43
52
  end
44
53
 
45
54
  Spree::Core::Engine.draw_routes
@@ -0,0 +1,25 @@
1
+ class CreateSpreeOrderCancellations < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :spree_order_cancellations do |t|
4
+ t.references :order, null: false, index: false
5
+ t.string :reason, null: false
6
+ t.text :note
7
+ t.boolean :restock_items, null: false
8
+ t.boolean :refund_payments, null: false
9
+ t.decimal :refund_amount, precision: 10, scale: 2
10
+ t.boolean :notify_customer, null: false
11
+ t.references :canceled_by, polymorphic: true, index: false
12
+ if t.respond_to? :jsonb
13
+ t.jsonb :metadata
14
+ else
15
+ t.json :metadata
16
+ end
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :spree_order_cancellations, :order_id
21
+ add_index :spree_order_cancellations, [:canceled_by_id, :canceled_by_type],
22
+ name: 'idx_order_cancellations_canceled_by'
23
+ add_index :spree_order_cancellations, :created_at
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ class CreateSpreeOrderApprovals < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :spree_order_approvals do |t|
4
+ t.references :order, null: false, index: false
5
+ t.string :status, null: false
6
+ t.string :level
7
+ t.text :note
8
+ t.references :approver, polymorphic: true, index: false
9
+ t.datetime :decided_at
10
+ if t.respond_to? :jsonb
11
+ t.jsonb :metadata
12
+ else
13
+ t.json :metadata
14
+ end
15
+ t.timestamps
16
+ end
17
+
18
+ add_index :spree_order_approvals, [:order_id, :status]
19
+ add_index :spree_order_approvals, [:approver_id, :approver_type],
20
+ name: 'idx_order_approvals_approver'
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ class AddStatusToSpreeOrders < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :spree_orders, :status, :string
4
+ add_index :spree_orders, :status
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ class AddScopesToSpreeApiKeys < ActiveRecord::Migration[7.2]
2
+ def change
3
+ change_table :spree_api_keys do |t|
4
+ if t.respond_to?(:jsonb)
5
+ t.jsonb :scopes
6
+ else
7
+ t.json :scopes
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ class CreateSpreeStockReservations < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :spree_stock_reservations do |t|
4
+ t.references :stock_item, null: false, index: false
5
+ t.references :line_item, null: false
6
+ t.references :order, null: false
7
+ t.integer :quantity, null: false
8
+ t.datetime :expires_at, null: false
9
+ t.timestamps
10
+ end
11
+
12
+ # Composite (stock_item_id, expires_at) is the hot-path Quantifier query;
13
+ # leading on stock_item_id makes it cover the standalone-by-stock-item lookup too.
14
+ add_index :spree_stock_reservations, [:stock_item_id, :expires_at]
15
+ add_index :spree_stock_reservations, [:stock_item_id, :line_item_id], unique: true,
16
+ name: 'idx_stock_reservations_item_line_item'
17
+ add_index :spree_stock_reservations, :expires_at
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ class AddTypeToSpreePaymentSetupSessions < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :spree_payment_setup_sessions, :type, :string
4
+ add_index :spree_payment_setup_sessions, :type
5
+ end
6
+ end
@@ -0,0 +1,23 @@
1
+ class CreateSpreeVariantMedia < ActiveRecord::Migration[7.2]
2
+ # `media_id` references spree_assets.id in 5.5 — the parent table renames to
3
+ # spree_media in 6.0 (see 5.4-6.0-product-media-system.md Phase 3 of the 6.0
4
+ # cleanup). Using `media_id` from the start saves a column rename in 6.0.
5
+ #
6
+ # Table name is `spree_variant_media` (not the Rails default `spree_variant_medias`)
7
+ # since "media" reads as collective; the model overrides `self.table_name`.
8
+ #
9
+ # No per-variant `position` column — gallery order comes from the asset's
10
+ # product-level position (Asset#position). Variant gallery resolution filters
11
+ # product media down to the linked subset and inherits the product's order.
12
+ def change
13
+ create_table :spree_variant_media do |t|
14
+ t.references :variant, null: false, index: false
15
+ t.bigint :media_id, null: false
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :spree_variant_media, [:variant_id, :media_id], unique: true,
20
+ name: 'idx_variant_media_unique'
21
+ add_index :spree_variant_media, :media_id, name: 'idx_variant_media_media'
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ class AddPickupToSpreeStockLocations < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_column :spree_stock_locations, :kind, :string, null: false, default: 'warehouse'
4
+ add_column :spree_stock_locations, :pickup_enabled, :boolean, null: false, default: false
5
+ add_column :spree_stock_locations, :pickup_stock_policy, :string, null: false, default: 'local'
6
+ add_column :spree_stock_locations, :pickup_ready_in_minutes, :integer
7
+ add_column :spree_stock_locations, :pickup_instructions, :text
8
+
9
+ add_index :spree_stock_locations, :pickup_enabled
10
+ add_index :spree_stock_locations, :kind
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ class CreateSpreeChannels < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :spree_channels do |t|
4
+ t.references :store, null: false
5
+ t.string :name, null: false
6
+ t.string :code, null: false
7
+ t.boolean :active, null: false
8
+ t.text :preferences
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :spree_channels, %i[store_id code], unique: true
13
+
14
+ # Default-channel backfill for existing stores lives in
15
+ # +rake spree:channels:create_defaults+ (data transformations don't belong
16
+ # in migrations per Spree's guidelines).
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ class CreateSpreeOrderRoutingRules < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :spree_order_routing_rules do |t|
4
+ t.references :store, null: false
5
+ t.references :channel, null: false
6
+ t.string :type, null: false
7
+ t.integer :position, null: false
8
+ t.boolean :active, null: false
9
+ t.text :preferences
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :spree_order_routing_rules, [:channel_id, :position]
14
+ add_index :spree_order_routing_rules, [:channel_id, :active, :position],
15
+ name: 'idx_order_routing_rules_lookup'
16
+ add_index :spree_order_routing_rules, :type
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ class AddPreferredStockLocationToSpreeOrders < ActiveRecord::Migration[7.2]
2
+ def change
3
+ add_reference :spree_orders, :preferred_stock_location
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ class AddChannelIdToSpreeOrders < ActiveRecord::Migration[7.2]
2
+ # Non-destructive: adds the FK column alongside the existing `channel`
3
+ # string column. Old reads keep working until the upgrade rake task
4
+ # `spree:order_routing:backfill_channel_ids` populates `channel_id`.
5
+ # The string column is dropped in a follow-up migration once the
6
+ # backfill has run.
7
+ def change
8
+ add_reference :spree_orders, :channel
9
+ end
10
+ end
@@ -0,0 +1,57 @@
1
+ class BackfillStatusOnSpreeOrders < ActiveRecord::Migration[7.2]
2
+ BATCH_SIZE = 10_000
3
+
4
+ STATE_TO_STATUS = {
5
+ 'placed' => %w[complete resumed returned awaiting_return],
6
+ 'canceled' => %w[canceled partially_canceled]
7
+ }.freeze
8
+
9
+ def up
10
+ STATE_TO_STATUS.each do |target_status, states|
11
+ backfill_in_batches(target_status, states)
12
+ end
13
+
14
+ # Anything still NULL (carts, in-progress checkouts) becomes draft.
15
+ backfill_in_batches('draft', nil)
16
+
17
+ change_column_default :spree_orders, :status, 'draft'
18
+ change_column_null :spree_orders, :status, false
19
+ end
20
+
21
+ def down
22
+ raise ActiveRecord::IrreversibleMigration,
23
+ 'Backfilled status values cannot be safely restored: writes made after deploy ' \
24
+ 'are indistinguishable from the backfill. Roll back manually if needed.'
25
+ end
26
+
27
+ private
28
+
29
+ def backfill_in_batches(target_status, source_states)
30
+ quoted_status = connection.quote(target_status)
31
+ state_filter =
32
+ if source_states
33
+ quoted = Array(source_states).map { |s| connection.quote(s) }.join(',')
34
+ "AND state IN (#{quoted})"
35
+ else
36
+ ''
37
+ end
38
+
39
+ loop do
40
+ # The nested SELECT wrap (`SELECT id FROM (SELECT id ... LIMIT N) AS t`) is
41
+ # required for MySQL, which refuses an UPDATE that selects from the same
42
+ # table in the WHERE clause without an intermediate derived table.
43
+ affected = connection.update(<<~SQL.squish)
44
+ UPDATE spree_orders SET status = #{quoted_status}
45
+ WHERE id IN (
46
+ SELECT id FROM (
47
+ SELECT id FROM spree_orders
48
+ WHERE status IS NULL #{state_filter}
49
+ LIMIT #{BATCH_SIZE}
50
+ ) AS spree_orders_batch
51
+ )
52
+ SQL
53
+
54
+ break if affected < BATCH_SIZE
55
+ end
56
+ end
57
+ end